From 93bdfa2e04ba8d13bdab6694acc7f2bf51db435d Mon Sep 17 00:00:00 2001 From: David Sherret Date: Fri, 18 Jun 2021 12:24:35 -0400 Subject: [PATCH 1/9] Add support for the --parent-pid flag. --- client/src/extension.ts | 9 +++++---- client/src/util.ts | 45 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/client/src/extension.ts b/client/src/extension.ts index 597b57c2..219099db 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -17,7 +17,7 @@ import type { Settings, TsLanguageFeaturesApiV0, } from "./types"; -import { assert, getDenoCommand } from "./util"; +import { assert, getDenoCommand, getDenoLspArgs } from "./util"; import * as path from "path"; import * as semver from "semver"; @@ -176,9 +176,10 @@ export async function activate( context: vscode.ExtensionContext, ): Promise { const command = await getDenoCommand(); + const args = await getDenoLspArgs(); const run: Executable = { command, - args: ["lsp"], + args, // deno-lint-ignore no-undef options: { env: { ...process.env, "NO_COLOR": true } }, }; @@ -186,8 +187,8 @@ export async function activate( const debug: Executable = { command, // disabled for now, as this gets super chatty during development - // args: ["lsp", "-L", "debug"], - args: ["lsp"], + // args: [...args, "-L", "debug"], + args, // deno-lint-ignore no-undef options: { env: { ...process.env, "NO_COLOR": true } }, }; diff --git a/client/src/util.ts b/client/src/util.ts index c8724516..7a72e66f 100644 --- a/client/src/util.ts +++ b/client/src/util.ts @@ -1,8 +1,11 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +import * as childProcess from "child_process"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; +import * as process from "process"; +import * as semver from "semver"; import * as vscode from "vscode"; /** Assert that the condition is "truthy", otherwise throw. */ @@ -12,6 +15,36 @@ export function assert(cond: unknown, msg = "Assertion failed."): asserts cond { } } +export async function getDenoLspArgs(): Promise { + const version = await getDenoVersion(); + // The --parent-pid flag was added in 1.11.2. + if (version == null || semver.lt(version, "1.11.2")) { + return ["lsp"]; + } else { + // The --parent-pid flag will cause the deno process to + // terminate itself when the vscode process no longer exists + return ["lsp", "--parent-pid", process.pid.toString()]; + } +} + +let memoizedVersion: semver.SemVer | undefined; + +export async function getDenoVersion(): Promise { + if (memoizedVersion === undefined) { + try { + const denoCommand = await getDenoCommand(); + const output = await execCommand(`${denoCommand} -V`); + const result = /[0-9]+\.[0-9]+\.[0-9]+/.exec(output); + if (result != null) { + memoizedVersion = new semver.SemVer(result[0]); + } + } catch (err) { + console.error(`Error getting deno version: ${err}`); + } + } + return memoizedVersion; +} + let memoizedCommand: string | undefined; export async function getDenoCommand(): Promise { @@ -97,3 +130,15 @@ function getDefaultDenoCommand() { }); } } + +function execCommand(command: string): Promise { + return new Promise((resolve, reject) => { + childProcess.exec(command, (err, stdout, stderr) => { + if (err) { + reject(stderr); + } else { + resolve(stdout); + } + }); + }); +} From d2eba691b87b2ba09b1bbadfdc55db84ef4f697d Mon Sep 17 00:00:00 2001 From: David Sherret Date: Mon, 21 Jun 2021 14:53:15 -0400 Subject: [PATCH 2/9] Require deno v1.11.2 and use new `--parent-pid` flag --- README.md | 2 +- client/src/extension.ts | 6 +++--- client/src/util.ts | 45 ----------------------------------------- package.json | 2 +- 4 files changed, 5 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index e0174ad9..0fe9a988 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This extension adds support for using [Deno](https://deno.land/) with Visual Studio Code, powered by the Deno language server. -> ⚠️ **Important:** You need to have a version of Deno CLI installed (v1.10.3 or +> ⚠️ **Important:** You need to have a version of Deno CLI installed (v1.11.2 or > later). The extension requires the executable and by default will use the > environment path. You can explicitly set the path to the executable in Visual > Studio Code Settings for `deno.path`. diff --git a/client/src/extension.ts b/client/src/extension.ts index 219099db..f4d16ea6 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -17,7 +17,7 @@ import type { Settings, TsLanguageFeaturesApiV0, } from "./types"; -import { assert, getDenoCommand, getDenoLspArgs } from "./util"; +import { assert, getDenoCommand } from "./util"; import * as path from "path"; import * as semver from "semver"; @@ -25,7 +25,7 @@ import * as vscode from "vscode"; import type { Executable } from "vscode-languageclient/node"; /** The minimum version of Deno that this extension is designed to support. */ -const SERVER_SEMVER = ">=1.10.3"; +const SERVER_SEMVER = ">=1.11.2"; /** The language IDs we care about. */ const LANGUAGES = [ @@ -176,7 +176,7 @@ export async function activate( context: vscode.ExtensionContext, ): Promise { const command = await getDenoCommand(); - const args = await getDenoLspArgs(); + const args = ["lsp", "--parent-pid", process.pid.toString()]; const run: Executable = { command, args, diff --git a/client/src/util.ts b/client/src/util.ts index 7a72e66f..c8724516 100644 --- a/client/src/util.ts +++ b/client/src/util.ts @@ -1,11 +1,8 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -import * as childProcess from "child_process"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import * as process from "process"; -import * as semver from "semver"; import * as vscode from "vscode"; /** Assert that the condition is "truthy", otherwise throw. */ @@ -15,36 +12,6 @@ export function assert(cond: unknown, msg = "Assertion failed."): asserts cond { } } -export async function getDenoLspArgs(): Promise { - const version = await getDenoVersion(); - // The --parent-pid flag was added in 1.11.2. - if (version == null || semver.lt(version, "1.11.2")) { - return ["lsp"]; - } else { - // The --parent-pid flag will cause the deno process to - // terminate itself when the vscode process no longer exists - return ["lsp", "--parent-pid", process.pid.toString()]; - } -} - -let memoizedVersion: semver.SemVer | undefined; - -export async function getDenoVersion(): Promise { - if (memoizedVersion === undefined) { - try { - const denoCommand = await getDenoCommand(); - const output = await execCommand(`${denoCommand} -V`); - const result = /[0-9]+\.[0-9]+\.[0-9]+/.exec(output); - if (result != null) { - memoizedVersion = new semver.SemVer(result[0]); - } - } catch (err) { - console.error(`Error getting deno version: ${err}`); - } - } - return memoizedVersion; -} - let memoizedCommand: string | undefined; export async function getDenoCommand(): Promise { @@ -130,15 +97,3 @@ function getDefaultDenoCommand() { }); } } - -function execCommand(command: string): Promise { - return new Promise((resolve, reject) => { - childProcess.exec(command, (err, stdout, stderr) => { - if (err) { - reject(stderr); - } else { - resolve(stdout); - } - }); - }); -} diff --git a/package.json b/package.json index 53467bda..1e88029b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vscode-deno", "displayName": "Deno", - "description": "A language server client for Deno. Requires Deno 1.8 or better.", + "description": "A language server client for Deno.", "author": "Deno Land Inc.", "license": "MIT", "version": "3.6.1", From 8e2119433932f13b02414e2e1ae13b90c73b653a Mon Sep 17 00:00:00 2001 From: David Sherret Date: Mon, 21 Jun 2021 17:25:50 -0400 Subject: [PATCH 3/9] Actually handle --parent-pid flag and old version of Deno. --- client/src/commands.ts | 92 +++++++++++++++++++++++++++++++--- client/src/content_provider.ts | 4 ++ client/src/extension.ts | 71 +++----------------------- client/src/types.d.ts | 4 +- client/src/util.ts | 52 ++++++++++++++++--- 5 files changed, 145 insertions(+), 78 deletions(-) diff --git a/client/src/commands.ts b/client/src/commands.ts index 2fdc0899..b60611dd 100644 --- a/client/src/commands.ts +++ b/client/src/commands.ts @@ -17,16 +17,22 @@ import { import * as tasks from "./tasks"; import type { DenoExtensionContext } from "./types"; import { WelcomePanel } from "./welcome"; -import { assert } from "./util"; +import { assert, getDenoCommandAndVersion } from "./util"; +import { registryState } from "./lsp_extensions"; +import { createRegistryStateHandler } from "./notification_handlers"; +import * as semver from "semver"; import * as vscode from "vscode"; -import { LanguageClient } from "vscode-languageclient/node"; +import { LanguageClient, ServerOptions } from "vscode-languageclient/node"; import type { DocumentUri, Location, Position, } from "vscode-languageclient/node"; +/** The minimum version of Deno that this extension is designed to support. */ +const SERVER_SEMVER = ">=1.11.2"; + // deno-lint-ignore no-explicit-any export type Callback = (...args: any[]) => unknown; export type Factory = ( @@ -42,14 +48,15 @@ export function cache( ): Callback { return (uris: DocumentUri[] = []) => { const activeEditor = vscode.window.activeTextEditor; - if (!activeEditor) { + const client = extensionContext.client; + if (!activeEditor || !client) { return; } return vscode.window.withProgress({ location: vscode.ProgressLocation.Window, title: "caching", }, () => { - return extensionContext.client.sendRequest( + return client.sendRequest( cacheReq, { referrer: { uri: activeEditor.document.uri.toString() }, @@ -86,7 +93,7 @@ export function reloadImportRegistries( _context: vscode.ExtensionContext, { client }: DenoExtensionContext, ): Callback { - return () => client.sendRequest(reloadImportRegistriesReq); + return () => client?.sendRequest(reloadImportRegistriesReq); } /** Start (or restart) the Deno Language Server */ @@ -95,16 +102,48 @@ export function startLanguageServer( extensionContext: DenoExtensionContext, ): Callback { return async () => { + // Reset the state and stop the existing language server const { statusBarItem } = extensionContext; if (extensionContext.client) { await extensionContext.client.stop(); statusBarItem.hide(); vscode.commands.executeCommand("setContext", ENABLEMENT_FLAG, false); } + + // Check the deno version before starting the language server + // This is necessary because the `--parent-pid ` flag for + // `deno lsp` won't exist in deno versions < 1.11.2 + const { command, version } = await getDenoCommandAndVersion(); + if (version == null) { + notifyDenoNotFound(command); + return; + } else if (!semver.satisfies(version, SERVER_SEMVER)) { + notifyServerSemver(version.version); + return; + } + + // Start the new language server + const args = ["lsp", "--parent-pid", process.pid.toString()]; + const serverOptions: ServerOptions = { + run: { + command, + args, + // deno-lint-ignore no-undef + options: { env: { ...process.env, "NO_COLOR": true } }, + }, + debug: { + command, + // disabled for now, as this gets super chatty during development + // args: [...args, "-L", "debug"], + args, + // deno-lint-ignore no-undef + options: { env: { ...process.env, "NO_COLOR": true } }, + }, + }; const client = extensionContext.client = new LanguageClient( LANGUAGE_CLIENT_ID, LANGUAGE_CLIENT_NAME, - extensionContext.serverOptions, + serverOptions, extensionContext.clientOptions, ); context.subscriptions.push(client.start()); @@ -119,14 +158,55 @@ export function startLanguageServer( statusBarItem.tooltip = client .initializeResult?.serverInfo?.version; statusBarItem.show(); + + context.subscriptions.push( + extensionContext.client.onNotification( + registryState, + createRegistryStateHandler(), + ), + ); + + showWelcomePageIfFirstUse(context, extensionContext); }; } +function notifyDenoNotFound(denoCommand: string) { + return vscode.window.showWarningMessage( + `Error resolving Deno executable. Please ensure Deno is on the PATH of this VS Code ` + + `process or specify a path to the executable in the "deno.path" setting. Could not ` + + `get version from command: ${denoCommand}`, + "OK", + ); +} + +function notifyServerSemver(serverVersion: string) { + return vscode.window.showWarningMessage( + `The version of Deno language server ("${serverVersion}") does not meet the requirements of the extension ("${SERVER_SEMVER}"). Please update Deno and restart.`, + "OK", + ); +} + +function showWelcomePageIfFirstUse( + context: vscode.ExtensionContext, + extensionContext: DenoExtensionContext, +) { + const welcomeShown = context.globalState.get("deno.welcomeShown") ?? + false; + + if (!welcomeShown) { + welcome(context, extensionContext)(); + context.globalState.update("deno.welcomeShown", true); + } +} + export function showReferences( _content: vscode.ExtensionContext, extensionContext: DenoExtensionContext, ): Callback { return (uri: string, position: Position, locations: Location[]) => { + if (!extensionContext.client) { + return; + } vscode.commands.executeCommand( "editor.action.showReferences", vscode.Uri.parse(uri), diff --git a/client/src/content_provider.ts b/client/src/content_provider.ts index e1297397..c9eec50a 100644 --- a/client/src/content_provider.ts +++ b/client/src/content_provider.ts @@ -20,6 +20,10 @@ export class DenoTextDocumentContentProvider uri: Uri, token: CancellationToken, ): ProviderResult { + if (!this.extensionContext.client) { + throw new Error("Deno language server has not started."); + } + return this.extensionContext.client.sendRequest( virtualTextDocument, { textDocument: { uri: uri.toString() } }, diff --git a/client/src/extension.ts b/client/src/extension.ts index f4d16ea6..698764db 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -9,23 +9,16 @@ import { } from "./constants"; import { DenoTextDocumentContentProvider, SCHEME } from "./content_provider"; import { DenoDebugConfigurationProvider } from "./debug_config_provider"; -import { registryState } from "./lsp_extensions"; -import { createRegistryStateHandler } from "./notification_handlers"; import { activateTaskProvider } from "./tasks"; import type { DenoExtensionContext, Settings, TsLanguageFeaturesApiV0, } from "./types"; -import { assert, getDenoCommand } from "./util"; +import { assert } from "./util"; import * as path from "path"; -import * as semver from "semver"; import * as vscode from "vscode"; -import type { Executable } from "vscode-languageclient/node"; - -/** The minimum version of Deno that this extension is designed to support. */ -const SERVER_SEMVER = ">=1.11.2"; /** The language IDs we care about. */ const LANGUAGES = [ @@ -123,6 +116,10 @@ function configurePlugin() { } function handleConfigurationChange(event: vscode.ConfigurationChangeEvent) { + if (!extensionContext.client) { + return; + } + if (event.affectsConfiguration(EXTENSION_NS)) { extensionContext.client.sendNotification( "workspace/didChangeConfiguration", @@ -175,25 +172,6 @@ const extensionContext = {} as DenoExtensionContext; export async function activate( context: vscode.ExtensionContext, ): Promise { - const command = await getDenoCommand(); - const args = ["lsp", "--parent-pid", process.pid.toString()]; - const run: Executable = { - command, - args, - // deno-lint-ignore no-undef - options: { env: { ...process.env, "NO_COLOR": true } }, - }; - - const debug: Executable = { - command, - // disabled for now, as this gets super chatty during development - // args: [...args, "-L", "debug"], - args, - // deno-lint-ignore no-undef - options: { env: { ...process.env, "NO_COLOR": true } }, - }; - - extensionContext.serverOptions = { run, debug }; extensionContext.clientOptions = { documentSelector: [ { scheme: "file", language: "javascript" }, @@ -267,32 +245,16 @@ export async function activate( registerCommand("welcome", commands.welcome); extensionContext.tsApi = await getTsApi(); - - await commands.startLanguageServer(context, extensionContext)(); - - context.subscriptions.push( - extensionContext.client.onNotification( - registryState, - createRegistryStateHandler(), - ), - ); - extensionContext.documentSettings = {}; extensionContext.workspaceSettings = getWorkspaceSettings(); configurePlugin(); + + await commands.startLanguageServer(context, extensionContext)(); + // when we activate, it might have been because a document was opened that // activated us, which we need to grab the config for and send it over to the // plugin handleDocumentOpen(...vscode.workspace.textDocuments); - - if ( - semver.valid(extensionContext.serverVersion) && - !semver.satisfies(extensionContext.serverVersion, SERVER_SEMVER) - ) { - notifyServerSemver(extensionContext.serverVersion); - } else { - showWelcomePage(context); - } } export function deactivate(): Thenable | undefined { @@ -305,23 +267,6 @@ export function deactivate(): Thenable | undefined { }); } -function notifyServerSemver(serverVersion: string) { - return vscode.window.showWarningMessage( - `The version of Deno language server ("${serverVersion}") does not meet the requirements of the extension ("${SERVER_SEMVER}"). Please update Deno and restart.`, - "OK", - ); -} - -function showWelcomePage(context: vscode.ExtensionContext) { - const welcomeShown = context.globalState.get("deno.welcomeShown") ?? - false; - - if (!welcomeShown) { - commands.welcome(context, extensionContext)(); - context.globalState.update("deno.welcomeShown", true); - } -} - /** Internal function factory that returns a registerCommand function that is * bound to the extension context. */ function createRegisterCommand( diff --git a/client/src/types.d.ts b/client/src/types.d.ts index 307a8bb6..6d3afc85 100644 --- a/client/src/types.d.ts +++ b/client/src/types.d.ts @@ -4,7 +4,6 @@ import type { ConfigurationScope, StatusBarItem } from "vscode"; import type { LanguageClient, LanguageClientOptions, - ServerOptions, } from "vscode-languageclient/node"; /** When `vscode.WorkspaceSettings` get serialized, they keys of the @@ -57,11 +56,10 @@ export interface DocumentSettings { } export interface DenoExtensionContext { - client: LanguageClient; + client: LanguageClient | undefined; clientOptions: LanguageClientOptions; /** A record of filepaths and their document settings. */ documentSettings: Record; - serverOptions: ServerOptions; serverVersion: string; statusBarItem: StatusBarItem; tsApi: TsLanguageFeaturesApiV0; diff --git a/client/src/util.ts b/client/src/util.ts index c8724516..3a459e55 100644 --- a/client/src/util.ts +++ b/client/src/util.ts @@ -1,8 +1,11 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +import * as childProcess from "child_process"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; +import * as process from "process"; +import * as semver from "semver"; import * as vscode from "vscode"; /** Assert that the condition is "truthy", otherwise throw. */ @@ -12,12 +15,34 @@ export function assert(cond: unknown, msg = "Assertion failed."): asserts cond { } } -let memoizedCommand: string | undefined; +export interface DenoCommandAndVersion { + command: string; + /** The version of the deno CLI or undefined if it couldn't be determined. */ + version: semver.SemVer | undefined; +} -export async function getDenoCommand(): Promise { - if (memoizedCommand !== undefined) { - return memoizedCommand; +export async function getDenoCommandAndVersion(): Promise< + DenoCommandAndVersion +> { + let version: semver.SemVer | undefined; + let command = "deno"; + try { + command = await getDenoCommand(); + const output = await execCommand(`${command} -V`); + const result = /[0-9]+\.[0-9]+\.[0-9]+/.exec(output); + if (result != null) { + version = new semver.SemVer(result[0]); + } + } catch (err) { + console.error(`Error getting deno version: ${err}`); } + return { + command, + version, + }; +} + +export async function getDenoCommand(): Promise { let command = vscode.workspace.getConfiguration("deno").get("path"); const workspaceFolders = vscode.workspace.workspaceFolders; const defaultCommand = await getDefaultDenoCommand(); @@ -27,7 +52,7 @@ export async function getDenoCommand(): Promise { // if sent a relative path, iterate over workspace folders to try and resolve. const list = []; for (const workspace of workspaceFolders) { - const dir = path.resolve(workspace.uri.path, command); + const dir = path.resolve(workspace.uri.fsPath, command); try { const stat = await fs.promises.stat(dir); if (stat.isFile()) { @@ -39,7 +64,7 @@ export async function getDenoCommand(): Promise { } command = list.shift() ?? defaultCommand; } - return memoizedCommand = command; + return command; } function getDefaultDenoCommand() { @@ -97,3 +122,18 @@ function getDefaultDenoCommand() { }); } } + +function execCommand(command: string): Promise { + return new Promise((resolve, reject) => { + const cwd = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + childProcess.exec(command, { + cwd, + }, (err, stdout, stderr) => { + if (err) { + reject(stderr); + } else { + resolve(stdout); + } + }); + }); +} From e7bea32e2c184b609ad93e4425888026ecefc6cd Mon Sep 17 00:00:00 2001 From: David Sherret Date: Mon, 21 Jun 2021 18:15:03 -0400 Subject: [PATCH 4/9] Start the language server last. --- client/src/extension.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/extension.ts b/client/src/extension.ts index 698764db..140f2996 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -249,12 +249,12 @@ export async function activate( extensionContext.workspaceSettings = getWorkspaceSettings(); configurePlugin(); - await commands.startLanguageServer(context, extensionContext)(); - // when we activate, it might have been because a document was opened that // activated us, which we need to grab the config for and send it over to the // plugin handleDocumentOpen(...vscode.workspace.textDocuments); + + await commands.startLanguageServer(context, extensionContext)(); } export function deactivate(): Thenable | undefined { From bdc4fe441d997140df6e073ebedb73acf605f8ac Mon Sep 17 00:00:00 2001 From: David Sherret Date: Mon, 21 Jun 2021 18:23:58 -0400 Subject: [PATCH 5/9] Add todo in `execCommand` --- client/src/util.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/src/util.ts b/client/src/util.ts index bb65b62e..f8015495 100644 --- a/client/src/util.ts +++ b/client/src/util.ts @@ -135,6 +135,9 @@ function getDefaultDenoCommand() { function execCommand(command: string): Promise { return new Promise((resolve, reject) => { + // todo(dsherret): this should handle multiple workspace folders + // in order to better support directory based deno executable + // version managers. For now, this is good enough. const cwd = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; childProcess.exec(command, { cwd, From 98c1e4109b1c0bebf6b85f93b593d20a99fb6434 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Tue, 22 Jun 2021 10:26:03 -0400 Subject: [PATCH 6/9] Revert the parent-pid changes in order to land a refactoring pr instead. --- README.md | 2 +- client/src/commands.ts | 58 +++++++++++++++++--------------------- client/src/extension.ts | 62 +++++++++++------------------------------ client/src/ts_api.ts | 33 ++++++++++++++++++++++ client/src/util.ts | 47 ------------------------------- 5 files changed, 76 insertions(+), 126 deletions(-) create mode 100644 client/src/ts_api.ts diff --git a/README.md b/README.md index 0fe9a988..e0174ad9 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This extension adds support for using [Deno](https://deno.land/) with Visual Studio Code, powered by the Deno language server. -> ⚠️ **Important:** You need to have a version of Deno CLI installed (v1.11.2 or +> ⚠️ **Important:** You need to have a version of Deno CLI installed (v1.10.3 or > later). The extension requires the executable and by default will use the > environment path. You can explicitly set the path to the executable in Visual > Studio Code Settings for `deno.path`. diff --git a/client/src/commands.ts b/client/src/commands.ts index b60611dd..76306b9d 100644 --- a/client/src/commands.ts +++ b/client/src/commands.ts @@ -17,7 +17,7 @@ import { import * as tasks from "./tasks"; import type { DenoExtensionContext } from "./types"; import { WelcomePanel } from "./welcome"; -import { assert, getDenoCommandAndVersion } from "./util"; +import { assert, getDenoCommand } from "./util"; import { registryState } from "./lsp_extensions"; import { createRegistryStateHandler } from "./notification_handlers"; @@ -29,9 +29,10 @@ import type { Location, Position, } from "vscode-languageclient/node"; +import { configurePlugin } from "./ts_api"; /** The minimum version of Deno that this extension is designed to support. */ -const SERVER_SEMVER = ">=1.11.2"; +const SERVER_SEMVER = ">=1.10.3"; // deno-lint-ignore no-explicit-any export type Callback = (...args: any[]) => unknown; @@ -102,45 +103,34 @@ export function startLanguageServer( extensionContext: DenoExtensionContext, ): Callback { return async () => { - // Reset the state and stop the existing language server + // Stop the existing language server and reset the state const { statusBarItem } = extensionContext; if (extensionContext.client) { await extensionContext.client.stop(); + extensionContext.client = undefined; statusBarItem.hide(); vscode.commands.executeCommand("setContext", ENABLEMENT_FLAG, false); } - // Check the deno version before starting the language server - // This is necessary because the `--parent-pid ` flag for - // `deno lsp` won't exist in deno versions < 1.11.2 - const { command, version } = await getDenoCommandAndVersion(); - if (version == null) { - notifyDenoNotFound(command); - return; - } else if (!semver.satisfies(version, SERVER_SEMVER)) { - notifyServerSemver(version.version); - return; - } - - // Start the new language server - const args = ["lsp", "--parent-pid", process.pid.toString()]; + // Start a new language server + const command = await getDenoCommand(); const serverOptions: ServerOptions = { run: { command, - args, + args: ["lsp"], // deno-lint-ignore no-undef options: { env: { ...process.env, "NO_COLOR": true } }, }, debug: { command, // disabled for now, as this gets super chatty during development - // args: [...args, "-L", "debug"], - args, + // args: ["lsp", "-L", "debug"], + args: ["lsp"], // deno-lint-ignore no-undef options: { env: { ...process.env, "NO_COLOR": true } }, }, }; - const client = extensionContext.client = new LanguageClient( + const client = new LanguageClient( LANGUAGE_CLIENT_ID, LANGUAGE_CLIENT_NAME, serverOptions, @@ -148,6 +138,10 @@ export function startLanguageServer( ); context.subscriptions.push(client.start()); await client.onReady(); + + // set this after a successful start + extensionContext.client = client; + vscode.commands.executeCommand("setContext", ENABLEMENT_FLAG, true); const serverVersion = extensionContext.serverVersion = (client.initializeResult?.serverInfo?.version ?? "") @@ -160,23 +154,23 @@ export function startLanguageServer( statusBarItem.show(); context.subscriptions.push( - extensionContext.client.onNotification( + client.onNotification( registryState, createRegistryStateHandler(), ), ); - showWelcomePageIfFirstUse(context, extensionContext); - }; -} + configurePlugin(extensionContext); -function notifyDenoNotFound(denoCommand: string) { - return vscode.window.showWarningMessage( - `Error resolving Deno executable. Please ensure Deno is on the PATH of this VS Code ` + - `process or specify a path to the executable in the "deno.path" setting. Could not ` + - `get version from command: ${denoCommand}`, - "OK", - ); + if ( + semver.valid(extensionContext.serverVersion) && + !semver.satisfies(extensionContext.serverVersion, SERVER_SEMVER) + ) { + notifyServerSemver(extensionContext.serverVersion); + } else { + showWelcomePageIfFirstUse(context, extensionContext); + } + }; } function notifyServerSemver(serverVersion: string) { diff --git a/client/src/extension.ts b/client/src/extension.ts index 140f2996..04782dee 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -1,24 +1,16 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. import * as commands from "./commands"; -import { - ENABLEMENT_FLAG, - EXTENSION_NS, - EXTENSION_TS_PLUGIN, - TS_LANGUAGE_FEATURES_EXTENSION, -} from "./constants"; +import { ENABLEMENT_FLAG, EXTENSION_NS } from "./constants"; import { DenoTextDocumentContentProvider, SCHEME } from "./content_provider"; import { DenoDebugConfigurationProvider } from "./debug_config_provider"; import { activateTaskProvider } from "./tasks"; -import type { - DenoExtensionContext, - Settings, - TsLanguageFeaturesApiV0, -} from "./types"; +import type { DenoExtensionContext, Settings } from "./types"; import { assert } from "./util"; import * as path from "path"; import * as vscode from "vscode"; +import { configurePlugin, getTsApi } from "./ts_api"; /** The language IDs we care about. */ const LANGUAGES = [ @@ -28,22 +20,6 @@ const LANGUAGES = [ "javascriptreact", ]; -interface TsLanguageFeatures { - getAPI(version: 0): TsLanguageFeaturesApiV0 | undefined; -} - -async function getTsApi(): Promise { - const extension: vscode.Extension | undefined = vscode - .extensions.getExtension(TS_LANGUAGE_FEATURES_EXTENSION); - const errorMessage = - "The Deno extension cannot load the built in TypeScript Language Features. Please try restarting Visual Studio Code."; - assert(extension, errorMessage); - const languageFeatures = await extension.activate(); - const api = languageFeatures.getAPI(0); - assert(api, errorMessage); - return api; -} - /** These are keys of settings that have a scope of window or machine. */ const workspaceSettingsKeys: Array = [ "codeLens", @@ -108,20 +84,9 @@ function getWorkspaceSettings(): Settings { return configToWorkspaceSettings(config); } -/** Update the typescript-deno-plugin with settings. */ -function configurePlugin() { - const { documentSettings: documents, tsApi, workspaceSettings: workspace } = - extensionContext; - tsApi.configurePlugin(EXTENSION_TS_PLUGIN, { workspace, documents }); -} - function handleConfigurationChange(event: vscode.ConfigurationChangeEvent) { - if (!extensionContext.client) { - return; - } - if (event.affectsConfiguration(EXTENSION_NS)) { - extensionContext.client.sendNotification( + extensionContext.client?.sendNotification( "workspace/didChangeConfiguration", // We actually set this to empty because the language server will // call back and get the configuration. There can be issues with the @@ -141,7 +106,11 @@ function handleConfigurationChange(event: vscode.ConfigurationChangeEvent) { ), }; } - configurePlugin(); + configurePlugin(extensionContext); + + // Restart the language server. This will allow for + // "deno.path" configuration changes to take effect. + vscode.commands.executeCommand("deno.restart"); } } @@ -161,7 +130,7 @@ function handleDocumentOpen(...documents: vscode.TextDocument[]) { didChange = true; } if (didChange) { - configurePlugin(); + configurePlugin(extensionContext); } } @@ -247,7 +216,6 @@ export async function activate( extensionContext.tsApi = await getTsApi(); extensionContext.documentSettings = {}; extensionContext.workspaceSettings = getWorkspaceSettings(); - configurePlugin(); // when we activate, it might have been because a document was opened that // activated us, which we need to grab the config for and send it over to the @@ -261,10 +229,12 @@ export function deactivate(): Thenable | undefined { if (!extensionContext.client) { return undefined; } - return extensionContext.client.stop().then(() => { - extensionContext.statusBarItem.hide(); - vscode.commands.executeCommand("setContext", ENABLEMENT_FLAG, false); - }); + + const client = extensionContext.client; + extensionContext.client = undefined; + extensionContext.statusBarItem.hide(); + vscode.commands.executeCommand("setContext", ENABLEMENT_FLAG, false); + return client.stop(); } /** Internal function factory that returns a registerCommand function that is diff --git a/client/src/ts_api.ts b/client/src/ts_api.ts new file mode 100644 index 00000000..e36cab6a --- /dev/null +++ b/client/src/ts_api.ts @@ -0,0 +1,33 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +import { + EXTENSION_TS_PLUGIN, + TS_LANGUAGE_FEATURES_EXTENSION, +} from "./constants"; +import type { DenoExtensionContext, TsLanguageFeaturesApiV0 } from "./types"; +import { assert } from "./util"; + +import * as vscode from "vscode"; + +interface TsLanguageFeatures { + getAPI(version: 0): TsLanguageFeaturesApiV0 | undefined; +} + +export async function getTsApi(): Promise { + const extension: vscode.Extension | undefined = vscode + .extensions.getExtension(TS_LANGUAGE_FEATURES_EXTENSION); + const errorMessage = + "The Deno extension cannot load the built in TypeScript Language Features. Please try restarting Visual Studio Code."; + assert(extension, errorMessage); + const languageFeatures = await extension.activate(); + const api = languageFeatures.getAPI(0); + assert(api, errorMessage); + return api; +} + +/** Update the typescript-deno-plugin with settings. */ +export function configurePlugin(extensionContext: DenoExtensionContext) { + const { documentSettings: documents, tsApi, workspaceSettings: workspace } = + extensionContext; + tsApi.configurePlugin(EXTENSION_TS_PLUGIN, { workspace, documents }); +} diff --git a/client/src/util.ts b/client/src/util.ts index f8015495..96b07022 100644 --- a/client/src/util.ts +++ b/client/src/util.ts @@ -1,11 +1,9 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -import * as childProcess from "child_process"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import * as process from "process"; -import * as semver from "semver"; import * as vscode from "vscode"; /** Assert that the condition is "truthy", otherwise throw. */ @@ -15,33 +13,6 @@ export function assert(cond: unknown, msg = "Assertion failed."): asserts cond { } } -export interface DenoCommandAndVersion { - command: string; - /** The version of the deno CLI or undefined if it couldn't be determined. */ - version: semver.SemVer | undefined; -} - -export async function getDenoCommandAndVersion(): Promise< - DenoCommandAndVersion -> { - let version: semver.SemVer | undefined; - let command = "deno"; - try { - command = await getDenoCommand(); - const output = await execCommand(`${command} -V`); - const result = /[0-9]+\.[0-9]+\.[0-9]+/.exec(output); - if (result != null) { - version = new semver.SemVer(result[0]); - } - } catch (err) { - console.error(`Error getting deno version: ${err}`); - } - return { - command, - version, - }; -} - export async function getDenoCommand(): Promise { let command = getWorkspaceConfigDenoExePath(); const workspaceFolders = vscode.workspace.workspaceFolders; @@ -132,21 +103,3 @@ function getDefaultDenoCommand() { }); } } - -function execCommand(command: string): Promise { - return new Promise((resolve, reject) => { - // todo(dsherret): this should handle multiple workspace folders - // in order to better support directory based deno executable - // version managers. For now, this is good enough. - const cwd = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; - childProcess.exec(command, { - cwd, - }, (err, stdout, stderr) => { - if (err) { - reject(stderr); - } else { - resolve(stdout); - } - }); - }); -} From 7eb0f19092437fae7afd6da3dd9e5a133ea4b8b3 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Tue, 22 Jun 2021 12:00:59 -0400 Subject: [PATCH 7/9] Only restart language server on configuration change when "deno.path" changes. --- client/src/commands.ts | 5 +++-- client/src/extension.ts | 7 ++++--- client/src/util.ts | 6 +++++- package.json | 3 ++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/client/src/commands.ts b/client/src/commands.ts index 76306b9d..162d4fa4 100644 --- a/client/src/commands.ts +++ b/client/src/commands.ts @@ -20,6 +20,7 @@ import { WelcomePanel } from "./welcome"; import { assert, getDenoCommand } from "./util"; import { registryState } from "./lsp_extensions"; import { createRegistryStateHandler } from "./notification_handlers"; +import { configurePlugin } from "./ts_api"; import * as semver from "semver"; import * as vscode from "vscode"; @@ -29,7 +30,6 @@ import type { Location, Position, } from "vscode-languageclient/node"; -import { configurePlugin } from "./ts_api"; /** The minimum version of Deno that this extension is designed to support. */ const SERVER_SEMVER = ">=1.10.3"; @@ -106,10 +106,11 @@ export function startLanguageServer( // Stop the existing language server and reset the state const { statusBarItem } = extensionContext; if (extensionContext.client) { - await extensionContext.client.stop(); + const client = extensionContext.client; extensionContext.client = undefined; statusBarItem.hide(); vscode.commands.executeCommand("setContext", ENABLEMENT_FLAG, false); + await client.stop(); } // Start a new language server diff --git a/client/src/extension.ts b/client/src/extension.ts index 04782dee..6d8d29be 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -108,9 +108,10 @@ function handleConfigurationChange(event: vscode.ConfigurationChangeEvent) { } configurePlugin(extensionContext); - // Restart the language server. This will allow for - // "deno.path" configuration changes to take effect. - vscode.commands.executeCommand("deno.restart"); + // restart when "deno.path" changes + if (event.affectsConfiguration("deno.path")) { + vscode.commands.executeCommand("deno.restart"); + } } } diff --git a/client/src/util.ts b/client/src/util.ts index 96b07022..8b7b559b 100644 --- a/client/src/util.ts +++ b/client/src/util.ts @@ -1,5 +1,7 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +import { EXTENSION_NS } from "./constants"; + import * as fs from "fs"; import * as os from "os"; import * as path from "path"; @@ -39,7 +41,9 @@ export async function getDenoCommand(): Promise { } function getWorkspaceConfigDenoExePath() { - const exePath = vscode.workspace.getConfiguration("deno").get("path"); + const exePath = vscode.workspace.getConfiguration(EXTENSION_NS).get( + "path", + ); // it is possible for the path to be blank. In that case, return undefined if (typeof exePath === "string" && exePath.trim().length === 0) { return undefined; diff --git a/package.json b/package.json index 1e88029b..b8f4f82e 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,8 @@ "command": "deno.reloadImportRegistries", "title": "Reload Import Registries Cache", "category": "Deno", - "description": "Reload any cached import registry responses." + "description": "Reload any cached import registry responses.", + "enablement": "deno:lspReady" }, { "command": "deno.restart", From dd06afe76a1f16bd1bdaede6dcf69d118f328ca8 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Tue, 22 Jun 2021 12:26:10 -0400 Subject: [PATCH 8/9] Burry complexity in ts_api.ts. --- client/src/commands.ts | 3 +-- client/src/extension.ts | 12 ++++++++---- client/src/ts_api.ts | 26 +++++++++++++++++--------- client/src/types.d.ts | 14 ++++++-------- client/src/util.ts | 5 ++--- 5 files changed, 34 insertions(+), 26 deletions(-) diff --git a/client/src/commands.ts b/client/src/commands.ts index 162d4fa4..053083ab 100644 --- a/client/src/commands.ts +++ b/client/src/commands.ts @@ -20,7 +20,6 @@ import { WelcomePanel } from "./welcome"; import { assert, getDenoCommand } from "./util"; import { registryState } from "./lsp_extensions"; import { createRegistryStateHandler } from "./notification_handlers"; -import { configurePlugin } from "./ts_api"; import * as semver from "semver"; import * as vscode from "vscode"; @@ -161,7 +160,7 @@ export function startLanguageServer( ), ); - configurePlugin(extensionContext); + extensionContext.tsApi.refresh(); if ( semver.valid(extensionContext.serverVersion) && diff --git a/client/src/extension.ts b/client/src/extension.ts index 6d8d29be..7504c8d6 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -10,7 +10,7 @@ import { assert } from "./util"; import * as path from "path"; import * as vscode from "vscode"; -import { configurePlugin, getTsApi } from "./ts_api"; +import { getTsApi } from "./ts_api"; /** The language IDs we care about. */ const LANGUAGES = [ @@ -106,7 +106,7 @@ function handleConfigurationChange(event: vscode.ConfigurationChangeEvent) { ), }; } - configurePlugin(extensionContext); + extensionContext.tsApi.refresh(); // restart when "deno.path" changes if (event.affectsConfiguration("deno.path")) { @@ -131,7 +131,7 @@ function handleDocumentOpen(...documents: vscode.TextDocument[]) { didChange = true; } if (didChange) { - configurePlugin(extensionContext); + extensionContext.tsApi.refresh(); } } @@ -214,7 +214,11 @@ export async function activate( registerCommand("test", commands.test); registerCommand("welcome", commands.welcome); - extensionContext.tsApi = await getTsApi(); + extensionContext.tsApi = await getTsApi(() => ({ + documents: extensionContext.documentSettings, + workspace: extensionContext.workspaceSettings, + })); + extensionContext.documentSettings = {}; extensionContext.workspaceSettings = getWorkspaceSettings(); diff --git a/client/src/ts_api.ts b/client/src/ts_api.ts index e36cab6a..222448a6 100644 --- a/client/src/ts_api.ts +++ b/client/src/ts_api.ts @@ -4,7 +4,7 @@ import { EXTENSION_TS_PLUGIN, TS_LANGUAGE_FEATURES_EXTENSION, } from "./constants"; -import type { DenoExtensionContext, TsLanguageFeaturesApiV0 } from "./types"; +import type { PluginSettings, TsApi } from "./types"; import { assert } from "./util"; import * as vscode from "vscode"; @@ -13,7 +13,16 @@ interface TsLanguageFeatures { getAPI(version: 0): TsLanguageFeaturesApiV0 | undefined; } -export async function getTsApi(): Promise { +interface TsLanguageFeaturesApiV0 { + configurePlugin( + pluginId: string, + configuration: PluginSettings, + ): void; +} + +export async function getTsApi( + getPluginSettings: () => PluginSettings, +): Promise { const extension: vscode.Extension | undefined = vscode .extensions.getExtension(TS_LANGUAGE_FEATURES_EXTENSION); const errorMessage = @@ -22,12 +31,11 @@ export async function getTsApi(): Promise { const languageFeatures = await extension.activate(); const api = languageFeatures.getAPI(0); assert(api, errorMessage); - return api; -} -/** Update the typescript-deno-plugin with settings. */ -export function configurePlugin(extensionContext: DenoExtensionContext) { - const { documentSettings: documents, tsApi, workspaceSettings: workspace } = - extensionContext; - tsApi.configurePlugin(EXTENSION_TS_PLUGIN, { workspace, documents }); + return { + refresh() { + const pluginSettings = getPluginSettings(); + api.configurePlugin(EXTENSION_TS_PLUGIN, pluginSettings); + }, + }; } diff --git a/client/src/types.d.ts b/client/src/types.d.ts index 6d3afc85..64eae30c 100644 --- a/client/src/types.d.ts +++ b/client/src/types.d.ts @@ -55,6 +55,11 @@ export interface DocumentSettings { settings: Partial; } +export interface TsApi { + /** Update the typescript-deno-plugin with settings. */ + refresh(): void; +} + export interface DenoExtensionContext { client: LanguageClient | undefined; clientOptions: LanguageClientOptions; @@ -62,14 +67,7 @@ export interface DenoExtensionContext { documentSettings: Record; serverVersion: string; statusBarItem: StatusBarItem; - tsApi: TsLanguageFeaturesApiV0; + tsApi: TsApi; /** The current workspace settings. */ workspaceSettings: Settings; } - -export interface TsLanguageFeaturesApiV0 { - configurePlugin( - pluginId: string, - configuration: PluginSettings, - ): void; -} diff --git a/client/src/util.ts b/client/src/util.ts index 8b7b559b..26ba6e5a 100644 --- a/client/src/util.ts +++ b/client/src/util.ts @@ -41,9 +41,8 @@ export async function getDenoCommand(): Promise { } function getWorkspaceConfigDenoExePath() { - const exePath = vscode.workspace.getConfiguration(EXTENSION_NS).get( - "path", - ); + const exePath = vscode.workspace.getConfiguration(EXTENSION_NS) + .get("path"); // it is possible for the path to be blank. In that case, return undefined if (typeof exePath === "string" && exePath.trim().length === 0) { return undefined; From 52aef02a0729cc4dc09850656b4947a58e793e8c Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 23 Jun 2021 16:04:42 -0400 Subject: [PATCH 9/9] Address feedback: * Fix bug in `reloadImportRegistries` * Move SERVER_SEMVER to constants.ts --- client/src/commands.ts | 8 +++----- client/src/constants.ts | 2 ++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/src/commands.ts b/client/src/commands.ts index 053083ab..97150e21 100644 --- a/client/src/commands.ts +++ b/client/src/commands.ts @@ -8,6 +8,7 @@ import { EXTENSION_NS, LANGUAGE_CLIENT_ID, LANGUAGE_CLIENT_NAME, + SERVER_SEMVER, } from "./constants"; import { pickInitWorkspace } from "./initialize_project"; import { @@ -30,9 +31,6 @@ import type { Position, } from "vscode-languageclient/node"; -/** The minimum version of Deno that this extension is designed to support. */ -const SERVER_SEMVER = ">=1.10.3"; - // deno-lint-ignore no-explicit-any export type Callback = (...args: any[]) => unknown; export type Factory = ( @@ -91,9 +89,9 @@ export function initializeWorkspace( export function reloadImportRegistries( _context: vscode.ExtensionContext, - { client }: DenoExtensionContext, + extensionContext: DenoExtensionContext, ): Callback { - return () => client?.sendRequest(reloadImportRegistriesReq); + return () => extensionContext.client?.sendRequest(reloadImportRegistriesReq); } /** Start (or restart) the Deno Language Server */ diff --git a/client/src/constants.ts b/client/src/constants.ts index 6fd065eb..b7b5a30e 100644 --- a/client/src/constants.ts +++ b/client/src/constants.ts @@ -8,3 +8,5 @@ export const LANGUAGE_CLIENT_ID = "deno-language-server"; export const LANGUAGE_CLIENT_NAME = "Deno Language Server"; export const TS_LANGUAGE_FEATURES_EXTENSION = "vscode.typescript-language-features"; +/** The minimum version of Deno that this extension is designed to support. */ +export const SERVER_SEMVER = ">=1.10.3";