Просмотр исходного кода

Add support for publishDiagnostics (#4912)

<img width="536" alt="Screenshot 2025-02-06 at 3 02 18 PM"
src="https://github.com/user-attachments/assets/27b7673d-f8d4-4f9f-9e5d-20c3a88b8c78"
/>

Requires the caching work in #4897
Jon Ross-Perkins 1 год назад
Родитель
Сommit
f89985d0f4

+ 9 - 0
toolchain/diagnostics/coverage_test.cpp

@@ -42,6 +42,15 @@ constexpr DiagnosticKind UntestedDiagnosticKinds[] = {
 
     // This is a little long but is tested in lex/numeric_literal_test.cpp.
     DiagnosticKind::TooManyDigits,
+
+    // TODO: This can only fire if the first message in a diagnostic is rooted
+    // in a file other than the file being compiled. The language server
+    // currently only supports compiling one file at a time. Do one of:
+    // - When imports are supported, find a diagnostic whose first message isn't
+    //   in the current file.
+    // - Require all diagnostics produced by compiling have their first location
+    //   be in the file being compiled, never an import.
+    DiagnosticKind::LanguageServerDiagnosticInWrongFile,
 };
 
 // Looks for diagnostic kinds that aren't covered by a file_test.

+ 3 - 0
toolchain/diagnostics/diagnostic.h

@@ -79,6 +79,9 @@ struct DiagnosticLoc {
 // A message composing a diagnostic. This may be the main message, but can also
 // be notes providing more information.
 struct DiagnosticMessage {
+  // Helper for calling `format_fn`.
+  auto Format() const -> std::string { return format_fn(*this); }
+
   // The diagnostic's kind.
   DiagnosticKind kind;
 

+ 1 - 1
toolchain/diagnostics/diagnostic_consumer.cpp

@@ -31,7 +31,7 @@ auto StreamDiagnosticConsumer::HandleDiagnostic(Diagnostic diagnostic) -> void {
       case DiagnosticLevel::LocationInfo:
         break;
     }
-    *stream_ << message.format_fn(message);
+    *stream_ << message.Format();
     if (include_diagnostic_kind_) {
       *stream_ << " [" << message.kind << "]";
     }

+ 1 - 0
toolchain/diagnostics/diagnostic_kind.def

@@ -452,6 +452,7 @@ CARBON_DIAGNOSTIC_KIND(LanguageServerUnsupportedNotification)
 CARBON_DIAGNOSTIC_KIND(LanguageServerOpenDuplicateFile)
 CARBON_DIAGNOSTIC_KIND(LanguageServerUnsupportedChanges)
 CARBON_DIAGNOSTIC_KIND(LanguageServerCloseUnknownFile)
+CARBON_DIAGNOSTIC_KIND(LanguageServerDiagnosticInWrongFile)
 
 // ============================================================================
 // Other diagnostics

+ 2 - 2
toolchain/diagnostics/mocks.cpp

@@ -11,8 +11,8 @@ void PrintTo(const Diagnostic& diagnostic, std::ostream* os) {
   PrintTo(diagnostic.level, os);
   for (const auto& message : diagnostic.messages) {
     *os << ", {" << message.loc.filename << ":" << message.loc.line_number
-        << ":" << message.loc.column_number << ", \""
-        << message.format_fn(message) << "}";
+        << ":" << message.loc.column_number << ", \"" << message.Format()
+        << "}";
   }
   *os << "\"}";
 }

+ 1 - 1
toolchain/diagnostics/mocks.h

@@ -19,7 +19,7 @@ class MockDiagnosticConsumer : public DiagnosticConsumer {
 // NOLINTNEXTLINE(modernize-use-trailing-return-type): From the macro.
 MATCHER_P(IsDiagnosticMessageString, matcher, "") {
   const DiagnosticMessage& message = arg;
-  return testing::ExplainMatchResult(matcher, message.format_fn(message),
+  return testing::ExplainMatchResult(matcher, message.Format(),
                                      result_listener);
 }
 

+ 4 - 2
toolchain/language_server/BUILD

@@ -31,17 +31,20 @@ cc_library(
     srcs = ["context.cpp"],
     hdrs = ["context.h"],
     deps = [
+        "//common:check",
         "//common:map",
+        "//common:raw_string_ostream",
         "//toolchain/base:shared_value_stores",
+        "//toolchain/check",
         "//toolchain/diagnostics:diagnostic_emitter",
         "//toolchain/diagnostics:file_diagnostics",
-        "//toolchain/diagnostics:null_diagnostics",
         "//toolchain/lex",
         "//toolchain/lex:tokenized_buffer",
         "//toolchain/parse",
         "//toolchain/parse:tree",
         "//toolchain/sem_ir:file",
         "//toolchain/source:source_buffer",
+        "@llvm-project//clang-tools-extra/clangd:ClangDaemon",
     ],
 )
 
@@ -52,7 +55,6 @@ cc_library(
     deps = [
         ":context",
         "//toolchain/base:shared_value_stores",
-        "//toolchain/diagnostics:null_diagnostics",
         "//toolchain/lex",
         "//toolchain/parse",
         "//toolchain/parse:node_kind",

+ 115 - 7
toolchain/language_server/context.cpp

@@ -6,8 +6,12 @@
 
 #include <memory>
 
+#include "common/check.h"
+#include "common/raw_string_ostream.h"
 #include "toolchain/base/shared_value_stores.h"
-#include "toolchain/diagnostics/null_diagnostics.h"
+#include "toolchain/check/check.h"
+#include "toolchain/diagnostics/diagnostic.h"
+#include "toolchain/diagnostics/diagnostic_consumer.h"
 #include "toolchain/lex/lex.h"
 #include "toolchain/lex/tokenized_buffer.h"
 #include "toolchain/parse/parse.h"
@@ -15,7 +19,86 @@
 
 namespace Carbon::LanguageServer {
 
-auto Context::File::SetText(Context& context, llvm::StringRef text) -> void {
+// A consumer for turning diagnostics into a `textDocument/publishDiagnostics`
+// notification.
+// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_publishDiagnostics
+class PublishDiagnosticConsumer : public DiagnosticConsumer {
+ public:
+  // Initializes params with the target file information.
+  explicit PublishDiagnosticConsumer(Context* context,
+                                     const clang::clangd::URIForFile& uri,
+                                     std::optional<int64_t> version)
+      : context_(context), params_{.uri = uri, .version = version} {}
+
+  // Turns a diagnostic into an LSP diagnostic.
+  auto HandleDiagnostic(Diagnostic diagnostic) -> void override {
+    const auto& message = diagnostic.messages[0];
+    if (message.loc.filename != params_.uri.file()) {
+      // `pushDiagnostic` requires diagnostics to be associated with a location
+      // in the current file. Suppress diagnostics rooted in other files.
+      // TODO: Consider if there's a better way to handle this.
+      RawStringOstream stream;
+      StreamDiagnosticConsumer consumer(&stream);
+      consumer.HandleDiagnostic(diagnostic);
+
+      CARBON_DIAGNOSTIC(LanguageServerDiagnosticInWrongFile, Warning,
+                        "dropping diagnostic in {0}:\n{1}", std::string,
+                        std::string);
+      context_->file_emitter().Emit(
+          params_.uri.file(), LanguageServerDiagnosticInWrongFile,
+          message.loc.filename.str(), stream.TakeStr());
+      return;
+    }
+
+    // Add the main message.
+    params_.diagnostics.push_back(clang::clangd::Diagnostic{
+        .range = GetRange(message.loc),
+        .severity = GetSeverity(diagnostic.level),
+        .source = "carbon",
+        .message = message.Format(),
+    });
+    // TODO: Figure out constructing URIs for note locations.
+  }
+
+  // Returns the constructed request.
+  auto params() -> llvm::json::Value { return params_; }
+
+ private:
+  // Returns the LSP range for a diagnostic. Note that Carbon uses 1-based
+  // numbers while LSP uses 0-based.
+  auto GetRange(const DiagnosticLoc& loc) -> clang::clangd::Range {
+    return {.start = {.line = loc.line_number - 1,
+                      .character = loc.column_number - 1},
+            .end = {.line = loc.line_number,
+                    .character = loc.column_number + loc.length}};
+  }
+
+  // Converts a diagnostic level to an LSP severity.
+  auto GetSeverity(DiagnosticLevel level) -> int {
+    // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticSeverity
+    enum class DiagnosticSeverity {
+      Error = 1,
+      Warning = 2,
+      Information = 3,
+      Hint = 4,
+    };
+
+    switch (level) {
+      case DiagnosticLevel::Error:
+        return static_cast<int>(DiagnosticSeverity::Error);
+      case DiagnosticLevel::Warning:
+        return static_cast<int>(DiagnosticSeverity::Warning);
+      default:
+        CARBON_FATAL("Unexpected diagnostic level: {0}", level);
+    }
+  }
+
+  Context* context_;
+  clang::clangd::PublishDiagnosticsParams params_;
+};
+
+auto Context::File::SetText(Context& context, std::optional<int64_t> version,
+                            llvm::StringRef text) -> void {
   // Clear state dependent on the source text.
   tree_and_subtrees_.reset();
   tree_.reset();
@@ -23,28 +106,53 @@ auto Context::File::SetText(Context& context, llvm::StringRef text) -> void {
   value_stores_.reset();
   source_.reset();
 
+  // A consumer to gather diagnostics for the file.
+  PublishDiagnosticConsumer consumer(&context, uri_, version);
+
   // TODO: Make the processing asynchronous, to better handle rapid text
   // updates.
   CARBON_CHECK(!source_ && !value_stores_ && !tokens_ && !tree_,
                "We currently cache everything together");
   // TODO: Diagnostics should be passed to the LSP instead of dropped.
-  auto& null_consumer = NullDiagnosticConsumer();
   std::optional source =
-      SourceBuffer::MakeFromStringCopy(filename_, text, null_consumer);
+      SourceBuffer::MakeFromStringCopy(uri_.file(), text, consumer);
   if (!source) {
     // Failing here should be rare, but provide stub data for recovery so that
     // we can have a simple API.
-    source = SourceBuffer::MakeFromStringCopy(filename_, "", null_consumer);
+    source = SourceBuffer::MakeFromStringCopy(uri_.file(), "", consumer);
     CARBON_CHECK(source, "Making an empty buffer should always succeed");
   }
   source_ = std::make_unique<SourceBuffer>(std::move(*source));
   value_stores_ = std::make_unique<SharedValueStores>();
   tokens_ = std::make_unique<Lex::TokenizedBuffer>(
-      Lex::Lex(*value_stores_, *source_, null_consumer));
+      Lex::Lex(*value_stores_, *source_, consumer));
   tree_ = std::make_unique<Parse::Tree>(
-      Parse::Parse(*tokens_, null_consumer, context.vlog_stream()));
+      Parse::Parse(*tokens_, consumer, context.vlog_stream()));
   tree_and_subtrees_ =
       std::make_unique<Parse::TreeAndSubtrees>(*tokens_, *tree_);
+
+  SemIR::File sem_ir(tree_.get(), SemIR::CheckIRId(0), tree_->packaging_decl(),
+                     *value_stores_, uri_.file().str());
+  auto getter = [this]() -> const Parse::TreeAndSubtrees& {
+    return *tree_and_subtrees_;
+  };
+  // TODO: Support cross-file checking when multiple files have edits.
+  llvm::SmallVector<Check::Unit> units = {{.consumer = &consumer,
+                                           .value_stores = value_stores_.get(),
+                                           .timings = nullptr,
+                                           .tree_and_subtrees_getter = getter,
+                                           .sem_ir = &sem_ir}};
+  llvm::IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> fs =
+      new llvm::vfs::InMemoryFileSystem;
+  // TODO: Include the prelude.
+  Check::CheckParseTrees(units, /*prelude_import=*/false, fs,
+                         context.vlog_stream(), /*fuzzing=*/false);
+
+  // Note we need to publish diagnostics even when empty.
+  // TODO: Consider caching previously published diagnostics and only publishing
+  // when they change.
+  context.outgoing().notify("textDocument/publishDiagnostics",
+                            consumer.params());
 }
 
 auto Context::LookupFile(llvm::StringRef filename) -> File* {

+ 15 - 7
toolchain/language_server/context.h

@@ -8,6 +8,7 @@
 #include <memory>
 #include <string>
 
+#include "clang-tools-extra/clangd/LSPBinder.h"
 #include "common/map.h"
 #include "toolchain/base/shared_value_stores.h"
 #include "toolchain/diagnostics/diagnostic_consumer.h"
@@ -26,10 +27,11 @@ class Context {
   // Cached information for an open file.
   class File {
    public:
-    explicit File(std::string filename) : filename_(std::move(filename)) {}
+    explicit File(clang::clangd::URIForFile uri) : uri_(std::move(uri)) {}
 
     // Changes the file's text, updating dependent state.
-    auto SetText(Context& context, llvm::StringRef text) -> void;
+    auto SetText(Context& context, std::optional<int64_t> version,
+                 llvm::StringRef text) -> void;
 
     auto tree_and_subtrees() const -> const Parse::TreeAndSubtrees& {
       return *tree_and_subtrees_;
@@ -37,7 +39,7 @@ class Context {
 
    private:
     // The filename, stable across instances.
-    std::string filename_;
+    clang::clangd::URIForFile uri_;
 
     // Current file content, and derived values.
     std::unique_ptr<SourceBuffer> source_;
@@ -47,19 +49,24 @@ class Context {
     std::unique_ptr<Parse::TreeAndSubtrees> tree_and_subtrees_;
   };
 
-  // `consumer` and `emitter` are required. `vlog_stream` is optional.
-  explicit Context(llvm::raw_ostream* vlog_stream, DiagnosticConsumer* consumer)
+  // `vlog_stream` is optional; other parameters are required.
+  explicit Context(llvm::raw_ostream* vlog_stream, DiagnosticConsumer* consumer,
+                   clang::clangd::LSPBinder::RawOutgoing* outgoing)
       : vlog_stream_(vlog_stream),
         file_emitter_(consumer),
-        no_loc_emitter_(consumer) {}
+        no_loc_emitter_(consumer),
+        outgoing_(outgoing) {}
 
   // Returns a reference to the file if it's known, or diagnoses and returns
   // null.
   auto LookupFile(llvm::StringRef filename) -> File*;
 
+  auto vlog_stream() -> llvm::raw_ostream* { return vlog_stream_; }
   auto file_emitter() -> FileDiagnosticEmitter& { return file_emitter_; }
   auto no_loc_emitter() -> NoLocDiagnosticEmitter& { return no_loc_emitter_; }
-  auto vlog_stream() -> llvm::raw_ostream* { return vlog_stream_; }
+  auto outgoing() -> clang::clangd::LSPBinder::RawOutgoing& {
+    return *outgoing_;
+  }
 
   auto files() -> Map<std::string, File>& { return files_; }
 
@@ -68,6 +75,7 @@ class Context {
   llvm::raw_ostream* vlog_stream_;
   FileDiagnosticEmitter file_emitter_;
   NoLocDiagnosticEmitter no_loc_emitter_;
+  clang::clangd::LSPBinder::RawOutgoing* outgoing_;
 
   // Content of files managed by the language client.
   Map<std::string, File> files_;

+ 0 - 1
toolchain/language_server/handle_document_symbol.cpp

@@ -3,7 +3,6 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
 #include "toolchain/base/shared_value_stores.h"
-#include "toolchain/diagnostics/null_diagnostics.h"
 #include "toolchain/language_server/handle.h"
 #include "toolchain/lex/lex.h"
 #include "toolchain/parse/node_kind.h"

+ 5 - 3
toolchain/language_server/handle_text_document.cpp

@@ -16,8 +16,9 @@ auto HandleDidOpenTextDocument(
   }
 
   auto insert_result = context.files().Insert(
-      filename, [&] { return Context::File(filename.str()); });
-  insert_result.value().SetText(context, params.textDocument.text);
+      filename, [&] { return Context::File(params.textDocument.uri); });
+  insert_result.value().SetText(context, params.textDocument.version,
+                                params.textDocument.text);
   if (!insert_result.is_inserted()) {
     CARBON_DIAGNOSTIC(LanguageServerOpenDuplicateFile, Warning,
                       "duplicate open file request; updating content");
@@ -43,7 +44,8 @@ auto HandleDidChangeTextDocument(
     return;
   }
   if (auto* file = context.LookupFile(filename)) {
-    file->SetText(context, params.contentChanges[0].text);
+    file->SetText(context, params.textDocument.version,
+                  params.contentChanges[0].text);
   }
 }
 

+ 2 - 3
toolchain/language_server/language_server.cpp

@@ -55,10 +55,9 @@ auto Run(FILE* input_stream, llvm::raw_ostream& output_stream,
       clang::clangd::newJSONTransport(input_stream, output_stream,
                                       /*InMirror=*/nullptr,
                                       /*Pretty=*/true));
-  Context context(vlog_stream, &consumer);
-  // TODO: Use error_stream in IncomingMessages to report dropped errors.
-  IncomingMessages incoming(transport.get(), &context);
   OutgoingMessages outgoing(transport.get());
+  Context context(vlog_stream, &consumer, &outgoing);
+  IncomingMessages incoming(transport.get(), &context);
 
   // Run the server loop.
   llvm::Error err = transport->loop(incoming);

+ 29 - 2
toolchain/language_server/testdata/document_symbol/basics.carbon

@@ -18,7 +18,7 @@
 ]]
 [[@LSP-NOTIFY:textDocument/didOpen:
   "textDocument": {"uri": "file:/invalid.carbon", "languageId": "carbon",
-                   "text": "text"}
+                   "text": "// Empty"}
 ]]
 [[@LSP-CALL:textDocument/documentSymbol:
   "textDocument": {"uri": "file:/invalid.carbon"}
@@ -34,18 +34,45 @@
 
 // --- AUTOUPDATE-SPLIT
 
-// CHECK:STDOUT: Content-Length: 49{{\r}}
+// CHECK:STDOUT: Content-Length: 145{{\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:///empty.carbon"
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }Content-Length: 49{{\r}}
 // CHECK:STDOUT: {{\r}}
 // CHECK:STDOUT: {
 // CHECK:STDOUT:   "id": 1,
 // CHECK:STDOUT:   "jsonrpc": "2.0",
 // CHECK:STDOUT:   "result": []
+// CHECK:STDOUT: }Content-Length: 147{{\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:///invalid.carbon"
+// CHECK:STDOUT:   }
 // CHECK:STDOUT: }Content-Length: 49{{\r}}
 // CHECK:STDOUT: {{\r}}
 // CHECK:STDOUT: {
 // CHECK:STDOUT:   "id": 2,
 // CHECK:STDOUT:   "jsonrpc": "2.0",
 // CHECK:STDOUT:   "result": []
+// CHECK:STDOUT: }Content-Length: 142{{\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:///fn.carbon"
+// CHECK:STDOUT:   }
 // CHECK:STDOUT: }Content-Length: 459{{\r}}
 // CHECK:STDOUT: {{\r}}
 // CHECK:STDOUT: {

+ 11 - 2
toolchain/language_server/testdata/text_document/change_count.carbon

@@ -11,7 +11,7 @@
 // --- STDIN
 [[@LSP-NOTIFY:textDocument/didOpen:
   "textDocument": {"uri": "file:/test.carbon", "languageId": "carbon",
-                   "text": "test"}
+                   "text": "// Empty"}
 ]]
 [[@LSP-NOTIFY:textDocument/didChange:
   "textDocument": {"uri": "file:/test.carbon"},
@@ -36,4 +36,13 @@
 // CHECK:STDERR:
 // CHECK:STDERR: /test.carbon: warning: received unsupported contentChanges count: 2 [LanguageServerUnsupportedChanges]
 // CHECK:STDERR:
-// 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: }

+ 109 - 0
toolchain/language_server/testdata/text_document/diagnostics.carbon

@@ -0,0 +1,109 @@
+// 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/diagnostics.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/text_document/diagnostics.carbon
+
+// --- STDIN
+[[@LSP-NOTIFY:textDocument/didOpen:
+  "textDocument": {"uri": "file:/test.carbon", "languageId": "carbon",
+                   "version": 1, "text": "{"}
+]]
+[[@LSP-NOTIFY:textDocument/didChange:
+  "textDocument": {"uri": "file:/test.carbon", "version": 2},
+  "contentChanges": [{"text": "fn F() { return (); }"}]
+]]
+[[@LSP-NOTIFY:textDocument/didClose:
+  "textDocument": {"uri": "file:/test.carbon"}
+]]
+[[@LSP-NOTIFY:exit]]
+
+// --- AUTOUPDATE-SPLIT
+
+// CHECK:STDOUT: Content-Length: 1164{{\r}}
+// CHECK:STDOUT: {{\r}}
+// CHECK:STDOUT: {
+// CHECK:STDOUT:   "jsonrpc": "2.0",
+// CHECK:STDOUT:   "method": "textDocument/publishDiagnostics",
+// CHECK:STDOUT:   "params": {
+// CHECK:STDOUT:     "diagnostics": [
+// CHECK:STDOUT:       {
+// CHECK:STDOUT:         "message": "opening symbol without a corresponding closing symbol",
+// CHECK:STDOUT:         "range": {
+// CHECK:STDOUT:           "end": {
+// CHECK:STDOUT:             "character": 2,
+// CHECK:STDOUT:             "line": 1
+// CHECK:STDOUT:           },
+// CHECK:STDOUT:           "start": {
+// CHECK:STDOUT:             "character": 0,
+// CHECK:STDOUT:             "line": 0
+// CHECK:STDOUT:           }
+// CHECK:STDOUT:         },
+// CHECK:STDOUT:         "severity": 1,
+// CHECK:STDOUT:         "source": "carbon"
+// CHECK:STDOUT:       },
+// CHECK:STDOUT:       {
+// CHECK:STDOUT:         "message": "unrecognized declaration introducer",
+// CHECK:STDOUT:         "range": {
+// CHECK:STDOUT:           "end": {
+// CHECK:STDOUT:             "character": 2,
+// CHECK:STDOUT:             "line": 1
+// CHECK:STDOUT:           },
+// CHECK:STDOUT:           "start": {
+// CHECK:STDOUT:             "character": 0,
+// CHECK:STDOUT:             "line": 0
+// CHECK:STDOUT:           }
+// CHECK:STDOUT:         },
+// CHECK:STDOUT:         "severity": 1,
+// CHECK:STDOUT:         "source": "carbon"
+// CHECK:STDOUT:       },
+// CHECK:STDOUT:       {
+// CHECK:STDOUT:         "message": "semantics TODO: `handle invalid parse trees in `check``",
+// CHECK:STDOUT:         "range": {
+// CHECK:STDOUT:           "end": {
+// CHECK:STDOUT:             "character": 2,
+// CHECK:STDOUT:             "line": 1
+// CHECK:STDOUT:           },
+// CHECK:STDOUT:           "start": {
+// CHECK:STDOUT:             "character": 0,
+// CHECK:STDOUT:             "line": 0
+// CHECK:STDOUT:           }
+// CHECK:STDOUT:         },
+// CHECK:STDOUT:         "severity": 1,
+// CHECK:STDOUT:         "source": "carbon"
+// CHECK:STDOUT:       }
+// CHECK:STDOUT:     ],
+// CHECK:STDOUT:     "uri": "file:///test.carbon",
+// CHECK:STDOUT:     "version": 1
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }Content-Length: 507{{\r}}
+// CHECK:STDOUT: {{\r}}
+// CHECK:STDOUT: {
+// CHECK:STDOUT:   "jsonrpc": "2.0",
+// CHECK:STDOUT:   "method": "textDocument/publishDiagnostics",
+// CHECK:STDOUT:   "params": {
+// CHECK:STDOUT:     "diagnostics": [
+// CHECK:STDOUT:       {
+// CHECK:STDOUT:         "message": "no return expression should be provided in this context",
+// CHECK:STDOUT:         "range": {
+// CHECK:STDOUT:           "end": {
+// CHECK:STDOUT:             "character": 20,
+// CHECK:STDOUT:             "line": 1
+// CHECK:STDOUT:           },
+// CHECK:STDOUT:           "start": {
+// CHECK:STDOUT:             "character": 9,
+// CHECK:STDOUT:             "line": 0
+// CHECK:STDOUT:           }
+// CHECK:STDOUT:         },
+// CHECK:STDOUT:         "severity": 1,
+// CHECK:STDOUT:         "source": "carbon"
+// CHECK:STDOUT:       }
+// CHECK:STDOUT:     ],
+// CHECK:STDOUT:     "uri": "file:///test.carbon",
+// CHECK:STDOUT:     "version": 2
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }

+ 21 - 3
toolchain/language_server/testdata/text_document/open_change_close.carbon

@@ -11,11 +11,11 @@
 // --- STDIN
 [[@LSP-NOTIFY:textDocument/didOpen:
   "textDocument": {"uri": "file:/test.carbon", "languageId": "carbon",
-                   "text": "test"}
+                   "text": "// Empty"}
 ]]
 [[@LSP-NOTIFY:textDocument/didChange:
   "textDocument": {"uri": "file:/test.carbon"},
-  "contentChanges": [{"text": "new content"}]
+  "contentChanges": [{"text": "// Differently empty"}]
 ]]
 [[@LSP-NOTIFY:textDocument/didClose:
   "textDocument": {"uri": "file:/test.carbon"}
@@ -24,4 +24,22 @@
 
 // --- AUTOUPDATE-SPLIT
 
-// 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: }

+ 21 - 3
toolchain/language_server/testdata/text_document/open_duplicate.carbon

@@ -11,11 +11,11 @@
 // --- STDIN
 [[@LSP-NOTIFY:textDocument/didOpen:
   "textDocument": {"uri": "file:/test.carbon", "languageId": "carbon",
-                   "text": "test"}
+                   "text": "// Empty"}
 ]]
 [[@LSP-NOTIFY:textDocument/didOpen:
   "textDocument": {"uri": "file:/test.carbon", "languageId": "carbon",
-                   "text": "test"}
+                   "text": "// Empty"}
 ]]
 [[@LSP-NOTIFY:exit]]
 
@@ -23,4 +23,22 @@
 
 // CHECK:STDERR: /test.carbon: warning: duplicate open file request; updating content [LanguageServerOpenDuplicateFile]
 // CHECK:STDERR:
-// 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: }