[analysis_server] Convert more LSP handlers to work over the legacy protocol

Change-Id: I9bd1f3ffd9bcdb017a4208e4aedcfb7436259fc8
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/318700
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
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
index 00f135c..121dbcf 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_call_hierarchy.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_call_hierarchy.dart
@@ -144,7 +144,7 @@
 /// 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 LspMessageHandler<
+class PrepareCallHierarchyHandler extends SharedMessageHandler<
     CallHierarchyPrepareParams,
     TextDocumentPrepareCallHierarchyResult> with _CallHierarchyUtils {
   PrepareCallHierarchyHandler(super.server);
@@ -222,7 +222,7 @@
 /// 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 LspMessageHandler<P, R> with _CallHierarchyUtils {
+    extends SharedMessageHandler<P, R> with _CallHierarchyUtils {
   _AbstractCallHierarchyCallsHandler(super.server);
 
   /// Gets the appropriate types of calls for this handler.
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_document_color.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_document_color.dart
index 4889101..b0918c4 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_document_color.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_document_color.dart
@@ -17,7 +17,7 @@
 /// to obtain the code to insert when a new color is selected (see
 /// [DocumentColorPresentationHandler]).
 class DocumentColorHandler
-    extends LspMessageHandler<DocumentColorParams, List<ColorInformation>> {
+    extends SharedMessageHandler<DocumentColorParams, List<ColorInformation>> {
   DocumentColorHandler(super.server);
   @override
   Method get handlesMessage => Method.textDocument_documentColor;
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_document_color_presentation.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_document_color_presentation.dart
index 69a2ca1..02863e0 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_document_color_presentation.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_document_color_presentation.dart
@@ -21,7 +21,7 @@
 /// using a color picker (in a location returned by textDocument/documentColor)
 /// and needs a representation of this color, including the edits to insert it
 /// into the source file.
-class DocumentColorPresentationHandler extends LspMessageHandler<
+class DocumentColorPresentationHandler extends SharedMessageHandler<
     ColorPresentationParams, List<ColorPresentation>> {
   DocumentColorPresentationHandler(super.server);
   @override
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_document_highlights.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_document_highlights.dart
index 9764dc2..11cc219 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_document_highlights.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_document_highlights.dart
@@ -8,7 +8,7 @@
 import 'package:analysis_server/src/lsp/handlers/handlers.dart';
 import 'package:analysis_server/src/lsp/mapping.dart';
 
-class DocumentHighlightsHandler extends LspMessageHandler<
+class DocumentHighlightsHandler extends SharedMessageHandler<
     TextDocumentPositionParams, List<DocumentHighlight>?> {
   DocumentHighlightsHandler(super.server);
   @override
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_format_on_type.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_format_on_type.dart
index 355005f..84f5e15 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_format_on_type.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_format_on_type.dart
@@ -8,8 +8,8 @@
 import 'package:analysis_server/src/lsp/mapping.dart';
 import 'package:analysis_server/src/lsp/source_edits.dart';
 
-class FormatOnTypeHandler
-    extends LspMessageHandler<DocumentOnTypeFormattingParams, List<TextEdit>?> {
+class FormatOnTypeHandler extends SharedMessageHandler<
+    DocumentOnTypeFormattingParams, List<TextEdit>?> {
   FormatOnTypeHandler(super.server);
   @override
   Method get handlesMessage => Method.textDocument_onTypeFormatting;
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_format_range.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_format_range.dart
index 83d04ef..1630647 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_format_range.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_format_range.dart
@@ -8,8 +8,8 @@
 import 'package:analysis_server/src/lsp/mapping.dart';
 import 'package:analysis_server/src/lsp/source_edits.dart';
 
-class FormatRangeHandler
-    extends LspMessageHandler<DocumentRangeFormattingParams, List<TextEdit>?> {
+class FormatRangeHandler extends SharedMessageHandler<
+    DocumentRangeFormattingParams, List<TextEdit>?> {
   FormatRangeHandler(super.server);
   @override
   Method get handlesMessage => Method.textDocument_rangeFormatting;
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_formatting.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_formatting.dart
index 1ec1ff9..57224d7 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_formatting.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_formatting.dart
@@ -9,7 +9,7 @@
 import 'package:analysis_server/src/lsp/source_edits.dart';
 
 class FormattingHandler
-    extends LspMessageHandler<DocumentFormattingParams, List<TextEdit>?> {
+    extends SharedMessageHandler<DocumentFormattingParams, List<TextEdit>?> {
   FormattingHandler(super.server);
   @override
   Method get handlesMessage => Method.textDocument_formatting;
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_select_range.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_selection_range.dart
similarity index 100%
rename from pkg/analysis_server/lib/src/lsp/handlers/handler_select_range.dart
rename to pkg/analysis_server/lib/src/lsp/handlers/handler_selection_range.dart
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 85962a4..4cc6807b 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart
@@ -33,7 +33,7 @@
 import 'package:analysis_server/src/lsp/handlers/handler_inlay_hint.dart';
 import 'package:analysis_server/src/lsp/handlers/handler_references.dart';
 import 'package:analysis_server/src/lsp/handlers/handler_rename.dart';
-import 'package:analysis_server/src/lsp/handlers/handler_select_range.dart';
+import 'package:analysis_server/src/lsp/handlers/handler_selection_range.dart';
 import 'package:analysis_server/src/lsp/handlers/handler_semantic_tokens.dart';
 import 'package:analysis_server/src/lsp/handlers/handler_shutdown.dart';
 import 'package:analysis_server/src/lsp/handlers/handler_signature_help.dart';
@@ -75,27 +75,18 @@
     TextDocumentCloseHandler.new,
     CompletionHandler.new,
     CompletionResolveHandler.new,
-    DocumentColorHandler.new,
-    DocumentColorPresentationHandler.new,
     SignatureHelpHandler.new,
     DefinitionHandler.new,
     TypeDefinitionHandler.new,
     SuperHandler.new,
     ReferencesHandler.new,
     ImplementationHandler.new,
-    FormattingHandler.new,
-    FormatOnTypeHandler.new,
-    FormatRangeHandler.new,
-    DocumentHighlightsHandler.new,
     DocumentSymbolHandler.new,
     CodeActionHandler.new,
     ExecuteCommandHandler.new,
     WorkspaceFoldersHandler.new,
     PrepareRenameHandler.new,
     RenameHandler.new,
-    PrepareCallHierarchyHandler.new,
-    IncomingCallHierarchyHandler.new,
-    OutgoingCallHierarchyHandler.new,
     PrepareTypeHierarchyHandler.new,
     TypeHierarchySubtypesHandler.new,
     TypeHierarchySupertypesHandler.new,
@@ -129,7 +120,16 @@
   /// Generators for handlers that work with any [AnalysisServer].
   static const sharedHandlerGenerators =
       <_RequestHandlerGenerator<AnalysisServer>>[
+    DocumentColorHandler.new,
+    DocumentColorPresentationHandler.new,
+    DocumentHighlightsHandler.new,
+    FormatOnTypeHandler.new,
+    FormatRangeHandler.new,
+    FormattingHandler.new,
     HoverHandler.new,
+    IncomingCallHierarchyHandler.new,
+    OutgoingCallHierarchyHandler.new,
+    PrepareCallHierarchyHandler.new,
   ];
 
   InitializedStateMessageHandler(
diff --git a/pkg/analysis_server/test/integration/lsp/handle_test.dart b/pkg/analysis_server/test/integration/lsp/handle_test.dart
index 1bbf751..1bd79b6 100644
--- a/pkg/analysis_server/test/integration/lsp/handle_test.dart
+++ b/pkg/analysis_server/test/integration/lsp/handle_test.dart
@@ -21,9 +21,22 @@
   });
 }
 
+/// Integration tests for using LSP over the Legacy protocol.
+///
+/// These tests are slow (each test spawns an out-of-process server) so these
+/// tests are intended only to ensure the basic functionality is available and
+/// not to test all handlers/functionality already are covered by LSP tests.
+///
+/// Additional tests (to verify each expected LSP handler is available over
+/// Legacy) are in `test/lsp_over_legacy/` and tests for all handler
+/// functionality are in `test/lsp`.
 @reflectiveTest
 class LspOverLegacyTest extends AbstractAnalysisServerIntegrationTest
     with LspRequestHelpersMixin {
+  late final testFile = sourcePath('lib/test.dart');
+
+  Uri get testFileUri => Uri.file(testFile);
+
   @override
   Future<T> expectSuccessfulResponseTo<T, R>(
     RequestMessage message,
@@ -63,20 +76,30 @@
   }
 
   Future<void> test_error_lspHandlerError() async {
-    // This file will not be created.
-    final testFile = sourcePath('lib/test.dart');
+    // testFile will not be created.
     await standardAnalysisSetup();
     await analysisFinished;
 
     await expectLater(
-      getHover(Uri.file(testFile), Position(character: 0, line: 0)),
+      getHover(testFileUri, Position(character: 0, line: 0)),
       throwsA(isResponseError(ServerErrorCodes.InvalidFilePath,
           message: 'File does not exist')),
     );
   }
 
+  Future<void> test_format() async {
+    const content = 'void     main() {}';
+    const expectedContent = 'void main() {}';
+    writeFile(testFile, content);
+    await standardAnalysisSetup();
+    await analysisFinished;
+
+    final edits = await formatDocument(testFileUri);
+    final formattedContents = applyTextEdits(content, edits!);
+    expect(formattedContents.trimRight(), equals(expectedContent));
+  }
+
   Future<void> test_hover() async {
-    final testFile = sourcePath('lib/test.dart');
     final code = TestCode.parse('''
 /// This is my class.
 class [!A^aa!] {}
@@ -86,7 +109,7 @@
     await standardAnalysisSetup();
     await analysisFinished;
 
-    final result = await getHover(Uri.file(testFile), code.position.position);
+    final result = await getHover(testFileUri, code.position.position);
 
     expect(result!.range, code.range.range);
     _expectMarkdown(
@@ -109,7 +132,6 @@
   /// in a way that is not abstracted by (or affected by refactors to) helper
   /// methods to ensure this never changes in a way that will affect clients.
   Future<void> test_hover_rawProtocol() async {
-    final testFile = sourcePath('lib/test.dart');
     final code = TestCode.parse('''
 /// This is my class.
 class [!A^aa!] {}
@@ -134,7 +156,7 @@
         'id': '12345',
         'method': Method.textDocument_hover.toString(),
         'params': {
-          "textDocument": {"uri": Uri.file(testFile).toString()},
+          "textDocument": {"uri": testFileUri.toString()},
           "position": code.position.position.toJson(),
         },
       }
diff --git a/pkg/analysis_server/test/lsp/change_verifier.dart b/pkg/analysis_server/test/lsp/change_verifier.dart
index 74947ef..bf44e2c 100644
--- a/pkg/analysis_server/test/lsp/change_verifier.dart
+++ b/pkg/analysis_server/test/lsp/change_verifier.dart
@@ -207,6 +207,38 @@
   }
 }
 
+/// An LSP TextEdit with its index, and a comparer to sort them in a way that
+/// can be applied sequentially while preserving expected behaviour.
+class TextEditWithIndex {
+  final int index;
+  final TextEdit edit;
+
+  TextEditWithIndex(this.index, this.edit);
+
+  TextEditWithIndex.fromUnion(
+      this.index, Either3<AnnotatedTextEdit, SnippetTextEdit, TextEdit> edit)
+      : edit = edit.map((e) => e, (e) => e, (e) => e);
+
+  /// Compares two [TextEditWithIndex] to sort them by the order in which they
+  /// can be sequentially applied to a String to match the behaviour of an LSP
+  /// client.
+  static int compare(TextEditWithIndex edit1, TextEditWithIndex edit2) {
+    final end1 = edit1.edit.range.end;
+    final end2 = edit2.edit.range.end;
+
+    // VS Code's implementation of this is here:
+    // https://github.com/microsoft/vscode/blob/856a306d1a9b0879727421daf21a8059e671e3ea/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts#L475
+
+    if (end1.line != end2.line) {
+      return end1.line.compareTo(end2.line) * -1;
+    } else if (end1.character != end2.character) {
+      return end1.character.compareTo(end2.character) * -1;
+    } else {
+      return edit1.index.compareTo(edit2.index) * -1;
+    }
+  }
+}
+
 class _Change {
   String? content;
   final actions = <String>[];
diff --git a/pkg/analysis_server/test/lsp/request_helpers_mixin.dart b/pkg/analysis_server/test/lsp/request_helpers_mixin.dart
index 8ac6ea2..5929d05 100644
--- a/pkg/analysis_server/test/lsp/request_helpers_mixin.dart
+++ b/pkg/analysis_server/test/lsp/request_helpers_mixin.dart
@@ -7,9 +7,14 @@
 import 'package:analysis_server/lsp_protocol/protocol.dart';
 import 'package:analysis_server/src/lsp/constants.dart';
 import 'package:analysis_server/src/lsp/json_parsing.dart';
+import 'package:analysis_server/src/lsp/mapping.dart';
+import 'package:analyzer/source/line_info.dart';
+import 'package:collection/collection.dart';
 import 'package:test/test.dart' hide expect;
 import 'package:test/test.dart' as test show expect;
 
+import 'change_verifier.dart';
+
 /// Helpers to simplify building LSP requests for use in tests.
 ///
 /// The actual sending of requests must be supplied by the implementing class
@@ -21,9 +26,74 @@
 mixin LspRequestHelpersMixin {
   int _id = 0;
 
+  final startOfDocPos = Position(line: 0, character: 0);
+
+  final startOfDocRange = Range(
+      start: Position(line: 0, character: 0),
+      end: Position(line: 0, character: 0));
+
   /// Whether to include 'clientRequestTime' fields in outgoing messages.
   bool includeClientRequestTime = false;
 
+  String applyTextEdit(String content, TextEdit edit) {
+    final startPos = edit.range.start;
+    final endPos = edit.range.end;
+    final lineInfo = LineInfo.fromContent(content);
+    final start = lineInfo.getOffsetOfLine(startPos.line) + startPos.character;
+    final end = lineInfo.getOffsetOfLine(endPos.line) + endPos.character;
+    return content.replaceRange(start, end, edit.newText);
+  }
+
+  String applyTextEdits(String content, List<TextEdit> changes) {
+    // Complex text manipulations are described with an array of TextEdit's,
+    // representing a single change to the document.
+    //
+    // All text edits ranges refer to positions in the original document. Text
+    // edits ranges must never overlap, that means no part of the original
+    // document must be manipulated by more than one edit. It is possible
+    // that multiple edits have the same start position (eg. multiple inserts in
+    // reverse order), however since that involves complicated tracking and we
+    // only apply edits here sequentially, we don't supported them. We do sort
+    // edits to ensure we apply the later ones first, so we can assume the locations
+    // in the edit are still valid against the new string as each edit is applied.
+
+    /// Ensures changes are simple enough to apply easily without any complicated
+    /// logic.
+    void validateChangesCanBeApplied() {
+      /// Check if a position is before (but not equal) to another position.
+      bool isBeforeOrEqual(Position p, Position other) =>
+          p.line < other.line ||
+          (p.line == other.line && p.character <= other.character);
+
+      /// Check if a position is after (but not equal) to another position.
+      bool isAfterOrEqual(Position p, Position other) =>
+          p.line > other.line ||
+          (p.line == other.line && p.character >= other.character);
+      // Check if two ranges intersect.
+      bool rangesIntersect(Range r1, Range r2) {
+        var endsBefore = isBeforeOrEqual(r1.end, r2.start);
+        var startsAfter = isAfterOrEqual(r1.start, r2.end);
+        return !(endsBefore || startsAfter);
+      }
+
+      for (final change1 in changes) {
+        for (final change2 in changes) {
+          if (change1 != change2 &&
+              rangesIntersect(change1.range, change2.range)) {
+            throw 'Test helper applyTextEdits does not support applying multiple edits '
+                'where the edits are not in reverse order.';
+          }
+        }
+      }
+    }
+
+    validateChangesCanBeApplied();
+
+    final indexedEdits = changes.mapIndexed(TextEditWithIndex.new).toList();
+    indexedEdits.sort(TextEditWithIndex.compare);
+    return indexedEdits.map((e) => e.edit).fold(content, applyTextEdit);
+  }
+
   Future<List<CallHierarchyIncomingCall>?> callHierarchyIncoming(
       CallHierarchyItem item) {
     final request = makeRequest(
@@ -44,6 +114,12 @@
         request, _fromJsonList(CallHierarchyOutgoingCall.fromJson));
   }
 
+  /// Gets the entire range for [code].
+  Range entireRange(String code) => Range(
+        start: startOfDocPos,
+        end: positionFromOffset(code.length, code),
+      );
+
   void expect(Object? actual, Matcher matcher, {String? reason}) =>
       test.expect(actual, matcher, reason: reason);
 
@@ -471,6 +547,11 @@
     );
   }
 
+  Position positionFromOffset(int offset, String contents) {
+    final lineInfo = LineInfo.fromContent(contents);
+    return toPosition(lineInfo.getLocation(offset));
+  }
+
   Future<List<CallHierarchyItem>?> prepareCallHierarchy(Uri uri, Position pos) {
     final request = makeRequest(
       Method.textDocument_prepareCallHierarchy,
diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart
index 43b46e0..e1190b5 100644
--- a/pkg/analysis_server/test/lsp/server_abstract.dart
+++ b/pkg/analysis_server/test/lsp/server_abstract.dart
@@ -10,13 +10,11 @@
 import 'package:analysis_server/src/lsp/constants.dart';
 import 'package:analysis_server/src/lsp/json_parsing.dart';
 import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
-import 'package:analysis_server/src/lsp/mapping.dart';
 import 'package:analysis_server/src/plugin/plugin_manager.dart';
 import 'package:analysis_server/src/server/crash_reporting_attachments.dart';
 import 'package:analysis_server/src/services/user_prompts/dart_fix_prompt_manager.dart';
 import 'package:analysis_server/src/utilities/mocks.dart';
 import 'package:analyzer/instrumentation/instrumentation.dart';
-import 'package:analyzer/source/line_info.dart';
 import 'package:analyzer/src/dart/analysis/experiments.dart';
 import 'package:analyzer/src/generated/sdk.dart';
 import 'package:analyzer/src/test_utilities/mock_sdk.dart';
@@ -915,10 +913,6 @@
       analysisOptionsPath;
   late Uri projectFolderUri, mainFileUri, pubspecFileUri, analysisOptionsUri;
   final String simplePubspecContent = 'name: my_project';
-  final startOfDocPos = Position(line: 0, character: 0);
-  final startOfDocRange = Range(
-      start: Position(line: 0, character: 0),
-      end: Position(line: 0, character: 0));
 
   /// The client capabilities sent to the server during initialization.
   ///
@@ -968,65 +962,6 @@
 
   Stream<Message> get serverToClient;
 
-  String applyTextEdit(String content, TextEdit edit) {
-    final startPos = edit.range.start;
-    final endPos = edit.range.end;
-    final lineInfo = LineInfo.fromContent(content);
-    final start = lineInfo.getOffsetOfLine(startPos.line) + startPos.character;
-    final end = lineInfo.getOffsetOfLine(endPos.line) + endPos.character;
-    return content.replaceRange(start, end, edit.newText);
-  }
-
-  String applyTextEdits(String content, List<TextEdit> changes) {
-    // Complex text manipulations are described with an array of TextEdit's,
-    // representing a single change to the document.
-    //
-    // All text edits ranges refer to positions in the original document. Text
-    // edits ranges must never overlap, that means no part of the original
-    // document must be manipulated by more than one edit. It is possible
-    // that multiple edits have the same start position (eg. multiple inserts in
-    // reverse order), however since that involves complicated tracking and we
-    // only apply edits here sequentially, we don't supported them. We do sort
-    // edits to ensure we apply the later ones first, so we can assume the locations
-    // in the edit are still valid against the new string as each edit is applied.
-
-    /// Ensures changes are simple enough to apply easily without any complicated
-    /// logic.
-    void validateChangesCanBeApplied() {
-      /// Check if a position is before (but not equal) to another position.
-      bool isBeforeOrEqual(Position p, Position other) =>
-          p.line < other.line ||
-          (p.line == other.line && p.character <= other.character);
-
-      /// Check if a position is after (but not equal) to another position.
-      bool isAfterOrEqual(Position p, Position other) =>
-          p.line > other.line ||
-          (p.line == other.line && p.character >= other.character);
-      // Check if two ranges intersect.
-      bool rangesIntersect(Range r1, Range r2) {
-        var endsBefore = isBeforeOrEqual(r1.end, r2.start);
-        var startsAfter = isAfterOrEqual(r1.start, r2.end);
-        return !(endsBefore || startsAfter);
-      }
-
-      for (final change1 in changes) {
-        for (final change2 in changes) {
-          if (change1 != change2 &&
-              rangesIntersect(change1.range, change2.range)) {
-            throw 'Test helper applyTextEdits does not support applying multiple edits '
-                'where the edits are not in reverse order.';
-          }
-        }
-      }
-    }
-
-    validateChangesCanBeApplied();
-
-    final indexedEdits = changes.mapIndexed(TextEditWithIndex.new).toList();
-    indexedEdits.sort(TextEditWithIndex.compare);
-    return indexedEdits.map((e) => e.edit).fold(content, applyTextEdit);
-  }
-
   Future<void> changeFile(
     int newVersion,
     Uri uri,
@@ -1066,12 +1001,6 @@
     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(
@@ -1404,9 +1333,9 @@
   Position positionFromMarker(String contents) =>
       positionFromOffset(withoutRangeMarkers(contents).indexOf('^'), contents);
 
+  @override
   Position positionFromOffset(int offset, String contents) {
-    final lineInfo = LineInfo.fromContent(withoutMarkers(contents));
-    return toPosition(lineInfo.getLocation(offset));
+    return super.positionFromOffset(offset, withoutMarkers(contents));
   }
 
   /// Calls the supplied function and responds to any `workspace/configuration`
@@ -1807,33 +1736,3 @@
         notification.method == Method.window_showMessage;
   }
 }
-
-class TextEditWithIndex {
-  final int index;
-  final TextEdit edit;
-
-  TextEditWithIndex(this.index, this.edit);
-
-  TextEditWithIndex.fromUnion(
-      this.index, Either3<AnnotatedTextEdit, SnippetTextEdit, TextEdit> edit)
-      : edit = edit.map((e) => e, (e) => e, (e) => e);
-
-  /// Compares two [TextEditWithIndex] to sort them by the order in which they
-  /// can be sequentially applied to a String to match the behaviour of an LSP
-  /// client.
-  static int compare(TextEditWithIndex edit1, TextEditWithIndex edit2) {
-    final end1 = edit1.edit.range.end;
-    final end2 = edit2.edit.range.end;
-
-    // VS Code's implementation of this is here:
-    // https://github.com/microsoft/vscode/blob/856a306d1a9b0879727421daf21a8059e671e3ea/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts#L475
-
-    if (end1.line != end2.line) {
-      return end1.line.compareTo(end2.line) * -1;
-    } else if (end1.character != end2.character) {
-      return end1.character.compareTo(end2.character) * -1;
-    } else {
-      return edit1.index.compareTo(edit2.index) * -1;
-    }
-  }
-}
diff --git a/pkg/analysis_server/test/lsp_over_legacy/call_hierarchy_test.dart b/pkg/analysis_server/test/lsp_over_legacy/call_hierarchy_test.dart
new file mode 100644
index 0000000..c122ae2
--- /dev/null
+++ b/pkg/analysis_server/test/lsp_over_legacy/call_hierarchy_test.dart
@@ -0,0 +1,74 @@
+// Copyright (c) 2023, 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 'package:analyzer/src/test_utilities/test_code_format.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../utils/test_code_extensions.dart';
+import 'abstract_lsp_over_legacy.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(CallHierarchyTest);
+  });
+}
+
+@reflectiveTest
+class CallHierarchyTest extends LspOverLegacyTest {
+  Future<void> test_incoming() async {
+    final content = '''
+String f^() => 'f';
+String g() => [!f!]();
+''';
+    final code = TestCode.parse(content);
+    newFile(testFilePath, code.code);
+    final prepareResults = await prepareCallHierarchy(
+      testFileUri,
+      code.position.position,
+    );
+    final prepareResult = prepareResults!.single;
+
+    final incomingResults = await callHierarchyIncoming(prepareResult);
+    final incomingResult = incomingResults!.single;
+
+    expect(incomingResult.from.name, 'g');
+    expect(incomingResult.fromRanges, code.ranges.ranges);
+  }
+
+  Future<void> test_outgoing() async {
+    final content = '''
+String f() => 'f';
+String g^() => [!f!]();
+''';
+    final code = TestCode.parse(content);
+    newFile(testFilePath, code.code);
+    final prepareResults = await prepareCallHierarchy(
+      testFileUri,
+      code.position.position,
+    );
+    final prepareResult = prepareResults!.single;
+
+    final outgoingResults = await callHierarchyOutgoing(prepareResult);
+    final outgoingResult = outgoingResults!.single;
+
+    expect(outgoingResult.to.name, 'f');
+    expect(outgoingResult.fromRanges, code.ranges.ranges);
+  }
+
+  Future<void> test_prepare() async {
+    final content = '''
+void f^() {}
+''';
+    final code = TestCode.parse(content);
+    newFile(testFilePath, code.code);
+    final results = await prepareCallHierarchy(
+      testFileUri,
+      code.position.position,
+    );
+    final result = results!.single;
+
+    expect(result.name, 'f');
+  }
+}
diff --git a/pkg/analysis_server/test/lsp_over_legacy/document_color_test.dart b/pkg/analysis_server/test/lsp_over_legacy/document_color_test.dart
new file mode 100644
index 0000000..df0a41d
--- /dev/null
+++ b/pkg/analysis_server/test/lsp_over_legacy/document_color_test.dart
@@ -0,0 +1,66 @@
+// Copyright (c) 2023, 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 'package:analyzer/src/test_utilities/test_code_format.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../utils/test_code_extensions.dart';
+import 'abstract_lsp_over_legacy.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(DocumentColorTest);
+  });
+}
+
+@reflectiveTest
+class DocumentColorTest extends LspOverLegacyTest {
+  @override
+  Future<void> setUp() async {
+    await super.setUp();
+    writeTestPackageConfig(flutter: true);
+  }
+
+  Future<void> test_color() async {
+    final content = '''
+import 'package:flutter/material.dart';
+
+const red = [!Colors.red!];
+''';
+    final code = TestCode.parse(content);
+    newFile(testFilePath, code.code);
+    final results = await getDocumentColors(testFileUri);
+    final result = results.single;
+
+    expect(result.color.alpha, 1);
+    expect(result.color.red, 1);
+    expect(result.color.green, 0);
+    expect(result.color.blue, 0);
+    expect(result.range, code.range.range);
+  }
+
+  Future<void> test_presentation() async {
+    final content = '''
+import 'package:flutter/material.dart';
+
+const red = [!Colors.red!];
+''';
+    final code = TestCode.parse(content);
+    newFile(testFilePath, code.code);
+    final colorResults = await getDocumentColors(testFileUri);
+    final colorResult = colorResults.single;
+
+    final colors = await getColorPresentation(
+        testFileUri, code.range.range, colorResult.color);
+    expect(
+      colors.map((c) => c.label),
+      containsAll([
+        'Color.fromARGB(255, 255, 0, 0)',
+        'Color.fromRGBO(255, 0, 0, 1.0)',
+        'Color(0xFFFF0000)',
+      ]),
+    );
+  }
+}
diff --git a/pkg/analysis_server/test/lsp_over_legacy/document_highlights_test.dart b/pkg/analysis_server/test/lsp_over_legacy/document_highlights_test.dart
new file mode 100644
index 0000000..e8454151
--- /dev/null
+++ b/pkg/analysis_server/test/lsp_over_legacy/document_highlights_test.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2023, 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 'package:analyzer/src/test_utilities/test_code_format.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../utils/test_code_extensions.dart';
+import 'abstract_lsp_over_legacy.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(DocumentHighlightsTest);
+  });
+}
+
+@reflectiveTest
+class DocumentHighlightsTest extends LspOverLegacyTest {
+  Future<void> test_highlights() async {
+    final content = '''
+var ^a = '';
+void f() {
+  a = '';
+  print(a);
+}
+''';
+    final code = TestCode.parse(content);
+    newFile(testFilePath, code.code);
+    final results =
+        await getDocumentHighlights(testFileUri, code.position.position);
+    expect(results, hasLength(3));
+  }
+}
diff --git a/pkg/analysis_server/test/lsp_over_legacy/format_test.dart b/pkg/analysis_server/test/lsp_over_legacy/format_test.dart
new file mode 100644
index 0000000..aa534dc0
--- /dev/null
+++ b/pkg/analysis_server/test/lsp_over_legacy/format_test.dart
@@ -0,0 +1,50 @@
+// Copyright (c) 2023, 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 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'abstract_lsp_over_legacy.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(FormatTest);
+  });
+}
+
+@reflectiveTest
+class FormatTest extends LspOverLegacyTest {
+  Future<void> test_format() async {
+    const content = 'void     main() {}';
+    const expectedContent = 'void main() {}';
+    newFile(testFilePath, content);
+    await waitForTasksFinished();
+
+    final edits = await formatDocument(testFileUri);
+    final formattedContents = applyTextEdits(content, edits!);
+    expect(formattedContents.trimRight(), equals(expectedContent));
+  }
+
+  Future<void> test_formatOnType() async {
+    const content = 'void     main() {}';
+    const expectedContent = 'void main() {}';
+    newFile(testFilePath, content);
+    await waitForTasksFinished();
+
+    final edits = await formatOnType(testFileUri, startOfDocPos, '}');
+    final formattedContents = applyTextEdits(content, edits!);
+    expect(formattedContents.trimRight(), equals(expectedContent));
+  }
+
+  Future<void> test_formatRange() async {
+    const content = 'void     main() {}';
+    const expectedContent = 'void main() {}';
+    newFile(testFilePath, content);
+    await waitForTasksFinished();
+
+    final edits = await formatRange(testFileUri, entireRange(content));
+    final formattedContents = applyTextEdits(content, edits!);
+    expect(formattedContents.trimRight(), equals(expectedContent));
+  }
+}
diff --git a/pkg/analysis_server/test/lsp_over_legacy/test_all.dart b/pkg/analysis_server/test/lsp_over_legacy/test_all.dart
index a398398..4a320c7 100644
--- a/pkg/analysis_server/test/lsp_over_legacy/test_all.dart
+++ b/pkg/analysis_server/test/lsp_over_legacy/test_all.dart
@@ -4,10 +4,18 @@
 
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
+import 'call_hierarchy_test.dart' as call_hierarchy;
+import 'document_color_test.dart' as document_color;
+import 'document_highlights_test.dart' as document_highlights;
+import 'format_test.dart' as format;
 import 'hover_test.dart' as hover;
 
 void main() {
   defineReflectiveSuite(() {
+    call_hierarchy.main();
+    document_color.main;
+    document_highlights.main();
+    format.main();
     hover.main();
   }, name: 'lsp_over_legacy');
 }
diff --git a/pkg/analysis_server/test/utils/test_code_extensions.dart b/pkg/analysis_server/test/utils/test_code_extensions.dart
index 799307b..6948351 100644
--- a/pkg/analysis_server/test/utils/test_code_extensions.dart
+++ b/pkg/analysis_server/test/utils/test_code_extensions.dart
@@ -7,6 +7,20 @@
 import 'package:analysis_server/src/lsp/mapping.dart';
 import 'package:analyzer/src/test_utilities/test_code_format.dart';
 
+extension ListTestCodePositionExtension on List<TestCodePosition> {
+  /// Return the LSP [Position]s of the markers.
+  ///
+  /// Positions are based on [TestCode.code], with all parsed markers removed.
+  List<Position> get positions => map((position) => position.position).toList();
+}
+
+extension ListTestCodeRangeExtension on List<TestCodeRange> {
+  /// The LSP [Range]s indicated by the markers.
+  ///
+  /// Ranges are based on [TestCode.code], with all parsed markers removed.
+  List<Range> get ranges => map((range) => range.range).toList();
+}
+
 extension TestCodePositionExtension on TestCodePosition {
   /// Return the LSP [Position] of the marker.
   ///