diff --git a/.vscodeignore b/.vscodeignore index a0ebbc0..0fce4c5 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -18,3 +18,6 @@ eslint.config.mjs justfile tsconfig.json webpack.config.js + +# Explicitly include bin directory for bundled just-lsp binaries +!bin/** diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e243ed..b17ebb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Or operator `||` from `just` release 1.37.0 - F-string specifier from `just` release 1.44.0 - Leverage VSCode's built-in formatting API for `justfile` formatting +- Integrate `just-lsp` with extension ## [0.8.0] - 2025-01-02 diff --git a/README.md b/README.md index 6c1cfaf..2d26d26 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Basic syntax highlighting for `just` files: - Recipes: recipe attributes, names, params and dependencies - Keywords, constants and operators - Some embedded languages +- Integration with [just-lsp](https://github.com/terror/just-lsp) Commands: @@ -33,7 +34,6 @@ Commands: - Run recipe - Task running - Demo: - VSCode with `just` syntax highlighting @@ -107,7 +107,6 @@ Outstanding: - [ ] Improve handling of recipe body highlighting - [ ] Improve handling of embedded languages -- [ ] Integrate [just-lsp](https://github.com/terror/just-lsp) - [ ] Inline command runner - [ ] Support code outline @@ -121,14 +120,7 @@ Completed: - [x] Format on save - [x] Run recipe - [x] Default formatter support - -### Beyond - -- Semantic highlighting / LSP - - To avoid implementing a parser for files, it would be ideal for `just` to expose the AST or other APIs for editor extensions to leverage. This would allow for more advanced features like semantic highlighting, code folding, and more. - - If VSCode works to support tree-sitter, [that](https://github.com/IndianBoy42/tree-sitter-just) would be a possible alternative. +- [x] Integrate [just-lsp](https://github.com/terror/just-lsp) ## Contributing diff --git a/package.json b/package.json index 8c54a06..511bd66 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,11 @@ "default": "just", "description": "Path to just binary." }, + "vscode-just.lspPath": { + "type": "string", + "default": "just-lsp", + "description": "Path to just-lsp binary." + }, "vscode-just.runInTerminal": { "type": "boolean", "default": false, @@ -146,6 +151,7 @@ }, "dependencies": { "@types/yargs-parser": "^21.0.3", + "vscode-languageclient": "^9.0.1", "yargs-parser": "^21.1.1" } } diff --git a/src/const.ts b/src/const.ts index 3d2d406..7111fbe 100644 --- a/src/const.ts +++ b/src/const.ts @@ -5,6 +5,7 @@ export const COMMANDS = { }; export const SETTINGS = { justPath: 'justPath', + lspPath: 'lspPath', runInTerminal: 'runInTerminal', useSingleTerminal: 'useSingleTerminal', logLevel: 'logLevel', diff --git a/src/extension.ts b/src/extension.ts index e418f06..e97042b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,6 +4,7 @@ import { COMMANDS, EXTENSION_NAME } from './const'; import { formatJustfileTempFile } from './format'; import { getLauncher } from './launcher'; import { getLogger } from './logger'; +import { createLanguageClient, stopLanguageClient } from './lsp'; import { runRecipeCommand } from './recipe'; import { TaskProvider } from './tasks'; @@ -37,21 +38,22 @@ export const activate = (context: vscode.ExtensionContext) => { }), ); - const runRecipeDisposable = vscode.commands.registerCommand( - COMMANDS.runRecipe, - async () => { + context.subscriptions.push( + vscode.commands.registerCommand(COMMANDS.runRecipe, async () => { runRecipeCommand(); - }, + }), ); - context.subscriptions.push(runRecipeDisposable); context.subscriptions.push( vscode.tasks.registerTaskProvider(EXTENSION_NAME, new TaskProvider()), ); + + createLanguageClient(); }; export const deactivate = () => { console.debug(`${EXTENSION_NAME} deactivated`); getLogger().dispose(); getLauncher().dispose(); + stopLanguageClient(); }; diff --git a/src/lsp.ts b/src/lsp.ts new file mode 100644 index 0000000..7f41a5c --- /dev/null +++ b/src/lsp.ts @@ -0,0 +1,102 @@ +import { spawn } from 'child_process'; +import * as vscode from 'vscode'; +import { + LanguageClient, + LanguageClientOptions, + ServerOptions, +} from 'vscode-languageclient/node'; + +import { EXTENSION_NAME, SETTINGS } from './const'; +import { getLogger } from './logger'; + +const LOGGER = getLogger(); + +let client: LanguageClient | undefined; + +export const createLanguageClient = async (): Promise => { + const lspPath = getLspPath(); + + const isAvailable = await checkLspAvailability(lspPath); + if (!isAvailable) { + vscode.window + .showWarningMessage( + `Just LSP binary found but not working: ${lspPath}. Please check the installation or configure the path in settings.`, + 'Install Instructions', + ) + .then((selection) => { + if (selection === 'Install Instructions') { + vscode.env.openExternal( + vscode.Uri.parse('https://github.com/terror/just-lsp#installation'), + ); + } + }); + + return null; + } + + const serverOptions: ServerOptions = { + command: lspPath, + args: [], + }; + + const clientOptions: LanguageClientOptions = { + documentSelector: [{ scheme: 'file', language: 'just' }], + synchronize: { + fileEvents: vscode.workspace.createFileSystemWatcher( + '**/{justfile,Justfile,.justfile,*.just}', + ), + }, + }; + + client = new LanguageClient( + 'just-lsp', + 'Just Language Server', + serverOptions, + clientOptions, + ); + + try { + await client.start(); + LOGGER.info(`Just LSP started successfully using: ${lspPath}`); + return client; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + LOGGER.error(`Failed to start Just LSP: ${message}`); + vscode.window.showErrorMessage(`Failed to start Just Language Server: ${message}`); + return null; + } +}; + +export const stopLanguageClient = async (): Promise => { + if (client) { + await client.stop(); + client = undefined; + } +}; + +const checkLspAvailability = (lspPath: string): Promise => { + return new Promise((resolve) => { + const process = spawn(lspPath, ['--version'], { stdio: 'ignore' }); + + process.on('close', (code: number) => { + resolve(code === 0); + }); + process.on('error', () => { + resolve(false); + }); + + setTimeout(() => { + process.kill(); + resolve(false); + }, 5000); + }); +}; + +const getLspPath = (): string => { + // TODO: support bundled LSP binary + return ( + (vscode.workspace + .getConfiguration(EXTENSION_NAME) + .get(SETTINGS.lspPath) as string) || 'just-lsp' + ); +}; diff --git a/yarn.lock b/yarn.lock index 7d84577..0572418 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2351,7 +2351,7 @@ minimatch@^3.0.3, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1, minimatch@^5.1.6: +minimatch@^5.0.1, minimatch@^5.1.0, minimatch@^5.1.6: version "5.1.6" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== @@ -2870,6 +2870,11 @@ semver@^7.3.4, semver@^7.3.5, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semve resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +semver@^7.3.7: + version "7.7.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" + integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== + serialize-javascript@^6.0.1, serialize-javascript@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" @@ -2982,7 +2987,16 @@ stoppable@^1.1.0: resolved "https://registry.yarnpkg.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b" integrity sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -3023,7 +3037,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -3273,6 +3294,33 @@ v8-to-istanbul@^9.0.0: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^2.0.0" +vscode-jsonrpc@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" + integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== + +vscode-languageclient@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz#cdfe20267726c8d4db839dc1e9d1816e1296e854" + integrity sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA== + dependencies: + minimatch "^5.1.0" + semver "^7.3.7" + vscode-languageserver-protocol "3.17.5" + +vscode-languageserver-protocol@3.17.5: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea" + integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== + dependencies: + vscode-jsonrpc "8.2.0" + vscode-languageserver-types "3.17.5" + +vscode-languageserver-types@3.17.5: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" + integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== + vscode-oniguruma@^1.5.1: version "1.7.0" resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz#439bfad8fe71abd7798338d1cd3dc53a8beea94b" @@ -3400,7 +3448,16 @@ workerpool@^6.5.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==