diff --git a/api/vscode-clangd.d.ts b/api/vscode-clangd.d.ts index 6e9b00d0..47a2f8fa 100644 --- a/api/vscode-clangd.d.ts +++ b/api/vscode-clangd.d.ts @@ -11,8 +11,19 @@ export interface ClangdApiV1 { languageClient: BaseLanguageClient|undefined } +export interface ClangdApiV2 { + // vscode-clangd's language clients keyed by workspace folder which can be used to send requests to the + // clangd language server(s) + // Standard requests: + // https://microsoft.github.io/language-server-protocol/specifications/specification-current + // clangd custom requests: + // https://clangd.llvm.org/extensions + languageClients: Map +} + export interface ClangdExtension { getApi(version: 1): ClangdApiV1; + getApi(version: 2): ClangdApiV2; } // clangd custom request types diff --git a/src/api.ts b/src/api.ts index 114e94cc..fc63bd15 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,14 +1,18 @@ -import {BaseLanguageClient} from 'vscode-languageclient'; - -import {ClangdApiV1, ClangdExtension} from '../api/vscode-clangd'; +import {ClangdApiV1, ClangdApiV2, ClangdExtension} from '../api/vscode-clangd'; +import {ClangdContext} from './clangd-context'; export class ClangdExtensionImpl implements ClangdExtension { - constructor(public client: BaseLanguageClient|undefined) {} + constructor(public context: ClangdContext) {} public getApi(version: 1): ClangdApiV1; + public getApi(version: 2): ClangdApiV2; public getApi(version: number): unknown { if (version === 1) { - return {languageClient: this.client}; + return {languageClient: this.context.clients.entries().next().value?.[1]}; + } + + if (version === 2) { + return {languageClients: [...this.context.clients.values()]}; } throw new Error(`No API version ${version} found`); diff --git a/src/ast.ts b/src/ast.ts index c136f982..02510ecd 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -3,61 +3,17 @@ import * as vscode from 'vscode'; import * as vscodelc from 'vscode-languageclient/node'; -import {ClangdContext} from './clangd-context'; +import {ClangdContext, ClangdLanguageClient} from './clangd-context'; import type {ASTParams, ASTNode} from '../api/vscode-clangd'; const ASTRequestMethod = 'textDocument/ast'; -export function activate(context: ClangdContext) { - const feature = new ASTFeature(context); - context.client.registerFeature(feature); -} - const ASTRequestType = new vscodelc.RequestType(ASTRequestMethod); -class ASTFeature implements vscodelc.StaticFeature { - constructor(private context: ClangdContext) { - // The adapter holds the currently inspected node. - const adapter = new TreeAdapter(); - // Create the AST view, showing data from the adapter. - const tree = - vscode.window.createTreeView('clangd.ast', {treeDataProvider: adapter}); - context.subscriptions.push( - tree, - // Ensure the AST view is visible exactly when the adapter has a node. - // clangd.ast.hasData controls the view visibility (package.json). - adapter.onDidChangeTreeData((_) => { - vscode.commands.executeCommand('setContext', 'clangd.ast.hasData', - adapter.hasRoot()); - // Work around https://github.com/microsoft/vscode/issues/90005 - // Show the AST tree even if it's been collapsed or closed. - // reveal(root) fails here: "Data tree node not found". - if (adapter.hasRoot()) - // @ts-ignore - tree.reveal(null); - }), - // Create the "Show AST" command for the context menu. - // It's only shown if the feature is dynamicaly available (package.json) - vscode.commands.registerTextEditorCommand( - 'clangd.ast', - async (editor, _edit) => { - const converter = this.context.client.code2ProtocolConverter; - const item = - await this.context.client.sendRequest(ASTRequestType, { - textDocument: - converter.asTextDocumentIdentifier(editor.document), - range: converter.asRange(editor.selection), - }); - if (!item) - vscode.window.showInformationMessage( - 'No AST node at selection'); - adapter.setRoot(item ?? undefined, editor.document.uri); - }), - // Clicking "close" will empty the adapter, which in turn hides the - // view. - vscode.commands.registerCommand( - 'clangd.ast.close', () => adapter.setRoot(undefined, undefined))); +export class ASTFeature implements vscodelc.StaticFeature { + constructor(client: ClangdLanguageClient) { + client.registerFeature(this); } fillClientCapabilities(capabilities: vscodelc.ClientCapabilities) {} @@ -65,8 +21,8 @@ class ASTFeature implements vscodelc.StaticFeature { // The "Show AST" command is enabled if the server advertises the capability. initialize(capabilities: vscodelc.ServerCapabilities, _documentSelector: vscodelc.DocumentSelector|undefined) { - vscode.commands.executeCommand('setContext', 'clangd.ast.supported', - 'astProvider' in capabilities); + if ('astProvider' in capabilities) + vscode.commands.executeCommand('setContext', 'clangd.ast.supported', true); } getState(): vscodelc.FeatureState { return {kind: 'static'}; } clear() {} @@ -108,10 +64,55 @@ function describe(role: string, kind: string): string { } // Map a root ASTNode onto a VSCode tree. -class TreeAdapter implements vscode.TreeDataProvider { +export class ASTProvider implements vscode.TreeDataProvider { private root?: ASTNode; private doc?: vscode.Uri; + constructor(context: ClangdContext) { + // Create the AST view, showing data from the adapter. + const tree = + vscode.window.createTreeView('clangd.ast', {treeDataProvider: this}); + context.subscriptions.push( + tree, + // Ensure the AST view is visible exactly when the adapter has a node. + // clangd.ast.hasData controls the view visibility (package.json). + this.onDidChangeTreeData((_) => { + vscode.commands.executeCommand('setContext', 'clangd.ast.hasData', + this.hasRoot()); + // Work around https://github.com/microsoft/vscode/issues/90005 + // Show the AST tree even if it's been collapsed or closed. + // reveal(root) fails here: "Data tree node not found". + if (this.hasRoot()) + // @ts-ignore + tree.reveal(null); + }), + // Create the "Show AST" command for the context menu. + // It's only shown if the feature is dynamicaly available (package.json) + vscode.commands.registerTextEditorCommand( + 'clangd.ast', + async (editor, _edit) => { + const client = context.getActiveClient(); + if (client === undefined) + return; + + const converter = client.code2ProtocolConverter; + const item = + await client.sendRequest(ASTRequestType, { + textDocument: + converter.asTextDocumentIdentifier(editor.document), + range: converter.asRange(editor.selection), + }); + if (!item) + vscode.window.showInformationMessage( + 'No AST node at selection'); + this.setRoot(item ?? undefined, editor.document.uri); + }), + // Clicking "close" will empty the adapter, which in turn hides the + // view. + vscode.commands.registerCommand( + 'clangd.ast.close', () => this.setRoot(undefined, undefined))); + } + hasRoot(): boolean { return this.root !== undefined; } setRoot(newRoot: ASTNode|undefined, newDoc: vscode.Uri|undefined) { diff --git a/src/clangd-context.ts b/src/clangd-context.ts index cd99b2fa..c59dd32b 100644 --- a/src/clangd-context.ts +++ b/src/clangd-context.ts @@ -1,31 +1,32 @@ import * as vscode from 'vscode'; import * as vscodelc from 'vscode-languageclient/node'; -import * as ast from './ast'; +import { ASTFeature, ASTProvider } from './ast'; import * as config from './config'; -import * as configFileWatcher from './config-file-watcher'; -import * as fileStatus from './file-status'; -import * as inactiveRegions from './inactive-regions'; -import * as inlayHints from './inlay-hints'; -import * as install from './install'; -import * as memoryUsage from './memory-usage'; +import { ConfigFileWatcher, ConfigFileWatcherFeature } from './config-file-watcher'; +import { FileStatus } from './file-status' +import { InactiveRegionsFeature } from './inactive-regions'; +import { InlayHintsFeature } from './inlay-hints'; +import { MemoryUsageProvider, MemoryUsageFeature } from './memory-usage'; import * as openConfig from './open-config'; import * as switchSourceHeader from './switch-source-header'; -import * as typeHierarchy from './type-hierarchy'; +import { TypeHierarchyProvider, TypeHierarchyFeature } from './type-hierarchy'; -export const clangdDocumentSelector = [ - {scheme: 'file', language: 'c'}, - {scheme: 'file', language: 'cpp'}, - {scheme: 'file', language: 'cuda-cpp'}, - {scheme: 'file', language: 'objective-c'}, - {scheme: 'file', language: 'objective-cpp'}, -]; +export function clangdDocumentSelector(folder: vscode.WorkspaceFolder|undefined): vscodelc.DocumentSelector { + return [{scheme: 'file', language: 'c', pattern: `${folder?.uri.fsPath}/**/*`}, + {scheme: 'file', language: 'cpp', pattern: `${folder?.uri.fsPath}/**/*`}, + {scheme: 'file', language: 'cuda-cpp', pattern: `${folder?.uri.fsPath}/**/*`}, + {scheme: 'file', language: 'objective-c', pattern: `${folder?.uri.fsPath}/**/*`}, + {scheme: 'file', language: 'objective-cpp', pattern: `${folder?.uri.fsPath}/**/*`}]; +} export function isClangdDocument(document: vscode.TextDocument) { - return vscode.languages.match(clangdDocumentSelector, document); + return vscode.languages.match(clangdDocumentSelector(vscode.workspace.getWorkspaceFolder(document.uri)), document); } -class ClangdLanguageClient extends vscodelc.LanguageClient { +export class ClangdLanguageClient extends vscodelc.LanguageClient { + subscriptions: vscode.Disposable[] = []; + // Override the default implementation for failed requests. The default // behavior is just to log failures in the output panel, however output panel // is designed for extension debugging purpose, normal users will not open it, @@ -34,6 +35,14 @@ class ClangdLanguageClient extends vscodelc.LanguageClient { // For user-interactive operations (e.g. applyFixIt, applyTweaks), we will // prompt up the failure to users. + constructor( + public readonly context: ClangdContext, + serverOptions: vscodelc.ServerOptions, + clientOptions: vscodelc.LanguageClientOptions, + ) { + super('clangd', serverOptions, clientOptions) + } + handleFailedRequest(type: vscodelc.MessageSignature, error: any, token: vscode.CancellationToken|undefined, defaultValue: T): T { @@ -43,9 +52,27 @@ class ClangdLanguageClient extends vscodelc.LanguageClient { return super.handleFailedRequest(type, token, error, defaultValue); } + + isStarting() { + return this.state == vscodelc.State.Starting; + } + + visibleEditors() { + return vscode.window.visibleTextEditors.filter((e) => isClangdDocument(e.document) && + vscode.workspace.getWorkspaceFolder(e.document.uri) == this.clientOptions.workspaceFolder); + } + + dispose(timeout?: number): Promise { + this.subscriptions.forEach((d) => { d.dispose(); }); + return super.dispose(timeout); + } } class EnableEditsNearCursorFeature implements vscodelc.StaticFeature { + constructor(client: ClangdLanguageClient) { + client.registerFeature(this); + } + initialize() {} fillClientCapabilities(capabilities: vscodelc.ClientCapabilities): void { const extendedCompletionCapabilities: any = @@ -58,42 +85,50 @@ class EnableEditsNearCursorFeature implements vscodelc.StaticFeature { export class ClangdContext implements vscode.Disposable { subscriptions: vscode.Disposable[]; - client: ClangdLanguageClient; - - static async create(globalStoragePath: string, - outputChannel: vscode.OutputChannel): - Promise { - const subscriptions: vscode.Disposable[] = []; - const clangdPath = await install.activate(subscriptions, globalStoragePath); - if (!clangdPath) { - subscriptions.forEach((d) => { d.dispose(); }); - return null; - } + clients: Map; - return new ClangdContext(subscriptions, clangdPath, outputChannel); - } + configFileWatcher: ConfigFileWatcher|undefined; + fileStatus: FileStatus; + + constructor(private outputChannel: vscode.OutputChannel) { + this.subscriptions = []; + this.clients = new Map(); + + new ASTProvider(this); + new MemoryUsageProvider(this); + new TypeHierarchyProvider(this); + + if (config.get('onConfigChanged') !== 'ignore') + this.configFileWatcher = new ConfigFileWatcher(this); + + this.fileStatus = new FileStatus(this); + + openConfig.activate(this); + switchSourceHeader.activate(this); + InlayHintsFeature.activate(this); + }; - private constructor(subscriptions: vscode.Disposable[], clangdPath: string, - outputChannel: vscode.OutputChannel) { - this.subscriptions = subscriptions; - const useScriptAsExecutable = config.get('useScriptAsExecutable'); - let clangdArguments = config.get('arguments'); + public addClient(folder: vscode.WorkspaceFolder) { + const useScriptAsExecutable = config.get('useScriptAsExecutable', folder); + let clangdPath = config.get('path', folder); + let clangdArguments = config.get('arguments', folder); if (useScriptAsExecutable) { let quote = (str: string) => { return `"${str}"`; }; - clangdPath = quote(clangdPath) + clangdPath = quote(clangdPath); for (var i = 0; i < clangdArguments.length; i++) { clangdArguments[i] = quote(clangdArguments[i]); } + } const clangd: vscodelc.Executable = { command: clangdPath, args: clangdArguments, options: { - cwd: vscode.workspace.rootPath || process.cwd(), + cwd: folder?.uri.fsPath || process.cwd(), shell: useScriptAsExecutable } }; - const traceFile = config.get('trace'); + const traceFile = config.get('trace', folder); if (!!traceFile) { const trace = {CLANGD_TRACE: traceFile}; clangd.options = {env: {...process.env, ...trace}}; @@ -102,12 +137,13 @@ export class ClangdContext implements vscode.Disposable { const clientOptions: vscodelc.LanguageClientOptions = { // Register the server for c-family and cuda files. - documentSelector: clangdDocumentSelector, + documentSelector: clangdDocumentSelector(folder), + workspaceFolder: folder, initializationOptions: { clangdFileStatus: true, - fallbackFlags: config.get('fallbackFlags') + fallbackFlags: config.get('fallbackFlags', folder) }, - outputChannel: outputChannel, + outputChannel: this.outputChannel, // Do not switch to output window when clangd returns output. revealOutputChannelOn: vscodelc.RevealOutputChannelOn.Never, @@ -128,10 +164,10 @@ export class ClangdContext implements vscode.Disposable { middleware: { provideCompletionItem: async (document, position, context, token, next) => { - if (!config.get('enableCodeCompletion')) + if (!config.get('enableCodeCompletion', folder)) return new vscode.CompletionList([], /*isIncomplete=*/ false); let list = await next(document, position, context, token); - if (!config.get('serverCompletionRanking')) + if (!config.get('serverCompletionRanking', folder)) return list; let items = (!list ? [] : Array.isArray(list) ? list : list.items); items = items.map(item => { @@ -165,7 +201,7 @@ export class ClangdContext implements vscode.Disposable { return new vscode.CompletionList(items, /*isIncomplete=*/ true); }, provideHover: async (document, position, token, next) => { - if (!config.get('enableHover')) + if (!config.get('enableHover', folder)) return null; return next(document, position, token); }, @@ -199,24 +235,49 @@ export class ClangdContext implements vscode.Disposable { }, }; - this.client = new ClangdLanguageClient('Clang Language Server', - serverOptions, clientOptions); - this.client.clientOptions.errorHandler = - this.client.createDefaultErrorHandler( + const client = new ClangdLanguageClient(this, serverOptions, clientOptions); + this.subscriptions.push(client); + + client.clientOptions.errorHandler = + client.createDefaultErrorHandler( // max restart count - config.get('restartAfterCrash') ? /*default*/ 4 : 0); - this.client.registerFeature(new EnableEditsNearCursorFeature); - typeHierarchy.activate(this); - inlayHints.activate(this); - memoryUsage.activate(this); - ast.activate(this); - openConfig.activate(this); - inactiveRegions.activate(this); - configFileWatcher.activate(this); - this.client.start(); + config.get('restartAfterCrash', folder) ? /*default*/ 4 : 0); + new EnableEditsNearCursorFeature(client); + new TypeHierarchyFeature(client); + new InlayHintsFeature(client); + new MemoryUsageFeature(client); + new ASTFeature(client); + new InactiveRegionsFeature(client); + new ConfigFileWatcherFeature(client); + + client.onDidChangeState(({newState}) => { + if (newState === vscodelc.State.Running) { + // clangd starts or restarts after crash. + client.onNotification( + 'textDocument/clangd.fileStatus', + (fileStatus) => { this.fileStatus.onFileUpdated(fileStatus); }); + } else if (newState === vscodelc.State.Stopped) { + // Clear all cached statuses when clangd crashes. + this.fileStatus.clear(); + } + }); + + client.start(); console.log('Clang Language Server is now active!'); - fileStatus.activate(this); - switchSourceHeader.activate(this); + + this.clients.set(folder.name, client); + } + + public removeClient(folder: vscode.WorkspaceFolder) { + const client = this.clients.get(folder.name); + if (client) { + this.clients.delete(folder.name); + client.dispose(); + } + } + + public hasClient(folder: vscode.WorkspaceFolder): boolean { + return this.clients.has(folder.name); } get visibleClangdEditors(): vscode.TextEditor[] { @@ -224,14 +285,32 @@ export class ClangdContext implements vscode.Disposable { (e) => isClangdDocument(e.document)); } - clientIsStarting() { - return this.client && this.client.state == vscodelc.State.Starting; + public getActiveFolder(): vscode.WorkspaceFolder|undefined { + let folder = undefined; + + if (vscode.window.activeTextEditor !== undefined) { + folder = vscode.workspace.getWorkspaceFolder(vscode.window.activeTextEditor.document.uri) + } else if (vscode.workspace.workspaceFolders !== undefined) { + folder = vscode.workspace.workspaceFolders[0]; + } + + return folder; + } + + + public getActiveClient(): ClangdLanguageClient|undefined { + const folder = this.getActiveFolder(); + if (folder === undefined) { + return undefined; + } + + return this.clients.get(folder.name); } dispose() { this.subscriptions.forEach((d) => { d.dispose(); }); - if (this.client) - this.client.stop(); this.subscriptions = [] + this.clients.forEach((client) => { client.stop(); }); + this.clients.clear(); } } diff --git a/src/config-file-watcher.ts b/src/config-file-watcher.ts index 61337ec6..02c087bb 100644 --- a/src/config-file-watcher.ts +++ b/src/config-file-watcher.ts @@ -1,22 +1,16 @@ import * as vscode from 'vscode'; import * as vscodelc from 'vscode-languageclient/node'; -import {ClangdContext} from './clangd-context'; +import {ClangdContext, ClangdLanguageClient} from './clangd-context'; import * as config from './config'; -export function activate(context: ClangdContext) { - if (config.get('onConfigChanged') !== 'ignore') { - context.client.registerFeature(new ConfigFileWatcherFeature(context)); - } -} - // Clangd extension capabilities. interface ClangdClientCapabilities { compilationDatabase?: {automaticReload?: boolean;}, } -class ConfigFileWatcherFeature implements vscodelc.StaticFeature { - constructor(private context: ClangdContext) {} +export class ConfigFileWatcherFeature implements vscodelc.StaticFeature { + constructor(private client: ClangdLanguageClient) {} fillClientCapabilities(capabilities: vscodelc.ClientCapabilities) {} initialize(capabilities: vscodelc.ServerCapabilities, @@ -26,21 +20,17 @@ class ConfigFileWatcherFeature implements vscodelc.StaticFeature { .compilationDatabase?.automaticReload) { return; } - this.context.subscriptions.push(new ConfigFileWatcher(this.context)); + if (this.client.context.configFileWatcher !== undefined) + this.client.context.configFileWatcher = new ConfigFileWatcher(this.client.context); } getState(): vscodelc.FeatureState { return {kind: 'static'}; } clear() {} } -class ConfigFileWatcher implements vscode.Disposable { +export class ConfigFileWatcher { private databaseWatcher?: vscode.FileSystemWatcher; private debounceTimer?: NodeJS.Timeout; - dispose() { - if (this.databaseWatcher) - this.databaseWatcher.dispose(); - } - constructor(private context: ClangdContext) { this.createFileSystemWatcher(); context.subscriptions.push(vscode.workspace.onDidChangeWorkspaceFolders( @@ -55,6 +45,7 @@ class ConfigFileWatcher implements vscode.Disposable { '{' + vscode.workspace.workspaceFolders.map(f => f.uri.fsPath).join(',') + '}/{build/compile_commands.json,compile_commands.json,compile_flags.txt}'); + this.context.subscriptions.push(this.databaseWatcher); this.context.subscriptions.push(this.databaseWatcher.onDidChange( this.debouncedHandleConfigFilesChanged.bind(this))); this.context.subscriptions.push(this.databaseWatcher.onDidCreate( diff --git a/src/config.ts b/src/config.ts index f9708b98..c573d656 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,8 +3,8 @@ import * as path from 'path'; import * as vscode from 'vscode'; // Gets the config value `clangd.`. Applies ${variable} substitutions. -export function get(key: string): T { - return substitute(vscode.workspace.getConfiguration('clangd').get(key)!); +export function get(key: string, folder?: vscode.WorkspaceFolder|undefined): T { + return substitute(vscode.workspace.getConfiguration('clangd', folder).get(key)!, folder); } // Sets the config value `clangd.`. Does not apply substitutions. @@ -14,11 +14,11 @@ export function update(key: string, value: T, } // Traverse a JSON value, replacing placeholders in all strings. -function substitute(val: T): T { +function substitute(val: T, folder?: vscode.WorkspaceFolder|undefined): T { if (typeof val === 'string') { val = val.replace(/\$\{(.*?)\}/g, (match, name) => { // If there's no replacement available, keep the placeholder. - return replacement(name) ?? match; + return replacement(name, folder) ?? match; }) as unknown as T; } else if (Array.isArray(val)) { val = val.map((x) => substitute(x)) as unknown as T; @@ -35,21 +35,20 @@ function substitute(val: T): T { // Subset of substitution variables that are most likely to be useful. // https://code.visualstudio.com/docs/editor/variables-reference -function replacement(name: string): string|undefined { +function replacement(name: string, folder?: vscode.WorkspaceFolder|undefined): string|undefined { if (name === 'userHome') { return homedir(); } if (name === 'workspaceRoot' || name === 'workspaceFolder' || name === 'cwd') { - if (vscode.workspace.rootPath !== undefined) - return vscode.workspace.rootPath; + if (folder !== undefined) + return folder.uri.fsPath; if (vscode.window.activeTextEditor !== undefined) return path.dirname(vscode.window.activeTextEditor.document.uri.fsPath); return process.cwd(); } - if (name === 'workspaceFolderBasename' && - vscode.workspace.rootPath !== undefined) { - return path.basename(vscode.workspace.rootPath); + if (name === 'workspaceFolderBasename' && folder !== undefined) { + return path.basename(folder.uri.fsPath); } const envPrefix = 'env:'; if (name.startsWith(envPrefix)) diff --git a/src/extension.ts b/src/extension.ts index 815866fc..2be02a01 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,8 +3,9 @@ import * as vscode from 'vscode'; import {ClangdExtension} from '../api/vscode-clangd'; import {ClangdExtensionImpl} from './api'; -import {ClangdContext} from './clangd-context'; +import {ClangdContext, isClangdDocument} from './clangd-context'; import {get, update} from './config'; +import * as install from './install'; let apiInstance: ClangdExtensionImpl|undefined; @@ -17,7 +18,9 @@ export async function activate(context: vscode.ExtensionContext): const outputChannel = vscode.window.createOutputChannel('clangd'); context.subscriptions.push(outputChannel); - let clangdContext: ClangdContext|null = null; + const clangdContext = new ClangdContext(outputChannel); + context.subscriptions.push(clangdContext); + await install.activate(context.subscriptions, context.globalStoragePath); // An empty place holder for the activate command, otherwise we'll get an // "command is not registered" error. @@ -25,7 +28,12 @@ export async function activate(context: vscode.ExtensionContext): vscode.commands.registerCommand('clangd.activate', async () => {})); context.subscriptions.push( vscode.commands.registerCommand('clangd.restart', async () => { - if (!get('enable')) { + const folder = clangdContext.getActiveFolder(); + if (folder === undefined) { + return; + } + + if (!get('enable', folder)) { vscode.window .showInformationMessage( 'Language features from Clangd are currently disabled. Would you like to enable them?', @@ -39,34 +47,51 @@ export async function activate(context: vscode.ExtensionContext): return; } + const client = clangdContext.getActiveClient(); + if (client === undefined) { + return; + } + // clangd.restart can be called when the extension is not yet activated. // In such a case, vscode will activate the extension and then run this // handler. Detect this situation and bail out (doing an extra // 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 && clangdContext.clientIsStarting()) { - return; - } - if (clangdContext) - clangdContext.dispose(); - clangdContext = await ClangdContext.create(context.globalStoragePath, - outputChannel); - if (clangdContext) - context.subscriptions.push(clangdContext); - if (apiInstance) { - apiInstance.client = clangdContext?.client; + if (client.isStarting()) { + return; } + + clangdContext.removeClient(folder); + clangdContext.addClient(folder); })); + function didOpenTextDocument(document: vscode.TextDocument) { + if (!isClangdDocument(document) || document.uri.scheme !== 'file') { + return; + } + + let folder = vscode.workspace.getWorkspaceFolder(document.uri); + if (folder === undefined) { + return; + } + + if (!clangdContext.hasClient(folder)) { + clangdContext.addClient(folder); + } + } + + vscode.workspace.onDidOpenTextDocument(didOpenTextDocument); + vscode.workspace.textDocuments.forEach(didOpenTextDocument); + vscode.workspace.onDidChangeWorkspaceFolders((event) => { + for (const folder of event.removed) { + clangdContext.removeClient(folder); + } + }); + let shouldCheck = false; if (vscode.workspace.getConfiguration('clangd').get('enable')) { - clangdContext = - await ClangdContext.create(context.globalStoragePath, outputChannel); - if (clangdContext) - context.subscriptions.push(clangdContext); - shouldCheck = vscode.workspace.getConfiguration('clangd').get( 'detectExtensionConflicts') ?? false; @@ -104,6 +129,6 @@ export async function activate(context: vscode.ExtensionContext): }, 5000); } - apiInstance = new ClangdExtensionImpl(clangdContext?.client); + apiInstance = new ClangdExtensionImpl(clangdContext); return apiInstance; } diff --git a/src/file-status.ts b/src/file-status.ts index 15570a4a..a9a3deae 100644 --- a/src/file-status.ts +++ b/src/file-status.ts @@ -1,35 +1,19 @@ import * as vscode from 'vscode'; -import * as vscodelc from 'vscode-languageclient/node'; import {ClangdContext} from './clangd-context'; -export function activate(context: ClangdContext) { - context.subscriptions.push(vscode.commands.registerCommand( - 'clangd.openOutputPanel', () => context.client.outputChannel.show())); - const status = new FileStatus('clangd.openOutputPanel'); - context.subscriptions.push(vscode.Disposable.from(status)); - context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor( - () => { status.updateStatus(); })); - context.subscriptions.push(context.client.onDidChangeState(({newState}) => { - if (newState === vscodelc.State.Running) { - // clangd starts or restarts after crash. - context.client.onNotification( - 'textDocument/clangd.fileStatus', - (fileStatus) => { status.onFileUpdated(fileStatus); }); - } else if (newState === vscodelc.State.Stopped) { - // Clear all cached statuses when clangd crashes. - status.clear(); - } - })); -} - -class FileStatus { +export class FileStatus { private statuses = new Map(); - private readonly statusBarItem = - vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 10); - - constructor(onClickCommand: string) { - this.statusBarItem.command = onClickCommand; + private statusBarItem: vscode.StatusBarItem; + + constructor(context: ClangdContext) { + context.subscriptions.push(vscode.commands.registerCommand( + 'clangd.openOutputPanel', () => context.getActiveClient()?.outputChannel.show())); + this.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 10); + this.statusBarItem.command = 'clangd.openOutputPanel' + context.subscriptions.push(this.statusBarItem); + context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor( + () => { this.updateStatus(); })); } onFileUpdated(fileStatus: any) { diff --git a/src/inactive-regions.ts b/src/inactive-regions.ts index 277f4169..8e81d8fe 100644 --- a/src/inactive-regions.ts +++ b/src/inactive-regions.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import * as vscodelc from 'vscode-languageclient/node'; -import {ClangdContext} from './clangd-context'; +import {ClangdLanguageClient} from './clangd-context'; import * as config from './config'; // Parameters for the inactive regions (server-side) push notification. @@ -19,19 +19,14 @@ export const NotificationType = new vscodelc.NotificationType( 'textDocument/inactiveRegions'); -export function activate(context: ClangdContext) { - const feature = new InactiveRegionsFeature(context); - context.client.registerFeature(feature); - context.client.onNotification(NotificationType, - feature.handleNotification.bind(feature)); -} - export class InactiveRegionsFeature implements vscodelc.StaticFeature { private decorationType?: vscode.TextEditorDecorationType; private files: Map = new Map(); - private context: ClangdContext; - constructor(context: ClangdContext) { this.context = context; } + constructor(private readonly client: ClangdLanguageClient) { + client.registerFeature(this); + client.subscriptions.push(client.onNotification(NotificationType, this.handleNotification.bind(this))); + } fillClientCapabilities(capabilities: vscodelc.ClientCapabilities) { // Extend the ClientCapabilities type and add inactive regions @@ -51,11 +46,11 @@ export class InactiveRegionsFeature implements vscodelc.StaticFeature { {inactiveRegionsProvider?: any} = capabilities; if (serverCapabilities.inactiveRegionsProvider) { this.updateDecorationType(); - this.context.subscriptions.push( + this.client.subscriptions.push( vscode.window.onDidChangeVisibleTextEditors( - (editors) => editors.forEach( + () => this.client.visibleEditors().forEach( (e) => this.applyHighlights(e.document.fileName)))); - this.context.subscriptions.push( + this.client.subscriptions.push( vscode.workspace.onDidChangeConfiguration((conf) => { const inactiveSettingsChanged = conf.affectsConfiguration( @@ -82,14 +77,14 @@ export class InactiveRegionsFeature implements vscodelc.StaticFeature { handleNotification(params: InactiveRegionsParams) { const filePath = vscode.Uri.parse(params.textDocument.uri, true).fsPath; const ranges: vscode.Range[] = params.regions.map( - (r) => this.context.client.protocol2CodeConverter.asRange(r)); + (r) => this.client.protocol2CodeConverter.asRange(r)); this.files.set(filePath, ranges); this.applyHighlights(filePath); } updateDecorationType() { this.decorationType?.dispose(); - if (config.get('inactiveRegions.useBackgroundHighlight')) { + if (config.get('inactiveRegions.useBackgroundHighlight', this.client.clientOptions.workspaceFolder)) { this.decorationType = vscode.window.createTextEditorDecorationType({ isWholeLine: true, backgroundColor: @@ -98,7 +93,7 @@ export class InactiveRegionsFeature implements vscodelc.StaticFeature { } else { this.decorationType = vscode.window.createTextEditorDecorationType({ isWholeLine: true, - opacity: config.get('inactiveRegions.opacity').toString() + opacity: config.get('inactiveRegions.opacity', this.client.clientOptions.workspaceFolder).toString() }); } } @@ -107,7 +102,7 @@ export class InactiveRegionsFeature implements vscodelc.StaticFeature { const ranges = this.files.get(filePath); if (!ranges) return; - this.context.visibleClangdEditors.forEach((e) => { + this.client.visibleEditors().forEach((e) => { if (!this.decorationType) return; if (e.document.fileName !== filePath) diff --git a/src/inlay-hints.ts b/src/inlay-hints.ts index a99b3892..cd7a63df 100644 --- a/src/inlay-hints.ts +++ b/src/inlay-hints.ts @@ -11,12 +11,7 @@ import * as vscode from 'vscode'; import * as vscodelc from 'vscode-languageclient/node'; -import {ClangdContext, clangdDocumentSelector} from './clangd-context'; - -export function activate(context: ClangdContext) { - const feature = new InlayHintsFeature(context); - context.client.registerFeature(feature); -} +import {ClangdContext, ClangdLanguageClient} from './clangd-context'; namespace protocol { @@ -40,10 +35,32 @@ export const type = } // namespace protocol -class InlayHintsFeature implements vscodelc.StaticFeature { - private commandRegistered = false; +export class InlayHintsFeature implements vscodelc.StaticFeature { + public static activate(context: ClangdContext) { + const enabledSetting = 'editor.inlayHints.enabled'; + context.subscriptions.push( + vscode.commands.registerCommand('clangd.inlayHints.toggle', () => { + // This used to be a boolean, and then became a 4-state enum. + var val = vscode.workspace.getConfiguration().get( + enabledSetting, 'on'); + if (val === true || val === 'on') + val = 'off'; + else if (val === false || val === 'off') + val = 'on'; + else if (val === 'offUnlessPressed') + val = 'onUnlessPressed'; + else if (val == 'onUnlessPressed') + val = 'offUnlessPressed'; + else + return; + vscode.workspace.getConfiguration().update( + enabledSetting, val, vscode.ConfigurationTarget.Global); + })); + } - constructor(private readonly context: ClangdContext) {} + constructor(client: ClangdLanguageClient) { + client.registerFeature(this); + } fillClientCapabilities(_capabilities: vscodelc.ClientCapabilities) {} fillInitializeParams(_params: vscodelc.InitializeParams) {} @@ -53,81 +70,11 @@ class InlayHintsFeature implements vscodelc.StaticFeature { const serverCapabilities: vscodelc.ServerCapabilities& {clangdInlayHintsProvider?: boolean, inlayHintProvider?: any} = capabilities; - vscode.commands.executeCommand( - 'setContext', 'clangd.inlayHints.supported', - serverCapabilities.clangdInlayHintsProvider || - serverCapabilities.inlayHintProvider); - if (!this.commandRegistered) { - // The command provides a quick way to toggle inlay hints - // (key-bindable). - // FIXME: this is a core VSCode setting, ideally they provide the - // command. We toggle it globally, language-specific is nicer but - // undiscoverable. - this.commandRegistered = true; - const enabledSetting = 'editor.inlayHints.enabled'; - this.context.subscriptions.push( - vscode.commands.registerCommand('clangd.inlayHints.toggle', () => { - // This used to be a boolean, and then became a 4-state enum. - var val = vscode.workspace.getConfiguration().get( - enabledSetting, 'on'); - if (val === true || val === 'on') - val = 'off'; - else if (val === false || val === 'off') - val = 'on'; - else if (val === 'offUnlessPressed') - val = 'onUnlessPressed'; - else if (val == 'onUnlessPressed') - val = 'offUnlessPressed'; - else - return; - vscode.workspace.getConfiguration().update( - enabledSetting, val, vscode.ConfigurationTarget.Global); - })); + + if (serverCapabilities.clangdInlayHintsProvider || serverCapabilities.inlayHintProvider) { + vscode.commands.executeCommand('setContext', 'clangd.inlayHints.supported', true); } - // If the clangd server supports LSP 3.17 inlay hints, these are handled by - // the vscode-languageclient library - don't send custom requests too! - if (!serverCapabilities.clangdInlayHintsProvider || - serverCapabilities.inlayHintProvider) - return; - this.context.subscriptions.push(vscode.languages.registerInlayHintsProvider( - clangdDocumentSelector, new Provider(this.context))); } getState(): vscodelc.FeatureState { return {kind: 'static'}; } clear() {} } - -class Provider implements vscode.InlayHintsProvider { - constructor(private context: ClangdContext) {} - - decodeKind(kind: string): vscode.InlayHintKind|undefined { - if (kind == 'type') - return vscode.InlayHintKind.Type; - if (kind == 'parameter') - return vscode.InlayHintKind.Parameter; - return undefined; - } - - decode(hint: protocol.InlayHint): vscode.InlayHint { - return { - position: - this.context.client.protocol2CodeConverter.asPosition(hint.position!), - kind: this.decodeKind(hint.kind), - label: hint.label.trim(), - paddingLeft: hint.label.startsWith(' '), - paddingRight: hint.label.endsWith(' '), - }; - } - - async provideInlayHints(document: vscode.TextDocument, range: vscode.Range, - token: vscode.CancellationToken): - Promise { - const request: protocol.InlayHintsParams = { - textDocument: {uri: document.uri.toString()}, - range: this.context.client.code2ProtocolConverter.asRange(range), - }; - - const result = await this.context.client.sendRequest( - protocol.InlayHintsRequest.type, request, token); - return result.map(this.decode, this); - } -} diff --git a/src/memory-usage.ts b/src/memory-usage.ts index 0ca0e9d5..0f8282dc 100644 --- a/src/memory-usage.ts +++ b/src/memory-usage.ts @@ -8,12 +8,7 @@ import * as vscode from 'vscode'; import * as vscodelc from 'vscode-languageclient/node'; -import {ClangdContext} from './clangd-context'; - -export function activate(context: ClangdContext) { - const feature = new MemoryUsageFeature(context); - context.client.registerFeature(feature); -} +import {ClangdContext, ClangdLanguageClient} from './clangd-context'; // LSP wire format for this clangd feature. interface NoParams {} @@ -48,22 +43,9 @@ function convert(m: WireTree, title: string): InternalTree { }; } -class MemoryUsageFeature implements vscodelc.StaticFeature { - constructor(private context: ClangdContext) { - const adapter = new TreeAdapter(); - adapter.onDidChangeTreeData((e) => vscode.commands.executeCommand( - 'setContext', 'clangd.memoryUsage.hasData', - adapter.root !== undefined)); - this.context.subscriptions.push( - vscode.window.registerTreeDataProvider('clangd.memoryUsage', adapter)); - this.context.subscriptions.push( - vscode.commands.registerCommand('clangd.memoryUsage', async () => { - const usage = - await this.context.client.sendRequest(MemoryUsageRequest, {}); - adapter.root = convert(usage, ''); - })); - this.context.subscriptions.push(vscode.commands.registerCommand( - 'clangd.memoryUsage.close', () => adapter.root = undefined)); +export class MemoryUsageFeature implements vscodelc.StaticFeature { + constructor(client: ClangdLanguageClient) { + client.registerFeature(this); } fillClientCapabilities(capabilities: vscodelc.ClientCapabilities) {} @@ -71,16 +53,33 @@ class MemoryUsageFeature implements vscodelc.StaticFeature { initialize(capabilities: vscodelc.ServerCapabilities, _documentSelector: vscodelc.DocumentSelector|undefined) { - vscode.commands.executeCommand('setContext', 'clangd.memoryUsage.supported', - 'memoryUsageProvider' in capabilities); + if ('memoryUsageProvider' in capabilities) + vscode.commands.executeCommand('setContext', 'clangd.memoryUsage.supported', true); } getState(): vscodelc.FeatureState { return {kind: 'static'}; } clear() {} } -class TreeAdapter implements vscode.TreeDataProvider { +export class MemoryUsageProvider implements vscode.TreeDataProvider { private root_?: InternalTree; + constructor(context: ClangdContext) { + context.subscriptions.push(this.onDidChangeTreeData((e) => vscode.commands.executeCommand( + 'setContext', 'clangd.memoryUsage.hasData', + this.root !== undefined))); + context.subscriptions.push( + vscode.window.registerTreeDataProvider('clangd.memoryUsage', this)); + context.subscriptions.push( + vscode.commands.registerCommand('clangd.memoryUsage', async () => { + const usage = + await context.getActiveClient()?.sendRequest(MemoryUsageRequest, {}); + if (usage !== undefined) + this.root = convert(usage, ''); + })); + context.subscriptions.push(vscode.commands.registerCommand( + 'clangd.memoryUsage.close', () => this.root = undefined)); + } + get root(): InternalTree|undefined { return this.root_; } set root(n: InternalTree|undefined) { this.root_ = n; diff --git a/src/switch-source-header.ts b/src/switch-source-header.ts index 4d3bf286..55401dc3 100644 --- a/src/switch-source-header.ts +++ b/src/switch-source-header.ts @@ -1,11 +1,14 @@ import * as vscode from 'vscode'; import * as vscodelc from 'vscode-languageclient/node'; - -import {ClangdContext} from './clangd-context'; +import { ClangdContext } from './clangd-context'; export function activate(context: ClangdContext) { context.subscriptions.push(vscode.commands.registerCommand( - 'clangd.switchheadersource', () => switchSourceHeader(context.client))); + 'clangd.switchheadersource', () => { + const active = context.getActiveClient() + if (active !== undefined) + switchSourceHeader(active) + })); } namespace SwitchSourceHeaderRequest { @@ -31,4 +34,4 @@ async function switchSourceHeader(client: vscodelc.LanguageClient): const doc = await vscode.workspace.openTextDocument(vscode.Uri.parse(sourceUri)); vscode.window.showTextDocument(doc); -} \ No newline at end of file +} diff --git a/src/type-hierarchy.ts b/src/type-hierarchy.ts index f4134572..8bac00a6 100644 --- a/src/type-hierarchy.ts +++ b/src/type-hierarchy.ts @@ -8,12 +8,7 @@ import * as vscode from 'vscode'; import * as vscodelc from 'vscode-languageclient/node'; -import {ClangdContext} from './clangd-context'; - -export function activate(context: ClangdContext) { - const feature = new TypeHierarchyFeature(context); - context.client.registerFeature(feature); -} +import {ClangdContext, ClangdLanguageClient} from './clangd-context'; export namespace TypeHierarchyDirection { export const Children = 0; @@ -98,18 +93,17 @@ class TypeHierarchyTreeItem extends vscode.TreeItem { } } -class TypeHierarchyFeature implements vscodelc.StaticFeature { +export class TypeHierarchyFeature implements vscodelc.StaticFeature { private serverSupportsTypeHierarchy = false; private state!: vscodelc.State; - private context: ClangdContext; - constructor(context: ClangdContext) { - this.context = context; - new TypeHierarchyProvider(context); - context.subscriptions.push(context.client.onDidChangeState(stateChange => { + constructor(private client: ClangdLanguageClient) { + client.subscriptions.push(client.onDidChangeState(stateChange => { this.state = stateChange.newState; this.recomputeEnableTypeHierarchy(); })); + + client.registerFeature(this); } fillClientCapabilities(capabilities: vscodelc.ClientCapabilities) {} @@ -126,8 +120,7 @@ class TypeHierarchyFeature implements vscodelc.StaticFeature { if (serverCapabilities.typeHierarchyProvider && !serverCapabilities.standardTypeHierarchyProvider) { // Disable mis-guided support for standard type-hierarchy feature. - this.context.client.getFeature('textDocument/prepareTypeHierarchy') - .clear(); + this.client.getFeature('textDocument/prepareTypeHierarchy').clear(); this.serverSupportsTypeHierarchy = true; this.recomputeEnableTypeHierarchy(); } else { @@ -149,10 +142,10 @@ class TypeHierarchyFeature implements vscodelc.StaticFeature { } } -class TypeHierarchyProvider implements +export class TypeHierarchyProvider implements vscode.TreeDataProvider { - private client: vscodelc.LanguageClient; + public client?: vscodelc.LanguageClient; private _onDidChangeTreeData = new vscode.EventEmitter(); @@ -167,8 +160,6 @@ class TypeHierarchyProvider implements private startingItem!: TypeHierarchyItem; constructor(context: ClangdContext) { - this.client = context.client; - context.subscriptions.push(vscode.commands.registerTextEditorCommand( 'clangd.typeHierarchy', this.reveal, this)); context.subscriptions.push(vscode.commands.registerCommand( @@ -182,6 +173,13 @@ class TypeHierarchyProvider implements 'clangd.typeHierarchy.viewChildren', () => this.setDirection(TypeHierarchyDirection.Children))); + context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(() => { + const active = context.getActiveClient(); + if (active !== undefined) { + this.client = active; + } + })); + this.treeView = vscode.window.createTreeView('clangd.typeHierarchyView', {treeDataProvider: this}); context.subscriptions.push(this.treeView); @@ -190,6 +188,10 @@ class TypeHierarchyProvider implements } public async gotoItem(item: TypeHierarchyItem) { + if (this.client === undefined) { + return; + } + const uri = vscode.Uri.parse(item.uri); const range = this.client.protocol2CodeConverter.asRange(item.selectionRange); @@ -251,7 +253,7 @@ class TypeHierarchyProvider implements return element.parents ?? []; } // Otherwise, this.direction === Children. - if (!element.children) { + if (!element.children && this.client !== undefined) { // Children are not resolved yet, resolve them now. const resolved = await this.client.sendRequest(ResolveTypeHierarchyRequest.type, { @@ -297,6 +299,10 @@ class TypeHierarchyProvider implements vscode.commands.executeCommand('setContext', 'clangd.typeHierarchyVisible', true); + if (this.client === undefined) { + return; + } + const item = await this.client.sendRequest(TypeHierarchyRequest.type, { ...this.client.code2ProtocolConverter.asTextDocumentPositionParams( editor.document, editor.selection.active),