Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: better handling when language server fails to start #454

Merged
merged 10 commits into from
Jun 24, 2021
90 changes: 81 additions & 9 deletions client/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
EXTENSION_NS,
LANGUAGE_CLIENT_ID,
LANGUAGE_CLIENT_NAME,
SERVER_SEMVER,
} from "./constants";
import { pickInitWorkspace } from "./initialize_project";
import {
Expand All @@ -17,10 +18,13 @@ import {
import * as tasks from "./tasks";
import type { DenoExtensionContext } from "./types";
import { WelcomePanel } from "./welcome";
import { assert } from "./util";
import { assert, getDenoCommand } 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,
Expand All @@ -42,14 +46,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() },
Expand Down Expand Up @@ -84,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 */
Expand All @@ -95,20 +100,46 @@ export function startLanguageServer(
extensionContext: DenoExtensionContext,
): Callback {
return async () => {
// 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();
}
const client = extensionContext.client = new LanguageClient(

// Start a new language server
const command = await getDenoCommand();
const serverOptions: ServerOptions = {
run: {
command,
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: ["lsp", "-L", "debug"],
args: ["lsp"],
// deno-lint-ignore no-undef
options: { env: { ...process.env, "NO_COLOR": true } },
},
};
const client = new LanguageClient(
LANGUAGE_CLIENT_ID,
LANGUAGE_CLIENT_NAME,
extensionContext.serverOptions,
serverOptions,
extensionContext.clientOptions,
);
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 ?? "")
Expand All @@ -119,14 +150,55 @@ export function startLanguageServer(
statusBarItem.tooltip = client
.initializeResult?.serverInfo?.version;
statusBarItem.show();

context.subscriptions.push(
client.onNotification(
registryState,
createRegistryStateHandler(),
),
);

extensionContext.tsApi.refresh();

if (
semver.valid(extensionContext.serverVersion) &&
!semver.satisfies(extensionContext.serverVersion, SERVER_SEMVER)
) {
notifyServerSemver(extensionContext.serverVersion);
} else {
showWelcomePageIfFirstUse(context, extensionContext);
}
};
}

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<boolean>("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),
Expand Down
2 changes: 2 additions & 0 deletions client/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
4 changes: 4 additions & 0 deletions client/src/content_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export class DenoTextDocumentContentProvider
uri: Uri,
token: CancellationToken,
): ProviderResult<string> {
if (!this.extensionContext.client) {
throw new Error("Deno language server has not started.");
}

return this.extensionContext.client.sendRequest(
virtualTextDocument,
{ textDocument: { uri: uri.toString() } },
Expand Down
125 changes: 23 additions & 102 deletions client/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,31 +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 { 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 type { DenoExtensionContext, Settings } from "./types";
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.10.3";
import { getTsApi } from "./ts_api";

/** The language IDs we care about. */
const LANGUAGES = [
Expand All @@ -35,22 +20,6 @@ const LANGUAGES = [
"javascriptreact",
];

interface TsLanguageFeatures {
getAPI(version: 0): TsLanguageFeaturesApiV0 | undefined;
}

async function getTsApi(): Promise<TsLanguageFeaturesApiV0> {
const extension: vscode.Extension<TsLanguageFeatures> | 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;
}
kitsonk marked this conversation as resolved.
Show resolved Hide resolved

/** These are keys of settings that have a scope of window or machine. */
const workspaceSettingsKeys: Array<keyof Settings> = [
"codeLens",
Expand Down Expand Up @@ -115,16 +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 (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
Expand All @@ -144,7 +106,12 @@ function handleConfigurationChange(event: vscode.ConfigurationChangeEvent) {
),
};
}
configurePlugin();
extensionContext.tsApi.refresh();

// restart when "deno.path" changes
if (event.affectsConfiguration("deno.path")) {
vscode.commands.executeCommand("deno.restart");
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restarts the language server when someone changes "deno.path". This is a bit more convenient for users.

Side note: These command names and setting names should probably be moved to constants...

}
}

Expand All @@ -164,7 +131,7 @@ function handleDocumentOpen(...documents: vscode.TextDocument[]) {
didChange = true;
}
if (didChange) {
configurePlugin();
extensionContext.tsApi.refresh();
}
}

Expand All @@ -175,24 +142,6 @@ const extensionContext = {} as DenoExtensionContext;
export async function activate(
context: vscode.ExtensionContext,
): Promise<void> {
const command = await getDenoCommand();
const run: Executable = {
command,
args: ["lsp"],
// 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: ["lsp", "-L", "debug"],
args: ["lsp"],
// deno-lint-ignore no-undef
options: { env: { ...process.env, "NO_COLOR": true } },
};

extensionContext.serverOptions = { run, debug };
extensionContext.clientOptions = {
documentSelector: [
{ scheme: "file", language: "javascript" },
Expand Down Expand Up @@ -265,60 +214,32 @@ export async function activate(
registerCommand("test", commands.test);
registerCommand("welcome", commands.welcome);

extensionContext.tsApi = await getTsApi();

await commands.startLanguageServer(context, extensionContext)();

context.subscriptions.push(
extensionContext.client.onNotification(
registryState,
createRegistryStateHandler(),
),
);
extensionContext.tsApi = await getTsApi(() => ({
documents: extensionContext.documentSettings,
workspace: extensionContext.workspaceSettings,
}));

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
// plugin
handleDocumentOpen(...vscode.workspace.textDocuments);

if (
semver.valid(extensionContext.serverVersion) &&
!semver.satisfies(extensionContext.serverVersion, SERVER_SEMVER)
) {
notifyServerSemver(extensionContext.serverVersion);
} else {
showWelcomePage(context);
}
await commands.startLanguageServer(context, extensionContext)();
}

export function deactivate(): Thenable<void> | undefined {
if (!extensionContext.client) {
return undefined;
}
return extensionContext.client.stop().then(() => {
extensionContext.statusBarItem.hide();
vscode.commands.executeCommand("setContext", ENABLEMENT_FLAG, false);
});
}

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<boolean>("deno.welcomeShown") ??
false;

if (!welcomeShown) {
commands.welcome(context, extensionContext)();
context.globalState.update("deno.welcomeShown", true);
}
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
Expand Down
Loading