[analysis_server] Add support for LSP Call Hierarchy
Fixes https://github.com/Dart-Code/Dart-Code/issues/612.
Change-Id: Ic51d4be2340317443b5a1cdae83e6eeaac4fa06b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/251460
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/computer/computer_call_hierarchy.dart b/pkg/analysis_server/lib/src/computer/computer_call_hierarchy.dart
index b6f563d..82a4176 100644
--- a/pkg/analysis_server/lib/src/computer/computer_call_hierarchy.dart
+++ b/pkg/analysis_server/lib/src/computer/computer_call_hierarchy.dart
@@ -78,6 +78,15 @@
/// The range of the code for the declaration of this item.
final SourceRange codeRange;
+ CallHierarchyItem({
+ required this.displayName,
+ required this.containerName,
+ required this.kind,
+ required this.file,
+ required this.nameRange,
+ required this.codeRange,
+ });
+
CallHierarchyItem.forElement(Element element)
: displayName = _getDisplayName(element),
nameRange = _nameRangeForElement(element),
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_call_hierarchy.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_call_hierarchy.dart
new file mode 100644
index 0000000..725ea58
--- /dev/null
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_call_hierarchy.dart
@@ -0,0 +1,426 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
+import 'package:analysis_server/lsp_protocol/protocol_special.dart';
+import 'package:analysis_server/src/computer/computer_call_hierarchy.dart'
+ as call_hierarchy;
+import 'package:analysis_server/src/lsp/handlers/handlers.dart';
+import 'package:analysis_server/src/lsp/mapping.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:analyzer/dart/analysis/session.dart';
+import 'package:analyzer/source/line_info.dart';
+import 'package:analyzer/source/source_range.dart';
+
+/// A handler for `callHierarchy/incoming` that returns the incoming calls for
+/// the target supplied by the client.
+class IncomingCallHierarchyHandler extends _AbstractCallHierarchyCallsHandler<
+ CallHierarchyIncomingCallsParams,
+ CallHierarchyIncomingCallsResult,
+ CallHierarchyIncomingCall> with _CallHierarchyUtils {
+ IncomingCallHierarchyHandler(super.server);
+ @override
+ Method get handlesMessage => Method.callHierarchy_incomingCalls;
+
+ @override
+ LspJsonHandler<CallHierarchyIncomingCallsParams> get jsonHandler =>
+ CallHierarchyIncomingCallsParams.jsonHandler;
+
+ /// Fetches incoming calls from a [call_hierarchy.DartCallHierarchyComputer].
+ ///
+ /// This method is invoked by the superclass which handles similar logic for
+ /// both incoming and outgoing calls.
+ @override
+ Future<List<call_hierarchy.CallHierarchyCalls>> getCalls(
+ call_hierarchy.DartCallHierarchyComputer computer,
+ call_hierarchy.CallHierarchyItem target,
+ ) =>
+ computer.findIncomingCalls(target, server.searchEngine);
+
+ /// Handles the request by passing the target item to a shared implementation
+ /// in the superclass.
+ @override
+ Future<ErrorOr<CallHierarchyIncomingCallsResult>> handle(
+ CallHierarchyIncomingCallsParams params,
+ MessageInfo message,
+ CancellationToken token) =>
+ handleCalls(params.item);
+
+ /// Converts a server [call_hierarchy.CallHierarchyCalls] into the correct LSP
+ /// type for incoming calls.
+ @override
+ CallHierarchyIncomingCall toCall(
+ call_hierarchy.CallHierarchyCalls calls, {
+ required LineInfo localLineInfo,
+ required LineInfo itemLineInfo,
+ required Set<SymbolKind> supportedSymbolKinds,
+ }) {
+ return CallHierarchyIncomingCall(
+ from: toLspItem(
+ calls.item,
+ itemLineInfo,
+ supportedSymbolKinds: supportedSymbolKinds,
+ ),
+ fromRanges: calls.ranges
+ // For incoming calls, ranges are in the referenced item so we use
+ // itemLineInfo and not localLineInfo (which is for the original
+ // target we're collecting calls to).
+ .map((call) => sourceRangeToRange(itemLineInfo, call))
+ .toList(),
+ );
+ }
+}
+
+/// A handler for `callHierarchy/outgoing` that returns the outgoing calls for
+/// the target supplied by the client.
+class OutgoingCallHierarchyHandler extends _AbstractCallHierarchyCallsHandler<
+ CallHierarchyOutgoingCallsParams,
+ CallHierarchyOutgoingCallsResult,
+ CallHierarchyOutgoingCall> with _CallHierarchyUtils {
+ OutgoingCallHierarchyHandler(super.server);
+ @override
+ Method get handlesMessage => Method.callHierarchy_outgoingCalls;
+
+ @override
+ LspJsonHandler<CallHierarchyOutgoingCallsParams> get jsonHandler =>
+ CallHierarchyOutgoingCallsParams.jsonHandler;
+
+ /// Fetches outgoing calls from a [call_hierarchy.DartCallHierarchyComputer].
+ ///
+ /// This method is invoked by the superclass which handles similar logic for
+ /// both incoming and outgoing calls.
+ @override
+ Future<List<call_hierarchy.CallHierarchyCalls>> getCalls(
+ call_hierarchy.DartCallHierarchyComputer computer,
+ call_hierarchy.CallHierarchyItem target,
+ ) =>
+ computer.findOutgoingCalls(target);
+
+ /// Handles the request by passing the target item to a shared implementation
+ /// in the superclass.
+ @override
+ Future<ErrorOr<CallHierarchyOutgoingCallsResult>> handle(
+ CallHierarchyOutgoingCallsParams params,
+ MessageInfo message,
+ CancellationToken token) =>
+ handleCalls(params.item);
+
+ /// Converts a server [call_hierarchy.CallHierarchyCalls] into the correct LSP
+ /// type for outgoing calls.
+ @override
+ CallHierarchyOutgoingCall toCall(
+ call_hierarchy.CallHierarchyCalls calls, {
+ required LineInfo localLineInfo,
+ required LineInfo itemLineInfo,
+ required Set<SymbolKind> supportedSymbolKinds,
+ }) {
+ return CallHierarchyOutgoingCall(
+ to: toLspItem(
+ calls.item,
+ itemLineInfo,
+ supportedSymbolKinds: supportedSymbolKinds,
+ ),
+ fromRanges: calls.ranges
+ // For incoming calls, ranges are in original target so we use
+ // localLineInfo and not itemLineInfo (which is for call target
+ // the outbound call points to).
+ .map((call) => sourceRangeToRange(localLineInfo, call))
+ .toList(),
+ );
+ }
+}
+
+/// A handler for the initial "prepare" request for starting navigation with
+/// Call Hierarchy.
+///
+/// This handler returns the initial target based on the offset where the
+/// feature is invoked. Invocations at call sites will resolve to the respective
+/// declarations.
+///
+/// The target returned by this handler will be sent back to the server for
+/// incoming/outgoing calls as the user navigates the call hierarchy in the
+/// client.
+class PrepareCallHierarchyHandler extends MessageHandler<
+ CallHierarchyPrepareParams,
+ TextDocumentPrepareCallHierarchyResult> with _CallHierarchyUtils {
+ PrepareCallHierarchyHandler(super.server);
+ @override
+ Method get handlesMessage => Method.textDocument_prepareCallHierarchy;
+
+ @override
+ LspJsonHandler<CallHierarchyPrepareParams> get jsonHandler =>
+ CallHierarchyPrepareParams.jsonHandler;
+
+ @override
+ Future<ErrorOr<TextDocumentPrepareCallHierarchyResult>> handle(
+ CallHierarchyPrepareParams params,
+ MessageInfo message,
+ CancellationToken token) async {
+ if (!isDartDocument(params.textDocument)) {
+ return success(const []);
+ }
+
+ final clientCapabilities = server.clientCapabilities;
+ if (clientCapabilities == null) {
+ // This should not happen unless a client misbehaves.
+ return serverNotInitializedError;
+ }
+
+ final pos = params.position;
+ final path = pathOfDoc(params.textDocument);
+ final unit = await path.mapResult(requireResolvedUnit);
+ final offset = await unit.mapResult((unit) => toOffset(unit.lineInfo, pos));
+ return offset.mapResult((offset) {
+ final supportedSymbolKinds = clientCapabilities.documentSymbolKinds;
+ final computer = call_hierarchy.DartCallHierarchyComputer(unit.result);
+ final target = computer.findTarget(offset);
+ if (target == null) {
+ return success(null);
+ }
+
+ return _convertTarget(
+ unit.result.session,
+ target,
+ supportedSymbolKinds,
+ );
+ });
+ }
+
+ /// Converts a server [call_hierarchy.CallHierarchyItem] to the LSP protocol
+ /// equivalent.
+ Future<ErrorOr<TextDocumentPrepareCallHierarchyResult>> _convertTarget(
+ AnalysisSession session,
+ call_hierarchy.CallHierarchyItem target,
+ Set<SymbolKind> supportedSymbolKinds,
+ ) async {
+ // Since the target might be in a different file (for example if invoked at
+ // a call site), we need to get a consistent LineInfo for the target file
+ // for this session.
+ final targetFile = session.getFile(target.file);
+ if (targetFile is! FileResult) {
+ return error(
+ ErrorCodes.InternalError,
+ 'Call Hierarchy target was in an unavailable file: '
+ '${target.displayName} in ${target.file}',
+ );
+ }
+ final targetLineInfo = targetFile.lineInfo;
+
+ final item = toLspItem(
+ target,
+ targetLineInfo,
+ supportedSymbolKinds: supportedSymbolKinds,
+ );
+ return success([item]);
+ }
+}
+
+/// An abstract base class for incoming and outgoing CallHierarchy handlers
+/// which perform largely the same task using different LSP classes.
+abstract class _AbstractCallHierarchyCallsHandler<P, R, C>
+ extends MessageHandler<P, R> with _CallHierarchyUtils {
+ _AbstractCallHierarchyCallsHandler(super.server);
+
+ /// Gets the appropriate types of calls for this handler.
+ Future<List<call_hierarchy.CallHierarchyCalls>> getCalls(
+ call_hierarchy.DartCallHierarchyComputer computer,
+ call_hierarchy.CallHierarchyItem target);
+
+ /// Handles a request for incoming or outgoing calls (handled by the concrete
+ /// implementation) by delegating fetching and converting calls to the
+ /// subclass.
+ Future<ErrorOr<List<C>?>> handleCalls(CallHierarchyItem item) async {
+ if (!isDartUri(item.uri)) {
+ return success(const []);
+ }
+
+ final clientCapabilities = server.clientCapabilities;
+ if (clientCapabilities == null) {
+ // This should not happen unless a client misbehaves.
+ return failure(serverNotInitializedError);
+ }
+
+ final pos = item.selectionRange.start;
+ final path = pathOfUri(Uri.parse(item.uri));
+ final unit = await path.mapResult(requireResolvedUnit);
+ final offset = await unit.mapResult((unit) => toOffset(unit.lineInfo, pos));
+ return offset.mapResult((offset) async {
+ final supportedSymbolKinds = clientCapabilities.documentSymbolKinds;
+ final computer = call_hierarchy.DartCallHierarchyComputer(unit.result);
+
+ // Convert the clients item back to one in the servers format so that we
+ // can use it to get incoming/outgoing calls.
+ final target = toServerItem(
+ item,
+ unit.result.lineInfo,
+ supportedSymbolKinds: supportedSymbolKinds,
+ );
+
+ if (target == null) {
+ return error(
+ ErrorCodes.ContentModified,
+ 'Content was modified since Call Hierarchy node was produced',
+ );
+ }
+
+ final calls = await getCalls(computer, target);
+ final results = _convertCalls(
+ unit.result,
+ calls,
+ supportedSymbolKinds,
+ );
+ return success(results);
+ });
+ }
+
+ /// Converts a server [call_hierarchy.CallHierarchyCalls] to the appropriate
+ /// LSP type [C].
+ C toCall(
+ call_hierarchy.CallHierarchyCalls calls, {
+ required LineInfo localLineInfo,
+ required LineInfo itemLineInfo,
+ required Set<SymbolKind> supportedSymbolKinds,
+ });
+
+ C? _convertCall(
+ AnalysisSession session,
+ LineInfo localLineInfo,
+ Map<String, LineInfo?> lineInfoCache,
+ call_hierarchy.CallHierarchyCalls calls,
+ Set<SymbolKind> supportedSymbolKinds,
+ ) {
+ final filePath = calls.item.file;
+ final itemLineInfo = lineInfoCache.putIfAbsent(filePath, () {
+ final file = session.getFile(filePath);
+ return file is FileResult ? file.lineInfo : null;
+ });
+ if (itemLineInfo == null) {
+ return null;
+ }
+
+ return toCall(
+ calls,
+ localLineInfo: localLineInfo,
+ itemLineInfo: itemLineInfo,
+ supportedSymbolKinds: supportedSymbolKinds,
+ );
+ }
+
+ List<C> _convertCalls(
+ ResolvedUnitResult unit,
+ List<call_hierarchy.CallHierarchyCalls> calls,
+ Set<SymbolKind> supportedSymbolKinds,
+ ) {
+ final session = unit.session;
+ final lineInfoCache = <String, LineInfo?>{};
+ final results = convert(
+ calls,
+ (call_hierarchy.CallHierarchyCalls call) => _convertCall(
+ session,
+ unit.lineInfo,
+ lineInfoCache,
+ call,
+ supportedSymbolKinds,
+ ),
+ );
+ return results.toList();
+ }
+}
+
+/// Utility methods used by all Call Hierarchy handlers.
+mixin _CallHierarchyUtils {
+ /// A mapping from server kinds to LSP [SymbolKind]s.
+ static const toSymbolKindMapping = {
+ call_hierarchy.CallHierarchyKind.class_: SymbolKind.Class,
+ call_hierarchy.CallHierarchyKind.constructor: SymbolKind.Constructor,
+ call_hierarchy.CallHierarchyKind.extension: SymbolKind.Class,
+ call_hierarchy.CallHierarchyKind.file: SymbolKind.File,
+ call_hierarchy.CallHierarchyKind.function: SymbolKind.Function,
+ call_hierarchy.CallHierarchyKind.method: SymbolKind.Method,
+ call_hierarchy.CallHierarchyKind.mixin: SymbolKind.Class,
+ call_hierarchy.CallHierarchyKind.property: SymbolKind.Property,
+ };
+
+ /// A mapping from LSP [SymbolKind]s to server kinds.
+ static final fromSymbolKindMapping = {
+ for (final entry in toSymbolKindMapping.entries) entry.value: entry.key,
+ };
+
+ /// Converts a [SymbolKind] passed back from the client over LSP to a server
+ /// [call_hierarchy.CallHierarchyKind].
+ call_hierarchy.CallHierarchyKind fromSymbolKind(SymbolKind kind) {
+ var result = fromSymbolKindMapping[kind];
+ assert(result != null);
+
+ return result ?? call_hierarchy.CallHierarchyKind.unknown;
+ }
+
+ /// Converts a server [SourceRange] to an LSP [Range].
+ Range sourceRangeToRange(LineInfo lineInfo, SourceRange range) =>
+ toRange(lineInfo, range.offset, range.length);
+
+ /// Converts a server [call_hierarchy.CallHierarchyItem] to an LSP
+ /// [CallHierarchyItem].
+ CallHierarchyItem toLspItem(
+ call_hierarchy.CallHierarchyItem item,
+ LineInfo lineInfo, {
+ required Set<SymbolKind> supportedSymbolKinds,
+ }) {
+ return CallHierarchyItem(
+ name: item.displayName,
+ detail: item.containerName,
+ kind: toSymbolKind(supportedSymbolKinds, item.kind),
+ uri: Uri.file(item.file).toString(),
+ range: sourceRangeToRange(lineInfo, item.codeRange),
+ selectionRange: sourceRangeToRange(lineInfo, item.nameRange),
+ );
+ }
+
+ /// Converts an LSP [CallHierarchyItem] supplied by the client back to a
+ /// server [call_hierarchy.CallHierarchyItem] to use to look up calls.
+ ///
+ /// Returns `null` if the supplied item is no longer valid (for example its
+ /// ranges are no longer valid in the current state of the document).
+ call_hierarchy.CallHierarchyItem? toServerItem(
+ CallHierarchyItem item,
+ LineInfo lineInfo, {
+ required Set<SymbolKind> supportedSymbolKinds,
+ }) {
+ final nameRange = toSourceRange(lineInfo, item.selectionRange);
+ final codeRange = toSourceRange(lineInfo, item.range);
+ if (nameRange.isError || codeRange.isError) {
+ return null;
+ }
+
+ return call_hierarchy.CallHierarchyItem(
+ displayName: item.name,
+ containerName: item.detail,
+ kind: fromSymbolKind(item.kind),
+ file: Uri.parse(item.uri).toFilePath(),
+ nameRange: nameRange.result,
+ codeRange: codeRange.result,
+ );
+ }
+
+ /// Converts a server [call_hierarchy.CallHierarchyKind] to a [SymbolKind]
+ /// used in the LSP Protocol.
+ SymbolKind toSymbolKind(Set<SymbolKind> supportedSymbolKinds,
+ call_hierarchy.CallHierarchyKind kind) {
+ var result = toSymbolKindMapping[kind];
+ assert(result != null);
+
+ // Handle fallbacks and not-supported kinds.
+ if (!supportedSymbolKinds.contains(result)) {
+ if (result == SymbolKind.File) {
+ result = SymbolKind.Module;
+ } else {
+ result = null;
+ }
+ }
+
+ return result ?? SymbolKind.Obj;
+ }
+}
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart
index d9a9cfa..d19f007 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart
@@ -9,6 +9,7 @@
import 'package:analysis_server/src/lsp/handlers/custom/handler_diagnostic_server.dart';
import 'package:analysis_server/src/lsp/handlers/custom/handler_reanalyze.dart';
import 'package:analysis_server/src/lsp/handlers/custom/handler_super.dart';
+import 'package:analysis_server/src/lsp/handlers/handler_call_hierarchy.dart';
import 'package:analysis_server/src/lsp/handlers/handler_change_workspace_folders.dart';
import 'package:analysis_server/src/lsp/handlers/handler_code_actions.dart';
import 'package:analysis_server/src/lsp/handlers/handler_completion.dart';
@@ -97,6 +98,9 @@
server, !options.onlyAnalyzeProjectsWithOpenFiles));
registerHandler(PrepareRenameHandler(server));
registerHandler(RenameHandler(server));
+ registerHandler(PrepareCallHierarchyHandler(server));
+ registerHandler(IncomingCallHierarchyHandler(server));
+ registerHandler(OutgoingCallHierarchyHandler(server));
registerHandler(FoldingHandler(server));
registerHandler(DiagnosticServerHandler(server));
registerHandler(WorkspaceSymbolHandler(server));
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart b/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart
index 6f2bea3..24366d6 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart
@@ -24,10 +24,10 @@
/// Converts an iterable using the provided function and skipping over any
/// null values.
-Iterable<T> convert<T, E>(Iterable<E> items, T Function(E) converter) {
+Iterable<T> convert<T, E>(Iterable<E> items, T? Function(E) converter) {
// TODO(dantup): Now this is used outside of handlers, is there somewhere
// better to put it, and/or a better name for it?
- return items.map(converter).where((item) => item != null);
+ return items.map(converter).where((item) => item != null).cast<T>();
}
abstract class CommandHandler<P, R> with Handler<P, R> {
diff --git a/pkg/analysis_server/lib/src/lsp/mapping.dart b/pkg/analysis_server/lib/src/lsp/mapping.dart
index 7e2c591..284c4ad 100644
--- a/pkg/analysis_server/lib/src/lsp/mapping.dart
+++ b/pkg/analysis_server/lib/src/lsp/mapping.dart
@@ -400,7 +400,10 @@
case server.ElementKind.CLASS_TYPE_ALIAS:
return const [lsp.CompletionItemKind.Class];
case server.ElementKind.COMPILATION_UNIT:
- return const [lsp.CompletionItemKind.Module];
+ return const [
+ lsp.CompletionItemKind.File,
+ lsp.CompletionItemKind.Module,
+ ];
case server.ElementKind.CONSTRUCTOR:
case server.ElementKind.CONSTRUCTOR_INVOCATION:
return const [lsp.CompletionItemKind.Constructor];
@@ -468,7 +471,7 @@
case server.ElementKind.CLASS_TYPE_ALIAS:
return const [lsp.SymbolKind.Class];
case server.ElementKind.COMPILATION_UNIT:
- return const [lsp.SymbolKind.Module];
+ return const [lsp.SymbolKind.File];
case server.ElementKind.CONSTRUCTOR:
case server.ElementKind.CONSTRUCTOR_INVOCATION:
return const [lsp.SymbolKind.Constructor];
@@ -655,8 +658,9 @@
return tags != null && tags.isNotEmpty ? tags : null;
}
-bool isDartDocument(lsp.TextDocumentIdentifier? doc) =>
- doc?.uri.endsWith('.dart') ?? false;
+bool isDartDocument(lsp.TextDocumentIdentifier doc) => isDartUri(doc.uri);
+
+bool isDartUri(String uri) => uri.endsWith('.dart');
/// Converts a [server.Location] to an [lsp.Range] by translating the
/// offset/length using a `LineInfo`.
diff --git a/pkg/analysis_server/lib/src/lsp/server_capabilities_computer.dart b/pkg/analysis_server/lib/src/lsp/server_capabilities_computer.dart
index ca9e238..b25addb 100644
--- a/pkg/analysis_server/lib/src/lsp/server_capabilities_computer.dart
+++ b/pkg/analysis_server/lib/src/lsp/server_capabilities_computer.dart
@@ -39,6 +39,7 @@
Method.textDocument_foldingRange,
Method.textDocument_selectionRange,
Method.textDocument_typeDefinition,
+ Method.textDocument_prepareCallHierarchy,
// workspace.fileOperations covers all file operation methods but we only
// support this one.
Method.workspace_willRenameFiles,
@@ -50,6 +51,9 @@
ClientDynamicRegistrations(this._capabilities);
+ bool get callHierarchy =>
+ _capabilities.textDocument?.callHierarchy?.dynamicRegistration ?? false;
+
bool get codeActions =>
_capabilities.textDocument?.foldingRange?.dynamicRegistration ?? false;
@@ -186,6 +190,10 @@
willSaveWaitUntil: false,
save: null,
)),
+ callHierarchyProvider: dynamicRegistrations.callHierarchy
+ ? null
+ : Either3<bool, CallHierarchyOptions,
+ CallHierarchyRegistrationOptions>.t1(true),
completionProvider: dynamicRegistrations.completion
? null
: CompletionOptions(
@@ -511,6 +519,13 @@
),
);
register(
+ dynamicRegistrations.callHierarchy,
+ Method.textDocument_prepareCallHierarchy,
+ CallHierarchyRegistrationOptions(
+ documentSelector: [dartFiles],
+ ),
+ );
+ register(
dynamicRegistrations.semanticTokens,
CustomMethods.semanticTokenDynamicRegistration,
SemanticTokensRegistrationOptions(
diff --git a/pkg/analysis_server/test/lsp/call_hierarchy_test.dart b/pkg/analysis_server/test/lsp/call_hierarchy_test.dart
new file mode 100644
index 0000000..9e90f33
--- /dev/null
+++ b/pkg/analysis_server/test/lsp/call_hierarchy_test.dart
@@ -0,0 +1,750 @@
+// Copyright (c) 2022, the Dart project authors. Please see the FooUTHORS file
+// for details. Fooll rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'server_abstract.dart';
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(PrepareCallHierarchyTest);
+ defineReflectiveTests(IncomingCallHierarchyTest);
+ defineReflectiveTests(OutgoingCallHierarchyTest);
+ });
+}
+
+@reflectiveTest
+class IncomingCallHierarchyTest extends AbstractLspAnalysisServerTest {
+ late final Uri otherFileUri;
+
+ /// Calls textDocument/prepareCallHierarchy at the location of `^` in
+ /// [mainContents] and uses the single result to call
+ /// `callHierarchy/incomingCalls` and ensures the results match
+ /// [expectedResults].
+ Future<void> expectResults({
+ required String mainContents,
+ String? otherContents,
+ required List<CallHierarchyIncomingCall> expectedResults,
+ }) async {
+ await initialize();
+ await openFile(mainFileUri, withoutMarkers(mainContents));
+
+ if (otherContents != null) {
+ await openFile(otherFileUri, withoutMarkers(otherContents));
+ }
+
+ final prepareResult = await prepareCallHierarchy(
+ mainFileUri,
+ positionFromMarker(mainContents),
+ );
+ final result = await callHierarchyIncoming(prepareResult!.single);
+
+ expect(result!, unorderedEquals(expectedResults));
+ }
+
+ @override
+ void setUp() {
+ super.setUp();
+ otherFileUri = Uri.file(join(projectFolderPath, 'lib', 'other.dart'));
+ }
+
+ Future<void> test_constructor() async {
+ final contents = '''
+ class Foo {
+ Fo^o();
+ }
+ ''';
+
+ final otherContents = '''
+ import 'main.dart';
+
+ class Bar {
+ final foo = Foo();
+ }
+ ''';
+
+ await expectResults(
+ mainContents: contents,
+ otherContents: otherContents,
+ expectedResults: [
+ CallHierarchyIncomingCall(
+ // Container of the call
+ from: CallHierarchyItem(
+ name: 'Bar',
+ detail: 'other.dart',
+ kind: SymbolKind.Class,
+ uri: otherFileUri.toString(),
+ range: rangeOfPattern(
+ otherContents, RegExp(r'class Bar \{.*\}', dotAll: true)),
+ selectionRange: rangeOfString(otherContents, 'Bar'),
+ ),
+ // Ranges of calls within this container
+ fromRanges: [
+ rangeOfString(otherContents, 'Foo'),
+ ],
+ ),
+ ],
+ );
+ }
+
+ Future<void> test_function() async {
+ final contents = '''
+ String fo^o() {}
+ ''';
+
+ final otherContents = '''
+ import 'main.dart';
+
+ final x = foo();
+ ''';
+
+ await expectResults(
+ mainContents: contents,
+ otherContents: otherContents,
+ expectedResults: [
+ CallHierarchyIncomingCall(
+ // Container of the call
+ from: CallHierarchyItem(
+ name: 'other.dart',
+ detail: null,
+ kind: SymbolKind.File,
+ uri: otherFileUri.toString(),
+ range: entireRange(otherContents),
+ selectionRange: startOfDocRange,
+ ),
+ // Ranges of calls within this container
+ fromRanges: [
+ rangeOfString(otherContents, 'foo'),
+ ],
+ ),
+ ],
+ );
+ }
+
+ Future<void> test_implicitConstructor() async {
+ final contents = '''
+ import 'other.dart';
+
+ void main() {
+ final foo = Fo^o();
+ }
+ ''';
+
+ final otherContents = '''
+ class Foo {}
+ ''';
+
+ await expectResults(
+ mainContents: contents,
+ otherContents: otherContents,
+ expectedResults: [
+ CallHierarchyIncomingCall(
+ // Container of the call
+ from: CallHierarchyItem(
+ name: 'main',
+ detail: 'main.dart',
+ kind: SymbolKind.Function,
+ uri: mainFileUri.toString(),
+ range: rangeOfPattern(
+ contents, RegExp(r'void main\(\) \{.*\}', dotAll: true)),
+ selectionRange: rangeOfString(contents, 'main'),
+ ),
+ // Ranges of calls within this container
+ fromRanges: [
+ rangeOfString(contents, 'Foo'),
+ ],
+ ),
+ ],
+ );
+ }
+
+ Future<void> test_method() async {
+ final contents = '''
+ class A {
+ String fo^o() {}
+ }
+ ''';
+
+ final otherContents = '''
+ import 'main.dart';
+
+ class B {
+ String bar() {
+ A().foo();
+ }
+ }
+ ''';
+
+ await expectResults(
+ mainContents: contents,
+ otherContents: otherContents,
+ expectedResults: [
+ CallHierarchyIncomingCall(
+ // Container of the call
+ from: CallHierarchyItem(
+ name: 'bar',
+ detail: 'B',
+ kind: SymbolKind.Method,
+ uri: otherFileUri.toString(),
+ range: rangeOfPattern(otherContents,
+ RegExp(r'String bar\(\) \{.*\ }', dotAll: true)),
+ selectionRange: rangeOfString(otherContents, 'bar'),
+ ),
+ // Ranges of calls within this container
+ fromRanges: [
+ rangeOfString(otherContents, 'foo'),
+ ],
+ ),
+ ],
+ );
+ }
+
+ Future<void> test_namedConstructor() async {
+ final contents = '''
+ class Foo {
+ Foo.nam^ed();
+ }
+ ''';
+
+ final otherContents = '''
+ import 'main.dart';
+
+ class Bar {
+ final foo = Foo.named();
+ }
+ ''';
+
+ await expectResults(
+ mainContents: contents,
+ otherContents: otherContents,
+ expectedResults: [
+ CallHierarchyIncomingCall(
+ // Container of the call
+ from: CallHierarchyItem(
+ name: 'Bar',
+ detail: 'other.dart',
+ kind: SymbolKind.Class,
+ uri: otherFileUri.toString(),
+ range: rangeOfPattern(
+ otherContents, RegExp(r'class Bar \{.*\}', dotAll: true)),
+ selectionRange: rangeOfString(otherContents, 'Bar'),
+ ),
+ // Ranges of calls within this container
+ fromRanges: [
+ rangeOfString(otherContents, 'named'),
+ ],
+ ),
+ ],
+ );
+ }
+}
+
+@reflectiveTest
+class OutgoingCallHierarchyTest extends AbstractLspAnalysisServerTest {
+ late final Uri otherFileUri;
+
+ /// Calls textDocument/prepareCallHierarchy at the location of `^` in
+ /// [mainContents] and uses the single result to call
+ /// `callHierarchy/outgoingCalls` and ensures the results match
+ /// [expectedResults].
+ Future<void> expectResults({
+ required String mainContents,
+ String? otherContents,
+ required List<CallHierarchyOutgoingCall> expectedResults,
+ }) async {
+ await initialize();
+ await openFile(mainFileUri, withoutMarkers(mainContents));
+
+ if (otherContents != null) {
+ await openFile(otherFileUri, withoutMarkers(otherContents));
+ }
+
+ final prepareResult = await prepareCallHierarchy(
+ mainFileUri,
+ positionFromMarker(mainContents),
+ );
+ final result = await callHierarchyOutgoing(prepareResult!.single);
+
+ expect(result!, unorderedEquals(expectedResults));
+ }
+
+ @override
+ void setUp() {
+ super.setUp();
+ otherFileUri = Uri.file(join(projectFolderPath, 'lib', 'other.dart'));
+ }
+
+ Future<void> test_constructor() async {
+ final contents = '''
+ import 'other.dart';
+
+ class Foo {
+ Fo^o() {
+ final b = Bar();
+ }
+ }
+ ''';
+
+ final otherContents = '''
+ class Bar {
+ Bar();
+ }
+ ''';
+
+ await expectResults(
+ mainContents: contents,
+ otherContents: otherContents,
+ expectedResults: [
+ CallHierarchyOutgoingCall(
+ // Target of the call.
+ to: CallHierarchyItem(
+ name: 'Bar',
+ detail: 'Bar',
+ kind: SymbolKind.Constructor,
+ uri: otherFileUri.toString(),
+ range: rangeOfString(otherContents, 'Bar();'),
+ selectionRange:
+ rangeStartingAtString(otherContents, 'Bar();', 'Bar'),
+ ),
+ // Ranges of the outbound call.
+ fromRanges: [
+ rangeOfString(contents, 'Bar'),
+ ],
+ ),
+ ],
+ );
+ }
+
+ Future<void> test_function() async {
+ final contents = '''
+ import 'other.dart';
+
+ void fo^o() {
+ bar();
+ }
+ ''';
+
+ final otherContents = '''
+ void bar() {}
+ ''';
+
+ await expectResults(
+ mainContents: contents,
+ otherContents: otherContents,
+ expectedResults: [
+ CallHierarchyOutgoingCall(
+ // Target of the call.
+ to: CallHierarchyItem(
+ name: 'bar',
+ detail: 'other.dart',
+ kind: SymbolKind.Function,
+ uri: otherFileUri.toString(),
+ range: rangeOfString(otherContents, 'void bar() {}'),
+ selectionRange: rangeOfString(otherContents, 'bar'),
+ ),
+ // Ranges of the outbound call.
+ fromRanges: [
+ rangeOfString(contents, 'bar'),
+ ],
+ ),
+ ],
+ );
+ }
+
+ Future<void> test_implicitConstructor() async {
+ final contents = '''
+ import 'other.dart';
+
+ class Foo {
+ Fo^o() {
+ final b = Bar();
+ }
+ }
+ ''';
+
+ final otherContents = '''
+ class Bar {}
+ ''';
+
+ await expectResults(
+ mainContents: contents,
+ otherContents: otherContents,
+ expectedResults: [
+ CallHierarchyOutgoingCall(
+ // Target of the call.
+ to: CallHierarchyItem(
+ name: 'Bar',
+ detail: 'Bar',
+ kind: SymbolKind.Constructor,
+ uri: otherFileUri.toString(),
+ range: rangeOfString(otherContents, 'class Bar {}'),
+ selectionRange: rangeOfString(otherContents, 'Bar'),
+ ),
+ // Ranges of the outbound call.
+ fromRanges: [
+ rangeOfString(contents, 'Bar'),
+ ],
+ ),
+ ],
+ );
+ }
+
+ Future<void> test_method() async {
+ final contents = '''
+ import 'other.dart';
+
+ class Foo {
+ final b = Bar();
+ void f^oo() {
+ b.bar();
+ }
+ }
+ ''';
+
+ final otherContents = '''
+ class Bar {
+ void bar() {}
+ }
+ ''';
+
+ await expectResults(
+ mainContents: contents,
+ otherContents: otherContents,
+ expectedResults: [
+ CallHierarchyOutgoingCall(
+ // Target of the call.
+ to: CallHierarchyItem(
+ name: 'bar',
+ detail: 'Bar',
+ kind: SymbolKind.Method,
+ uri: otherFileUri.toString(),
+ range: rangeOfString(otherContents, 'void bar() {}'),
+ selectionRange: rangeOfString(otherContents, 'bar'),
+ ),
+ // Ranges of the outbound call.
+ fromRanges: [
+ rangeOfString(contents, 'bar'),
+ ],
+ ),
+ ],
+ );
+ }
+
+ Future<void> test_namedConstructor() async {
+ final contents = '''
+ import 'other.dart';
+
+ class Foo {
+ Foo.nam^ed() {
+ final b = Bar.named();
+ }
+ }
+ ''';
+
+ final otherContents = '''
+ class Bar {
+ Bar.named();
+ }
+ ''';
+
+ await expectResults(
+ mainContents: contents,
+ otherContents: otherContents,
+ expectedResults: [
+ CallHierarchyOutgoingCall(
+ // Target of the call.
+ to: CallHierarchyItem(
+ name: 'Bar.named',
+ detail: 'Bar',
+ kind: SymbolKind.Constructor,
+ uri: otherFileUri.toString(),
+ range: rangeOfString(otherContents, 'Bar.named();'),
+ selectionRange: rangeOfString(otherContents, 'named'),
+ ),
+ // Ranges of the outbound call.
+ fromRanges: [
+ rangeStartingAtString(contents, 'named();', 'named'),
+ ],
+ ),
+ ],
+ );
+ }
+}
+
+@reflectiveTest
+class PrepareCallHierarchyTest extends AbstractLspAnalysisServerTest {
+ late final Uri otherFileUri;
+
+ /// Calls textDocument/prepareCallHierarchy at the location of `^` in
+ /// [mainContents] and expects a null result.
+ Future<void> expectNullResults(String mainContents) async {
+ await initialize();
+ await openFile(mainFileUri, withoutMarkers(mainContents));
+ final result = await prepareCallHierarchy(
+ mainFileUri,
+ positionFromMarker(mainContents),
+ );
+ expect(result, isNull);
+ }
+
+ /// Calls textDocument/prepareCallHierarchy at the location of `^` in
+ /// [mainContents] and ensures the results match [expectedResults].
+ Future<void> expectResults({
+ required String mainContents,
+ String? otherContents,
+ required CallHierarchyItem expectedResult,
+ }) async {
+ await initialize();
+ await openFile(mainFileUri, withoutMarkers(mainContents));
+
+ if (otherContents != null) {
+ await openFile(otherFileUri, withoutMarkers(otherContents));
+ }
+
+ final results = await prepareCallHierarchy(
+ mainFileUri,
+ positionFromMarker(mainContents),
+ );
+
+ expect(results, isNotNull);
+ expect(results!, hasLength(1));
+ expect(results.single, expectedResult);
+ }
+
+ @override
+ void setUp() {
+ super.setUp();
+ otherFileUri = Uri.file(join(projectFolderPath, 'lib', 'other.dart'));
+ }
+
+ Future<void> test_args() async {
+ await expectNullResults('main(int ^a) {}');
+ }
+
+ Future<void> test_block() async {
+ await expectNullResults('main() {^}');
+ }
+
+ Future<void> test_comment() async {
+ await expectNullResults('main() {} // this is a ^comment');
+ }
+
+ Future<void> test_constructor() async {
+ final contents = '''
+ class Foo {
+ [[Fo^o]](String a) {}
+ }
+ ''';
+
+ await expectResults(
+ mainContents: contents,
+ expectedResult: CallHierarchyItem(
+ name: 'Foo',
+ detail: 'Foo', // Containing class name
+ kind: SymbolKind.Constructor,
+ uri: mainFileUri.toString(),
+ range: rangeOfString(contents, 'Foo(String a) {}'),
+ selectionRange: rangeFromMarkers(contents)),
+ );
+ }
+
+ Future<void> test_constructorCall() async {
+ final contents = '''
+ import 'other.dart';
+
+ main() {
+ final foo = Fo^o();
+ }
+ ''';
+
+ final otherContents = '''
+ class Foo {
+ [[Foo]]();
+ }
+ ''';
+
+ await expectResults(
+ mainContents: contents,
+ otherContents: otherContents,
+ expectedResult: CallHierarchyItem(
+ name: 'Foo',
+ detail: 'Foo', // Containing class name
+ kind: SymbolKind.Constructor,
+ uri: otherFileUri.toString(),
+ range: rangeOfString(otherContents, 'Foo();'),
+ selectionRange: rangeFromMarkers(otherContents)));
+ }
+
+ Future<void> test_function() async {
+ final contents = '''
+ void myFun^ction() {}
+ ''';
+
+ await expectResults(
+ mainContents: contents,
+ expectedResult: CallHierarchyItem(
+ name: 'myFunction',
+ detail: 'main.dart', // Containing file name
+ kind: SymbolKind.Function,
+ uri: mainFileUri.toString(),
+ range: rangeOfString(contents, 'void myFunction() {}'),
+ selectionRange: rangeOfString(contents, 'myFunction')),
+ );
+ }
+
+ Future<void> test_functionCall() async {
+ final contents = '''
+ import 'other.dart' as f;
+
+ main() {
+ f.myFun^ction();
+ }
+ ''';
+
+ final otherContents = '''
+ void myFunction() {}
+ ''';
+
+ await expectResults(
+ mainContents: contents,
+ otherContents: otherContents,
+ expectedResult: CallHierarchyItem(
+ name: 'myFunction',
+ detail: 'other.dart', // Containing file name
+ kind: SymbolKind.Function,
+ uri: otherFileUri.toString(),
+ range: rangeOfString(otherContents, 'void myFunction() {}'),
+ selectionRange: rangeOfString(otherContents, 'myFunction')),
+ );
+ }
+
+ Future<void> test_implicitConstructorCall() async {
+ // Even if a constructor is implicit, we might want to be able to get the
+ // incoming calls, so invoking it here should still return an element
+ // (the class).
+ final contents = '''
+ import 'other.dart';
+
+ main() {
+ final foo = Fo^o();
+ }
+ ''';
+
+ final otherContents = '''
+ class Foo {}
+ ''';
+
+ await expectResults(
+ mainContents: contents,
+ otherContents: otherContents,
+ expectedResult: CallHierarchyItem(
+ name: 'Foo',
+ detail: 'Foo', // Containing class name
+ kind: SymbolKind.Constructor,
+ uri: otherFileUri.toString(),
+ range: rangeOfString(otherContents, 'class Foo {}'),
+ selectionRange: rangeOfString(otherContents, 'Foo')),
+ );
+ }
+
+ Future<void> test_method() async {
+ final contents = '''
+ class Foo {
+ void myMet^hod() {}
+ }
+ ''';
+
+ await expectResults(
+ mainContents: contents,
+ expectedResult: CallHierarchyItem(
+ name: 'myMethod',
+ detail: 'Foo', // Containing class name
+ kind: SymbolKind.Method,
+ uri: mainFileUri.toString(),
+ range: rangeOfString(contents, 'void myMethod() {}'),
+ selectionRange: rangeOfString(contents, 'myMethod')),
+ );
+ }
+
+ Future<void> test_methodCall() async {
+ final contents = '''
+ import 'other.dart';
+
+ main() {
+ Foo().myMet^hod();
+ }
+ ''';
+
+ final otherContents = '''
+ class Foo {
+ void myMethod() {}
+ }
+ ''';
+
+ await expectResults(
+ mainContents: contents,
+ otherContents: otherContents,
+ expectedResult: CallHierarchyItem(
+ name: 'myMethod',
+ detail: 'Foo', // Containing class name
+ kind: SymbolKind.Method,
+ uri: otherFileUri.toString(),
+ range: rangeOfString(otherContents, 'void myMethod() {}'),
+ selectionRange: rangeOfString(otherContents, 'myMethod')),
+ );
+ }
+
+ Future<void> test_namedConstructor() async {
+ final contents = '''
+ class Foo {
+ Foo.Ba^r(String a) {}
+ }
+ ''';
+
+ await expectResults(
+ mainContents: contents,
+ expectedResult: CallHierarchyItem(
+ name: 'Foo.Bar',
+ detail: 'Foo', // Containing class name
+ kind: SymbolKind.Constructor,
+ uri: mainFileUri.toString(),
+ range: rangeOfString(contents, 'Foo.Bar(String a) {}'),
+ selectionRange: rangeOfString(contents, 'Bar')),
+ );
+ }
+
+ Future<void> test_namedConstructorCall() async {
+ final contents = '''
+ import 'other.dart';
+
+ main() {
+ final foo = Foo.Ba^r();
+ }
+ ''';
+
+ final otherContents = '''
+ class Foo {
+ Foo.Bar();
+ }
+ ''';
+
+ await expectResults(
+ mainContents: contents,
+ otherContents: otherContents,
+ expectedResult: CallHierarchyItem(
+ name: 'Foo.Bar',
+ detail: 'Foo', // Containing class name
+ kind: SymbolKind.Constructor,
+ uri: otherFileUri.toString(),
+ range: rangeOfString(otherContents, 'Foo.Bar();'),
+ selectionRange: rangeOfString(otherContents, 'Bar')),
+ );
+ }
+
+ Future<void> test_whitespace() async {
+ await expectNullResults(' ^ main() {}');
+ }
+}
diff --git a/pkg/analysis_server/test/lsp/initialization_test.dart b/pkg/analysis_server/test/lsp/initialization_test.dart
index d304655..28bd71b 100644
--- a/pkg/analysis_server/test/lsp/initialization_test.dart
+++ b/pkg/analysis_server/test/lsp/initialization_test.dart
@@ -327,6 +327,7 @@
expect(options.change, equals(TextDocumentSyncKind.Incremental));
},
);
+ expect(initResult.capabilities.callHierarchyProvider, isNotNull);
expect(initResult.capabilities.completionProvider, isNotNull);
expect(initResult.capabilities.hoverProvider, isNotNull);
expect(initResult.capabilities.signatureHelpProvider, isNotNull);
@@ -387,6 +388,7 @@
// Ensure no static registrations. This list should include all server equivalents
// of the dynamic registrations listed in `ClientDynamicRegistrations.supported`.
expect(initResult.capabilities.textDocumentSync, isNull);
+ expect(initResult.capabilities.callHierarchyProvider, isNull);
expect(initResult.capabilities.completionProvider, isNull);
expect(initResult.capabilities.hoverProvider, isNull);
expect(initResult.capabilities.signatureHelpProvider, isNull);
diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart
index 9d9c88e..d9acebe 100644
--- a/pkg/analysis_server/test/lsp/server_abstract.dart
+++ b/pkg/analysis_server/test/lsp/server_abstract.dart
@@ -299,6 +299,7 @@
// the fields listed in `ClientDynamicRegistrations.supported`.
return extendTextDocumentCapabilities(source, {
'synchronization': {'dynamicRegistration': true},
+ 'callHierarchy': {'dynamicRegistration': true},
'completion': {'dynamicRegistration': true},
'hover': {'dynamicRegistration': true},
'signatureHelp': {'dynamicRegistration': true},
@@ -840,6 +841,26 @@
return newContent;
}
+ Future<List<CallHierarchyIncomingCall>?> callHierarchyIncoming(
+ CallHierarchyItem item) {
+ final request = makeRequest(
+ Method.callHierarchy_incomingCalls,
+ CallHierarchyIncomingCallsParams(item: item),
+ );
+ return expectSuccessfulResponseTo(
+ request, _fromJsonList(CallHierarchyIncomingCall.fromJson));
+ }
+
+ Future<List<CallHierarchyOutgoingCall>?> callHierarchyOutgoing(
+ CallHierarchyItem item) {
+ final request = makeRequest(
+ Method.callHierarchy_outgoingCalls,
+ CallHierarchyOutgoingCallsParams(item: item),
+ );
+ return expectSuccessfulResponseTo(
+ request, _fromJsonList(CallHierarchyOutgoingCall.fromJson));
+ }
+
Future changeFile(
int newVersion,
Uri uri,
@@ -878,6 +899,12 @@
await sendNotificationToServer(notification);
}
+ /// Gets the entire range for [code].
+ Range entireRange(String code) => Range(
+ start: startOfDocPos,
+ end: positionFromOffset(code.length, code),
+ );
+
Future<Object?> executeCodeAction(
Either2<Command, CodeAction> codeAction) async {
final command = codeAction.map(
@@ -1636,6 +1663,18 @@
return toPosition(lineInfo.getLocation(offset));
}
+ Future<List<CallHierarchyItem>?> prepareCallHierarchy(Uri uri, Position pos) {
+ final request = makeRequest(
+ Method.textDocument_prepareCallHierarchy,
+ CallHierarchyPrepareParams(
+ textDocument: TextDocumentIdentifier(uri: uri.toString()),
+ position: pos,
+ ),
+ );
+ return expectSuccessfulResponseTo(
+ request, _fromJsonList(CallHierarchyItem.fromJson));
+ }
+
Future<PlaceholderAndRange?> prepareRename(Uri uri, Position pos) {
final request = makeRequest(
Method.textDocument_prepareRename,
@@ -1694,18 +1733,20 @@
}
}
- /// Returns the range of [searchText] in [content].
- Range? rangeOfString(String content, String searchText) {
+ /// Returns the range of [pattern] in [content].
+ Range rangeOfPattern(String content, Pattern pattern) {
content = withoutMarkers(content);
- final startOffset = content.indexOf(searchText);
- return startOffset == -1
- ? null
- : Range(
- start: positionFromOffset(startOffset, content),
- end: positionFromOffset(startOffset + searchText.length, content),
- );
+ final match = pattern.allMatches(content).first;
+ return Range(
+ start: positionFromOffset(match.start, content),
+ end: positionFromOffset(match.end, content),
+ );
}
+ /// Returns the range of [searchText] in [content].
+ Range rangeOfString(String content, String searchText) =>
+ rangeOfPattern(content, searchText);
+
/// Returns all ranges surrounded by `[[markers]]` in the provided string,
/// excluding the markers themselves (as well as position markers `^` from
/// the offsets).
@@ -1743,6 +1784,18 @@
return rangesFromMarkersImpl(content).toList();
}
+ /// Gets the range in [content] that beings with the string [prefix] and
+ /// has a length matching [text].
+ Range rangeStartingAtString(String content, String prefix, String text) {
+ content = withoutMarkers(content);
+ final offset = content.indexOf(prefix);
+ final end = offset + text.length;
+ return Range(
+ start: positionFromOffset(offset, content),
+ end: positionFromOffset(end, content),
+ );
+ }
+
Future<WorkspaceEdit?> rename(
Uri uri,
int? version,
diff --git a/pkg/analysis_server/test/lsp/test_all.dart b/pkg/analysis_server/test/lsp/test_all.dart
index c563746..9bf0972 100644
--- a/pkg/analysis_server/test/lsp/test_all.dart
+++ b/pkg/analysis_server/test/lsp/test_all.dart
@@ -6,6 +6,7 @@
import '../src/lsp/lsp_packet_transformer_test.dart' as lsp_packet_transformer;
import 'analyzer_status_test.dart' as analyzer_status;
+import 'call_hierarchy_test.dart' as call_hierarchy;
import 'cancel_request_test.dart' as cancel_request;
import 'change_workspace_folders_test.dart' as change_workspace_folders;
import 'client_configuration_test.dart' as client_configuration;
@@ -50,6 +51,7 @@
void main() {
defineReflectiveSuite(() {
analyzer_status.main();
+ call_hierarchy.main();
cancel_request.main();
change_workspace_folders.main();
client_configuration.main();
diff --git a/pkg/analysis_server/tool/lsp_spec/README.md b/pkg/analysis_server/tool/lsp_spec/README.md
index 5776154..186d133 100644
--- a/pkg/analysis_server/tool/lsp_spec/README.md
+++ b/pkg/analysis_server/tool/lsp_spec/README.md
@@ -102,9 +102,9 @@
| textDocument/inlineValue | | | |
| textDocument/linkedEditingRange | | | |
| textDocument/moniker | | | |
-| textDocument/prepareCallHierarchy | | | |
-| callHierarchy/incomingCalls | | | |
-| callHierarchy/outgoingCalls | | | |
+| textDocument/prepareCallHierarchy | ✅ | | |
+| callHierarchy/incomingCalls | ✅ | | |
+| callHierarchy/outgoingCalls | ✅ | | |
| textDocument/prepareRename | ✅ | | |
| textDocument/rename | ✅ | | |
| textDocument/prepareTypeHierarchy | | | |