[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 | | | |