Skip to content

Commit d3f8f05

Browse files
Enable basic CMake language services (#4204)
* Enable colorization * Colorization matching twxs. Initial in memory support of hover. * initial naive providing of completion items * fix nit with variables and resolveCompletionItem * initial support for modules * add gulp tasks for localizing language service data * make the insertion a better snippet * add slight protections * update changelog
1 parent 73cc13f commit d3f8f05

File tree

8 files changed

+7375
-12
lines changed

8 files changed

+7375
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Features:
1010
executed. This adds test execution type, "Run with coverage", on the `ctest`
1111
section of the Testing tab.
1212
[#4040](https://github.com/microsoft/vscode-cmake-tools/issues/4040)
13+
- Add basic CMake language services: quick hover and completions for CMake built-ins. [PR #4204](https://github.com/microsoft/vscode-cmake-tools/pull/4204)
1314

1415
Improvements:
1516

assets/commands.json

Lines changed: 1060 additions & 0 deletions
Large diffs are not rendered by default.

assets/modules.json

Lines changed: 1015 additions & 0 deletions
Large diffs are not rendered by default.

assets/variables.json

Lines changed: 5035 additions & 0 deletions
Large diffs are not rendered by default.

gulpfile.js

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ const jsonSchemaFilesPatterns = [
2626
"*/*-schema.json"
2727
];
2828

29+
// Patterns to find language services json files.
30+
const languageServicesFilesPatterns = [
31+
"assets/*.json"
32+
];
33+
2934
const languages = [
3035
{ id: "zh-tw", folderName: "cht", transifexId: "zh-hant" },
3136
{ id: "zh-cn", folderName: "chs", transifexId: "zh-hans" },
@@ -94,7 +99,7 @@ const traverseJson = (jsonTree, descriptionCallback, prefixPath) => {
9499

95100
// Traverses schema json files looking for "description" fields to localized.
96101
// The path to the "description" field is used to create a localization key.
97-
const processJsonSchemaFiles = () => {
102+
const processJsonFiles = () => {
98103
return es.through(function (file) {
99104
let jsonTree = JSON.parse(file.contents.toString());
100105
let localizationJsonContents = {};
@@ -133,10 +138,13 @@ gulp.task("translations-export", (done) => {
133138

134139
// Scan schema files
135140
let jsonSchemaStream = gulp.src(jsonSchemaFilesPatterns)
136-
.pipe(processJsonSchemaFiles());
141+
.pipe(processJsonFiles());
142+
143+
let jsonLanguageServicesStream = gulp.src(languageServicesFilesPatterns)
144+
.pipe(processJsonFiles());
137145

138146
// Merge files from all source streams
139-
es.merge(jsStream, jsonSchemaStream)
147+
es.merge(jsStream, jsonSchemaStream, jsonLanguageServicesStream)
140148

141149
// Filter down to only the files we need
142150
.pipe(filter(['**/*.nls.json', '**/*.nls.metadata.json']))
@@ -214,7 +222,7 @@ const generatedSrcLocBundle = () => {
214222
.pipe(gulp.dest('dist'));
215223
};
216224

217-
const generateLocalizedJsonSchemaFiles = () => {
225+
const generateLocalizedJsonFiles = (paths) => {
218226
return es.through(function (file) {
219227
let jsonTree = JSON.parse(file.contents.toString());
220228
languages.map((language) => {
@@ -237,7 +245,7 @@ const generateLocalizedJsonSchemaFiles = () => {
237245
traverseJson(jsonTree, descriptionCallback, "");
238246
let newContent = JSON.stringify(jsonTree, null, '\t');
239247
this.queue(new vinyl({
240-
path: path.join("schema", language.id, relativePath),
248+
path: path.join(...paths, language.id, relativePath),
241249
contents: Buffer.from(newContent, 'utf8')
242250
}));
243251
});
@@ -249,11 +257,17 @@ const generateLocalizedJsonSchemaFiles = () => {
249257
// Generate new version of the JSON schema file in dist/schema/<language_id>/<path>
250258
const generateJsonSchemaLoc = () => {
251259
return gulp.src(jsonSchemaFilesPatterns)
252-
.pipe(generateLocalizedJsonSchemaFiles())
260+
.pipe(generateLocalizedJsonFiles(["schema"]))
261+
.pipe(gulp.dest('dist'));
262+
};
263+
264+
const generateJsonLanguageServicesLoc = () => {
265+
return gulp.src(languageServicesFilesPatterns)
266+
.pipe(generateLocalizedJsonFiles(["languageServices"]))
253267
.pipe(gulp.dest('dist'));
254268
};
255269

256-
gulp.task('translations-generate', gulp.series(generatedSrcLocBundle, generatedAdditionalLocFiles, generateJsonSchemaLoc));
270+
gulp.task('translations-generate', gulp.series(generatedSrcLocBundle, generatedAdditionalLocFiles, generateJsonSchemaLoc, generateJsonLanguageServicesLoc));
257271

258272
const allTypeScript = [
259273
'src/**/*.ts',

package.json

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
},
2727
"categories": [
2828
"Other",
29-
"Debuggers"
29+
"Debuggers",
30+
"Programming Languages"
3031
],
3132
"galleryBanner": {
3233
"color": "#13578c",
@@ -63,7 +64,9 @@
6364
"workspaceContains:*/*/CMakeLists.txt",
6465
"workspaceContains:*/*/*/CMakeLists.txt",
6566
"workspaceContains:.vscode/cmake-kits.json",
66-
"onFileSystem:cmake-tools-schema"
67+
"onFileSystem:cmake-tools-schema",
68+
"onLanguage:cmake",
69+
"onLanguage:cmake-cache"
6770
],
6871
"main": "./dist/main",
6972
"contributes": {
@@ -111,6 +114,31 @@
111114
}
112115
}
113116
},
117+
"languages": [
118+
{
119+
"id": "cmake",
120+
"extensions": [".cmake"],
121+
"filenames": ["CMakelists.txt"],
122+
"aliases": ["CMake"]
123+
},
124+
{
125+
"id": "cmake-cache",
126+
"filenames": ["CMakeCache.txt"],
127+
"aliases": ["CMake Cache"]
128+
}
129+
],
130+
"grammars": [
131+
{
132+
"language": "cmake",
133+
"scopeName": "source.cmake",
134+
"path": "./syntaxes/CMake.tmLanguage"
135+
},
136+
{
137+
"language": "cmake-cache",
138+
"scopeName": "source.cmakecache",
139+
"path": "./syntaxes/CMakeCache.tmLanguage"
140+
}
141+
],
114142
"commands": [
115143
{
116144
"command": "cmake.openCMakePresets",
@@ -3817,8 +3845,5 @@
38173845
"minimatch": "^3.0.5",
38183846
"**/braces": "^3.0.3"
38193847
},
3820-
"extensionPack": [
3821-
"twxs.cmake"
3822-
],
38233848
"packageManager": "yarn@1.22.19"
38243849
}

src/extension.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { getCMakeExecutableInformation } from '@cmt/cmakeExecutable';
4949
import { DebuggerInformation, getDebuggerPipeName } from '@cmt/debug/cmakeDebugger/debuggerConfigureDriver';
5050
import { DebugConfigurationProvider, DynamicDebugConfigurationProvider } from '@cmt/debug/cmakeDebugger/debugConfigurationProvider';
5151
import { deIntegrateTestExplorer } from "@cmt/ctest";
52+
import { LanguageServiceData } from './languageServices/languageServiceData';
5253

5354
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
5455
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
@@ -2357,6 +2358,69 @@ export async function activate(context: vscode.ExtensionContext): Promise<api.CM
23572358
await vscode.window.showWarningMessage(localize('uninstall.old.cmaketools', 'Please uninstall any older versions of the CMake Tools extension. It is now published by Microsoft starting with version 1.2.0.'));
23582359
}
23592360

2361+
const CMAKE_LANGUAGE = "cmake";
2362+
const CMAKE_SELECTOR: vscode.DocumentSelector = [
2363+
{ language: CMAKE_LANGUAGE, scheme: 'file'},
2364+
{ language: CMAKE_LANGUAGE, scheme: 'untitled'}
2365+
];
2366+
2367+
try {
2368+
const languageServices = await LanguageServiceData.create();
2369+
vscode.languages.registerHoverProvider(CMAKE_SELECTOR, languageServices);
2370+
vscode.languages.registerCompletionItemProvider(CMAKE_SELECTOR, languageServices);
2371+
} catch {
2372+
log.error(localize('language.service.failed', 'Failed to initialize language services'));
2373+
}
2374+
2375+
vscode.languages.setLanguageConfiguration(CMAKE_LANGUAGE, {
2376+
indentationRules: {
2377+
// ^(.*\*/)?\s*\}.*$
2378+
decreaseIndentPattern: /^(.*\*\/)?\s*\}.*$/,
2379+
// ^.*\{[^}"']*$
2380+
increaseIndentPattern: /^.*\{[^}"']*$/
2381+
},
2382+
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
2383+
comments: {
2384+
lineComment: '#'
2385+
},
2386+
brackets: [
2387+
['{', '}'],
2388+
['(', ')']
2389+
],
2390+
2391+
__electricCharacterSupport: {
2392+
brackets: [
2393+
{ tokenType: 'delimiter.curly.ts', open: '{', close: '}', isElectric: true },
2394+
{ tokenType: 'delimiter.square.ts', open: '[', close: ']', isElectric: true },
2395+
{ tokenType: 'delimiter.paren.ts', open: '(', close: ')', isElectric: true }
2396+
]
2397+
},
2398+
2399+
__characterPairSupport: {
2400+
autoClosingPairs: [
2401+
{ open: '{', close: '}' },
2402+
{ open: '(', close: ')' },
2403+
{ open: '"', close: '"', notIn: ['string'] }
2404+
]
2405+
}
2406+
});
2407+
2408+
if (vscode.workspace.getConfiguration('cmake').get('showOptionsMovedNotification')) {
2409+
void vscode.window.showInformationMessage(
2410+
localize('options.moved.notification.body', "Some status bar options in CMake Tools have now moved to the Project Status View in the CMake Tools sidebar. You can customize your view with the 'cmake.options' property in settings."),
2411+
localize('options.moved.notification.configure.cmake.options', 'Configure CMake Options Visibility'),
2412+
localize('options.moved.notification.do.not.show', "Do Not Show Again")
2413+
).then(async (selection) => {
2414+
if (selection !== undefined) {
2415+
if (selection === localize('options.moved.notification.configure.cmake.options', 'Configure CMake Options Visibility')) {
2416+
await vscode.commands.executeCommand('workbench.action.openSettings', 'cmake.options');
2417+
} else if (selection === localize('options.moved.notification.do.not.show', "Do Not Show Again")) {
2418+
await vscode.workspace.getConfiguration('cmake').update('showOptionsMovedNotification', false, vscode.ConfigurationTarget.Global);
2419+
}
2420+
}
2421+
});
2422+
}
2423+
23602424
// Start with a partial feature set view. The first valid CMake project will cause a switch to full feature set.
23612425
await enableFullFeatureSet(false);
23622426

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import * as vscode from "vscode";
2+
import * as path from "path";
3+
import { fs } from "@cmt/pr";
4+
import { thisExtensionPath } from "@cmt/util";
5+
import * as util from "@cmt/util";
6+
7+
interface Commands {
8+
[key: string]: Command;
9+
}
10+
11+
interface Command {
12+
name: string;
13+
description: string;
14+
syntax_examples: string[];
15+
}
16+
17+
// Same as variables right now. If we modify, create individual interfaces.
18+
interface Modules extends Variables {
19+
20+
}
21+
22+
interface Variables {
23+
[key: string]: Variable;
24+
}
25+
26+
interface Variable {
27+
name: string;
28+
description: string;
29+
}
30+
31+
enum LanguageType {
32+
Variable,
33+
Command,
34+
Module
35+
}
36+
37+
export class LanguageServiceData implements vscode.HoverProvider, vscode.CompletionItemProvider {
38+
private commands: Commands = {};
39+
private variables: Variables = {}; // variables and properties
40+
private modules: Modules = {};
41+
42+
private constructor() {
43+
}
44+
45+
private async getFile(fileEnding: string, locale: string): Promise<string> {
46+
let filePath: string = path.join(thisExtensionPath(), "dist/languageServices", locale, "assets", fileEnding);
47+
const fileExists: boolean = await util.checkFileExists(filePath);
48+
if (!fileExists) {
49+
filePath = path.join(thisExtensionPath(), "assets", fileEnding);
50+
}
51+
return fs.readFile(filePath);
52+
}
53+
54+
private async load(): Promise<void> {
55+
const locale: string = util.getLocaleId();
56+
this.commands = JSON.parse(await this.getFile("commands.json", locale));
57+
this.variables = JSON.parse(await this.getFile("variables.json", locale));
58+
this.modules = JSON.parse(await this.getFile("modules.json", locale));
59+
}
60+
61+
private getCompletionSuggestionsHelper(currentWord: string, data: Commands | Modules | Variables, type: LanguageType): vscode.CompletionItem[] {
62+
function moduleInsertText(module: string): vscode.SnippetString {
63+
if (module.indexOf("Find") === 0) {
64+
return new vscode.SnippetString(`find_package(${module.replace("Find", "")}\${1: REQUIRED})`);
65+
} else {
66+
return new vscode.SnippetString(`include(${module})`);
67+
}
68+
}
69+
70+
function variableInsertText(variable: string): vscode.SnippetString {
71+
return new vscode.SnippetString(variable.replace(/<(.*)>/g, "${1:<$1>}"));
72+
}
73+
74+
function commandInsertText(func: string): vscode.SnippetString {
75+
const scopedFunctions = ["if", "function", "while", "macro", "foreach"];
76+
const is_scoped = scopedFunctions.includes(func);
77+
if (is_scoped) {
78+
return new vscode.SnippetString(`${func}(\${1})\n\t\nend${func}(\${1})\n`);
79+
} else {
80+
return new vscode.SnippetString(`${func}(\${1})`);
81+
}
82+
}
83+
84+
return Object.keys(data).map((key) => {
85+
if (data[key].name.includes(currentWord)) {
86+
const completionItem = new vscode.CompletionItem(data[key].name);
87+
completionItem.insertText = type === LanguageType.Command ? commandInsertText(data[key].name) : type === LanguageType.Variable ? variableInsertText(data[key].name) : moduleInsertText(data[key].name);
88+
completionItem.kind = type === LanguageType.Command ? vscode.CompletionItemKind.Function : type === LanguageType.Variable ? vscode.CompletionItemKind.Variable : vscode.CompletionItemKind.Module;
89+
return completionItem;
90+
}
91+
return null;
92+
}).filter((value) => value !== null) as vscode.CompletionItem[];
93+
}
94+
95+
private getCompletionSuggestions(currentWord: string): vscode.CompletionItem[] {
96+
return this.getCompletionSuggestionsHelper(currentWord, this.commands, LanguageType.Command)
97+
.concat(this.getCompletionSuggestionsHelper(currentWord, this.variables, LanguageType.Variable))
98+
.concat(this.getCompletionSuggestionsHelper(currentWord, this.modules, LanguageType.Module));
99+
}
100+
101+
public static async create(): Promise<LanguageServiceData> {
102+
const data = new LanguageServiceData();
103+
await data.load();
104+
return data;
105+
}
106+
107+
provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, _context: vscode.CompletionContext): vscode.ProviderResult<vscode.CompletionItem[] | vscode.CompletionList> {
108+
const wordAtPosition = document.getWordRangeAtPosition(position);
109+
110+
let currentWord = "";
111+
if (wordAtPosition && wordAtPosition.start.character < position.character) {
112+
const word = document.getText(wordAtPosition);
113+
currentWord = word.substr(0, position.character - wordAtPosition.start.character);
114+
}
115+
116+
if (token.isCancellationRequested) {
117+
return null;
118+
}
119+
120+
return this.getCompletionSuggestions(currentWord);
121+
}
122+
123+
resolveCompletionItem?(item: vscode.CompletionItem, _token: vscode.CancellationToken): vscode.ProviderResult<vscode.CompletionItem> {
124+
return item;
125+
}
126+
127+
provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.ProviderResult<vscode.Hover> {
128+
const range = document.getWordRangeAtPosition(position);
129+
const value = document.getText(range);
130+
131+
if (token.isCancellationRequested) {
132+
return null;
133+
}
134+
135+
const hoverSuggestions = this.commands[value] || this.variables[value] || this.modules[value];
136+
137+
const markdown: vscode.MarkdownString = new vscode.MarkdownString();
138+
markdown.appendMarkdown(hoverSuggestions.description);
139+
hoverSuggestions.syntax_examples?.forEach((example) => {
140+
markdown.appendCodeblock(`\t${example}`, "cmake");
141+
});
142+
143+
if (hoverSuggestions) {
144+
return new vscode.Hover(markdown);
145+
}
146+
147+
return null;
148+
}
149+
}

0 commit comments

Comments
 (0)