فهرست منبع

Language Server - implement incremental document sync (#4926)

The language server is now able to subscribe to and apply incremental
changes to the document. Source code is assumed to be utf-8. It does not
currently handle utf-16 code points.

---------

Co-authored-by: Carbon Infra Bot <carbon-external-infra@google.com>
Co-authored-by: Jon Ross-Perkins <jperkins@google.com>
Nirmal Patel 1 سال پیش
والد
کامیت
a881e21432

+ 0 - 1
toolchain/diagnostics/diagnostic_kind.def

@@ -458,7 +458,6 @@ CARBON_DIAGNOSTIC_KIND(LanguageServerUnsupportedNotification)
 
 
 // Document handling.
 // Document handling.
 CARBON_DIAGNOSTIC_KIND(LanguageServerOpenDuplicateFile)
 CARBON_DIAGNOSTIC_KIND(LanguageServerOpenDuplicateFile)
-CARBON_DIAGNOSTIC_KIND(LanguageServerUnsupportedChanges)
 CARBON_DIAGNOSTIC_KIND(LanguageServerCloseUnknownFile)
 CARBON_DIAGNOSTIC_KIND(LanguageServerCloseUnknownFile)
 CARBON_DIAGNOSTIC_KIND(LanguageServerDiagnosticInWrongFile)
 CARBON_DIAGNOSTIC_KIND(LanguageServerDiagnosticInWrongFile)
 
 

+ 2 - 0
toolchain/language_server/context.h

@@ -33,6 +33,8 @@ class Context {
     auto SetText(Context& context, std::optional<int64_t> version,
     auto SetText(Context& context, std::optional<int64_t> version,
                  llvm::StringRef text) -> void;
                  llvm::StringRef text) -> void;
 
 
+    auto text() const -> llvm::StringRef { return source_->text(); }
+
     auto tree_and_subtrees() const -> const Parse::TreeAndSubtrees& {
     auto tree_and_subtrees() const -> const Parse::TreeAndSubtrees& {
       return *tree_and_subtrees_;
       return *tree_and_subtrees_;
     }
     }

+ 1 - 1
toolchain/language_server/handle_initialize.cpp

@@ -12,7 +12,7 @@ auto HandleInitialize(
     llvm::function_ref<auto(llvm::Expected<llvm::json::Object>)->void> on_done)
     llvm::function_ref<auto(llvm::Expected<llvm::json::Object>)->void> on_done)
     -> void {
     -> void {
   llvm::json::Object capabilities{{"documentSymbolProvider", true},
   llvm::json::Object capabilities{{"documentSymbolProvider", true},
-                                  {"textDocumentSync", /*Full=*/1}};
+                                  {"textDocumentSync", /*Incremental=*/2}};
   llvm::json::Object reply{{"capabilities", std::move(capabilities)}};
   llvm::json::Object reply{{"capabilities", std::move(capabilities)}};
   on_done(reply);
   on_done(reply);
 }
 }

+ 73 - 10
toolchain/language_server/handle_text_document.cpp

@@ -26,6 +26,74 @@ auto HandleDidOpenTextDocument(
   }
   }
 }
 }
 
 
+// Takes start and end positions and returns a tuple with start and end
+// offsets. Positions are based on row and column numbers in the source
+// code. We often need to know the offsets when modifying strings, so
+// this function helps us calculate the offsets. It assumes that the start
+// position comes before the end position.
+static auto PositionToIndex(const std::string& contents,
+                            const clang::clangd::Position& start,
+                            const clang::clangd::Position& end)
+    -> std::tuple<size_t, size_t> {
+  size_t start_index = 0;
+  size_t end_index = 0;
+
+  for (auto line_number : llvm::seq(end.line)) {
+    const size_t newline_index = contents.find('\n', end_index);
+
+    CARBON_CHECK(newline_index != std::string::npos,
+                 "Line number greater than number of lines in the file");
+
+    end_index = newline_index + 1;
+
+    // This condition won't be met if start.line == end.line
+    // so we need to also check this outside the loop.
+    if (line_number == start.line) {
+      start_index = end_index;
+    }
+  }
+
+  if (start.line == end.line) {
+    start_index = end_index;
+  }
+
+  start_index += start.character;
+  end_index += end.character;
+
+  CARBON_CHECK(end_index <= contents.size(),
+               "Position greater than source code size");
+
+  return {start_index, end_index};
+}
+
+// LSP allows full and incremental document synchronization. It sends the
+// entire source code when doing full sync. However, when doing incremental
+// sync, it only sends the list of changes to be performed on the source
+// code. It is necessary to apply changes in the order in which they are
+// received. These changes can have one of 1) full document or 2) start
+// position, end position, and the text which replaces original text between
+// these start and end positions. If the range property is absent, then we
+// do full sync. Otherwise, we calculate start and end offsets and replace
+// the contents between them with the text we received in change.text.
+static auto ApplyChanges(
+    std::string& source,
+    const std::vector<clang::clangd::TextDocumentContentChangeEvent>&
+        content_changes) -> void {
+  for (const auto& change : content_changes) {
+    // If range is not present, then we replace entire text.
+    if (!change.range) {
+      source = change.text;
+      continue;
+    }
+
+    // TODO: Use lexer line number data but avoid re-lexing multiple times.
+    auto [start_index, end_index] =
+        PositionToIndex(source, change.range->start, change.range->end);
+
+    source.replace(start_index, end_index - start_index, change.text);
+  }
+}
+
 auto HandleDidChangeTextDocument(
 auto HandleDidChangeTextDocument(
     Context& context, const clang::clangd::DidChangeTextDocumentParams& params)
     Context& context, const clang::clangd::DidChangeTextDocumentParams& params)
     -> void {
     -> void {
@@ -35,17 +103,12 @@ auto HandleDidChangeTextDocument(
     return;
     return;
   }
   }
 
 
-  // Full text is sent if full sync is specified in capabilities.
-  if (params.contentChanges.size() != 1) {
-    CARBON_DIAGNOSTIC(LanguageServerUnsupportedChanges, Warning,
-                      "received unsupported contentChanges count: {0}", int);
-    context.file_emitter().Emit(filename, LanguageServerUnsupportedChanges,
-                                params.contentChanges.size());
-    return;
-  }
   if (auto* file = context.LookupFile(filename)) {
   if (auto* file = context.LookupFile(filename)) {
-    file->SetText(context, params.textDocument.version,
-                  params.contentChanges[0].text);
+    // We copy the document to a new string, apply changes to the string, and
+    // set the string as the new text.
+    std::string source = file->text().str();
+    ApplyChanges(source, params.contentChanges);
+    file->SetText(context, params.textDocument.version, source);
   }
   }
 }
 }
 
 

+ 1 - 1
toolchain/language_server/testdata/basics/fail_shutdown_without_exit.carbon

@@ -23,7 +23,7 @@
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:       "documentSymbolProvider": true,
 // CHECK:STDOUT:       "documentSymbolProvider": true,
-// CHECK:STDOUT:       "textDocumentSync": 1
+// CHECK:STDOUT:       "textDocumentSync": 2
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }Content-Length: 51{{\r}}
 // CHECK:STDOUT: }Content-Length: 51{{\r}}

+ 1 - 1
toolchain/language_server/testdata/basics/initialize.carbon

@@ -23,7 +23,7 @@
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:       "documentSymbolProvider": true,
 // CHECK:STDOUT:       "documentSymbolProvider": true,
-// CHECK:STDOUT:       "textDocumentSync": 1
+// CHECK:STDOUT:       "textDocumentSync": 2
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }Content-Length: 51{{\r}}
 // CHECK:STDOUT: }Content-Length: 51{{\r}}

+ 1 - 1
toolchain/language_server/testdata/basics/notify_parse_error.carbon

@@ -27,7 +27,7 @@
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:       "documentSymbolProvider": true,
 // CHECK:STDOUT:       "documentSymbolProvider": true,
-// CHECK:STDOUT:       "textDocumentSync": 1
+// CHECK:STDOUT:       "textDocumentSync": 2
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }Content-Length: 51{{\r}}
 // CHECK:STDOUT: }Content-Length: 51{{\r}}

+ 1 - 1
toolchain/language_server/testdata/document_symbol/basics.carbon

@@ -44,7 +44,7 @@
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:       "documentSymbolProvider": true,
 // CHECK:STDOUT:       "documentSymbolProvider": true,
-// CHECK:STDOUT:       "textDocumentSync": 1
+// CHECK:STDOUT:       "textDocumentSync": 2
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }Content-Length: 145{{\r}}
 // CHECK:STDOUT: }Content-Length: 145{{\r}}

+ 1 - 1
toolchain/language_server/testdata/document_symbol/language.carbon

@@ -28,7 +28,7 @@
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:       "documentSymbolProvider": true,
 // CHECK:STDOUT:       "documentSymbolProvider": true,
-// CHECK:STDOUT:       "textDocumentSync": 1
+// CHECK:STDOUT:       "textDocumentSync": 2
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }Content-Length: 51{{\r}}
 // CHECK:STDOUT: }Content-Length: 51{{\r}}

+ 1 - 1
toolchain/language_server/testdata/document_symbol/unknown.carbon

@@ -28,7 +28,7 @@
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:       "documentSymbolProvider": true,
 // CHECK:STDOUT:       "documentSymbolProvider": true,
-// CHECK:STDOUT:       "textDocumentSync": 1
+// CHECK:STDOUT:       "textDocumentSync": 2
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }Content-Length: 51{{\r}}
 // CHECK:STDOUT: }Content-Length: 51{{\r}}

+ 0 - 67
toolchain/language_server/testdata/text_document/change_count.carbon

@@ -1,67 +0,0 @@
-// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
-// Exceptions. See /LICENSE for license information.
-// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-//
-// AUTOUPDATE
-// TIP: To test this file alone, run:
-// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/language_server/testdata/text_document/change_count.carbon
-// TIP: To dump output, run:
-// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/text_document/change_count.carbon
-
-// --- STDIN
-[[@LSP-CALL:initialize]]
-[[@LSP-NOTIFY:textDocument/didOpen:
-  "textDocument": {"uri": "file:/test.carbon", "languageId": "carbon",
-                   "text": "// Empty"}
-]]
-[[@LSP-NOTIFY:textDocument/didChange:
-  "textDocument": {"uri": "file:/test.carbon"},
-  "contentChanges": []
-]]
-[[@LSP-NOTIFY:textDocument/didChange:
-  "textDocument": {"uri": "file:/test.carbon"},
-  "contentChanges": [
-    {"range": {"start": {"line": 5, "character": 23 },
-               "end": {"line": 6, "character": 0 }},
-     "text": "a"},
-    {"range": {"start": {"line": 5, "character": 23 },
-               "end": {"line": 6, "character": 0 }},
-     "text": "a"}
-  ]
-]]
-[[@LSP-CALL:shutdown]]
-[[@LSP-NOTIFY:exit]]
-
-// --- AUTOUPDATE-SPLIT
-
-// CHECK:STDERR: /test.carbon: warning: received unsupported contentChanges count: 0 [LanguageServerUnsupportedChanges]
-// CHECK:STDERR:
-// CHECK:STDERR: /test.carbon: warning: received unsupported contentChanges count: 2 [LanguageServerUnsupportedChanges]
-// CHECK:STDERR:
-// CHECK:STDOUT: Content-Length: 146{{\r}}
-// CHECK:STDOUT: {{\r}}
-// CHECK:STDOUT: {
-// CHECK:STDOUT:   "id": 1,
-// CHECK:STDOUT:   "jsonrpc": "2.0",
-// CHECK:STDOUT:   "result": {
-// CHECK:STDOUT:     "capabilities": {
-// CHECK:STDOUT:       "documentSymbolProvider": true,
-// CHECK:STDOUT:       "textDocumentSync": 1
-// CHECK:STDOUT:     }
-// CHECK:STDOUT:   }
-// CHECK:STDOUT: }Content-Length: 144{{\r}}
-// CHECK:STDOUT: {{\r}}
-// CHECK:STDOUT: {
-// CHECK:STDOUT:   "jsonrpc": "2.0",
-// CHECK:STDOUT:   "method": "textDocument/publishDiagnostics",
-// CHECK:STDOUT:   "params": {
-// CHECK:STDOUT:     "diagnostics": [],
-// CHECK:STDOUT:     "uri": "file:///test.carbon"
-// CHECK:STDOUT:   }
-// CHECK:STDOUT: }Content-Length: 51{{\r}}
-// CHECK:STDOUT: {{\r}}
-// CHECK:STDOUT: {
-// CHECK:STDOUT:   "id": 2,
-// CHECK:STDOUT:   "jsonrpc": "2.0",
-// CHECK:STDOUT:   "result": null
-// CHECK:STDOUT: }

+ 1 - 1
toolchain/language_server/testdata/text_document/change_unknown.carbon

@@ -29,7 +29,7 @@
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:       "documentSymbolProvider": true,
 // CHECK:STDOUT:       "documentSymbolProvider": true,
-// CHECK:STDOUT:       "textDocumentSync": 1
+// CHECK:STDOUT:       "textDocumentSync": 2
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }Content-Length: 51{{\r}}
 // CHECK:STDOUT: }Content-Length: 51{{\r}}

+ 1 - 1
toolchain/language_server/testdata/text_document/close_unknown.carbon

@@ -28,7 +28,7 @@
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:       "documentSymbolProvider": true,
 // CHECK:STDOUT:       "documentSymbolProvider": true,
-// CHECK:STDOUT:       "textDocumentSync": 1
+// CHECK:STDOUT:       "textDocumentSync": 2
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }Content-Length: 51{{\r}}
 // CHECK:STDOUT: }Content-Length: 51{{\r}}

+ 1 - 1
toolchain/language_server/testdata/text_document/diagnostics.carbon

@@ -34,7 +34,7 @@
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:       "documentSymbolProvider": true,
 // CHECK:STDOUT:       "documentSymbolProvider": true,
-// CHECK:STDOUT:       "textDocumentSync": 1
+// CHECK:STDOUT:       "textDocumentSync": 2
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }Content-Length: 1164{{\r}}
 // CHECK:STDOUT: }Content-Length: 1164{{\r}}

+ 290 - 0
toolchain/language_server/testdata/text_document/incremental_sync.carbon

@@ -0,0 +1,290 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/language_server/testdata/text_document/incremental_sync.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/text_document/incremental_sync.carbon
+
+// --- STDIN
+[[@LSP-CALL:initialize]]
+[[@LSP-NOTIFY:textDocument/didOpen:
+  "textDocument": {"uri": "file:/test.carbon", "languageId": "carbon",
+                   "text": "fn SecondFunCTIonNNN() {}\n\nfn ThirdFn() {}\n"}
+]]
+[[@LSP-NOTIFY:textDocument/didChange:
+  "textDocument": {"uri": "file:/test.carbon"},
+  "contentChanges": [
+    {"range": {"start": {"line": 0, "character": 17},
+               "end": {"line": 0, "character": 20}},
+     "text": ""},
+    {"range": {"start": {"line": 0, "character": 12},
+               "end": {"line": 0, "character": 13}},
+     "text": "c"},
+    {"range": {"start": {"line": 0, "character": 13},
+               "end": {"line": 0, "character": 15}},
+     "text": "ti"},
+    {"range": {"start": {"line": 2, "character": 9},
+               "end": {"line": 2, "character": 9}},
+     "text": "u"},
+    {"range": {"start": {"line": 2, "character": 11},
+               "end": {"line": 2, "character": 11}},
+     "text": "ction"}
+  ]
+]]
+[[@LSP-NOTIFY:textDocument/didChange:
+  "textDocument": {"uri": "file:/test.carbon"},
+  "contentChanges": [
+    {"range": {"start": {"line": 3, "character": 0},
+               "end": {"line": 3, "character": 0}},
+     "text": "\nfn FourthFunction() {}\n"}
+  ]
+]]
+[[@LSP-NOTIFY:textDocument/didChange:
+  "textDocument": {"uri": "file:/test.carbon"},
+  "contentChanges": [
+    {"range": {"start": {"line": 0, "character": 0},
+               "end": {"line": 0, "character": 0}},
+     "text": "fn FirstFunction() {}\n\n"}
+  ]
+]]
+[[@LSP-CALL:textDocument/documentSymbol:
+  "textDocument": {"uri": "file:/test.carbon"}
+]]
+[[@LSP-NOTIFY:textDocument/didChange:
+  "textDocument": {"uri": "file:/test.carbon"},
+  "contentChanges": [
+    {"text": "fn Foo() {}\n\nfn Bar() {}\n"}
+  ]
+]]
+[[@LSP-CALL:textDocument/documentSymbol:
+  "textDocument": {"uri": "file:/test.carbon"}
+]]
+[[@LSP-CALL:shutdown]]
+[[@LSP-NOTIFY:exit]]
+
+// --- AUTOUPDATE-SPLIT
+
+// CHECK:STDOUT: Content-Length: 146{{\r}}
+// CHECK:STDOUT: {{\r}}
+// CHECK:STDOUT: {
+// CHECK:STDOUT:   "id": 1,
+// CHECK:STDOUT:   "jsonrpc": "2.0",
+// CHECK:STDOUT:   "result": {
+// CHECK:STDOUT:     "capabilities": {
+// CHECK:STDOUT:       "documentSymbolProvider": true,
+// CHECK:STDOUT:       "textDocumentSync": 2
+// CHECK:STDOUT:     }
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }Content-Length: 144{{\r}}
+// CHECK:STDOUT: {{\r}}
+// CHECK:STDOUT: {
+// CHECK:STDOUT:   "jsonrpc": "2.0",
+// CHECK:STDOUT:   "method": "textDocument/publishDiagnostics",
+// CHECK:STDOUT:   "params": {
+// CHECK:STDOUT:     "diagnostics": [],
+// CHECK:STDOUT:     "uri": "file:///test.carbon"
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }Content-Length: 144{{\r}}
+// CHECK:STDOUT: {{\r}}
+// CHECK:STDOUT: {
+// CHECK:STDOUT:   "jsonrpc": "2.0",
+// CHECK:STDOUT:   "method": "textDocument/publishDiagnostics",
+// CHECK:STDOUT:   "params": {
+// CHECK:STDOUT:     "diagnostics": [],
+// CHECK:STDOUT:     "uri": "file:///test.carbon"
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }Content-Length: 144{{\r}}
+// CHECK:STDOUT: {{\r}}
+// CHECK:STDOUT: {
+// CHECK:STDOUT:   "jsonrpc": "2.0",
+// CHECK:STDOUT:   "method": "textDocument/publishDiagnostics",
+// CHECK:STDOUT:   "params": {
+// CHECK:STDOUT:     "diagnostics": [],
+// CHECK:STDOUT:     "uri": "file:///test.carbon"
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }Content-Length: 144{{\r}}
+// CHECK:STDOUT: {{\r}}
+// CHECK:STDOUT: {
+// CHECK:STDOUT:   "jsonrpc": "2.0",
+// CHECK:STDOUT:   "method": "textDocument/publishDiagnostics",
+// CHECK:STDOUT:   "params": {
+// CHECK:STDOUT:     "diagnostics": [],
+// CHECK:STDOUT:     "uri": "file:///test.carbon"
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }Content-Length: 1741{{\r}}
+// CHECK:STDOUT: {{\r}}
+// CHECK:STDOUT: {
+// CHECK:STDOUT:   "id": 2,
+// CHECK:STDOUT:   "jsonrpc": "2.0",
+// CHECK:STDOUT:   "result": [
+// CHECK:STDOUT:     {
+// CHECK:STDOUT:       "kind": 12,
+// CHECK:STDOUT:       "name": "FirstFunction",
+// CHECK:STDOUT:       "range": {
+// CHECK:STDOUT:         "end": {
+// CHECK:STDOUT:           "character": 21,
+// CHECK:STDOUT:           "line": 0
+// CHECK:STDOUT:         },
+// CHECK:STDOUT:         "start": {
+// CHECK:STDOUT:           "character": 0,
+// CHECK:STDOUT:           "line": 0
+// CHECK:STDOUT:         }
+// CHECK:STDOUT:       },
+// CHECK:STDOUT:       "selectionRange": {
+// CHECK:STDOUT:         "end": {
+// CHECK:STDOUT:           "character": 16,
+// CHECK:STDOUT:           "line": 0
+// CHECK:STDOUT:         },
+// CHECK:STDOUT:         "start": {
+// CHECK:STDOUT:           "character": 3,
+// CHECK:STDOUT:           "line": 0
+// CHECK:STDOUT:         }
+// CHECK:STDOUT:       }
+// CHECK:STDOUT:     },
+// CHECK:STDOUT:     {
+// CHECK:STDOUT:       "kind": 12,
+// CHECK:STDOUT:       "name": "SecondFunction",
+// CHECK:STDOUT:       "range": {
+// CHECK:STDOUT:         "end": {
+// CHECK:STDOUT:           "character": 22,
+// CHECK:STDOUT:           "line": 2
+// CHECK:STDOUT:         },
+// CHECK:STDOUT:         "start": {
+// CHECK:STDOUT:           "character": 0,
+// CHECK:STDOUT:           "line": 2
+// CHECK:STDOUT:         }
+// CHECK:STDOUT:       },
+// CHECK:STDOUT:       "selectionRange": {
+// CHECK:STDOUT:         "end": {
+// CHECK:STDOUT:           "character": 17,
+// CHECK:STDOUT:           "line": 2
+// CHECK:STDOUT:         },
+// CHECK:STDOUT:         "start": {
+// CHECK:STDOUT:           "character": 3,
+// CHECK:STDOUT:           "line": 2
+// CHECK:STDOUT:         }
+// CHECK:STDOUT:       }
+// CHECK:STDOUT:     },
+// CHECK:STDOUT:     {
+// CHECK:STDOUT:       "kind": 12,
+// CHECK:STDOUT:       "name": "ThirdFunction",
+// CHECK:STDOUT:       "range": {
+// CHECK:STDOUT:         "end": {
+// CHECK:STDOUT:           "character": 21,
+// CHECK:STDOUT:           "line": 4
+// CHECK:STDOUT:         },
+// CHECK:STDOUT:         "start": {
+// CHECK:STDOUT:           "character": 0,
+// CHECK:STDOUT:           "line": 4
+// CHECK:STDOUT:         }
+// CHECK:STDOUT:       },
+// CHECK:STDOUT:       "selectionRange": {
+// CHECK:STDOUT:         "end": {
+// CHECK:STDOUT:           "character": 16,
+// CHECK:STDOUT:           "line": 4
+// CHECK:STDOUT:         },
+// CHECK:STDOUT:         "start": {
+// CHECK:STDOUT:           "character": 3,
+// CHECK:STDOUT:           "line": 4
+// CHECK:STDOUT:         }
+// CHECK:STDOUT:       }
+// CHECK:STDOUT:     },
+// CHECK:STDOUT:     {
+// CHECK:STDOUT:       "kind": 12,
+// CHECK:STDOUT:       "name": "FourthFunction",
+// CHECK:STDOUT:       "range": {
+// CHECK:STDOUT:         "end": {
+// CHECK:STDOUT:           "character": 22,
+// CHECK:STDOUT:           "line": 6
+// CHECK:STDOUT:         },
+// CHECK:STDOUT:         "start": {
+// CHECK:STDOUT:           "character": 0,
+// CHECK:STDOUT:           "line": 6
+// CHECK:STDOUT:         }
+// CHECK:STDOUT:       },
+// CHECK:STDOUT:       "selectionRange": {
+// CHECK:STDOUT:         "end": {
+// CHECK:STDOUT:           "character": 17,
+// CHECK:STDOUT:           "line": 6
+// CHECK:STDOUT:         },
+// CHECK:STDOUT:         "start": {
+// CHECK:STDOUT:           "character": 3,
+// CHECK:STDOUT:           "line": 6
+// CHECK:STDOUT:         }
+// CHECK:STDOUT:       }
+// CHECK:STDOUT:     }
+// CHECK:STDOUT:   ]
+// CHECK:STDOUT: }Content-Length: 144{{\r}}
+// CHECK:STDOUT: {{\r}}
+// CHECK:STDOUT: {
+// CHECK:STDOUT:   "jsonrpc": "2.0",
+// CHECK:STDOUT:   "method": "textDocument/publishDiagnostics",
+// CHECK:STDOUT:   "params": {
+// CHECK:STDOUT:     "diagnostics": [],
+// CHECK:STDOUT:     "uri": "file:///test.carbon"
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }Content-Length: 873{{\r}}
+// CHECK:STDOUT: {{\r}}
+// CHECK:STDOUT: {
+// CHECK:STDOUT:   "id": 3,
+// CHECK:STDOUT:   "jsonrpc": "2.0",
+// CHECK:STDOUT:   "result": [
+// CHECK:STDOUT:     {
+// CHECK:STDOUT:       "kind": 12,
+// CHECK:STDOUT:       "name": "Foo",
+// CHECK:STDOUT:       "range": {
+// CHECK:STDOUT:         "end": {
+// CHECK:STDOUT:           "character": 11,
+// CHECK:STDOUT:           "line": 0
+// CHECK:STDOUT:         },
+// CHECK:STDOUT:         "start": {
+// CHECK:STDOUT:           "character": 0,
+// CHECK:STDOUT:           "line": 0
+// CHECK:STDOUT:         }
+// CHECK:STDOUT:       },
+// CHECK:STDOUT:       "selectionRange": {
+// CHECK:STDOUT:         "end": {
+// CHECK:STDOUT:           "character": 6,
+// CHECK:STDOUT:           "line": 0
+// CHECK:STDOUT:         },
+// CHECK:STDOUT:         "start": {
+// CHECK:STDOUT:           "character": 3,
+// CHECK:STDOUT:           "line": 0
+// CHECK:STDOUT:         }
+// CHECK:STDOUT:       }
+// CHECK:STDOUT:     },
+// CHECK:STDOUT:     {
+// CHECK:STDOUT:       "kind": 12,
+// CHECK:STDOUT:       "name": "Bar",
+// CHECK:STDOUT:       "range": {
+// CHECK:STDOUT:         "end": {
+// CHECK:STDOUT:           "character": 11,
+// CHECK:STDOUT:           "line": 2
+// CHECK:STDOUT:         },
+// CHECK:STDOUT:         "start": {
+// CHECK:STDOUT:           "character": 0,
+// CHECK:STDOUT:           "line": 2
+// CHECK:STDOUT:         }
+// CHECK:STDOUT:       },
+// CHECK:STDOUT:       "selectionRange": {
+// CHECK:STDOUT:         "end": {
+// CHECK:STDOUT:           "character": 6,
+// CHECK:STDOUT:           "line": 2
+// CHECK:STDOUT:         },
+// CHECK:STDOUT:         "start": {
+// CHECK:STDOUT:           "character": 3,
+// CHECK:STDOUT:           "line": 2
+// CHECK:STDOUT:         }
+// CHECK:STDOUT:       }
+// CHECK:STDOUT:     }
+// CHECK:STDOUT:   ]
+// CHECK:STDOUT: }Content-Length: 51{{\r}}
+// CHECK:STDOUT: {{\r}}
+// CHECK:STDOUT: {
+// CHECK:STDOUT:   "id": 4,
+// CHECK:STDOUT:   "jsonrpc": "2.0",
+// CHECK:STDOUT:   "result": null
+// CHECK:STDOUT: }

+ 1 - 1
toolchain/language_server/testdata/text_document/open_change_close.carbon

@@ -34,7 +34,7 @@
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:       "documentSymbolProvider": true,
 // CHECK:STDOUT:       "documentSymbolProvider": true,
-// CHECK:STDOUT:       "textDocumentSync": 1
+// CHECK:STDOUT:       "textDocumentSync": 2
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }Content-Length: 144{{\r}}
 // CHECK:STDOUT: }Content-Length: 144{{\r}}

+ 1 - 1
toolchain/language_server/testdata/text_document/open_duplicate.carbon

@@ -33,7 +33,7 @@
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:   "result": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:     "capabilities": {
 // CHECK:STDOUT:       "documentSymbolProvider": true,
 // CHECK:STDOUT:       "documentSymbolProvider": true,
-// CHECK:STDOUT:       "textDocumentSync": 1
+// CHECK:STDOUT:       "textDocumentSync": 2
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   }
 // CHECK:STDOUT: }Content-Length: 144{{\r}}
 // CHECK:STDOUT: }Content-Length: 144{{\r}}