diff --git a/package-lock.json b/package-lock.json index 4fbc738..4cbaa1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,10 @@ "": { "name": "stencila", "version": "0.0.1", + "dependencies": { + "vscode-languageclient": "^9.0.1", + "vscode-languageserver": "^9.0.1" + }, "devDependencies": { "@types/mocha": "^10.0.6", "@types/node": "18.x", @@ -686,8 +690,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/binary-extensions": { "version": "2.3.0", @@ -705,7 +708,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -2447,7 +2449,6 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -2462,7 +2463,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -2817,6 +2817,63 @@ "node": ">=10.12.0" } }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", + "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", + "dependencies": { + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.5" + }, + "engines": { + "vscode": "^1.82.0" + } + }, + "node_modules/vscode-languageclient/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2950,8 +3007,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yargs": { "version": "17.7.2", diff --git a/package.json b/package.json index e0b4760..be42995 100644 --- a/package.json +++ b/package.json @@ -81,5 +81,9 @@ "eslint": "^8.57.0", "js-yaml": "^4.1.0", "typescript": "^5.3.3" + }, + "dependencies": { + "vscode-languageclient": "^9.0.1", + "vscode-languageserver": "^9.0.1" } } diff --git a/src/extension.ts b/src/extension.ts index eb22c2d..3de7d5a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,26 +1,63 @@ -// The module 'vscode' contains the VS Code extensibility API -// Import the module and reference it with the alias vscode in your code below -import * as vscode from 'vscode'; +import * as path from "path"; -// This method is called when your extension is activated -// Your extension is activated the very first time the command is executed -export function activate(context: vscode.ExtensionContext) { +import * as vscode from "vscode"; +import { + LanguageClient, + LanguageClientOptions, + ServerOptions, + TransportKind, +} from "vscode-languageclient/node"; + +let client: LanguageClient; - // Use the console to output diagnostic information (console.log) and errors (console.error) - // This line of code will only be executed once when your extension is activated - console.log('Congratulations, your extension "stencila" is now active!'); +/** + * Activate the extension + */ +export function activate(context: vscode.ExtensionContext) { + // Use the console to output diagnostic information (console.log) and errors (console.error) + // This line of code will only be executed once when your extension is activated + console.log('Congratulations, your extension "stencila" is now active!'); - // The command has been defined in the package.json file - // Now provide the implementation of the command with registerCommand - // The commandId parameter must match the command field in package.json - let disposable = vscode.commands.registerCommand('stencila.helloWorld', () => { - // The code you place here will be executed every time your command is executed - // Display a message box to the user - vscode.window.showInformationMessage('Hello World from Stencila!'); - }); + // The command has been defined in the package.json file + // Now provide the implementation of the command with registerCommand + // The commandId parameter must match the command field in package.json + let disposable = vscode.commands.registerCommand( + "stencila.helloWorld", + () => { + // The code you place here will be executed every time your command is executed + // Display a message box to the user + vscode.window.showInformationMessage("Hello World from Stencila!"); + } + ); + context.subscriptions.push(disposable); - context.subscriptions.push(disposable); + // Start the language server client + const serverModule = context.asAbsolutePath(path.join("out", "server.js")); + const serverOptions: ServerOptions = { + run: { module: serverModule, transport: TransportKind.ipc }, + debug: { + module: serverModule, + transport: TransportKind.ipc, + }, + }; + const clientOptions: LanguageClientOptions = { + documentSelector: [{ scheme: "file", language: "smd" }], + }; + client = new LanguageClient( + "stencila", + "Stencila", + serverOptions, + clientOptions + ); + client.start(); } -// This method is called when your extension is deactivated -export function deactivate() {} +/** + * Deactivate the extension + */ +export function deactivate() { + if (!client) { + return undefined; + } + return client.stop(); +} diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..c641858 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,28 @@ +/** + * A module to run the Stencila language server from within Node.js + * + * Rather than bundling the Stencila CLI binary with this VSCode extension + * and spawning `stencila lsp` this script calls Stencila LSP functions + * exposed by the `@stencila/node` bindings. This has two main advantages: + * + * - distributing `@stencila/node` is easier than distributing `stencila` binaries + * + * - we can prototype interactions between VSCode and the Stencila language server + * i.e. event handlers in this module act as mocks for what the Rust-based + * language server function will implement + */ + +import { createConnection } from "vscode-languageserver/node"; + +// Custom logging functions for better visibility in server outputs +const debug = (...args: any[]) => process.stderr.write(`DEBUG ${JSON.stringify(args)}`); +const info = (...args: any[]) => process.stderr.write(`INFO ${JSON.stringify(args)}`); +const warning = (...args: any[]) => process.stderr.write(`WARN ${JSON.stringify(args)}`); +const error = (...args: any[]) => process.stderr.write(`ERROR ${JSON.stringify(args)}`); + +// Create a connection for the server, using Node's IPC as a transport. +const connection = createConnection(); + +// Listen on the connection +info("Listening on LSP connection"); +connection.listen();