diff --git a/api/README.md b/api/README.md index f3e95d2..dd2c6fc 100644 --- a/api/README.md +++ b/api/README.md @@ -17,6 +17,11 @@ const provideHover = async (document: vscode.TextDocument, position: vscode.Posi if (clangdExtension) { const api = (await clangdExtension.activate()).getApi(CLANGD_API_VERSION); + + // Extension may be disabled or have failed to initialize + if (!api.languageClient) { + return undefined; + } const textDocument = api.languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document); const range = api.languageClient.code2ProtocolConverter.asRange(new vscode.Range(position, position)); diff --git a/api/vscode-clangd.d.ts b/api/vscode-clangd.d.ts index 1bbbda4..6e9b00d 100644 --- a/api/vscode-clangd.d.ts +++ b/api/vscode-clangd.d.ts @@ -8,7 +8,7 @@ export interface ClangdApiV1 { // https://microsoft.github.io/language-server-protocol/specifications/specification-current // clangd custom requests: // https://clangd.llvm.org/extensions - languageClient: BaseLanguageClient + languageClient: BaseLanguageClient|undefined } export interface ClangdExtension { diff --git a/src/api.ts b/src/api.ts index fa9b443..114e94c 100644 --- a/src/api.ts +++ b/src/api.ts @@ -3,7 +3,7 @@ import {BaseLanguageClient} from 'vscode-languageclient'; import {ClangdApiV1, ClangdExtension} from '../api/vscode-clangd'; export class ClangdExtensionImpl implements ClangdExtension { - constructor(public client: BaseLanguageClient) {} + constructor(public client: BaseLanguageClient|undefined) {} public getApi(version: 1): ClangdApiV1; public getApi(version: number): unknown { diff --git a/src/clangd-context.ts b/src/clangd-context.ts index 51beadb..db2412b 100644 --- a/src/clangd-context.ts +++ b/src/clangd-context.ts @@ -58,17 +58,26 @@ class EnableEditsNearCursorFeature implements vscodelc.StaticFeature { export class ClangdContext implements vscode.Disposable { subscriptions: vscode.Disposable[] = []; - client!: ClangdLanguageClient; + client: ClangdLanguageClient; - async activate(globalStoragePath: string, - outputChannel: vscode.OutputChannel) { - const clangdPath = await install.activate(this, globalStoragePath); + static async create(globalStoragePath: string, + outputChannel: vscode.OutputChannel): + Promise { + const subscriptions: vscode.Disposable[] = []; + const clangdPath = await install.activate(subscriptions, globalStoragePath); if (!clangdPath) - return; + return null; + const clangdArguments = await config.get('arguments'); + + return new ClangdContext(clangdPath, clangdArguments, outputChannel); + } + + private constructor(clangdPath: string, clangdArguments: string[], + outputChannel: vscode.OutputChannel) { const clangd: vscodelc.Executable = { command: clangdPath, - args: await config.get('arguments'), + args: clangdArguments, options: {cwd: vscode.workspace.rootPath || process.cwd()} }; const traceFile = config.get('trace'); @@ -111,7 +120,8 @@ export class ClangdContext implements vscode.Disposable { let list = await next(document, position, context, token); if (!config.get('serverCompletionRanking')) return list; - let items = (Array.isArray(list) ? list : list!.items).map(item => { + let items = (!list ? [] : Array.isArray(list) ? list : list.items); + items = items.map(item => { // Gets the prefix used by VSCode when doing fuzzymatch. let prefix = document.getText( new vscode.Range((item.range as vscode.Range).start, position)) diff --git a/src/extension.ts b/src/extension.ts index fb9370a..2f9a79c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,8 +16,7 @@ export async function activate(context: vscode.ExtensionContext): const outputChannel = vscode.window.createOutputChannel('clangd'); context.subscriptions.push(outputChannel); - const clangdContext = new ClangdContext; - context.subscriptions.push(clangdContext); + let clangdContext: ClangdContext|null = null; // An empty place holder for the activate command, otherwise we'll get an // "command is not registered" error. @@ -31,20 +30,27 @@ export async function activate(context: vscode.ExtensionContext): // stop/start cycle in this situation is pointless, and doesn't work // anyways because the client can't be stop()-ped when it's still in the // Starting state). - if (clangdContext.clientIsStarting()) { + if (clangdContext && clangdContext.clientIsStarting()) { return; } - await clangdContext.dispose(); - await clangdContext.activate(context.globalStoragePath, outputChannel); + if (clangdContext) + clangdContext.dispose(); + clangdContext = await ClangdContext.create(context.globalStoragePath, + outputChannel); + if (clangdContext) + context.subscriptions.push(clangdContext); if (apiInstance) { - apiInstance.client = clangdContext.client; + apiInstance.client = clangdContext?.client; } })); let shouldCheck = false; if (vscode.workspace.getConfiguration('clangd').get('enable')) { - await clangdContext.activate(context.globalStoragePath, outputChannel); + clangdContext = + await ClangdContext.create(context.globalStoragePath, outputChannel); + if (clangdContext) + context.subscriptions.push(clangdContext); shouldCheck = vscode.workspace.getConfiguration('clangd').get( 'detectExtensionConflicts') ?? @@ -83,6 +89,6 @@ export async function activate(context: vscode.ExtensionContext): }, 5000); } - apiInstance = new ClangdExtensionImpl(clangdContext.client); + apiInstance = new ClangdExtensionImpl(clangdContext?.client); return apiInstance; } diff --git a/src/install.ts b/src/install.ts index d9c3bd8..b333c40 100644 --- a/src/install.ts +++ b/src/install.ts @@ -6,23 +6,23 @@ import AbortController from 'abort-controller'; import * as path from 'path'; import * as vscode from 'vscode'; -import {ClangdContext} from './clangd-context'; import * as config from './config'; // Returns the clangd path to be used, or null if clangd is not installed. -export async function activate( - context: ClangdContext, globalStoragePath: string): Promise { - const ui = new UI(context, globalStoragePath); - context.subscriptions.push(vscode.commands.registerCommand( +export async function activate(disposables: vscode.Disposable[], + globalStoragePath: string): + Promise { + const ui = new UI(disposables, globalStoragePath); + disposables.push(vscode.commands.registerCommand( 'clangd.install', async () => common.installLatest(ui))); - context.subscriptions.push(vscode.commands.registerCommand( + disposables.push(vscode.commands.registerCommand( 'clangd.update', async () => common.checkUpdates(true, ui))); const status = await common.prepare(ui, config.get('checkUpdates')); return status.clangdPath; } class UI { - constructor(private context: ClangdContext, + constructor(private disposables: vscode.Disposable[], private globalStoragePath: string) {} get storagePath(): string { return this.globalStoragePath; } @@ -60,8 +60,7 @@ class UI { error(s: string) { vscode.window.showErrorMessage(s); } info(s: string) { vscode.window.showInformationMessage(s); } command(name: string, body: () => any) { - this.context.subscriptions.push( - vscode.commands.registerCommand(name, body)); + this.disposables.push(vscode.commands.registerCommand(name, body)); } async shouldReuse(release: string): Promise { @@ -121,7 +120,7 @@ class UI { } get clangdPath(): string { - let p = config.get('path')!; + let p = config.get('path'); // Backwards compatibility: if it's a relative path with a slash, interpret // relative to project root. if (!path.isAbsolute(p) && p.includes(path.sep) &&