UNPKG

coc-ccls

Version:

C/C++/ObjC language server supporting cross references, hierarchies, completion and semantic highlighting

675 lines (616 loc) 22.5 kB
import * as cp from "child_process"; import { commands, LanguageClient, LanguageClientOptions, ProvideCodeLensesSignature, RevealOutputChannelOn, ServerOptions, workspace, WorkspaceConfiguration } from "coc.nvim"; // import { Converter } from "vscode-languageclient/lib/protocolConverter"; import { CancellationToken, CodeLens, Disposable, Position, Range, TextDocument, } from "vscode-languageserver-protocol"; import * as ls from "vscode-languageserver-types"; import Uri from "vscode-uri"; import WebSocket from 'ws'; import { CclsErrorHandler } from "./cclsErrorHandler"; import { cclsChan, logChan } from './globalContext'; // import { CallHierarchyProvider } from "./hierarchies/callHierarchy"; // import { InheritanceHierarchyProvider } from "./hierarchies/inheritanceHierarchy"; // import { MemberHierarchyProvider } from "./hierarchies/memberHierarchy"; import { InactiveRegionsProvider } from "./inactiveRegions"; import { PublishSemanticHighlightArgs, SemanticContext, semanticTypes } from "./semantic"; import { StatusBarIconProvider } from "./statusBarIcon"; import { ClientConfig, IHierarchyNode } from './types'; import { disposeAll, normalizeUri, unwrap, wait } from "./utils"; import { jumpToUriAtPosition } from "./vscodeUtils"; const window = workspace; interface LastGoto { id: any; clockTime: number; } function flatObjectImpl(obj: any, pref: string, result: Map<string, string>) { if (typeof obj === "object") { for (const key of Object.keys(obj)) { const val = obj[key]; const newpref = `${pref}.${key}`; if (typeof val === "object" || val instanceof Array) { flatObjectImpl(val, newpref, result); } else { result.set(newpref, `${val}`); } } } else if (obj instanceof Array) { let idx = 0; for (const val of obj) { const newpref = `${pref}.${idx}`; if (typeof val === "object" || val instanceof Array) { flatObjectImpl(val, newpref, result); } else { result.set(newpref, `${val}`); } idx++; } } } function flatObject(obj: any, pref = ""): Map<string, string> { const result = new Map<string, string>(); flatObjectImpl(obj, pref, result); return result; } function getClientConfig(wsRoot: string): ClientConfig { function hasAnySemanticHighlight() { const hlconfig = workspace.getConfiguration('ccls.highlighting.enabled'); for (const name of Object.keys(semanticTypes)) { if (hlconfig.get(name, false)) return true; } return false; } function resolveVariablesInString(value: string) { return value.replace('${workspaceFolder}', wsRoot); } function resloveVariablesInArray(value: any[]): any[] { return value.map((v) => resolveVariables(v)); } function resolveVariables(value: any) { if (typeof(value) === 'string') { return resolveVariablesInString(value); } if (Array.isArray(value)) { return resloveVariablesInArray(value); } return value; } function setConfig(config: ClientConfig, dottedName: string, value: any) { if (value != null) { const subprops = dottedName.split('.'); let subconfig = config; for (const subprop of subprops.slice(0, subprops.length - 1)) { if (!subconfig.hasOwnProperty(subprop)) { subconfig[subprop] = {}; } subconfig = subconfig[subprop]; } subconfig[subprops[subprops.length - 1]] = resolveVariables(value); } } function setClientConfigFromWorkspaceConfig( cliConfig: ClientConfig, workspaceConfig: WorkspaceConfiguration, mapping: Map<string, string>, blacklist: Set<string> ) { function recurse(config: WorkspaceConfiguration, parentPath = '') { for (const key of Object.keys(config)) { const value = config[key]; const currentPath = (parentPath ? parentPath + '.' : '') + key; if (blacklist.has(currentPath) || typeof value === 'function') { continue; } if (typeof value === 'object' && value !== null && !(value instanceof Array)) { recurse(value, currentPath); } else { setConfig(cliConfig, mapping.get(currentPath) || currentPath, value); } } } recurse(workspaceConfig); } // Read prefs; this map goes from `vscode prefs name` => `ccls/js name`. // For flags which have different name between vscode-ccls prefs and // ClientConfig / ccls initializationOption const configMapping = new Map([ ['launch.command', 'launchCommand'], ['launch.args', 'launchArgs'], ['misc.compilationDatabaseCommand', 'compilationDatabaseCommand'], ['misc.compilationDatabaseDirectory', 'compilationDatabaseDirectory'], ['completion.enableSnippetInsertion', 'client.snippetSupport'], ]); // For flags which should not be populated in ClientConfig (used only by other parts of vscode-ccls) // It seems like ccls happily ignores extra keys in initializationOption so this is not required. const configBlacklist = new Set([ 'codeLens.enabled', 'codeLens.renderInline', 'highlighting', 'misc.showInactiveRegions', 'theme', 'trace', 'treeViews', ]); const clientConfig: ClientConfig = { highlight: { blacklist: hasAnySemanticHighlight() ? [] : [".*"], lsRanges: true, }, launchArgs: [] as string[], launchCommand: '', statusUpdateInterval: 0, traceEndpoint: '', }; setClientConfigFromWorkspaceConfig( clientConfig, workspace.getConfiguration('ccls'), configMapping, configBlacklist ); return clientConfig; } /** instance represents running instance of ccls */ export class ServerContext implements Disposable { private client: LanguageClient; private clientPid?: number; private cliConfig: ClientConfig; private ignoredConf = new Array<string>(); private _dispose: Disposable[] = []; // private p2c: Converter; private lastGoto: LastGoto = { clockTime: 0, id: undefined, }; public constructor( public readonly cwd: string, lazyMode: boolean = false ) { this.cliConfig = getClientConfig(cwd); if (lazyMode) { this.ignoredConf.push(".index.initialBlacklist"); this.cliConfig.index.initialBlacklist = [".*"]; } workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this._dispose); this.client = this.initClient(); // this.p2c = this.client.protocol2CodeConverter; } public dispose() { return disposeAll(this._dispose); } public async start() { this._dispose.push(this.client.start()); try { await this.client.onReady(); } catch (e) { window.showMessage(`Failed to start ccls with command "${ this.cliConfig.launchCommand }".`); } // General commands. this._dispose.push(commands.registerCommand("ccls.vars", this.makeRefHandler("$ccls/vars"))); this._dispose.push(commands.registerCommand("ccls.call", this.makeRefHandler("$ccls/call"))); this._dispose.push(commands.registerCommand("ccls.member", this.makeRefHandler("$ccls/member"))); this._dispose.push(commands.registerCommand( "ccls.base", this.makeRefHandler("$ccls/inheritance", { derived: false }, true))); // this._dispose.push(commands.registerCommand("ccls.showXrefs", this.showXrefsHandlerCmd, this)); // The language client does not correctly deserialize arguments, so we have a // wrapper command that does it for us. this._dispose.push(commands.registerCommand('ccls.showReferences', this.showReferencesCmd, this)); this._dispose.push(commands.registerCommand('ccls.goto', this.gotoCmd, this)); this._dispose.push(commands.registerCommand("ccls._applyFixIt", this.fixItCmd, this)); this._dispose.push(commands.registerCommand('ccls._autoImplement', this.autoImplementCmd, this)); this._dispose.push(commands.registerCommand('ccls._insertInclude', this.insertIncludeCmd, this)); const config = workspace.getConfiguration('ccls'); if (config.get('misc.showInactiveRegions')) { const inact = new InactiveRegionsProvider(this.client); this._dispose.push(inact); } // const inheritanceHierarchyProvider = new InheritanceHierarchyProvider(this.client); // this._dispose.push(inheritanceHierarchyProvider); // this._dispose.push(window.registerTreeDataProvider( // "ccls.inheritanceHierarchy", inheritanceHierarchyProvider // )); // const callHierarchyProvider = new CallHierarchyProvider(this.client); // this._dispose.push(callHierarchyProvider); // this._dispose.push(window.registerTreeDataProvider( // 'ccls.callHierarchy', callHierarchyProvider // )); // const memberHierarchyProvider = new MemberHierarchyProvider(this.client); // this._dispose.push(memberHierarchyProvider); // this._dispose.push(window.registerTreeDataProvider( // 'ccls.memberHierarchy', memberHierarchyProvider // )); // Common between tree views. this._dispose.push(commands.registerCommand( "ccls.gotoForTreeView", this.gotoForTreeView, this )); this._dispose.push(commands.registerCommand( "ccls.hackGotoForTreeView", this.hackGotoForTreeView, this )); // Semantic highlighting const semantic = new SemanticContext(); this._dispose.push(semantic); this.client.onNotification('$ccls/publishSemanticHighlight', (args: PublishSemanticHighlightArgs) => semantic.publishSemanticHighlight(args) ); this._dispose.push(commands.registerCommand( 'ccls.navigate', this.makeNavigateHandler('$ccls/navigate') )); const interval = this.cliConfig.statusUpdateInterval; if (interval) { const statusBarIconProvider = new StatusBarIconProvider(this.client, interval); this._dispose.push(statusBarIconProvider); } this._dispose.push(commands.registerCommand("ccls.reload", this.reloadIndex, this)); } public async stop() { const pid = unwrap(this.clientPid); const serverResponds = await Promise.race([ (async () => { await wait(300); return false; })(), (async () => { await this.client.stop(); return true; })() ]); // waitpid was called in client.stop if (!serverResponds) { console.info('Server does not repsond, killing'); try { process.kill(pid, 'SIGTERM'); } catch (e) { console.info('Kill failed: ' + (e as Error).message); } } this.clientPid = undefined; } private reloadIndex() { this.client.sendNotification("$ccls/reload"); } private async onDidChangeConfiguration() { const newConfig = getClientConfig(this.cwd); const newflat = flatObject(newConfig); const oldflat = flatObject(this.cliConfig); for (const [key, newVal] of newflat) { const oldVal = oldflat.get(key); if (newVal === undefined || this.ignoredConf.some((e) => key.startsWith(e))) { continue; } if (oldVal !== newVal) { const kRestart = 'Restart'; const message = `Please restart server to apply the "ccls${key}" configuration change.`; const selected = await window.showMessage(message); // if (selected === kRestart) // commands.executeCommand('ccls.restart'); // break; } } } private async provideCodeLens( document: TextDocument, token: CancellationToken, next: ProvideCodeLensesSignature ): Promise<CodeLens[]> { const config = workspace.getConfiguration('ccls'); const enableCodeLens = config.get('codeLens.enabled'); if (!enableCodeLens) return []; const enableInlineCodeLens = config.get('codeLens.renderInline', false); if (!enableInlineCodeLens) { const uri = document.uri; const position = document.positionAt(0); const lensesObjs = await this.client.sendRequest<Array<any>>('textDocument/codeLens', { position, textDocument: { uri: uri.toString(), }, }); // const lenses = this.p2c.asCodeLenses(lensesObjs); // return lenses.map((lense: CodeLens) => { // const cmd = lense.command; // if (cmd && cmd.command === 'ccls.xref') { // // Change to a custom command which will fetch and then show the results // cmd.command = 'ccls.showXrefs'; // cmd.arguments = [ // uri, // lense.range.start, // cmd.arguments, // ]; // } // return this.p2c.asCodeLens(lense); // }); } // We run the codeLens request ourselves so we can intercept the response. const a = await this.client.sendRequest<ls.CodeLens[]>( 'textDocument/codeLens', { textDocument: { uri: document.uri.toString(), }, } ); // const result: CodeLens[] = this.p2c.asCodeLenses(a); // this.displayCodeLens(document, result); return []; } // private displayCodeLens(document: TextDocument, allCodeLens: CodeLens[]) { // const decorationOpts: DecorationRenderOptions = { // after: { // color: new ThemeColor('editorCodeLens.foreground'), // fontStyle: 'italic', // }, // rangeBehavior: DecorationRangeBehavior.ClosedClosed, // }; // // const codeLensDecoration = window.createTextEditorDecorationType(decorationOpts); // for (const editor of window.visibleTextEditors) { // if (editor.document !== document) // continue; // // const opts: DecorationOptions[] = []; // // for (const codeLens of allCodeLens) { // // FIXME: show a real warning or disable on-the-side code lens. // if (!codeLens.isResolved) // console.error('Code lens is not resolved'); // // // Default to after the content. // let position = codeLens.range.end; // // // If multiline push to the end of the first line - works better for // // functions. // if (codeLens.range.start.line !== codeLens.range.end.line) // position = new Position(codeLens.range.start.line, 1000000); // // const range = new Range(position, position); // const opt: DecorationOptions = { // range, // renderOptions: // {after: {contentText: ' ' + unwrap(codeLens.command, "lens").title + ' '}} // }; // // opts.push(opt); // } // // editor.setDecorations(codeLensDecoration, opts); // } // } private initClient(): LanguageClient { const args = this.cliConfig.launchArgs; const env: any = {}; const kToForward = [ 'ProgramData', 'PATH', 'CPATH', 'LIBRARY_PATH', ]; for (const e of kToForward) env[e] = process.env[e]; const serverOptions: ServerOptions = async (): Promise<cp.ChildProcess> => { const opts: cp.SpawnOptions = { cwd: this.cwd, env }; const child = cp.spawn( this.cliConfig.launchCommand, args, opts ); this.clientPid = child.pid; return child; }; const config = workspace.getConfiguration('ccls'); // Options to control the language client const clientOptions: LanguageClientOptions = { diagnosticCollectionName: 'ccls', documentSelector: ['c', 'cpp', 'objective-c', 'objective-cpp'], // synchronize: { // configurationSection: 'ccls', // fileEvents: workspace.createFileSystemWatcher('**/.cc') // }, errorHandler: new CclsErrorHandler(config), initializationFailedHandler: (e) => { console.log(e); return false; }, initializationOptions: this.cliConfig, middleware: {provideCodeLenses: (doc, next, token) => this.provideCodeLens(doc, next, token)}, outputChannel: cclsChan, revealOutputChannelOn: RevealOutputChannelOn.Never, }; const traceEndpoint = config.get<string>('trace.websocketEndpointUrl'); if (traceEndpoint) { const socket = new WebSocket(traceEndpoint); let log = ''; clientOptions.outputChannel = { name: 'websocket', append(value: string) { log += value; }, appendLine(value: string) { log += value; if (socket && socket.readyState === WebSocket.OPEN) { socket.send(log); } log = ''; }, clear() {/**/}, show() {/**/}, hide() {/**/}, dispose() { socket.close(); } }; } // Create the language client and start the client. return new LanguageClient('ccls', 'ccls', serverOptions, clientOptions); } private makeRefHandler( methodName: string, extraParams: object = {}, autoGotoIfSingle = false) { return async (userParams: any) => { /* userParams: a dict defined as `args` in keybindings.json (or passed by other extensions like VSCodeVIM) Values defined by user have higher priority than `extraParams` */ // const editor = unwrap(window.activeTextEditor, "window.activeTextEditor"); const position = workspace.getCursorPosition(); const uri = workspace.uri; const locations = await this.client.sendRequest<Array<ls.Location>>( methodName, { position, textDocument: { uri: uri.toString(), }, ...extraParams, ...userParams } ); if (autoGotoIfSingle && locations.length === 1) { // const location = this.p2c.asLocation(locations[0]); // const location = workspace.getQuickfixItem(locations[0]); // commands.executeCommand( // 'ccls.goto', location.uri, location.range.start, []); } else { // commands.executeCommand( // 'editor.action.showReferences', uri, position, // locations.map(this.p2c.asLocation)); } }; } // private async showXrefsHandlerCmd(uri: Uri, position: Position, xrefArgs: any[]) { // const locations = commands.executeCommand('ccls.xref', ...xrefArgs); // if (!locations) // return; // commands.executeCommand( // 'editor.action.showReferences', // uri, this.p2c.asPosition(position), // locations.map(this.p2c.asLocation) // ); // } private showReferencesCmd(uri: string, position: ls.Position, locations: ls.Location[]) { // commands.executeCommand( // 'editor.action.showReferences', // this.p2c.asUri(uri), // this.p2c.asPosition(position), // locations.map(this.p2c.asLocation) // ); } private async gotoCmd(uri: string, position: ls.Position, locations: ls.Location[]) { // return jumpToUriAtPosition( // this.p2c.asUri(uri), // this.p2c.asPosition(position), // false [>preserveFocus<] // ); } private async fixItCmd(uri: string, pTextEdits: ls.TextEdit[]) { // const textEdits = this.p2c.asTextEdits(pTextEdits); async function applyEdits() { // for (const edit of textEdits) { // editBuilder.replace(edit.range, edit.newText); // workspace.applyEdit(edit); // } // if (!success) { // window.showErrorMessage("Failed to apply FixIt"); // } } // Find existing open document. if (workspace.document.toString() === normalizeUri(uri)) { applyEdits(); return; } // Failed, open new document. const d = await workspace.openResource(uri); // const e = await window.showTextDocument(d); // if (!e) { // FIXME seems to be redundant // window.showErrorMessage("Failed to to get editor for FixIt"); // } applyEdits(); } private async autoImplementCmd(uri: string, pTextEdits: ls.TextEdit[]) { await commands.executeCommand('ccls._applyFixIt', uri, pTextEdits); commands.executeCommand('ccls.goto', uri, pTextEdits[0].range.start); } private async insertIncludeCmd(uri: string, pTextEdits: ls.TextEdit[]) { if (pTextEdits.length === 1) commands.executeCommand('ccls._applyFixIt', uri, pTextEdits); else { // class MyQuickPick implements QuickPickItem { // constructor( // public label: string, public description: string, // public edit: any) {} // } // const items: Array<MyQuickPick> = []; // for (const edit of pTextEdits) { // items.push(new MyQuickPick(edit.newText, '', edit)); // } // const selected = await window.showQuickPick(items); // if (!selected) // return; // commands.executeCommand('ccls._applyFixIt', uri, [selected.edit]); } } private async gotoForTreeView(node: IHierarchyNode) { if (!node.location) return; const parsedUri = Uri.parse(node.location.uri); // const parsedPosition = this.p2c.asPosition(node.location.range.start); // return jumpToUriAtPosition(parsedUri, parsedPosition, true [>preserveFocus<]); } private async hackGotoForTreeView( node: IHierarchyNode, hasChildren: boolean ) { if (!node.location) return; if (!hasChildren) { commands.executeCommand('ccls.gotoForTreeView', node); return; } if (this.lastGoto.id !== node.id) { this.lastGoto.id = node.id; this.lastGoto.clockTime = Date.now(); return; } const config = workspace.getConfiguration('ccls'); const kDoubleClickTimeMs = config.get('treeViews.doubleClickTimeoutMs', 500); const elapsed = Date.now() - this.lastGoto.clockTime; this.lastGoto.clockTime = Date.now(); if (elapsed < kDoubleClickTimeMs) commands.executeCommand('ccls.gotoForTreeView', node); } private makeNavigateHandler(methodName: string) { return async (userParams: any) => { const editor = unwrap(window, "window.activeTextEditor"); const position = workspace.getCursorPosition(); const uri = editor.document; const locations = await this.client.sendRequest<Array<ls.Location>>( methodName, { position, textDocument: { uri: uri.toString(), }, ...userParams } ); if (locations.length === 1) { // const location = this.p2c.asLocation(locations[0]); // await jumpToUriAtPosition( // location.uri, location.range.start, // false [>preserveFocus<]); } }; } }