Przeglądaj źródła

Add restart LSP command to VSCode extension (#4916)

This PR adds a new command to the Carbon VSCode extension "carbon:
Restart language server" which acts similar to clangd / bazel
extensions. While not useful during regular working of the extension
this helps when LSP either crashes or the toolchain is updated.

In order to support graceful restart, I needed to add a Call handler for
"shutdown" which is noop for now. Lsp clients always call shutdown
before sending exit.

---------

Co-authored-by: jonmeow <jperkins@google.com>
DavidLoftus 1 rok temu
rodzic
commit
2eddfbd7bf

+ 6 - 0
toolchain/language_server/handle.h

@@ -39,6 +39,12 @@ auto HandleInitialize(
     llvm::function_ref<void(llvm::Expected<llvm::json::Object>)> on_done)
     -> void;
 
+// Prepares LSP for shutdown.
+auto HandleShutdown(
+    Context& /*context*/,
+    const clang::clangd::NoParams& /*client_capabilities*/,
+    llvm::function_ref<void(llvm::Expected<std::nullptr_t>)> on_done) -> void;
+
 }  // namespace Carbon::LanguageServer
 
 #endif  // CARBON_TOOLCHAIN_LANGUAGE_SERVER_HANDLE_H_

+ 21 - 0
toolchain/language_server/handle_shutdown.cpp

@@ -0,0 +1,21 @@
+// 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
+
+#include "toolchain/language_server/handle.h"
+
+namespace Carbon::LanguageServer {
+
+auto HandleShutdown(
+    Context& /*context*/,
+    const clang::clangd::NoParams& /*client_capabilities*/,
+    llvm::function_ref<void(llvm::Expected<std::nullptr_t>)> on_done) -> void {
+  // TODO: Track that `shutdown` was called, and:
+  // - Warn on duplicate calls.
+  // - Make `exit` return `1` if `shutdown` wasn't called.
+  //   https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#exit
+  // - Error on other post-`shutdown` calls.
+  on_done(nullptr);
+}
+
+}  // namespace Carbon::LanguageServer

+ 1 - 0
toolchain/language_server/incoming_messages.cpp

@@ -74,6 +74,7 @@ IncomingMessages::IncomingMessages(clang::clangd::Transport* transport,
     : transport_(transport), context_(context) {
   AddCallHandler("textDocument/documentSymbol", &HandleDocumentSymbol);
   AddCallHandler("initialize", &HandleInitialize);
+  AddCallHandler("shutdown", &HandleShutdown);
   AddNotificationHandler("textDocument/didChange",
                          &HandleDidChangeTextDocument);
   AddNotificationHandler("textDocument/didClose", &HandleDidCloseTextDocument);

+ 35 - 0
toolchain/language_server/testdata/basics/fail_shutdown_without_exit.carbon

@@ -0,0 +1,35 @@
+// 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/basics/fail_shutdown_without_exit.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/basics/fail_shutdown_without_exit.carbon
+
+// --- STDIN
+[[@LSP-CALL:initialize]]
+[[@LSP-CALL:shutdown]]
+// --- AUTOUPDATE-SPLIT
+
+// CHECK:STDERR: error: Input/output error [LanguageServerTransportError]
+// 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: 51{{\r}}
+// CHECK:STDOUT: {{\r}}
+// CHECK:STDOUT: {
+// CHECK:STDOUT:   "id": 2,
+// CHECK:STDOUT:   "jsonrpc": "2.0",
+// CHECK:STDOUT:   "result": null
+// CHECK:STDOUT: }

+ 7 - 0
toolchain/language_server/testdata/basics/initialize.carbon

@@ -10,6 +10,7 @@
 
 // --- STDIN
 [[@LSP-CALL:initialize]]
+[[@LSP-CALL:shutdown]]
 [[@LSP-NOTIFY:exit]]
 
 // --- AUTOUPDATE-SPLIT
@@ -25,4 +26,10 @@
 // CHECK:STDOUT:       "textDocumentSync": 1
 // CHECK:STDOUT:     }
 // 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: }

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

@@ -10,11 +10,30 @@
 
 
 // --- STDIN
+[[@LSP-CALL:initialize]]
 [[@LSP-NOTIFY:textDocument/didOpen:"nope": "bad"]]
+[[@LSP-CALL:shutdown]]
 [[@LSP-NOTIFY:exit]]
 
 // --- AUTOUPDATE-SPLIT
 
 // CHECK:STDERR: warning: -32602: in call to `textDocument/didOpen`, JSON parse failed: missing value at (root).textDocument [LanguageServerNotificationParseError]
 // CHECK:STDERR:
-// CHECK:STDOUT:
+// 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: 51{{\r}}
+// CHECK:STDOUT: {{\r}}
+// CHECK:STDOUT: {
+// CHECK:STDOUT:   "id": 2,
+// CHECK:STDOUT:   "jsonrpc": "2.0",
+// CHECK:STDOUT:   "result": null
+// CHECK:STDOUT: }

+ 23 - 4
toolchain/language_server/testdata/document_symbol/basics.carbon

@@ -9,6 +9,7 @@
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/document_symbol/basics.carbon
 
 // --- STDIN
+[[@LSP-CALL:initialize]]
 [[@LSP-NOTIFY:textDocument/didOpen:
   "textDocument": {"uri": "file:/empty.carbon", "languageId": "carbon",
                    "text": ""}
@@ -30,11 +31,23 @@
 [[@LSP-CALL:textDocument/documentSymbol:
   "textDocument": {"uri": "file:/fn.carbon"}
 ]]
+[[@LSP-CALL:shutdown]]
 [[@LSP-NOTIFY:exit]]
 
 // --- AUTOUPDATE-SPLIT
 
-// CHECK:STDOUT: Content-Length: 145{{\r}}
+// 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: 145{{\r}}
 // CHECK:STDOUT: {{\r}}
 // CHECK:STDOUT: {
 // CHECK:STDOUT:   "jsonrpc": "2.0",
@@ -46,7 +59,7 @@
 // CHECK:STDOUT: }Content-Length: 49{{\r}}
 // CHECK:STDOUT: {{\r}}
 // CHECK:STDOUT: {
-// CHECK:STDOUT:   "id": 1,
+// CHECK:STDOUT:   "id": 2,
 // CHECK:STDOUT:   "jsonrpc": "2.0",
 // CHECK:STDOUT:   "result": []
 // CHECK:STDOUT: }Content-Length: 147{{\r}}
@@ -61,7 +74,7 @@
 // CHECK:STDOUT: }Content-Length: 49{{\r}}
 // CHECK:STDOUT: {{\r}}
 // CHECK:STDOUT: {
-// CHECK:STDOUT:   "id": 2,
+// CHECK:STDOUT:   "id": 3,
 // CHECK:STDOUT:   "jsonrpc": "2.0",
 // CHECK:STDOUT:   "result": []
 // CHECK:STDOUT: }Content-Length: 142{{\r}}
@@ -76,7 +89,7 @@
 // CHECK:STDOUT: }Content-Length: 459{{\r}}
 // CHECK:STDOUT: {{\r}}
 // CHECK:STDOUT: {
-// CHECK:STDOUT:   "id": 3,
+// CHECK:STDOUT:   "id": 4,
 // CHECK:STDOUT:   "jsonrpc": "2.0",
 // CHECK:STDOUT:   "result": [
 // CHECK:STDOUT:     {
@@ -104,4 +117,10 @@
 // CHECK:STDOUT:       }
 // CHECK:STDOUT:     }
 // CHECK:STDOUT:   ]
+// CHECK:STDOUT: }Content-Length: 51{{\r}}
+// CHECK:STDOUT: {{\r}}
+// CHECK:STDOUT: {
+// CHECK:STDOUT:   "id": 5,
+// CHECK:STDOUT:   "jsonrpc": "2.0",
+// CHECK:STDOUT:   "result": null
 // CHECK:STDOUT: }

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

@@ -9,13 +9,32 @@
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/document_symbol/language.carbon
 
 // --- STDIN
+[[@LSP-CALL:initialize]]
 [[@LSP-CALL:textDocument/documentSymbol:
   "textDocument": {"uri": "file:/test.cpp"}
 ]]
+[[@LSP-CALL:shutdown]]
 [[@LSP-NOTIFY:exit]]
 
 // --- AUTOUPDATE-SPLIT
 
 // CHECK:STDERR: /test.cpp: warning: non-Carbon file requested [LanguageServerFileUnsupported]
 // CHECK:STDERR:
-// CHECK:STDOUT:
+// 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: 51{{\r}}
+// CHECK:STDOUT: {{\r}}
+// CHECK:STDOUT: {
+// CHECK:STDOUT:   "id": 3,
+// CHECK:STDOUT:   "jsonrpc": "2.0",
+// CHECK:STDOUT:   "result": null
+// CHECK:STDOUT: }

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

@@ -9,13 +9,32 @@
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/document_symbol/unknown.carbon
 
 // --- STDIN
+[[@LSP-CALL:initialize]]
 [[@LSP-CALL:textDocument/documentSymbol:
   "textDocument": {"uri": "file:/test.carbon"}
 ]]
+[[@LSP-CALL:shutdown]]
 [[@LSP-NOTIFY:exit]]
 
 // --- AUTOUPDATE-SPLIT
 
 // CHECK:STDERR: /test.carbon: warning: unknown file requested [LanguageServerFileUnknown]
 // CHECK:STDERR:
-// CHECK:STDOUT:
+// 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: 51{{\r}}
+// CHECK:STDOUT: {{\r}}
+// CHECK:STDOUT: {
+// CHECK:STDOUT:   "id": 3,
+// CHECK:STDOUT:   "jsonrpc": "2.0",
+// CHECK:STDOUT:   "result": null
+// CHECK:STDOUT: }

+ 20 - 1
toolchain/language_server/testdata/text_document/change_count.carbon

@@ -9,6 +9,7 @@
 // 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"}
@@ -28,6 +29,7 @@
      "text": "a"}
   ]
 ]]
+[[@LSP-CALL:shutdown]]
 [[@LSP-NOTIFY:exit]]
 
 // --- AUTOUPDATE-SPLIT
@@ -36,7 +38,18 @@
 // CHECK:STDERR:
 // CHECK:STDERR: /test.carbon: warning: received unsupported contentChanges count: 2 [LanguageServerUnsupportedChanges]
 // CHECK:STDERR:
-// CHECK:STDOUT: Content-Length: 144{{\r}}
+// 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",
@@ -45,4 +58,10 @@
 // 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: }

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

@@ -9,14 +9,33 @@
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/text_document/change_unknown.carbon
 
 // --- STDIN
+[[@LSP-CALL:initialize]]
 [[@LSP-NOTIFY:textDocument/didChange:
   "textDocument": {"uri": "file:/test.carbon"},
   "contentChanges": [{"text": "new content"}]
 ]]
+[[@LSP-CALL:shutdown]]
 [[@LSP-NOTIFY:exit]]
 
 // --- AUTOUPDATE-SPLIT
 
 // CHECK:STDERR: /test.carbon: warning: unknown file requested [LanguageServerFileUnknown]
 // CHECK:STDERR:
-// CHECK:STDOUT:
+// 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: 51{{\r}}
+// CHECK:STDOUT: {{\r}}
+// CHECK:STDOUT: {
+// CHECK:STDOUT:   "id": 2,
+// CHECK:STDOUT:   "jsonrpc": "2.0",
+// CHECK:STDOUT:   "result": null
+// CHECK:STDOUT: }

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

@@ -9,13 +9,32 @@
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/text_document/close_unknown.carbon
 
 // --- STDIN
+[[@LSP-CALL:initialize]]
 [[@LSP-NOTIFY:textDocument/didClose:
   "textDocument": {"uri": "file:/test.carbon"}
 ]]
+[[@LSP-CALL:shutdown]]
 [[@LSP-NOTIFY:exit]]
 
 // --- AUTOUPDATE-SPLIT
 
 // CHECK:STDERR: /test.carbon: warning: tried closing unknown file; ignoring request [LanguageServerCloseUnknownFile]
 // CHECK:STDERR:
-// CHECK:STDOUT:
+// 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: 51{{\r}}
+// CHECK:STDOUT: {{\r}}
+// CHECK:STDOUT: {
+// CHECK:STDOUT:   "id": 2,
+// CHECK:STDOUT:   "jsonrpc": "2.0",
+// CHECK:STDOUT:   "result": null
+// CHECK:STDOUT: }

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

@@ -9,6 +9,7 @@
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/text_document/diagnostics.carbon
 
 // --- STDIN
+[[@LSP-CALL:initialize]]
 [[@LSP-NOTIFY:textDocument/didOpen:
   "textDocument": {"uri": "file:/test.carbon", "languageId": "carbon",
                    "version": 1, "text": "{"}
@@ -20,11 +21,23 @@
 [[@LSP-NOTIFY:textDocument/didClose:
   "textDocument": {"uri": "file:/test.carbon"}
 ]]
+[[@LSP-CALL:shutdown]]
 [[@LSP-NOTIFY:exit]]
 
 // --- AUTOUPDATE-SPLIT
 
-// CHECK:STDOUT: Content-Length: 1164{{\r}}
+// 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: 1164{{\r}}
 // CHECK:STDOUT: {{\r}}
 // CHECK:STDOUT: {
 // CHECK:STDOUT:   "jsonrpc": "2.0",
@@ -106,4 +119,10 @@
 // CHECK:STDOUT:     "uri": "file:///test.carbon",
 // CHECK:STDOUT:     "version": 2
 // 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: }

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

@@ -9,6 +9,7 @@
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/text_document/open_change_close.carbon
 
 // --- STDIN
+[[@LSP-CALL:initialize]]
 [[@LSP-NOTIFY:textDocument/didOpen:
   "textDocument": {"uri": "file:/test.carbon", "languageId": "carbon",
                    "text": "// Empty"}
@@ -20,11 +21,23 @@
 [[@LSP-NOTIFY:textDocument/didClose:
   "textDocument": {"uri": "file:/test.carbon"}
 ]]
+[[@LSP-CALL:shutdown]]
 [[@LSP-NOTIFY:exit]]
 
 // --- AUTOUPDATE-SPLIT
 
-// CHECK:STDOUT: Content-Length: 144{{\r}}
+// 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",
@@ -42,4 +55,10 @@
 // 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: }

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

@@ -9,6 +9,7 @@
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/text_document/open_duplicate.carbon
 
 // --- STDIN
+[[@LSP-CALL:initialize]]
 [[@LSP-NOTIFY:textDocument/didOpen:
   "textDocument": {"uri": "file:/test.carbon", "languageId": "carbon",
                    "text": "// Empty"}
@@ -17,13 +18,25 @@
   "textDocument": {"uri": "file:/test.carbon", "languageId": "carbon",
                    "text": "// Empty"}
 ]]
+[[@LSP-CALL:shutdown]]
 [[@LSP-NOTIFY:exit]]
 
 // --- AUTOUPDATE-SPLIT
 
 // CHECK:STDERR: /test.carbon: warning: duplicate open file request; updating content [LanguageServerOpenDuplicateFile]
 // CHECK:STDERR:
-// CHECK:STDOUT: Content-Length: 144{{\r}}
+// 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",
@@ -41,4 +54,10 @@
 // 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: }

+ 7 - 1
utils/vscode/package.json

@@ -46,7 +46,13 @@
           "default": "./bazel-bin/toolchain/carbon"
         }
       }
-    }
+    },
+    "commands": [
+      {
+        "command": "carbon.lsp.restart",
+        "title": "carbon: Restart language server"
+      }
+    ]
   },
   "scripts": {
     "vscode:prepublish": "npm run package",

+ 7 - 1
utils/vscode/src/extension.ts

@@ -8,7 +8,7 @@
  * This is the main launcher for the LSP extension.
  */
 
-import { workspace, ExtensionContext } from 'vscode';
+import { workspace, ExtensionContext, commands } from 'vscode';
 
 import {
   LanguageClient,
@@ -43,6 +43,12 @@ export function activate(context: ExtensionContext) {
     clientOptions
   );
   client.start();
+
+  context.subscriptions.push(
+    commands.registerCommand('carbon.lsp.restart', () => {
+      client.restart();
+    })
+  );
 }
 
 export function deactivate(): Thenable<void> | undefined {