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

Enable basic CMake language services #4204

Merged
merged 12 commits into from
Jan 10, 2025
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Features:
executed. This adds test execution type, "Run with coverage", on the `ctest`
section of the Testing tab.
[#4040](https://github.com/microsoft/vscode-cmake-tools/issues/4040)
- Add basic CMake language services: quick hover and completions for CMake built-ins. [PR #4204](https://github.com/microsoft/vscode-cmake-tools/pull/4204)

Improvements:

Expand Down
1,060 changes: 1,060 additions & 0 deletions assets/commands.json

Large diffs are not rendered by default.

1,015 changes: 1,015 additions & 0 deletions assets/modules.json

Large diffs are not rendered by default.

5,035 changes: 5,035 additions & 0 deletions assets/variables.json

Large diffs are not rendered by default.

28 changes: 21 additions & 7 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ const jsonSchemaFilesPatterns = [
"*/*-schema.json"
];

// Patterns to find language services json files.
const languageServicesFilesPatterns = [
"assets/*.json"
];

const languages = [
{ id: "zh-tw", folderName: "cht", transifexId: "zh-hant" },
{ id: "zh-cn", folderName: "chs", transifexId: "zh-hans" },
Expand Down Expand Up @@ -94,7 +99,7 @@ const traverseJson = (jsonTree, descriptionCallback, prefixPath) => {

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

// Scan schema files
let jsonSchemaStream = gulp.src(jsonSchemaFilesPatterns)
.pipe(processJsonSchemaFiles());
.pipe(processJsonFiles());

let jsonLanguageServicesStream = gulp.src(languageServicesFilesPatterns)
.pipe(processJsonFiles());

// Merge files from all source streams
es.merge(jsStream, jsonSchemaStream)
es.merge(jsStream, jsonSchemaStream, jsonLanguageServicesStream)

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

const generateLocalizedJsonSchemaFiles = () => {
const generateLocalizedJsonFiles = (paths) => {
return es.through(function (file) {
let jsonTree = JSON.parse(file.contents.toString());
languages.map((language) => {
Expand All @@ -237,7 +245,7 @@ const generateLocalizedJsonSchemaFiles = () => {
traverseJson(jsonTree, descriptionCallback, "");
let newContent = JSON.stringify(jsonTree, null, '\t');
this.queue(new vinyl({
path: path.join("schema", language.id, relativePath),
path: path.join(...paths, language.id, relativePath),
contents: Buffer.from(newContent, 'utf8')
}));
});
Expand All @@ -249,11 +257,17 @@ const generateLocalizedJsonSchemaFiles = () => {
// Generate new version of the JSON schema file in dist/schema/<language_id>/<path>
const generateJsonSchemaLoc = () => {
return gulp.src(jsonSchemaFilesPatterns)
.pipe(generateLocalizedJsonSchemaFiles())
.pipe(generateLocalizedJsonFiles(["schema"]))
.pipe(gulp.dest('dist'));
};

const generateJsonLanguageServicesLoc = () => {
return gulp.src(languageServicesFilesPatterns)
.pipe(generateLocalizedJsonFiles(["languageServices"]))
.pipe(gulp.dest('dist'));
};

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

const allTypeScript = [
'src/**/*.ts',
Expand Down
35 changes: 30 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
},
"categories": [
"Other",
"Debuggers"
"Debuggers",
"Programming Languages"
],
"galleryBanner": {
"color": "#13578c",
Expand Down Expand Up @@ -63,7 +64,9 @@
"workspaceContains:*/*/CMakeLists.txt",
"workspaceContains:*/*/*/CMakeLists.txt",
"workspaceContains:.vscode/cmake-kits.json",
"onFileSystem:cmake-tools-schema"
"onFileSystem:cmake-tools-schema",
"onLanguage:cmake",
"onLanguage:cmake-cache"
],
"main": "./dist/main",
"contributes": {
Expand Down Expand Up @@ -111,6 +114,31 @@
}
}
},
"languages": [
{
"id": "cmake",
"extensions": [".cmake"],
"filenames": ["CMakelists.txt"],
"aliases": ["CMake"]
},
{
"id": "cmake-cache",
"filenames": ["CMakeCache.txt"],
"aliases": ["CMake Cache"]
}
],
"grammars": [
{
"language": "cmake",
"scopeName": "source.cmake",
"path": "./syntaxes/CMake.tmLanguage"
},
{
"language": "cmake-cache",
"scopeName": "source.cmakecache",
"path": "./syntaxes/CMakeCache.tmLanguage"
}
],
"commands": [
{
"command": "cmake.openCMakePresets",
Expand Down Expand Up @@ -3817,8 +3845,5 @@
"minimatch": "^3.0.5",
"**/braces": "^3.0.3"
},
"extensionPack": [
"twxs.cmake"
],
"packageManager": "[email protected]"
}
64 changes: 64 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { getCMakeExecutableInformation } from '@cmt/cmakeExecutable';
import { DebuggerInformation, getDebuggerPipeName } from '@cmt/debug/cmakeDebugger/debuggerConfigureDriver';
import { DebugConfigurationProvider, DynamicDebugConfigurationProvider } from '@cmt/debug/cmakeDebugger/debugConfigurationProvider';
import { deIntegrateTestExplorer } from "@cmt/ctest";
import { LanguageServiceData } from './languageServices/languageServiceData';

nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
Expand Down Expand Up @@ -2357,6 +2358,69 @@ export async function activate(context: vscode.ExtensionContext): Promise<api.CM
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.'));
}

const CMAKE_LANGUAGE = "cmake";
const CMAKE_SELECTOR: vscode.DocumentSelector = [
{ language: CMAKE_LANGUAGE, scheme: 'file'},
{ language: CMAKE_LANGUAGE, scheme: 'untitled'}
];

try {
const languageServices = await LanguageServiceData.create();
vscode.languages.registerHoverProvider(CMAKE_SELECTOR, languageServices);
vscode.languages.registerCompletionItemProvider(CMAKE_SELECTOR, languageServices);
} catch {
log.error(localize('language.service.failed', 'Failed to initialize language services'));
}

vscode.languages.setLanguageConfiguration(CMAKE_LANGUAGE, {
indentationRules: {
// ^(.*\*/)?\s*\}.*$
decreaseIndentPattern: /^(.*\*\/)?\s*\}.*$/,
// ^.*\{[^}"']*$
increaseIndentPattern: /^.*\{[^}"']*$/
},
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
comments: {
lineComment: '#'
},
brackets: [
['{', '}'],
['(', ')']
],

__electricCharacterSupport: {
brackets: [
{ tokenType: 'delimiter.curly.ts', open: '{', close: '}', isElectric: true },
{ tokenType: 'delimiter.square.ts', open: '[', close: ']', isElectric: true },
{ tokenType: 'delimiter.paren.ts', open: '(', close: ')', isElectric: true }
]
},

__characterPairSupport: {
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '(', close: ')' },
{ open: '"', close: '"', notIn: ['string'] }
]
}
});

if (vscode.workspace.getConfiguration('cmake').get('showOptionsMovedNotification')) {
void vscode.window.showInformationMessage(
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."),
localize('options.moved.notification.configure.cmake.options', 'Configure CMake Options Visibility'),
localize('options.moved.notification.do.not.show', "Do Not Show Again")
).then(async (selection) => {
if (selection !== undefined) {
if (selection === localize('options.moved.notification.configure.cmake.options', 'Configure CMake Options Visibility')) {
await vscode.commands.executeCommand('workbench.action.openSettings', 'cmake.options');
} else if (selection === localize('options.moved.notification.do.not.show', "Do Not Show Again")) {
await vscode.workspace.getConfiguration('cmake').update('showOptionsMovedNotification', false, vscode.ConfigurationTarget.Global);
}
}
});
}

// Start with a partial feature set view. The first valid CMake project will cause a switch to full feature set.
await enableFullFeatureSet(false);

Expand Down
149 changes: 149 additions & 0 deletions src/languageServices/languageServiceData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import * as vscode from "vscode";
import * as path from "path";
import { fs } from "@cmt/pr";
import { thisExtensionPath } from "@cmt/util";
import * as util from "@cmt/util";

interface Commands {
[key: string]: Command;
}

interface Command {
name: string;
description: string;
syntax_examples: string[];
}

// Same as variables right now. If we modify, create individual interfaces.
interface Modules extends Variables {

}

interface Variables {
[key: string]: Variable;
}

interface Variable {
name: string;
description: string;
}

enum LanguageType {
Variable,
Command,
Module
}

export class LanguageServiceData implements vscode.HoverProvider, vscode.CompletionItemProvider {
private commands: Commands = {};
private variables: Variables = {}; // variables and properties
private modules: Modules = {};

private constructor() {
}

private async getFile(fileEnding: string, locale: string): Promise<string> {
let filePath: string = path.join(thisExtensionPath(), "dist/languageServices", locale, "assets", fileEnding);
const fileExists: boolean = await util.checkFileExists(filePath);
if (!fileExists) {
filePath = path.join(thisExtensionPath(), "assets", fileEnding);
}
return fs.readFile(filePath);
}

private async load(): Promise<void> {
const locale: string = util.getLocaleId();
this.commands = JSON.parse(await this.getFile("commands.json", locale));
this.variables = JSON.parse(await this.getFile("variables.json", locale));
this.modules = JSON.parse(await this.getFile("modules.json", locale));
}

private getCompletionSuggestionsHelper(currentWord: string, data: Commands | Modules | Variables, type: LanguageType): vscode.CompletionItem[] {
function moduleInsertText(module: string): vscode.SnippetString {
if (module.indexOf("Find") === 0) {
return new vscode.SnippetString(`find_package(${module.replace("Find", "")}\${1: REQUIRED})`);
} else {
return new vscode.SnippetString(`include(${module})`);
}
}

function variableInsertText(variable: string): vscode.SnippetString {
return new vscode.SnippetString(variable.replace(/<(.*)>/g, "${1:<$1>}"));
}

function commandInsertText(func: string): vscode.SnippetString {
const scopedFunctions = ["if", "function", "while", "macro", "foreach"];
const is_scoped = scopedFunctions.includes(func);
if (is_scoped) {
return new vscode.SnippetString(`${func}(\${1})\n\t\nend${func}(\${1})\n`);
} else {
return new vscode.SnippetString(`${func}(\${1})`);
}
}

return Object.keys(data).map((key) => {
if (data[key].name.includes(currentWord)) {
const completionItem = new vscode.CompletionItem(data[key].name);
completionItem.insertText = type === LanguageType.Command ? commandInsertText(data[key].name) : type === LanguageType.Variable ? variableInsertText(data[key].name) : moduleInsertText(data[key].name);
completionItem.kind = type === LanguageType.Command ? vscode.CompletionItemKind.Function : type === LanguageType.Variable ? vscode.CompletionItemKind.Variable : vscode.CompletionItemKind.Module;
return completionItem;
}
return null;
}).filter((value) => value !== null) as vscode.CompletionItem[];
}

private getCompletionSuggestions(currentWord: string): vscode.CompletionItem[] {
return this.getCompletionSuggestionsHelper(currentWord, this.commands, LanguageType.Command)
.concat(this.getCompletionSuggestionsHelper(currentWord, this.variables, LanguageType.Variable))
.concat(this.getCompletionSuggestionsHelper(currentWord, this.modules, LanguageType.Module));
}

public static async create(): Promise<LanguageServiceData> {
const data = new LanguageServiceData();
await data.load();
return data;
}

provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, _context: vscode.CompletionContext): vscode.ProviderResult<vscode.CompletionItem[] | vscode.CompletionList> {
const wordAtPosition = document.getWordRangeAtPosition(position);

let currentWord = "";
if (wordAtPosition && wordAtPosition.start.character < position.character) {
const word = document.getText(wordAtPosition);
currentWord = word.substr(0, position.character - wordAtPosition.start.character);
}

if (token.isCancellationRequested) {
return null;
}

return this.getCompletionSuggestions(currentWord);
}

resolveCompletionItem?(item: vscode.CompletionItem, _token: vscode.CancellationToken): vscode.ProviderResult<vscode.CompletionItem> {
return item;
}

provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.ProviderResult<vscode.Hover> {
const range = document.getWordRangeAtPosition(position);
const value = document.getText(range);

if (token.isCancellationRequested) {
return null;
}

const hoverSuggestions = this.commands[value] || this.variables[value] || this.modules[value];

const markdown: vscode.MarkdownString = new vscode.MarkdownString();
markdown.appendMarkdown(hoverSuggestions.description);
hoverSuggestions.syntax_examples?.forEach((example) => {
markdown.appendCodeblock(`\t${example}`, "cmake");
});

if (hoverSuggestions) {
return new vscode.Hover(markdown);
}

return null;
}
}