From ba03b535892b16771fabbc8928a0f7e7e29c21be Mon Sep 17 00:00:00 2001 From: Miles Ziemer Date: Fri, 28 Mar 2025 19:51:54 -0400 Subject: [PATCH 01/11] Add run command Adds a config option to specify a specific command to run the language server. When not set, the extension falls back to running with coursier. By default, the config option is not set. I need to clean up the config option a bit, since when you specify a specific command, you can just use stdio, meaning the arguments passed to the language server should just be "0". So really no args should be set in the config option. --- TODO.md | 2 + package.json | 21 ++++ src/extension.ts | 266 +++++++++++++++++++++++++---------------------- 3 files changed, 164 insertions(+), 125 deletions(-) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..ceba977 --- /dev/null +++ b/TODO.md @@ -0,0 +1,2 @@ +I don't like single name imports. Would rather them be qualified names. But I'm +not sure if that will cause everything to be imported, so I should check. diff --git a/package.json b/package.json index 6be6b85..9041a5c 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,27 @@ "type": "boolean", "default": false, "description": "Whether to only re-load the Smithy model on save. Use this if the server feels slow as you type." + }, + "smithyLsp.runCmd": { + "type": "object", + "default": null, + "required": [ + "command", + "args" + ], + "properties": { + "command": { + "type": "string", + "description": "The command to run the language server" + }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The arguments to pass to the language server" + } + } } } } diff --git a/src/extension.ts b/src/extension.ts index 7ee69d0..7105712 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,19 +2,10 @@ import * as net from 'net'; import * as fs from 'fs'; import * as child_process from 'child_process'; import * as vscode from 'vscode'; +import * as lsp from 'vscode-languageclient/node'; import { SelectorDecorator } from './selector/selector-decorator'; import { selectorRunCommandHandler, selectorClearCommandHandler } from './selector/selector-command-handlers'; -import { - CancellationToken, - DocumentFormattingRequest, - LanguageClient, - LanguageClientOptions, - RequestType, - RevealOutputChannelOn, - StreamInfo, - TextDocumentIdentifier, -} from 'vscode-languageclient/node'; import { getCoursierExecutable } from './coursier/coursier'; // Couriser uses an index to determine where to download jvms from: https://get-coursier.io/docs/2.0.6/cli-java#jvm-index @@ -25,110 +16,136 @@ import { getCoursierExecutable } from './coursier/coursier'; // as a standalone executable, and will no longer need couriser to manage the jvm version. const COURSIER_JVM_INDEX = 'https://raw.githubusercontent.com/coursier/jvm-index/master/index.json'; -let client: LanguageClient; +let client: lsp.LanguageClient; -export function activate(context: vscode.ExtensionContext) { - async function createServer(): Promise { - function startServer(executable: string): Promise { - console.log(`Executable located at ${executable}.`); - return new Promise((resolve, reject) => { - const server = net - .createServer((socket) => { - console.log('Creating server'); - - resolve({ - reader: socket, - writer: socket, - }); +type RunCmd = { + command: string; + args: string[]; +}; - socket.on('end', () => console.log('Disconnected')); - }) - .on('error', (err) => { - // handle errors here - reject(err); - }); +function runWithCmd(context: vscode.ExtensionContext, cmd: RunCmd): lsp.StreamInfo { + const options = { cwd: context.extensionPath }; + const cp = child_process.spawn(cmd.command, cmd.args, options); + + return { + writer: cp.stdin, + reader: cp.stdout, + }; +} - // grab a random port. - server.listen(() => { - // Start the child java process - let options = { cwd: context.extensionPath }; - - let port = (server.address() as net.AddressInfo).port; - - let version = vscode.workspace.getConfiguration('smithyLsp').get('version', '`'); - - // Downloading latest poms - let resolveArgs = [ - 'resolve', - '--mode', - 'force', - 'software.amazon.smithy:smithy-language-server:' + version, - '-r', - 'm2local', - ]; - let resolveProcess = child_process.spawn(executable, resolveArgs, options); - console.log(resolveArgs); - resolveProcess.on('exit', (exitCode) => { - console.log('Exit code : ' + exitCode); - if (exitCode == 0) { - console.log('Launching smithy-language-server version:' + version); - - let launchargs = [ - 'launch', - 'software.amazon.smithy:smithy-language-server:' + version, - // Configure couriser to use java 21 - '--jvm', - // By default, coursier uses AdoptOpenJDK: https://get-coursier.io/docs/2.0.6/cli-java - // We could just say '21' here, and let coursier default to adopt jdk - // 21, but later versions of the jdk are released under the name adoptium. - 'corretto:21', - // The location to download the jvm from is provided by the jvm index. - '--jvm-index', - COURSIER_JVM_INDEX, - '-r', - 'm2local', - '-M', - 'software.amazon.smithy.lsp.Main', - '--', - port.toString(), - ]; - - console.log(launchargs); - - let childProcess = child_process.spawn(executable, launchargs, options); - - childProcess.stdout.on('data', (data) => { - console.log(`stdout: ${data}`); - }); - - childProcess.stderr.on('data', (data) => { - console.error(`stderr: ${data}`); - }); - - childProcess.on('close', (code) => { - console.log(`LSP exited with code ${code}`); - }); - } else { - console.log(`Could not resolve smithy-language-server implementation`); - } +async function runWithCoursier(context: vscode.ExtensionContext): Promise { + function startServer(executable: string): Promise { + console.log(`Executable located at ${executable}.`); + return new Promise((resolve, reject) => { + const server = net + .createServer((socket) => { + console.log('Creating server'); + + resolve({ + reader: socket, + writer: socket, }); - // Send raw output to a file - if (context.storageUri) { - if (!fs.existsSync(context.storageUri.fsPath)) { - fs.mkdirSync(context.storageUri.fsPath); - } + socket.on('end', () => console.log('Disconnected')); + }) + .on('error', (err) => { + // handle errors here + reject(err); + }); + + // grab a random port. + server.listen(() => { + // Start the child java process + let options = { cwd: context.extensionPath }; + + let port = (server.address() as net.AddressInfo).port; + + let version = vscode.workspace.getConfiguration('smithyLsp').get('version', '`'); + + // Downloading latest poms + let resolveArgs = [ + 'resolve', + '--mode', + 'force', + 'software.amazon.smithy:smithy-language-server:' + version, + '-r', + 'm2local', + ]; + let resolveProcess = child_process.spawn(executable, resolveArgs, options); + console.log(resolveArgs); + resolveProcess.on('exit', (exitCode) => { + console.log('Exit code : ' + exitCode); + if (exitCode == 0) { + console.log('Launching smithy-language-server version:' + version); + + let launchargs = [ + 'launch', + 'software.amazon.smithy:smithy-language-server:' + version, + // Configure couriser to use java 21 + '--jvm', + // By default, coursier uses AdoptOpenJDK: https://get-coursier.io/docs/2.0.6/cli-java + // We could just say '21' here, and let coursier default to adopt jdk + // 21, but later versions of the jdk are released under the name adoptium. + 'corretto:21', + // The location to download the jvm from is provided by the jvm index. + '--jvm-index', + COURSIER_JVM_INDEX, + '-r', + 'm2local', + '-M', + 'software.amazon.smithy.lsp.Main', + '--', + port.toString(), + ]; + + console.log(launchargs); + + let childProcess = child_process.spawn(executable, launchargs, options); + + childProcess.stdout.on('data', (data) => { + console.log(`stdout: ${data}`); + }); + + childProcess.stderr.on('data', (data) => { + console.error(`stderr: ${data}`); + }); + + childProcess.on('close', (code) => { + console.log(`LSP exited with code ${code}`); + }); + } else { + console.log(`Could not resolve smithy-language-server implementation`); } }); + + // Send raw output to a file + if (context.storageUri) { + if (!fs.existsSync(context.storageUri.fsPath)) { + fs.mkdirSync(context.storageUri.fsPath); + } + } }); - } + }); + } + + const binaryPath = await getCoursierExecutable(context.globalStoragePath); + return await startServer(binaryPath); +} - const binaryPath = await getCoursierExecutable(context.globalStoragePath); - return await startServer(binaryPath); +export function activate(context: vscode.ExtensionContext) { + async function createServer(): Promise { + const cmd: RunCmd | undefined = vscode.workspace.getConfiguration('smithyLsp').get('runCmd'); + if (cmd) { + console.log('Running with command: ', JSON.stringify(cmd, null, 4)); + return runWithCmd(context, cmd); + } else { + console.log('Running with coursier'); + return await runWithCoursier(context); + } } // Create the language client and start the client. - client = new LanguageClient('smithyLsp', 'Smithy LSP', createServer, getClientOptions()); + client = new lsp.LanguageClient('smithyLsp', 'Smithy LSP', createServer, getClientOptions()); // Set client on `this` context to use with command handlers. this.client = client; @@ -157,10 +174,10 @@ export function activate(context: vscode.ExtensionContext) { client.start(); } -function getClientOptions(): LanguageClientOptions { - let workspaceFolder: vscode.WorkspaceFolder; +function getClientOptions(): lsp.LanguageClientOptions { + let workspaceFolder: vscode.WorkspaceFolder | undefined; - let rootPath: string = vscode.workspace.getConfiguration('smithyLsp').get('rootPath'); + let rootPath: string | undefined = vscode.workspace.getConfiguration('smithyLsp').get('rootPath'); if (rootPath) { const workspaceRoot = getWorkspaceRoot(); @@ -176,12 +193,13 @@ function getClientOptions(): LanguageClientOptions { // Configure file patterns relative to the workspace folder. let filePattern: vscode.GlobPattern = '**/{smithy-build}.json'; - let selectorPattern: string = null; + let selectorPattern: string | undefined; if (workspaceFolder) { filePattern = new vscode.RelativePattern(workspaceFolder, filePattern); selectorPattern = `${workspaceFolder.uri.fsPath}/**/*`; } + lsp.DocumentSelector; // Options to control the language client return { // Register the server for plain text documents @@ -206,7 +224,7 @@ function getClientOptions(): LanguageClientOptions { }, // Don't switch to output window when the server returns output. - revealOutputChannelOn: RevealOutputChannelOn.Never, + revealOutputChannelOn: lsp.RevealOutputChannelOn.Never, progressOnInitialization: true, }; } @@ -218,7 +236,7 @@ export function deactivate(): Thenable | undefined { return client.stop(); } -function getWorkspaceRoot(): string | undefined { +function getWorkspaceRoot(): string { let folders = vscode.workspace.workspaceFolders; if (!folders || folders.length === 0) { return ''; @@ -230,32 +248,30 @@ function getWorkspaceRoot(): string | undefined { return ''; } -function createSmithyContentProvider(languageClient: LanguageClient): vscode.TextDocumentContentProvider { - return { - provideTextDocumentContent: async (uri: vscode.Uri, token: CancellationToken): Promise => { - return languageClient - .sendRequest(ClassFileContentsRequest.type, { uri: uri.toString() }, token) - .then((v: string): string => { - return v || ''; - }); +function createSmithyContentProvider(languageClient: lsp.LanguageClient): vscode.TextDocumentContentProvider { + return { + provideTextDocumentContent: async (uri: vscode.Uri, token: lsp.CancellationToken): Promise => { + return languageClient.sendRequest(ClassFileContentsRequest.type, { uri: uri.toString() }, token); }, }; } -function createSmithyFormattingEditProvider(languageClient: LanguageClient): vscode.DocumentFormattingEditProvider { +function createSmithyFormattingEditProvider(languageClient: lsp.LanguageClient): vscode.DocumentFormattingEditProvider { return { provideDocumentFormattingEdits: async ( document: vscode.TextDocument, options: vscode.FormattingOptions, - token: CancellationToken + token: lsp.CancellationToken ): Promise => { - document.uri; + const params = { + textDocument: { + uri: document.uri.toString(), + }, + options: options, + }; + return languageClient - .sendRequest( - DocumentFormattingRequest.type, - { textDocument: { uri: document.uri.toString() }, options: options }, - token - ) + .sendRequest(lsp.DocumentFormattingRequest.type, params, token) .then((v: vscode.TextEdit[]): vscode.TextEdit[] => { return v; }); @@ -264,5 +280,5 @@ function createSmithyFormattingEditProvider(languageClient: LanguageClient): vsc } export namespace ClassFileContentsRequest { - export const type = new RequestType('smithy/jarFileContents'); + export const type = new lsp.RequestType('smithy/jarFileContents'); } From f29576a5c310fd70feb62c0f4aea8e63194bbd55 Mon Sep 17 00:00:00 2001 From: Miles Ziemer Date: Sat, 29 Mar 2025 12:12:15 -0400 Subject: [PATCH 02/11] Remove unecessary format provider The language server tells vscode it provides a formatter through its capabilities, so we don't need any client-side code. --- src/extension.ts | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 7105712..2497d63 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -155,11 +155,6 @@ export function activate(context: vscode.ExtensionContext) { vscode.workspace.registerTextDocumentContentProvider('smithyjar', smithyContentProvider) ); - const smithyFormattingEditProvider = createSmithyFormattingEditProvider(client); - context.subscriptions.push( - vscode.languages.registerDocumentFormattingEditProvider('smithy', smithyFormattingEditProvider) - ); - // Set default expression input, and use context to hold state between command invocations. this.expression = 'Enter Selector Expression'; this.selectorDecorator = new SelectorDecorator(); @@ -256,29 +251,6 @@ function createSmithyContentProvider(languageClient: lsp.LanguageClient): vscode }; } -function createSmithyFormattingEditProvider(languageClient: lsp.LanguageClient): vscode.DocumentFormattingEditProvider { - return { - provideDocumentFormattingEdits: async ( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: lsp.CancellationToken - ): Promise => { - const params = { - textDocument: { - uri: document.uri.toString(), - }, - options: options, - }; - - return languageClient - .sendRequest(lsp.DocumentFormattingRequest.type, params, token) - .then((v: vscode.TextEdit[]): vscode.TextEdit[] => { - return v; - }); - }, - }; -} - export namespace ClassFileContentsRequest { export const type = new lsp.RequestType('smithy/jarFileContents'); } From 259d45b942fdb9923f72c37559a1e72643569af3 Mon Sep 17 00:00:00 2001 From: Miles Ziemer Date: Sat, 29 Mar 2025 12:47:52 -0400 Subject: [PATCH 03/11] Refactor lsp extensions Cleaned up the implementations and organization of the smithy/jarFileContents and smithy/selectorCommand lsp extension handlers. --- src/extension.ts | 35 +++-------- src/jar-file-contents.ts | 24 ++++++++ src/selector.ts | 75 +++++++++++++++++++++++ src/selector/selector-command-handlers.ts | 37 ----------- src/selector/selector-command-request.ts | 9 --- src/selector/selector-decorator.ts | 35 ----------- 6 files changed, 106 insertions(+), 109 deletions(-) create mode 100644 src/jar-file-contents.ts create mode 100644 src/selector.ts delete mode 100644 src/selector/selector-command-handlers.ts delete mode 100644 src/selector/selector-command-request.ts delete mode 100644 src/selector/selector-decorator.ts diff --git a/src/extension.ts b/src/extension.ts index 2497d63..502f0e9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,10 +3,10 @@ import * as fs from 'fs'; import * as child_process from 'child_process'; import * as vscode from 'vscode'; import * as lsp from 'vscode-languageclient/node'; -import { SelectorDecorator } from './selector/selector-decorator'; -import { selectorRunCommandHandler, selectorClearCommandHandler } from './selector/selector-command-handlers'; import { getCoursierExecutable } from './coursier/coursier'; +import JarFileContentsProvider from './jar-file-contents'; +import SelectorHandler from './selector'; // Couriser uses an index to determine where to download jvms from: https://get-coursier.io/docs/2.0.6/cli-java#jvm-index // Newer versions of coursier use this index, which is more up to date than the one @@ -147,22 +147,13 @@ export function activate(context: vscode.ExtensionContext) { // Create the language client and start the client. client = new lsp.LanguageClient('smithyLsp', 'Smithy LSP', createServer, getClientOptions()); - // Set client on `this` context to use with command handlers. - this.client = client; + const jarFileContentsProvider = new JarFileContentsProvider(client); + const selectorHandler = new SelectorHandler(client); - const smithyContentProvider = createSmithyContentProvider(client); context.subscriptions.push( - vscode.workspace.registerTextDocumentContentProvider('smithyjar', smithyContentProvider) - ); - - // Set default expression input, and use context to hold state between command invocations. - this.expression = 'Enter Selector Expression'; - this.selectorDecorator = new SelectorDecorator(); - - // Register selector commands. - context.subscriptions.push(vscode.commands.registerCommand('smithy.runSelector', selectorRunCommandHandler, this)); - context.subscriptions.push( - vscode.commands.registerCommand('smithy.clearSelector', selectorClearCommandHandler, this) + vscode.workspace.registerTextDocumentContentProvider('smithyjar', jarFileContentsProvider), + vscode.commands.registerCommand('smithy.runSelector', selectorHandler.run, selectorHandler), + vscode.commands.registerCommand('smithy.clearSelector', selectorHandler.clear, selectorHandler) ); // Start the client. This will also launch the server @@ -242,15 +233,3 @@ function getWorkspaceRoot(): string { } return ''; } - -function createSmithyContentProvider(languageClient: lsp.LanguageClient): vscode.TextDocumentContentProvider { - return { - provideTextDocumentContent: async (uri: vscode.Uri, token: lsp.CancellationToken): Promise => { - return languageClient.sendRequest(ClassFileContentsRequest.type, { uri: uri.toString() }, token); - }, - }; -} - -export namespace ClassFileContentsRequest { - export const type = new lsp.RequestType('smithy/jarFileContents'); -} diff --git a/src/jar-file-contents.ts b/src/jar-file-contents.ts new file mode 100644 index 0000000..3a0328a --- /dev/null +++ b/src/jar-file-contents.ts @@ -0,0 +1,24 @@ +import * as vscode from 'vscode'; +import * as lsp from 'vscode-languageclient/node'; + +namespace JarFileContentsRequest { + type Params = lsp.TextDocumentIdentifier; + + type Result = string; + + const method = 'smithy/jarFileContents'; + + export const type = new lsp.RequestType(method); +} + +export default class JarFileContentsProvider implements vscode.TextDocumentContentProvider { + private client: lsp.LanguageClient; + + constructor(client: lsp.LanguageClient) { + this.client = client; + } + + provideTextDocumentContent(uri: vscode.Uri, token: vscode.CancellationToken): vscode.ProviderResult { + return this.client.sendRequest(JarFileContentsRequest.type, { uri: uri.toString() }, token); + } +} diff --git a/src/selector.ts b/src/selector.ts new file mode 100644 index 0000000..54fe742 --- /dev/null +++ b/src/selector.ts @@ -0,0 +1,75 @@ +import * as vscode from 'vscode'; +import * as lsp from 'vscode-languageclient/node'; + +namespace SelectorCommandRequest { + type Params = { + expression: string; + }; + + type Result = lsp.Location[]; + + const method = 'smithy/selectorCommand'; + + export const type = new lsp.RequestType(method); +} + +export default class SelectorHandler { + private client: lsp.LanguageClient; + private expression: string = 'Enter selector expression'; + private decorationType: vscode.TextEditorDecorationType; + + constructor(client: lsp.LanguageClient) { + this.client = client; + this.decorationType = createDecorationType(); + } + + async run() { + const expression = await vscode.window.showInputBox({ + title: 'Run a selector', + value: this.expression, + }); + + // Don't do anything if expression was not populated. + if (!expression) { + return; + } + + // Don't do anything if there's no active editor. + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + return; + } + + await this.clear(); + this.expression = expression; + + const response = await this.client.sendRequest(SelectorCommandRequest.type, { expression }); + + const ranges: vscode.Range[] = []; + for (const location of response) { + if (location.uri.endsWith(activeEditor.document.fileName)) { + const range = new vscode.Range( + location.range.start.line, + location.range.start.character, + location.range.end.line, + location.range.end.character + ); + ranges.push(range); + } + } + + activeEditor.setDecorations(this.decorationType, ranges); + } + + async clear() { + this.decorationType.dispose(); + this.decorationType = createDecorationType(); + } +} + +function createDecorationType(): vscode.TextEditorDecorationType { + return vscode.window.createTextEditorDecorationType({ + border: 'dotted', + borderColor: '#C44536', + }); +} diff --git a/src/selector/selector-command-handlers.ts b/src/selector/selector-command-handlers.ts deleted file mode 100644 index c158692..0000000 --- a/src/selector/selector-command-handlers.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Position, Range, window } from 'vscode'; -import { SelectorCommandRequest } from './selector-command-request'; - -export async function selectorClearCommandHandler() { - this.selectorDecorator.clear(); -} - -export async function selectorRunCommandHandler() { - const expression = await window.showInputBox({ - title: 'Run a selector', - value: this.expression, - }); - const decorator = this.selectorDecorator; - let response = []; - // Don't do anything if expression was not populated. - if (expression) { - decorator.clear(); - this.expression = expression; - response = await this.client.sendRequest(SelectorCommandRequest.type, { expression: expression }); - const activeEditor = window.activeTextEditor; - const ranges = []; - for (const location of response) { - if (location['uri'].endsWith(activeEditor.document.fileName)) { - const startPosition = new Position( - location['range']['start']['line'], - location['range']['start']['character'] - ); - const endPosition = new Position( - location['range']['end']['line'], - location['range']['end']['character'] - ); - ranges.push(new Range(startPosition, endPosition)); - } - } - decorator.set(activeEditor, ranges); - } -} diff --git a/src/selector/selector-command-request.ts b/src/selector/selector-command-request.ts deleted file mode 100644 index 6f910f2..0000000 --- a/src/selector/selector-command-request.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { RequestType } from 'vscode-languageclient'; - -interface SelectorParams { - expression: String; -} - -export namespace SelectorCommandRequest { - export const type = new RequestType('smithy/selectorCommand'); -} diff --git a/src/selector/selector-decorator.ts b/src/selector/selector-decorator.ts deleted file mode 100644 index 80163cd..0000000 --- a/src/selector/selector-decorator.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Range, TextEditor, TextEditorDecorationType, window, workspace } from 'vscode'; - -export interface ISelectorDecorator { - getDecorationType(): TextEditorDecorationType; - clear(): void; - set(textEditor: TextEditor, ranges: readonly Range[]): void; -} - -export class SelectorDecorator { - private decorationType: TextEditorDecorationType; - - constructor() { - this.decorationType = this.createDecorationType(); - } - - getDecorationType(): TextEditorDecorationType { - return this.decorationType; - } - - clear(): void { - this.decorationType.dispose(); - this.decorationType = this.createDecorationType(); - } - - createDecorationType(): TextEditorDecorationType { - return window.createTextEditorDecorationType({ - border: 'dotted', - borderColor: '#C44536', - }); - } - - set(textEditor: TextEditor, ranges: readonly Range[]): void { - textEditor.setDecorations(this.decorationType, ranges); - } -} From 308f8d237cecda9f4e48e083a011ea62c47f0a0b Mon Sep 17 00:00:00 2001 From: Miles Ziemer Date: Sat, 29 Mar 2025 13:12:30 -0400 Subject: [PATCH 04/11] Cleanup client/server options Refactored language client options and server config sent on initialization, and removed unnecessary config. We had some logic to figure out the root directory and workspace folder which was used to refine which files the extension should care about, but I think this is now unnecessary because the language server will just find project roots anyway. There was also an extension config option to set the root path, which should be unnecessary for the same reason, so I removed it. The client still sends the workspace folder and root path in the initialize request, so we aren't losing any information here. I also removed the file system watcher, as the server will dynamically register that. --- package.json | 4 --- src/extension.ts | 92 ++++++++++++++++-------------------------------- 2 files changed, 30 insertions(+), 66 deletions(-) diff --git a/package.json b/package.json index 9041a5c..bae6fd9 100644 --- a/package.json +++ b/package.json @@ -109,10 +109,6 @@ "default": "0.6.0", "description": "Version of the Smithy Language Server (see https://github.com/smithy-lang/smithy-language-server)." }, - "smithyLsp.rootPath": { - "scope": "resource", - "type": "string" - }, "smithyLsp.diagnostics.minimumSeverity": { "scope": "window", "type": "string", diff --git a/src/extension.ts b/src/extension.ts index 502f0e9..bf35227 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -144,8 +144,10 @@ export function activate(context: vscode.ExtensionContext) { } } + const clientOptions = getClientOptions(); + // Create the language client and start the client. - client = new lsp.LanguageClient('smithyLsp', 'Smithy LSP', createServer, getClientOptions()); + client = new lsp.LanguageClient('smithyLsp', 'Smithy LSP', createServer, clientOptions); const jarFileContentsProvider = new JarFileContentsProvider(client); const selectorHandler = new SelectorHandler(client); @@ -160,76 +162,42 @@ export function activate(context: vscode.ExtensionContext) { client.start(); } -function getClientOptions(): lsp.LanguageClientOptions { - let workspaceFolder: vscode.WorkspaceFolder | undefined; - - let rootPath: string | undefined = vscode.workspace.getConfiguration('smithyLsp').get('rootPath'); - - if (rootPath) { - const workspaceRoot = getWorkspaceRoot(); - if (rootPath.startsWith('${workspaceRoot}') && workspaceRoot === '') { - console.warn(`Unable to retrieve the workspace root.`); - } - workspaceFolder = { - uri: vscode.Uri.file(rootPath.replace('${workspaceRoot}', workspaceRoot)), - name: 'smithy-lsp-root-path', - index: 1, - }; - } - - // Configure file patterns relative to the workspace folder. - let filePattern: vscode.GlobPattern = '**/{smithy-build}.json'; - let selectorPattern: string | undefined; - if (workspaceFolder) { - filePattern = new vscode.RelativePattern(workspaceFolder, filePattern); - selectorPattern = `${workspaceFolder.uri.fsPath}/**/*`; +export function deactivate(): Thenable | undefined { + if (!client) { + return undefined; } + return client.stop(); +} - lsp.DocumentSelector; - // Options to control the language client - return { - // Register the server for plain text documents - documentSelector: [ - { scheme: 'file', language: 'smithy', pattern: selectorPattern }, - { scheme: 'smithyjar', language: 'smithy', pattern: selectorPattern }, - { scheme: 'file', language: 'json', pattern: '**/{smithy-build,.smithy-project}.json' }, - ], - synchronize: { - // Notify the server about file changes to 'smithy-build.json' files contained in the workspace - fileEvents: vscode.workspace.createFileSystemWatcher(filePattern), - }, - outputChannelName: 'Smithy Language Server', +type ServerOptions = { + diagnostics?: { + minimumSeverity?: string; + }; + onlyReloadOnSave?: boolean; +}; - workspaceFolder, +function getClientOptions(): lsp.LanguageClientOptions { + const smithyConfig = vscode.workspace.getConfiguration('smithyLsp'); - initializationOptions: { - 'diagnostics.minimumSeverity': vscode.workspace - .getConfiguration('smithyLsp') - .get('diagnostics.minimumSeverity'), - onlyReloadOnSave: vscode.workspace.getConfiguration('smithyLsp').get('onlyReloadOnSave'), + const serverOptions: ServerOptions = { + diagnostics: { + minimumSeverity: smithyConfig.get('diagnostics.minimumSeverity'), }, + onlyReloadOnSave: smithyConfig.get('onlyReloadOnSave'), + }; + return { + outputChannelName: 'Smithy Language Server', // Don't switch to output window when the server returns output. revealOutputChannelOn: lsp.RevealOutputChannelOn.Never, progressOnInitialization: true, - }; -} -export function deactivate(): Thenable | undefined { - if (!client) { - return undefined; - } - return client.stop(); -} + documentSelector: [ + { language: 'smithy' }, + { scheme: 'smithyjar' }, + { pattern: '**/{smithy-build,.smithy-project}.json' }, + ], -function getWorkspaceRoot(): string { - let folders = vscode.workspace.workspaceFolders; - if (!folders || folders.length === 0) { - return ''; - } - let folder = folders[0]; - if (folder.uri.scheme === 'file') { - return folder.uri.fsPath; - } - return ''; + initializationOptions: serverOptions, + }; } From a99d6c59939f277f120525847bdf34554d70fcdf Mon Sep 17 00:00:00 2001 From: Miles Ziemer Date: Sat, 29 Mar 2025 18:22:54 -0400 Subject: [PATCH 05/11] Cleanup coursier setup Two things: Some refactoring to use execFile instead of spawn for checking if coursier is available, which is more lightweight. Changed the way the server is run with coursier to have vscode run the coursier command directly. Previously, we created a local tcp server that acted as a proxy for the language server, which we ran in a separate process. But I don't think this extra layer of indirection is necessary, since vscode's lsp api can be given a command and some args to use to start the server. --- src/coursier/coursier.ts | 26 ++-- src/coursier/download-coursier.ts | 2 +- src/coursier/path-check.ts | 41 ------ src/extension.ts | 205 +++++++++--------------------- 4 files changed, 78 insertions(+), 196 deletions(-) delete mode 100644 src/coursier/path-check.ts diff --git a/src/coursier/coursier.ts b/src/coursier/coursier.ts index 91c6aef..84fb574 100644 --- a/src/coursier/coursier.ts +++ b/src/coursier/coursier.ts @@ -1,12 +1,22 @@ -import { downloadCoursierIfRequired } from './download-coursier'; -import { findCoursierOnPath } from './path-check'; +import * as child_process from 'child_process'; +import * as vscode from 'vscode'; +import downloadCoursierIfRequired from './download-coursier'; -export function getCoursierExecutable(extensionPath: string): Promise { - return findCoursierOnPath(extensionPath).then((paths) => { - if (paths.length > 0) { - return paths[0]; - } else { - return downloadCoursierIfRequired(extensionPath, 'v2.0.6'); +export default async function getCoursierExecutable(context: vscode.ExtensionContext): Promise { + for (const command of ['cs', 'coursier']) { + if (await availableOnPath(command, ['--help'])) { + return command; } + } + + console.log('Coursier not found on path, downloading it instead.'); + return await downloadCoursierIfRequired(context.globalStoragePath, 'v2.0.6'); +} + +function availableOnPath(command: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + child_process.execFile(command, args, (e, _, __) => { + resolve(e == null); + }); }); } diff --git a/src/coursier/download-coursier.ts b/src/coursier/download-coursier.ts index d88df59..b127ffa 100644 --- a/src/coursier/download-coursier.ts +++ b/src/coursier/download-coursier.ts @@ -4,7 +4,7 @@ import { IncomingMessage } from 'http'; import * as fs from 'fs'; import { access, mkdir } from 'fs/promises'; -export function downloadCoursierIfRequired(extensionPath: string, versionPath: string): Promise { +export default function downloadCoursierIfRequired(extensionPath: string, versionPath: string): Promise { function binPath(filename: string) { return path.join(extensionPath, filename); } diff --git a/src/coursier/path-check.ts b/src/coursier/path-check.ts deleted file mode 100644 index 54a1142..0000000 --- a/src/coursier/path-check.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { spawn } from 'child_process'; - -/** - * This type is used to bypass the `defaultImpl` when running the tests. - */ -export type ExecForCode = { - run: (execName: string, args: Array, cwd: string) => Promise; -}; - -const defaultImpl: ExecForCode = { - run: (execName: string, args: Array, cwd: string) => { - return new Promise((resolve, reject) => { - const options = { cwd }; - const resolveProcess = spawn(execName, args, options); - resolveProcess.on('exit', (exitCode) => { - resolve(exitCode); - }); - resolveProcess.on('error', (err) => { - reject(err); - }); - }); - }, -}; - -export function findCoursierOnPath(cwd: string, execForCode: ExecForCode = defaultImpl): Promise> { - function availableOnPath(execName: string): Promise { - return execForCode - .run(execName, ['--help'], cwd) - .then((ec) => ec === 0) - .catch(() => false); - } - - const possibleCoursierNames = ['cs', 'coursier']; - return possibleCoursierNames.reduce((accP, current) => { - return accP.then((acc) => { - return availableOnPath(current).then((succeeeded) => { - return succeeeded ? [...acc, current] : acc; - }); - }); - }, Promise.resolve([])); -} diff --git a/src/extension.ts b/src/extension.ts index bf35227..6396ccf 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,153 +1,18 @@ -import * as net from 'net'; -import * as fs from 'fs'; -import * as child_process from 'child_process'; import * as vscode from 'vscode'; import * as lsp from 'vscode-languageclient/node'; -import { getCoursierExecutable } from './coursier/coursier'; import JarFileContentsProvider from './jar-file-contents'; import SelectorHandler from './selector'; - -// Couriser uses an index to determine where to download jvms from: https://get-coursier.io/docs/2.0.6/cli-java#jvm-index -// Newer versions of coursier use this index, which is more up to date than the one -// used by the coursier version used by the extension. -// This is a temporary solution to avoid adding logic that determines the version of -// coursier on the local machine. In the near future, we will vend the language server -// as a standalone executable, and will no longer need couriser to manage the jvm version. -const COURSIER_JVM_INDEX = 'https://raw.githubusercontent.com/coursier/jvm-index/master/index.json'; +import getCoursierExecutable from './coursier/coursier'; let client: lsp.LanguageClient; -type RunCmd = { - command: string; - args: string[]; -}; - -function runWithCmd(context: vscode.ExtensionContext, cmd: RunCmd): lsp.StreamInfo { - const options = { cwd: context.extensionPath }; - const cp = child_process.spawn(cmd.command, cmd.args, options); - - return { - writer: cp.stdin, - reader: cp.stdout, - }; -} - -async function runWithCoursier(context: vscode.ExtensionContext): Promise { - function startServer(executable: string): Promise { - console.log(`Executable located at ${executable}.`); - return new Promise((resolve, reject) => { - const server = net - .createServer((socket) => { - console.log('Creating server'); - - resolve({ - reader: socket, - writer: socket, - }); - - socket.on('end', () => console.log('Disconnected')); - }) - .on('error', (err) => { - // handle errors here - reject(err); - }); - - // grab a random port. - server.listen(() => { - // Start the child java process - let options = { cwd: context.extensionPath }; - - let port = (server.address() as net.AddressInfo).port; - - let version = vscode.workspace.getConfiguration('smithyLsp').get('version', '`'); - - // Downloading latest poms - let resolveArgs = [ - 'resolve', - '--mode', - 'force', - 'software.amazon.smithy:smithy-language-server:' + version, - '-r', - 'm2local', - ]; - let resolveProcess = child_process.spawn(executable, resolveArgs, options); - console.log(resolveArgs); - resolveProcess.on('exit', (exitCode) => { - console.log('Exit code : ' + exitCode); - if (exitCode == 0) { - console.log('Launching smithy-language-server version:' + version); - - let launchargs = [ - 'launch', - 'software.amazon.smithy:smithy-language-server:' + version, - // Configure couriser to use java 21 - '--jvm', - // By default, coursier uses AdoptOpenJDK: https://get-coursier.io/docs/2.0.6/cli-java - // We could just say '21' here, and let coursier default to adopt jdk - // 21, but later versions of the jdk are released under the name adoptium. - 'corretto:21', - // The location to download the jvm from is provided by the jvm index. - '--jvm-index', - COURSIER_JVM_INDEX, - '-r', - 'm2local', - '-M', - 'software.amazon.smithy.lsp.Main', - '--', - port.toString(), - ]; - - console.log(launchargs); - - let childProcess = child_process.spawn(executable, launchargs, options); - - childProcess.stdout.on('data', (data) => { - console.log(`stdout: ${data}`); - }); - - childProcess.stderr.on('data', (data) => { - console.error(`stderr: ${data}`); - }); - - childProcess.on('close', (code) => { - console.log(`LSP exited with code ${code}`); - }); - } else { - console.log(`Could not resolve smithy-language-server implementation`); - } - }); - - // Send raw output to a file - if (context.storageUri) { - if (!fs.existsSync(context.storageUri.fsPath)) { - fs.mkdirSync(context.storageUri.fsPath); - } - } - }); - }); - } - - const binaryPath = await getCoursierExecutable(context.globalStoragePath); - return await startServer(binaryPath); -} - -export function activate(context: vscode.ExtensionContext) { - async function createServer(): Promise { - const cmd: RunCmd | undefined = vscode.workspace.getConfiguration('smithyLsp').get('runCmd'); - if (cmd) { - console.log('Running with command: ', JSON.stringify(cmd, null, 4)); - return runWithCmd(context, cmd); - } else { - console.log('Running with coursier'); - return await runWithCoursier(context); - } - } - +export async function activate(context: vscode.ExtensionContext) { + const server = await getServer(context); const clientOptions = getClientOptions(); // Create the language client and start the client. - client = new lsp.LanguageClient('smithyLsp', 'Smithy LSP', createServer, clientOptions); + client = new lsp.LanguageClient('smithyLsp', 'Smithy LSP', server, clientOptions); const jarFileContentsProvider = new JarFileContentsProvider(client); const selectorHandler = new SelectorHandler(client); @@ -169,7 +34,11 @@ export function deactivate(): Thenable | undefined { return client.stop(); } -type ServerOptions = { +function getConfig(config: string, defaultValue?: T): T | undefined { + return vscode.workspace.getConfiguration('smithyLsp').get(config, defaultValue); +} + +type InitializationOptions = { diagnostics?: { minimumSeverity?: string; }; @@ -177,13 +46,11 @@ type ServerOptions = { }; function getClientOptions(): lsp.LanguageClientOptions { - const smithyConfig = vscode.workspace.getConfiguration('smithyLsp'); - - const serverOptions: ServerOptions = { + const initializationOptions: InitializationOptions = { diagnostics: { - minimumSeverity: smithyConfig.get('diagnostics.minimumSeverity'), + minimumSeverity: getConfig('minimumSeverity'), }, - onlyReloadOnSave: smithyConfig.get('onlyReloadOnSave'), + onlyReloadOnSave: getConfig('onlyReloadOnSave'), }; return { @@ -198,6 +65,52 @@ function getClientOptions(): lsp.LanguageClientOptions { { pattern: '**/{smithy-build,.smithy-project}.json' }, ], - initializationOptions: serverOptions, + initializationOptions: initializationOptions, }; } + +type RunCmd = { + command: string; + args: string[]; +}; + +// Couriser uses an index to determine where to download jvms from: https://get-coursier.io/docs/2.0.6/cli-java#jvm-index +// Newer versions of coursier use this index, which is more up to date than the one +// used by the coursier version used by the extension. +// This is a temporary solution to avoid adding logic that determines the version of +// coursier on the local machine. In the near future, we will vend the language server +// as a standalone executable, and will no longer need couriser to manage the jvm version. +const COURSIER_JVM_INDEX = 'https://raw.githubusercontent.com/coursier/jvm-index/master/index.json'; + +async function getServer(context: vscode.ExtensionContext): Promise { + const cmd = getConfig('runCmd'); + if (cmd) { + return { + command: cmd.command, + args: ['0'], + }; + } else { + const coursierExecutable = await getCoursierExecutable(context); + const languageServerVersion = getConfig('version', ''); + return { + command: coursierExecutable, + args: [ + 'launch', + 'software.amazon.smithy:smithy-language-server:' + languageServerVersion, + // Configure couriser to use java 21 + '--jvm', + // By default, coursier uses AdoptOpenJDK: https://get-coursier.io/docs/2.0.6/cli-java + // We could just say '21' here, and let coursier default to adopt jdk + // 21, but later versions of the jdk are released under the name adoptium. + 'corretto:21', + // The location to download the jvm from is provided by the jvm index. + '--jvm-index', + COURSIER_JVM_INDEX, + '-M', + 'software.amazon.smithy.lsp.Main', + '--', + '0', + ], + }; + } +} From 3edb9089ab2da196d160a1968a401f15d84f913e Mon Sep 17 00:00:00 2001 From: Miles Ziemer Date: Wed, 2 Apr 2025 13:49:58 -0400 Subject: [PATCH 06/11] todo --- package.json | 52 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index bae6fd9..ac8c743 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ }, "configuration": { "type": "object", - "title": "vscode-smithy configuration", + "title": "Smithy", "properties": { "smithyLsp.maxNumberOfProblems": { "scope": "resource", @@ -119,13 +119,59 @@ "ERROR" ], "default": "WARNING", - "description": "Minimum severity of Smithy validation events to display in the editor." + "description": "Minimum severity of Smithy validation events to display in the editor.", + "deprecationMessage": "Use smithy.server.initializationOptions.diagnostics.minimumSeverity instead." }, "smithyLsp.onlyReloadOnSave": { "scope": "window", "type": "boolean", "default": false, - "description": "Whether to only re-load the Smithy model on save. Use this if the server feels slow as you type." + "description": "Whether to only re-load the Smithy model on save. Use this if the server feels slow as you type.", + "deprecationMessage": "May cause features like definition, hover, and completions to behave incorrectly when you have unsaved changes." + }, + "smithy-language-server": { + "description": "Settings for how the extension should setup and use the Smithy Language Server.", + "type": "object", + "properties": { + "run": { + "description": "Settings for how the extension should run the Smithy Language Server.", + "type": "object", + "properties": { + "executable": { + "description": "Executable to run the language server. Can be the executable name if it is on your PATH, or an absolute path to the executable.", + "type": "string" + } + } + }, + "version": { + "description": "Version of the Smithy Language Server to use. Ignored if `server.run.executable` is provided.", + "type": "string", + "default": "0.6.0" + }, + "initializationOptions": { + "type": "object", + "description": "Options sent to the server in the [Initialize Request](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize)", + "properties": { + "diagnostics": { + "description": "Settings for diagnostics produced by the server.", + "type": "object", + "properties": { + "minimumSeverity": { + "description": "Minimum severity of Smithy validation events to display in the editor.", + "type": "string", + "enum": [ + "NOTE", + "WARNING", + "DANGER", + "ERROR" + ], + "default": "WARNING" + } + } + } + } + } + } }, "smithyLsp.runCmd": { "type": "object", From 1f3298afee163538a27580d0c04885477cf2ff06 Mon Sep 17 00:00:00 2001 From: Miles Ziemer Date: Wed, 2 Apr 2025 14:42:47 -0400 Subject: [PATCH 07/11] Cleanup config options Changed the prefix of config properties from `smithyLsp` to just `smithy` (or `smithy.server`, where applicable). I kept the existing config properties, but added a deprecation message to them, pointing to the new property. We will keep these in the next release, but remove them in the following release. Small note about the `smithy.server.version` property: I didn't give it a default value, since that would have been chosen over a user-defined version in the old property. When we remove the old properties, we can add the default to the new one. I also added a simple api for getting config settings in the extension, that prefers the new properties over the old. --- TODO.md | 4 +- package.json | 105 ++++++++++++++++++----------------------------- src/config.ts | 25 +++++++++++ src/extension.ts | 35 +++++----------- 4 files changed, 76 insertions(+), 93 deletions(-) create mode 100644 src/config.ts diff --git a/TODO.md b/TODO.md index ceba977..f78a737 100644 --- a/TODO.md +++ b/TODO.md @@ -1,2 +1,2 @@ -I don't like single name imports. Would rather them be qualified names. But I'm -not sure if that will cause everything to be imported, so I should check. +- Configuration: + - Should I reorder smithy.trace.server? diff --git a/package.json b/package.json index ac8c743..677c891 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,8 @@ "scope": "resource", "type": "number", "default": 100, - "description": "Controls the maximum number of problems produced by the server." + "description": "Controls the maximum number of problems produced by the server.", + "deprecationMessage": "Use smithy.maxNumberOfProblems instead." }, "smithyLsp.trace.server": { "scope": "window", @@ -101,13 +102,15 @@ "verbose" ], "default": "verbose", - "description": "Traces the communication between VS Code and the language server." + "description": "Traces the communication between VS Code and the language server.", + "deprecationMessage": "Use smithy.trace.server instead." }, "smithyLsp.version": { "scope": "window", "type": "string", "default": "0.6.0", - "description": "Version of the Smithy Language Server (see https://github.com/smithy-lang/smithy-language-server)." + "description": "Version of the Smithy Language Server (see https://github.com/smithy-lang/smithy-language-server).", + "deprecationMessage": "Use smithy.server.version instead." }, "smithyLsp.diagnostics.minimumSeverity": { "scope": "window", @@ -120,7 +123,7 @@ ], "default": "WARNING", "description": "Minimum severity of Smithy validation events to display in the editor.", - "deprecationMessage": "Use smithy.server.initializationOptions.diagnostics.minimumSeverity instead." + "deprecationMessage": "Use smithy.server.diagnostics.minimumSeverity instead." }, "smithyLsp.onlyReloadOnSave": { "scope": "window", @@ -129,70 +132,40 @@ "description": "Whether to only re-load the Smithy model on save. Use this if the server feels slow as you type.", "deprecationMessage": "May cause features like definition, hover, and completions to behave incorrectly when you have unsaved changes." }, - "smithy-language-server": { - "description": "Settings for how the extension should setup and use the Smithy Language Server.", - "type": "object", - "properties": { - "run": { - "description": "Settings for how the extension should run the Smithy Language Server.", - "type": "object", - "properties": { - "executable": { - "description": "Executable to run the language server. Can be the executable name if it is on your PATH, or an absolute path to the executable.", - "type": "string" - } - } - }, - "version": { - "description": "Version of the Smithy Language Server to use. Ignored if `server.run.executable` is provided.", - "type": "string", - "default": "0.6.0" - }, - "initializationOptions": { - "type": "object", - "description": "Options sent to the server in the [Initialize Request](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize)", - "properties": { - "diagnostics": { - "description": "Settings for diagnostics produced by the server.", - "type": "object", - "properties": { - "minimumSeverity": { - "description": "Minimum severity of Smithy validation events to display in the editor.", - "type": "string", - "enum": [ - "NOTE", - "WARNING", - "DANGER", - "ERROR" - ], - "default": "WARNING" - } - } - } - } - } - } + "smithy.maxNumberOfProblems": { + "scope": "resource", + "type": "number", + "default": 100, + "description": "Controls the maximum number of problems produced by the server." + }, + "smithy.trace.server": { + "type": "string", + "enum": [ + "off", + "messages", + "verbose" + ], + "default": "verbose", + "description": "Traces the communication between VS Code and the language server." + }, + "smithy.server.executable": { + "type": "string", + "description": "Executable to run the Smithy Language Server. Can be the executable name if it is on your PATH, or an absolute path to the executable." }, - "smithyLsp.runCmd": { - "type": "object", - "default": null, - "required": [ - "command", - "args" + "smithy.server.version": { + "type": "string", + "description": "Version of the Smithy Language Server to use. Ignored if smithy.server.executable is provided." + }, + "smithy.server.diagnostics.minimumSeverity": { + "type": "string", + "enum": [ + "NOTE", + "WARNING", + "DANGER", + "ERROR" ], - "properties": { - "command": { - "type": "string", - "description": "The command to run the language server" - }, - "args": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The arguments to pass to the language server" - } - } + "default": "WARNING", + "description": "Minimum severity of Smithy validation events to display in the editor." } } } diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..959e8b0 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,25 @@ +import * as vscode from 'vscode'; + +export function getServerDiagnosticsMinimumSeverity(): string | undefined { + return getConfig('server.diagnostics.minimumSeverity') ?? getOldConfig('diagnostics.minimumSeverity'); +} + +export function getServerOnlyReloadOnSave(): boolean | undefined { + return getOldConfig('onlyReloadOnSave'); +} + +export function getServerExecutable(): string | undefined { + return getConfig('server.executable'); +} + +export function getServerVersion(): string { + return getConfig('server.version') ?? getOldConfig('version'); +} + +function getConfig(key: string): T | undefined { + return vscode.workspace.getConfiguration('smithy').get(key); +} + +function getOldConfig(key: string): T | undefined { + return vscode.workspace.getConfiguration('smithyLsp').get(key); +} diff --git a/src/extension.ts b/src/extension.ts index 6396ccf..6567d10 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import * as lsp from 'vscode-languageclient/node'; +import * as config from './config'; import JarFileContentsProvider from './jar-file-contents'; import SelectorHandler from './selector'; import getCoursierExecutable from './coursier/coursier'; @@ -12,7 +13,7 @@ export async function activate(context: vscode.ExtensionContext) { const clientOptions = getClientOptions(); // Create the language client and start the client. - client = new lsp.LanguageClient('smithyLsp', 'Smithy LSP', server, clientOptions); + client = new lsp.LanguageClient('smithy', 'Smithy', server, clientOptions); const jarFileContentsProvider = new JarFileContentsProvider(client); const selectorHandler = new SelectorHandler(client); @@ -34,23 +35,12 @@ export function deactivate(): Thenable | undefined { return client.stop(); } -function getConfig(config: string, defaultValue?: T): T | undefined { - return vscode.workspace.getConfiguration('smithyLsp').get(config, defaultValue); -} - -type InitializationOptions = { - diagnostics?: { - minimumSeverity?: string; - }; - onlyReloadOnSave?: boolean; -}; - function getClientOptions(): lsp.LanguageClientOptions { - const initializationOptions: InitializationOptions = { + const initializationOptions = { diagnostics: { - minimumSeverity: getConfig('minimumSeverity'), + minimumSeverity: config.getServerDiagnosticsMinimumSeverity(), }, - onlyReloadOnSave: getConfig('onlyReloadOnSave'), + onlyReloadOnSave: config.getServerOnlyReloadOnSave(), }; return { @@ -65,15 +55,10 @@ function getClientOptions(): lsp.LanguageClientOptions { { pattern: '**/{smithy-build,.smithy-project}.json' }, ], - initializationOptions: initializationOptions, + initializationOptions, }; } -type RunCmd = { - command: string; - args: string[]; -}; - // Couriser uses an index to determine where to download jvms from: https://get-coursier.io/docs/2.0.6/cli-java#jvm-index // Newer versions of coursier use this index, which is more up to date than the one // used by the coursier version used by the extension. @@ -83,15 +68,15 @@ type RunCmd = { const COURSIER_JVM_INDEX = 'https://raw.githubusercontent.com/coursier/jvm-index/master/index.json'; async function getServer(context: vscode.ExtensionContext): Promise { - const cmd = getConfig('runCmd'); - if (cmd) { + const serverExecutable = config.getServerExecutable(); + if (serverExecutable) { return { - command: cmd.command, + command: serverExecutable, args: ['0'], }; } else { const coursierExecutable = await getCoursierExecutable(context); - const languageServerVersion = getConfig('version', ''); + const languageServerVersion = config.getServerVersion(); return { command: coursierExecutable, args: [ From 0b14a1f193e0c695641c1044f771aac74dc4d622 Mon Sep 17 00:00:00 2001 From: Miles Ziemer Date: Wed, 2 Apr 2025 14:53:09 -0400 Subject: [PATCH 08/11] Switch back to old init options In a previous commit, I changed the server initializationOptions to have a nested `diagnostics` object. That isn't what the server expects at the moment, and it is probably more complicated than necessary to change, so I changed it back. --- src/extension.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 6567d10..e4971aa 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -37,9 +37,7 @@ export function deactivate(): Thenable | undefined { function getClientOptions(): lsp.LanguageClientOptions { const initializationOptions = { - diagnostics: { - minimumSeverity: config.getServerDiagnosticsMinimumSeverity(), - }, + 'diagnostics.minimumSeverity': config.getServerDiagnosticsMinimumSeverity(), onlyReloadOnSave: config.getServerOnlyReloadOnSave(), }; From 38cd12d438aaacb26218c1279442abf91ce68383 Mon Sep 17 00:00:00 2001 From: Miles Ziemer Date: Mon, 7 Apr 2025 12:50:15 -0400 Subject: [PATCH 09/11] Fix config defaults For some reason, when a string config option isn't specified it gets a default value of `""`, so when we checked the new config option, the empty string value would get chosen instead. So I added a default of null for the old options, and I also switched the existing defaults to the new options, choosing the old options first if they're present. --- package.json | 6 ++++-- src/config.ts | 8 ++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 677c891..24e19f2 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "smithyLsp.version": { "scope": "window", "type": "string", - "default": "0.6.0", + "default": null, "description": "Version of the Smithy Language Server (see https://github.com/smithy-lang/smithy-language-server).", "deprecationMessage": "Use smithy.server.version instead." }, @@ -121,7 +121,7 @@ "DANGER", "ERROR" ], - "default": "WARNING", + "default": null, "description": "Minimum severity of Smithy validation events to display in the editor.", "deprecationMessage": "Use smithy.server.diagnostics.minimumSeverity instead." }, @@ -150,10 +150,12 @@ }, "smithy.server.executable": { "type": "string", + "default": null, "description": "Executable to run the Smithy Language Server. Can be the executable name if it is on your PATH, or an absolute path to the executable." }, "smithy.server.version": { "type": "string", + "default": "0.6.0", "description": "Version of the Smithy Language Server to use. Ignored if smithy.server.executable is provided." }, "smithy.server.diagnostics.minimumSeverity": { diff --git a/src/config.ts b/src/config.ts index 959e8b0..8fa6a81 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; export function getServerDiagnosticsMinimumSeverity(): string | undefined { - return getConfig('server.diagnostics.minimumSeverity') ?? getOldConfig('diagnostics.minimumSeverity'); + return getOldOrNewConfig('diagnostics.minimumSeverity', 'server.diagnostics.minimumSeverity'); } export function getServerOnlyReloadOnSave(): boolean | undefined { @@ -13,7 +13,11 @@ export function getServerExecutable(): string | undefined { } export function getServerVersion(): string { - return getConfig('server.version') ?? getOldConfig('version'); + return getOldOrNewConfig('version', 'server.version'); +} + +function getOldOrNewConfig(oldKey: string, newKey: string): T | undefined { + return getOldConfig(oldKey) || getConfig(newKey); } function getConfig(key: string): T | undefined { From 6fcb681a89f69a00188f82564433898298f9809f Mon Sep 17 00:00:00 2001 From: Miles Ziemer Date: Mon, 7 Apr 2025 12:58:39 -0400 Subject: [PATCH 10/11] Add back m2local repo to coursier Not sure why I removed it in the first place. --- TODO.md | 2 -- src/extension.ts | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index f78a737..0000000 --- a/TODO.md +++ /dev/null @@ -1,2 +0,0 @@ -- Configuration: - - Should I reorder smithy.trace.server? diff --git a/src/extension.ts b/src/extension.ts index e4971aa..0195095 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -89,6 +89,8 @@ async function getServer(context: vscode.ExtensionContext): Promise Date: Mon, 7 Apr 2025 14:54:18 -0400 Subject: [PATCH 11/11] Fix failing test --- tests/suite5/extension.test.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/suite5/extension.test.ts b/tests/suite5/extension.test.ts index b28f181..e019c24 100644 --- a/tests/suite5/extension.test.ts +++ b/tests/suite5/extension.test.ts @@ -10,11 +10,10 @@ suite('formatting tests', function () { const doc = await vscode.workspace.openTextDocument(smithyMainUri); await vscode.window.showTextDocument(doc); await waitForServerStartup(); - const edits: vscode.TextEdit[] = await vscode.commands.executeCommand( - 'vscode.executeFormatDocumentProvider', - smithyMainUri - ); - assert.strictEqual(edits.length > 0, true, 'expected edits from formatter, but got none'); - return Promise.resolve(); + + const beforeEditText = doc.getText(); + await vscode.commands.executeCommand('editor.action.formatDocument', smithyMainUri); + const afterEditText = doc.getText(); + assert.notStrictEqual(afterEditText, beforeEditText); }); });