Skip to content

Commit

Permalink
fix: better handling when language server fails to start (#454)
Browse files Browse the repository at this point in the history
  • Loading branch information
dsherret committed Jun 24, 2021
1 parent 8c10050 commit 6bf0454
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 132 deletions.
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;
}

/** 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");
}
}
}

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

0 comments on commit 6bf0454

Please sign in to comment.