VSCode Extension Architecture
This document describes the architecture of the slang-autos VSCode extension, following Microsoft’s recommended patterns for Language Server Protocol (LSP) integration.
Overview
The slang-autos VSCode extension provides editor integration for expanding verilog-mode style AUTO macros. It follows a client-server architecture where:
Extension (Client): TypeScript code running in VSCode’s Extension Host
Language Server: C++ executable communicating via LSP over stdin/stdout
This separation allows the heavy lifting (parsing SystemVerilog, expanding AUTOs) to happen in a separate process, keeping the editor responsive.
Architecture Diagram
┌─────────────────────────────────────────────────────────────────┐
│ VSCode │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Extension Host │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ slang-autos Extension │ │ │
│ │ │ │ │ │
│ │ │ User Action (Ctrl+Shift+A / Cmd+Shift+A) │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ slang-autos.expand (UI command) │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ vscode.commands.executeCommand( │ │ │
│ │ │ 'slang-autos.expandAutos', uri) │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ LanguageClient intercepts │ │ │
│ │ │ (command in executeCommandProvider) │ │ │
│ │ │ │ │ │ │
│ │ └─────────│─────────────────────────────────────────┘ │ │
│ └────────────│────────────────────────────────────────────┘ │
└───────────────│───────────────────────────────────────────────┘
│ JSON-RPC over stdin/stdout
│ workspace/executeCommand
▼
┌───────────────────────────────────────────────────────────────┐
│ slang-autos-lsp (C++) │
│ │
│ workspace/executeCommand handler │
│ │ │
│ ▼ │
│ expandAutos(uri) ──► Parse file with slang │
│ │ Expand AUTO macros │
│ │ Generate WorkspaceEdit │
│ ▼ │
│ Return WorkspaceEdit │
└───────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ Extension applies WorkspaceEdit to document │
└───────────────────────────────────────────────────────────────┘
Command Flow Pattern
The extension uses Microsoft’s recommended workspace/executeCommand pattern
for triggering server-side operations. This involves two layers of commands:
UI Commands (User-Facing)
These are registered in package.json and appear in the command palette:
{
"commands": [
{
"command": "slang-autos.expand",
"title": "Expand AUTOs",
"category": "slang-autos"
},
{
"command": "slang-autos.delete",
"title": "Delete AUTOs",
"category": "slang-autos"
},
{
"command": "slang-autos.restartServer",
"title": "Restart Language Server",
"category": "slang-autos"
}
]
}
The extension registers handlers for these commands that:
Validate preconditions (LSP running, file open)
Save the document
Call the LSP command via
vscode.commands.executeCommand()Apply the returned
WorkspaceEdit
LSP Commands (Server-Side)
These are advertised by the server in its executeCommandProvider capability:
{
"capabilities": {
"executeCommandProvider": {
"commands": ["slang-autos.expandAutos", "slang-autos.deleteAutos"]
}
}
}
When the client calls vscode.commands.executeCommand('slang-autos.expandAutos', uri),
the LanguageClient library automatically:
Recognizes the command is in
executeCommandProviderSends a
workspace/executeCommandrequest to the serverReturns the server’s response to the caller
Why This Pattern?
Microsoft recommends this pattern for several reasons:
Automatic Routing
The vscode-languageclient library automatically handles routing when you
call vscode.commands.executeCommand() for commands advertised by the server.
You don’t need to manually construct JSON-RPC messages.
Command Discovery
Commands in executeCommandProvider are discoverable. Other extensions can
call your commands, and they appear in LSP logs as standard command executions.
Consistency
This matches how other LSP features work (hover, completion, etc.) - the client makes a request, the server processes it, and returns a result.
Alternative: Custom Methods
LSP also allows custom request methods (e.g., slang-autos/expand). While
valid, this approach:
Requires explicit
client.sendRequest()callsBypasses VSCode’s command routing
Is better suited for internal implementation details
The workspace/executeCommand pattern is preferred when:
The operation should be user-triggerable
Other extensions might want to call it
You want standard LSP tooling to recognize the command
Extension Implementation
package.json
{
"activationEvents": [
"onLanguage:verilog",
"onLanguage:systemverilog"
],
"contributes": {
"commands": [
{
"command": "slang-autos.expand",
"title": "Expand AUTOs",
"category": "slang-autos"
}
],
"menus": {
"commandPalette": [
{
"command": "slang-autos.expand",
"when": "editorLangId == verilog || editorLangId == systemverilog"
}
]
},
"keybindings": [
{
"command": "slang-autos.expand",
"key": "ctrl+shift+a",
"mac": "cmd+shift+a",
"when": "editorTextFocus && (editorLangId == verilog || editorLangId == systemverilog)"
}
]
}
}
extension.ts
import * as vscode from 'vscode';
import { LanguageClient, State } from 'vscode-languageclient/node';
let client: LanguageClient;
export async function activate(context: vscode.ExtensionContext) {
// Register UI command FIRST (before async LSP startup)
const expandCmd = vscode.commands.registerCommand('slang-autos.expand', async () => {
if (!client || client.state !== State.Running) {
vscode.window.showErrorMessage('LSP not running');
return;
}
const editor = vscode.window.activeTextEditor;
if (!editor) return;
await editor.document.save();
const uri = editor.document.uri.toString();
// Call LSP command via vscode.commands.executeCommand
// LanguageClient routes this to workspace/executeCommand
const result = await vscode.commands.executeCommand<WorkspaceEdit>(
'slang-autos.expandAutos',
uri
);
// Apply the returned edit
if (result?.changes) {
await vscode.workspace.applyEdit(convertEdit(result));
}
});
context.subscriptions.push(expandCmd);
// Start language client
client = new LanguageClient('slang-autos', 'slang-autos LSP', serverOptions, clientOptions);
await client.start();
}
Server Implementation
The C++ server registers commands via registerCommand:
AutosServer.cpp
lsp::InitializeResult AutosServer::getInitialize(const lsp::InitializeParams& params) {
// Register workspace/executeCommand handler
registerWorkspaceExecuteCommand();
// Register commands - advertised in executeCommandProvider
registerCommand<std::string, lsp::WorkspaceEdit,
&AutosServer::expandAutos>("slang-autos.expandAutos");
registerCommand<std::string, lsp::WorkspaceEdit,
&AutosServer::deleteAutos>("slang-autos.deleteAutos");
return lsp::InitializeResult{
.capabilities = lsp::ServerCapabilities{
.executeCommandProvider = lsp::ExecuteCommandOptions{
.commands = getCommandList(), // Returns registered command names
},
},
.serverInfo = lsp::ServerInfo{
.name = "slang-autos-lsp",
.version = "0.1.0",
},
};
}
lsp::WorkspaceEdit AutosServer::expandAutos(const std::string& fileUri) {
// Parse file, expand AUTOs, return WorkspaceEdit
// ...
}
LSP Lifecycle
The server implements the full LSP lifecycle:
initialize - Exchange capabilities, register handlers
initialized - Server ready for requests
workspace/executeCommand - Handle command requests
shutdown - Prepare for exit
exit - Terminate process
AutosServer::AutosServer() {
registerInitialize();
registerInitialized();
registerShutdown();
registerExit();
}
Error Handling
The extension monitors server state for crash recovery:
client.onDidChangeState(({ oldState, newState }) => {
if (oldState === State.Running && newState === State.Stopped) {
vscode.window.showErrorMessage(
'slang-autos LSP has stopped unexpectedly.',
'Restart'
).then(selection => {
if (selection === 'Restart') {
vscode.commands.executeCommand('slang-autos.restartServer');
}
});
}
});
Testing
The LSP server can be tested directly via stdin/stdout:
cat << 'EOF' | ./slang-autos-lsp
Content-Length: 75
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{}}}
Content-Length: 52
{"jsonrpc":"2.0","method":"initialized","params":{}}
Content-Length: 149
{"jsonrpc":"2.0","id":2,"method":"workspace/executeCommand","params":{"command":"slang-autos.expandAutos","arguments":["file:///path/to/file.sv"]}}
EOF