/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { getLocation, JSONPath, parse, visit, Location } from 'jsonc-parser'; import * as vscode from 'vscode'; import { SettingsDocument } from './settingsDocumentHelper'; import { provideInstalledExtensionProposals } from './extensionsProposals'; import './importExportProfiles'; export function activate(context: vscode.ExtensionContext): void { //settings.json suggestions context.subscriptions.push(registerSettingsCompletions()); //extensions suggestions context.subscriptions.push(...registerExtensionsCompletions()); // launch.json variable suggestions context.subscriptions.push(registerVariableCompletions('**/launch.json')); // task.json variable suggestions context.subscriptions.push(registerVariableCompletions('**/tasks.json')); // Workspace file launch/tasks variable completions context.subscriptions.push(registerVariableCompletions('**/*.code-workspace')); // keybindings.json/package.json context key suggestions context.subscriptions.push(registerContextKeyCompletions()); } function registerSettingsCompletions(): vscode.Disposable { return vscode.languages.registerCompletionItemProvider({ language: 'jsonc', pattern: '**/settings.json' }, { provideCompletionItems(document, position, token) { return new SettingsDocument(document).provideCompletionItems(position, token); } }); } function registerVariableCompletions(pattern: string): vscode.Disposable { return vscode.languages.registerCompletionItemProvider({ language: 'jsonc', pattern }, { provideCompletionItems(document, position, _token) { const location = getLocation(document.getText(), document.offsetAt(position)); if (isCompletingInsidePropertyStringValue(document, location, position)) { if (document.fileName.endsWith('.code-workspace') && !isLocationInsideTopLevelProperty(location, ['launch', 'tasks'])) { return []; } let range = document.getWordRangeAtPosition(position, /\$\{[^"\}]*\}?/); if (!range || range.start.isEqual(position) || range.end.isEqual(position) && document.getText(range).endsWith('}')) { range = new vscode.Range(position, position); } return [ { label: 'workspaceFolder', detail: vscode.l10n.t("The path of the folder opened in VS Code") }, { label: 'workspaceFolderBasename', detail: vscode.l10n.t("The name of the folder opened in VS Code without any slashes (/)") }, { label: 'relativeFile', detail: vscode.l10n.t("The current opened file relative to ${workspaceFolder}") }, { label: 'relativeFileDirname', detail: vscode.l10n.t("The current opened file's dirname relative to ${workspaceFolder}") }, { label: 'file', detail: vscode.l10n.t("The current opened file") }, { label: 'cwd', detail: vscode.l10n.t("The task runner's current working directory on startup") }, { label: 'lineNumber', detail: vscode.l10n.t("The current selected line number in the active file") }, { label: 'selectedText', detail: vscode.l10n.t("The current selected text in the active file") }, { label: 'fileDirname', detail: vscode.l10n.t("The current opened file's dirname") }, { label: 'fileExtname', detail: vscode.l10n.t("The current opened file's extension") }, { label: 'fileBasename', detail: vscode.l10n.t("The current opened file's basename") }, { label: 'fileBasenameNoExtension', detail: vscode.l10n.t("The current opened file's basename with no file extension") }, { label: 'defaultBuildTask', detail: vscode.l10n.t("The name of the default build task. If there is not a single default build task then a quick pick is shown to choose the build task.") }, { label: 'pathSeparator', detail: vscode.l10n.t("The character used by the operating system to separate components in file paths") }, { label: 'extensionInstallFolder', detail: vscode.l10n.t("The path where an an extension is installed."), param: 'publisher.extension' }, ].map(variable => ({ label: `\${${variable.label}}`, range, insertText: variable.param ? new vscode.SnippetString(`\${${variable.label}:`).appendPlaceholder(variable.param).appendText('}') : (`\${${variable.label}}`), detail: variable.detail })); } return []; } }); } function isCompletingInsidePropertyStringValue(document: vscode.TextDocument, location: Location, pos: vscode.Position) { if (location.isAtPropertyKey) { return false; } const previousNode = location.previousNode; if (previousNode && previousNode.type === 'string') { const offset = document.offsetAt(pos); return offset > previousNode.offset && offset < previousNode.offset + previousNode.length; } return false; } function isLocationInsideTopLevelProperty(location: Location, values: string[]) { return values.includes(location.path[0] as string); } interface IExtensionsContent { recommendations: string[]; } function registerExtensionsCompletions(): vscode.Disposable[] { return [registerExtensionsCompletionsInExtensionsDocument(), registerExtensionsCompletionsInWorkspaceConfigurationDocument()]; } function registerExtensionsCompletionsInExtensionsDocument(): vscode.Disposable { return vscode.languages.registerCompletionItemProvider({ pattern: '**/extensions.json' }, { provideCompletionItems(document, position, _token) { const location = getLocation(document.getText(), document.offsetAt(position)); if (location.path[0] === 'recommendations') { const range = getReplaceRange(document, location, position); const extensionsContent = parse(document.getText()); return provideInstalledExtensionProposals(extensionsContent && extensionsContent.recommendations || [], '', range, false); } return []; } }); } function registerExtensionsCompletionsInWorkspaceConfigurationDocument(): vscode.Disposable { return vscode.languages.registerCompletionItemProvider({ pattern: '**/*.code-workspace' }, { provideCompletionItems(document, position, _token) { const location = getLocation(document.getText(), document.offsetAt(position)); if (location.path[0] === 'extensions' && location.path[1] === 'recommendations') { const range = getReplaceRange(document, location, position); const extensionsContent = parse(document.getText())['extensions']; return provideInstalledExtensionProposals(extensionsContent && extensionsContent.recommendations || [], '', range, false); } return []; } }); } function getReplaceRange(document: vscode.TextDocument, location: Location, position: vscode.Position) { const node = location.previousNode; if (node) { const nodeStart = document.positionAt(node.offset), nodeEnd = document.positionAt(node.offset + node.length); if (nodeStart.isBeforeOrEqual(position) && nodeEnd.isAfterOrEqual(position)) { return new vscode.Range(nodeStart, nodeEnd); } } return new vscode.Range(position, position); } vscode.languages.registerDocumentSymbolProvider({ pattern: '**/launch.json', language: 'jsonc' }, { provideDocumentSymbols(document: vscode.TextDocument, _token: vscode.CancellationToken): vscode.ProviderResult { const result: vscode.SymbolInformation[] = []; let name: string = ''; let lastProperty = ''; let startOffset = 0; let depthInObjects = 0; visit(document.getText(), { onObjectProperty: (property, _offset, _length) => { lastProperty = property; }, onLiteralValue: (value: any, _offset: number, _length: number) => { if (lastProperty === 'name') { name = value; } }, onObjectBegin: (offset: number, _length: number) => { depthInObjects++; if (depthInObjects === 2) { startOffset = offset; } }, onObjectEnd: (offset: number, _length: number) => { if (name && depthInObjects === 2) { result.push(new vscode.SymbolInformation(name, vscode.SymbolKind.Object, new vscode.Range(document.positionAt(startOffset), document.positionAt(offset)))); } depthInObjects--; }, }); return result; } }, { label: 'Launch Targets' }); function registerContextKeyCompletions(): vscode.Disposable { type ContextKeyInfo = { key: string; type?: string; description?: string }; const paths = new Map([ [{ language: 'jsonc', pattern: '**/keybindings.json' }, [ ['*', 'when'] ]], [{ language: 'json', pattern: '**/package.json' }, [ ['contributes', 'menus', '*', '*', 'when'], ['contributes', 'views', '*', '*', 'when'], ['contributes', 'viewsWelcome', '*', 'when'], ['contributes', 'keybindings', '*', 'when'], ['contributes', 'keybindings', 'when'], ]] ]); return vscode.languages.registerCompletionItemProvider( [...paths.keys()], { async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken) { const location = getLocation(document.getText(), document.offsetAt(position)); if (location.isAtPropertyKey) { return; } let isValidLocation = false; for (const [key, value] of paths) { if (vscode.languages.match(key, document)) { if (value.some(location.matches.bind(location))) { isValidLocation = true; break; } } } if (!isValidLocation || !isCompletingInsidePropertyStringValue(document, location, position)) { return; } const replacing = document.getWordRangeAtPosition(position, /[a-zA-Z.]+/) || new vscode.Range(position, position); const inserting = replacing.with(undefined, position); const data = await vscode.commands.executeCommand('getContextKeyInfo'); if (token.isCancellationRequested || !data) { return; } const result = new vscode.CompletionList(); for (const item of data) { const completion = new vscode.CompletionItem(item.key, vscode.CompletionItemKind.Constant); completion.detail = item.type; completion.range = { replacing, inserting }; completion.documentation = item.description; result.items.push(completion); } return result; } } ); }