Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add integration with cmake-tools extension #487

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
# Change Log

## Next (not yet released)
## Version 0.1.28: March 20, 2024

* Fix a regression in the behaviour of `clangd.restart` introduced in 0.1.27 [#599](https://github.com/clangd/vscode-clangd/issues/599)

## Version 0.1.27: March 16, 2024

* Trigger signature help when accepting code completions, where appropriate [#390](https://github.com/clangd/vscode-clangd/issues/390)
* Gracefully handle `clangd.restart` being invoked when the extension hasn't been activated yet [#502](https://github.com/clangd/vscode-clangd/issues/502)
* Add an option to disable code completion [#588](https://github.com/clangd/vscode-clangd/issues/588)

## Version 0.1.26: December 20, 2023

* Bump @clangd/install dependency to 0.1.17. This works around a bug in a
dependent library affecting node versions 18.16 and later that can cause
the downloaded clangd executable to be corrupt after unzipping.

## Version 0.1.25: August 15, 2023

* Combine inactive region style with client-side (textmate) token colors [#193](https://github.com/clangd/vscode-clangd/pull/193).
Requires clangd 17 or later.
Expand All @@ -9,6 +25,7 @@
* An alternative inactive region style of a background highlight can be enabled with
`clangd.inactiveRegions.useBackgroundHighlight=true`. The highlight color can be
customized with `clangd.inactiveRegions.background` in `workbench.colorCustomizations`.
* The variable substitution `${userHome}` is now supported in clangd configuration setting values [#486](https://github.com/clangd/vscode-clangd/pull/486)

## Version 0.1.24: April 21, 2023

Expand Down
151 changes: 114 additions & 37 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "vscode-clangd",
"displayName": "clangd",
"description": "C/C++ completion, navigation, and insights",
"version": "0.1.24",
"version": "0.1.28",
"publisher": "llvm-vs-code-extensions",
"license": "MIT",
"homepage": "https://clangd.llvm.org/",
Expand Down Expand Up @@ -47,7 +47,7 @@
"git-clang-format": "git-clang-format --extensions=ts"
},
"dependencies": {
"@clangd/install": "0.1.16",
"@clangd/install": "0.1.17",
"abort-controller": "^3.0.0",
"vscode-languageclient": "8.0.2"
},
Expand All @@ -65,7 +65,8 @@
"sinon": "^15.2.0",
"typescript": "^4.5.5",
"vsce": "^2.7.0",
"vscode-test": "^1.3.0"
"vscode-test": "^1.3.0",
"vscode-cmake-tools": "^1.0.0"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -170,6 +171,11 @@
"type": "number",
"default": 0.55,
"description": "Opacity of inactive regions (used only if clangd.inactiveRegions.useBackgroundHighlight=false)"
},
"clangd.enableCodeCompletion": {
"type": "boolean",
"default": true,
"description": "Enable code completion provided by the language server"
}
}
},
Expand Down
29 changes: 28 additions & 1 deletion src/clangd-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as vscode from 'vscode';
import * as vscodelc from 'vscode-languageclient/node';

import * as ast from './ast';
import * as cmakeTools from './cmake-tools';
import * as config from './config';
import * as configFileWatcher from './config-file-watcher';
import * as fileStatus from './file-status';
Expand Down Expand Up @@ -66,9 +67,14 @@ export class ClangdContext implements vscode.Disposable {
if (!clangdPath)
return;

let args = await config.get<string[]>('arguments');
if (cmakeTools.clang_resource_dir !== undefined) {
args = [...args, `--resource-dir=${cmakeTools.clang_resource_dir}`];
}

const clangd: vscodelc.Executable = {
command: clangdPath,
args: await config.get<string[]>('arguments'),
args: args,
options: {cwd: vscode.workspace.rootPath || process.cwd()}
};
const traceFile = config.get<string>('trace');
Expand Down Expand Up @@ -106,6 +112,8 @@ export class ClangdContext implements vscode.Disposable {
middleware: {
provideCompletionItem: async (document, position, context, token,
next) => {
if (!config.get<boolean>('enableCodeCompletion'))
return new vscode.CompletionList([], /*isIncomplete=*/ false);
let list = await next(document, position, context, token);
if (!config.get<boolean>('serverCompletionRanking'))
return list;
Expand All @@ -121,6 +129,20 @@ export class ClangdContext implements vscode.Disposable {
// notice until the behavior was in several releases, so we need
// to override it on the client.
item.commitCharacters = [];
// VSCode won't automatically trigger signature help when entering
// a placeholder, e.g. if the completion inserted brackets and
// placed the cursor inside them.
// https://github.com/microsoft/vscode/issues/164310
// They say a plugin should trigger this, but LSP has no mechanism.
// https://github.com/microsoft/language-server-protocol/issues/274
// (This workaround is incomplete, and only helps the first param).
if (item.insertText instanceof vscode.SnippetString &&
!item.command &&
item.insertText.value.match(/[([{<,] ?\$\{?[01]\D/))
item.command = {
title: 'Signature help',
command: 'editor.action.triggerParameterHints'
};
return item;
})
return new vscode.CompletionList(items, /*isIncomplete=*/ true);
Expand Down Expand Up @@ -168,13 +190,18 @@ export class ClangdContext implements vscode.Disposable {
fileStatus.activate(this);
switchSourceHeader.activate(this);
configFileWatcher.activate(this);
cmakeTools.activate(this);
}

get visibleClangdEditors(): vscode.TextEditor[] {
return vscode.window.visibleTextEditors.filter(
(e) => isClangdDocument(e.document));
}

clientIsStarting() {
return this.client && this.client.state == vscodelc.State.Starting;
}

dispose() {
this.subscriptions.forEach((d) => { d.dispose(); });
if (this.client)
Expand Down
221 changes: 221 additions & 0 deletions src/cmake-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import * as path from 'path';
import * as vscode from 'vscode';
import * as api from 'vscode-cmake-tools';
import * as vscodelc from 'vscode-languageclient/node';
import { exec } from 'child_process';

import {ClangdContext} from './clangd-context';

export function activate(context: ClangdContext) {
const feature = new CMakeToolsFeature(context);
context.client.registerFeature(feature);
}

export let clang_resource_dir: string | undefined;

namespace protocol {

export interface DidChangeConfigurationClientCapabilities {
dynamicRegistration?: boolean;
}

export interface ClangdCompileCommand {
// Directory
workingDirectory: string;
// Command line
compilationCommand: string[];
}

export interface ConfigurationSettings {
// File -> ClangdCompileCommand
compilationDatabaseChanges: Object;
}

export interface DidChangeConfigurationParams {
settings: ConfigurationSettings;
}

export namespace DidChangeConfigurationRequest {
export const type = new vscodelc.NotificationType<DidChangeConfigurationParams>(
'workspace/didChangeConfiguration');
}

} // namespace protocol

class CMakeToolsFeature implements vscodelc.StaticFeature {
private projectChange: vscode.Disposable = {dispose() {}};
private codeModelChange: vscode.Disposable|undefined;
private cmakeTools: api.CMakeToolsApi|undefined;
private project: api.Project|undefined;
private codeModel: Map<string, protocol.ClangdCompileCommand>|undefined;

constructor(private readonly context: ClangdContext) {
let cmakeTools = api.getCMakeToolsApi(api.Version.v1);
if (cmakeTools === undefined)
return;

cmakeTools.then(api => {
this.cmakeTools = api;
if (this.cmakeTools === undefined)
return;

this.projectChange = this.cmakeTools.onActiveProjectChanged(
this.onActiveProjectChanged, this);
if (vscode.workspace.workspaceFolders !== undefined) {
// FIXME: clangd not supported multi-workspace projects
const projectUri = vscode.workspace.workspaceFolders[0].uri;
this.onActiveProjectChanged(projectUri);
}
});
}

fillClientCapabilities(_capabilities: vscodelc.ClientCapabilities) {}
fillInitializeParams(_params: vscodelc.InitializeParams) {}

initialize(capabilities: vscodelc.ServerCapabilities,
_documentSelector: vscodelc.DocumentSelector|undefined) {}
getState(): vscodelc.FeatureState { return {kind: 'static'}; }
dispose() {
if (this.codeModelChange !== undefined)
this.codeModelChange.dispose();
this.projectChange.dispose();
}

async onActiveProjectChanged(path: vscode.Uri|undefined) {
if (this.codeModelChange !== undefined) {
this.codeModelChange.dispose();
this.codeModelChange = undefined;
}

if (path === undefined)
return;

this.cmakeTools?.getProject(path).then(project => {
this.project = project;
this.codeModelChange =
this.project?.onCodeModelChanged(this.onCodeModelChanged, this);
this.onCodeModelChanged();
});
}

async onCodeModelChanged() {
const content = this.project?.codeModel;
if (content === undefined)
return;

if (content.toolchains === undefined)
return;

const firstCompiler = content.toolchains.values().next().value as api.CodeModel.Toolchain || undefined;
if (firstCompiler !== undefined) {
let compilerName =
firstCompiler.path.substring(firstCompiler.path.lastIndexOf(path.sep) + 1)
.toLowerCase();
if (compilerName.endsWith('.exe'))
compilerName = compilerName.substring(0, compilerName.length - 4);
if (compilerName.indexOf('clang') !== -1) {
exec(`${firstCompiler.path} -print-file-name=`, (error, stdout, stderr) => {
if (error) {
return;
}
while (stdout.endsWith('\n') || stdout.endsWith('\r')) stdout = stdout.slice(0, -1);
if (stdout !== clang_resource_dir) {
clang_resource_dir = stdout;
vscode.commands.executeCommand('clangd.restart');
}
});
}
}

const request: protocol.DidChangeConfigurationParams = {
settings: {compilationDatabaseChanges: {}}
};

let codeModelChanges: Map<string, protocol.ClangdCompileCommand> =
new Map();
content.configurations.forEach(configuration => {
configuration.projects.forEach(project => {
let sourceDirectory = project.sourceDirectory;
project.targets.forEach(target => {
if (target.sourceDirectory !== undefined)
sourceDirectory = target.sourceDirectory;
let commandLine: string[] = [];
if (target.sysroot !== undefined)
commandLine.push(`--sysroot=${target.sysroot}`);
target.fileGroups?.forEach(fileGroup => {
if (fileGroup.language === undefined)
return;

const compiler = content.toolchains?.get(fileGroup.language);
if (compiler === undefined)
return;

commandLine.unshift(compiler.path);
if (compiler.target !== undefined)
commandLine.push(`--target=${compiler.target}`);

let compilerName =
compiler.path.substring(compiler.path.lastIndexOf(path.sep) + 1)
.toLowerCase();
if (compilerName.endsWith('.exe'))
compilerName = compilerName.substring(0, compilerName.length - 4);

const ClangCLMode =
compilerName === 'cl' || compilerName === 'clang-cl';
const incFlag = ClangCLMode ? '/I' : '-I';
const defFlag = ClangCLMode ? '/D' : '-D';

fileGroup.compileCommandFragments?.forEach(commands => {
commands.split(/\s/g).forEach(
command => { commandLine.push(command); });
});
fileGroup.includePath?.forEach(
include => { commandLine.push(`${incFlag}${include.path}`); });
fileGroup.defines?.forEach(
define => { commandLine.push(`${defFlag}${define}`); });
fileGroup.sources.forEach(source => {
const file = sourceDirectory.length != 0
? sourceDirectory + path.sep + source
: source;
const command: protocol.ClangdCompileCommand = {
workingDirectory: sourceDirectory,
compilationCommand: commandLine
};
codeModelChanges.set(file, command);
});
});
});
});
});

const codeModel = new Map(codeModelChanges);
this.codeModel?.forEach((cc, file) => {
if (!codeModelChanges.has(file)) {
const command: protocol.ClangdCompileCommand = {
workingDirectory: '',
compilationCommand: []
};
codeModelChanges.set(file, command);
return;
}
const command = codeModelChanges.get(file);
if (command?.workingDirectory === cc.workingDirectory &&
command?.compilationCommand.length === cc.compilationCommand.length &&
command?.compilationCommand.every(
(val, index) => val === cc.compilationCommand[index])) {
codeModelChanges.delete(file);
}
});
this.codeModel = codeModel;

if (codeModelChanges.size === 0)
return;

codeModelChanges.forEach(
(cc, file) => {Object.assign(
request.settings.compilationDatabaseChanges, {[file]: cc})});

this.context.client.sendNotification(
protocol.DidChangeConfigurationRequest.type, request);
}
}
9 changes: 9 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ export async function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand('clangd.activate', async () => {}));
context.subscriptions.push(
vscode.commands.registerCommand('clangd.restart', async () => {
// 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.clientIsStarting()) {
return;
}
await clangdContext.dispose();
await clangdContext.activate(context.globalStoragePath, outputChannel);
}));
Expand Down
Loading