coc-ccls
Version:
C/C++/ObjC language server supporting cross references, hierarchies, completion and semantic highlighting
675 lines (616 loc) • 22.5 kB
text/typescript
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<]);
}
};
}
}