Path: blob/main/extensions/configuration-editing/src/settingsDocumentHelper.ts
3291 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as vscode from 'vscode';6import { getLocation, Location, parse } from 'jsonc-parser';7import { provideInstalledExtensionProposals } from './extensionsProposals';89const OVERRIDE_IDENTIFIER_REGEX = /\[([^\[\]]*)\]/g;1011export class SettingsDocument {1213constructor(private document: vscode.TextDocument) { }1415public async provideCompletionItems(position: vscode.Position, _token: vscode.CancellationToken): Promise<vscode.CompletionItem[] | vscode.CompletionList> {16const location = getLocation(this.document.getText(), this.document.offsetAt(position));1718// window.title19if (location.path[0] === 'window.title') {20return this.provideWindowTitleCompletionItems(location, position);21}2223// files.association24if (location.path[0] === 'files.associations') {25return this.provideFilesAssociationsCompletionItems(location, position);26}2728// files.exclude, search.exclude, explorer.autoRevealExclude29if (location.path[0] === 'files.exclude' || location.path[0] === 'search.exclude' || location.path[0] === 'explorer.autoRevealExclude') {30return this.provideExcludeCompletionItems(location, position);31}3233// files.defaultLanguage34if (location.path[0] === 'files.defaultLanguage') {35return this.provideLanguageCompletionItems(location, position);36}3738// workbench.editor.label39if (location.path[0] === 'workbench.editor.label.patterns') {40return this.provideEditorLabelCompletionItems(location, position);41}4243// settingsSync.ignoredExtensions44if (location.path[0] === 'settingsSync.ignoredExtensions') {45let ignoredExtensions = [];46try {47ignoredExtensions = parse(this.document.getText())['settingsSync.ignoredExtensions'];48} catch (e) {/* ignore error */ }49const range = this.getReplaceRange(location, position);50return provideInstalledExtensionProposals(ignoredExtensions, '', range, true);51}5253// remote.extensionKind54if (location.path[0] === 'remote.extensionKind' && location.path.length === 2 && location.isAtPropertyKey) {55let alreadyConfigured: string[] = [];56try {57alreadyConfigured = Object.keys(parse(this.document.getText())['remote.extensionKind']);58} catch (e) {/* ignore error */ }59const range = this.getReplaceRange(location, position);60return provideInstalledExtensionProposals(alreadyConfigured, location.previousNode ? '' : `: [\n\t"ui"\n]`, range, true);61}6263// remote.portsAttributes64if (location.path[0] === 'remote.portsAttributes' && location.path.length === 2 && location.isAtPropertyKey) {65return this.providePortsAttributesCompletionItem(this.getReplaceRange(location, position));66}6768return this.provideLanguageOverridesCompletionItems(location, position);69}7071private getReplaceRange(location: Location, position: vscode.Position) {72const node = location.previousNode;73if (node) {74const nodeStart = this.document.positionAt(node.offset), nodeEnd = this.document.positionAt(node.offset + node.length);75if (nodeStart.isBeforeOrEqual(position) && nodeEnd.isAfterOrEqual(position)) {76return new vscode.Range(nodeStart, nodeEnd);77}78}79return new vscode.Range(position, position);80}8182private isCompletingPropertyValue(location: Location, pos: vscode.Position) {83if (location.isAtPropertyKey) {84return false;85}86const previousNode = location.previousNode;87if (previousNode) {88const offset = this.document.offsetAt(pos);89return offset >= previousNode.offset && offset <= previousNode.offset + previousNode.length;90}91return true;92}9394private async provideWindowTitleCompletionItems(location: Location, pos: vscode.Position): Promise<vscode.CompletionItem[]> {95const completions: vscode.CompletionItem[] = [];9697if (!this.isCompletingPropertyValue(location, pos)) {98return completions;99}100101let range = this.document.getWordRangeAtPosition(pos, /\$\{[^"\}]*\}?/);102if (!range || range.start.isEqual(pos) || range.end.isEqual(pos) && this.document.getText(range).endsWith('}')) {103range = new vscode.Range(pos, pos);104}105106const getText = (variable: string) => {107const text = '${' + variable + '}';108return location.previousNode ? text : JSON.stringify(text);109};110111112completions.push(this.newSimpleCompletionItem(getText('activeEditorShort'), range, vscode.l10n.t("the file name (e.g. myFile.txt)")));113completions.push(this.newSimpleCompletionItem(getText('activeEditorMedium'), range, vscode.l10n.t("the path of the file relative to the workspace folder (e.g. myFolder/myFileFolder/myFile.txt)")));114completions.push(this.newSimpleCompletionItem(getText('activeEditorLong'), range, vscode.l10n.t("the full path of the file (e.g. /Users/Development/myFolder/myFileFolder/myFile.txt)")));115completions.push(this.newSimpleCompletionItem(getText('activeFolderShort'), range, vscode.l10n.t("the name of the folder the file is contained in (e.g. myFileFolder)")));116completions.push(this.newSimpleCompletionItem(getText('activeFolderMedium'), range, vscode.l10n.t("the path of the folder the file is contained in, relative to the workspace folder (e.g. myFolder/myFileFolder)")));117completions.push(this.newSimpleCompletionItem(getText('activeFolderLong'), range, vscode.l10n.t("the full path of the folder the file is contained in (e.g. /Users/Development/myFolder/myFileFolder)")));118completions.push(this.newSimpleCompletionItem(getText('rootName'), range, vscode.l10n.t("name of the workspace with optional remote name and workspace indicator if applicable (e.g. myFolder, myRemoteFolder [SSH] or myWorkspace (Workspace))")));119completions.push(this.newSimpleCompletionItem(getText('rootNameShort'), range, vscode.l10n.t("shortened name of the workspace without suffixes (e.g. myFolder or myWorkspace)")));120completions.push(this.newSimpleCompletionItem(getText('rootPath'), range, vscode.l10n.t("file path of the workspace (e.g. /Users/Development/myWorkspace)")));121completions.push(this.newSimpleCompletionItem(getText('folderName'), range, vscode.l10n.t("name of the workspace folder the file is contained in (e.g. myFolder)")));122completions.push(this.newSimpleCompletionItem(getText('folderPath'), range, vscode.l10n.t("file path of the workspace folder the file is contained in (e.g. /Users/Development/myFolder)")));123completions.push(this.newSimpleCompletionItem(getText('appName'), range, vscode.l10n.t("e.g. VS Code")));124completions.push(this.newSimpleCompletionItem(getText('remoteName'), range, vscode.l10n.t("e.g. SSH")));125completions.push(this.newSimpleCompletionItem(getText('dirty'), range, vscode.l10n.t("an indicator for when the active editor has unsaved changes")));126completions.push(this.newSimpleCompletionItem(getText('separator'), range, vscode.l10n.t("a conditional separator (' - ') that only shows when surrounded by variables with values")));127completions.push(this.newSimpleCompletionItem(getText('activeRepositoryName'), range, vscode.l10n.t("the name of the active repository (e.g. vscode)")));128completions.push(this.newSimpleCompletionItem(getText('activeRepositoryBranchName'), range, vscode.l10n.t("the name of the active branch in the active repository (e.g. main)")));129completions.push(this.newSimpleCompletionItem(getText('activeEditorState'), range, vscode.l10n.t("the state of the active editor (e.g. modified).")));130return completions;131}132133private async provideEditorLabelCompletionItems(location: Location, pos: vscode.Position): Promise<vscode.CompletionItem[]> {134const completions: vscode.CompletionItem[] = [];135136if (!this.isCompletingPropertyValue(location, pos)) {137return completions;138}139140let range = this.document.getWordRangeAtPosition(pos, /\$\{[^"\}]*\}?/);141if (!range || range.start.isEqual(pos) || range.end.isEqual(pos) && this.document.getText(range).endsWith('}')) {142range = new vscode.Range(pos, pos);143}144145const getText = (variable: string) => {146const text = '${' + variable + '}';147return location.previousNode ? text : JSON.stringify(text);148};149150151completions.push(this.newSimpleCompletionItem(getText('dirname'), range, vscode.l10n.t("The parent folder name of the editor (e.g. myFileFolder)")));152completions.push(this.newSimpleCompletionItem(getText('dirname(1)'), range, vscode.l10n.t("The nth parent folder name of the editor")));153completions.push(this.newSimpleCompletionItem(getText('filename'), range, vscode.l10n.t("The file name of the editor without its directory or extension (e.g. myFile)")));154completions.push(this.newSimpleCompletionItem(getText('extname'), range, vscode.l10n.t("The file extension of the editor (e.g. txt)")));155return completions;156}157158private async provideFilesAssociationsCompletionItems(location: Location, position: vscode.Position): Promise<vscode.CompletionItem[]> {159const completions: vscode.CompletionItem[] = [];160161if (location.path.length === 2) {162// Key163if (location.path[1] === '') {164const range = this.getReplaceRange(location, position);165166completions.push(this.newSnippetCompletionItem({167label: vscode.l10n.t("Files with Extension"),168documentation: vscode.l10n.t("Map all files matching the glob pattern in their filename to the language with the given identifier."),169snippet: location.isAtPropertyKey ? '"*.${1:extension}": "${2:language}"' : '{ "*.${1:extension}": "${2:language}" }',170range171}));172173completions.push(this.newSnippetCompletionItem({174label: vscode.l10n.t("Files with Path"),175documentation: vscode.l10n.t("Map all files matching the absolute path glob pattern in their path to the language with the given identifier."),176snippet: location.isAtPropertyKey ? '"/${1:path to file}/*.${2:extension}": "${3:language}"' : '{ "/${1:path to file}/*.${2:extension}": "${3:language}" }',177range178}));179} else if (this.isCompletingPropertyValue(location, position)) {180// Value181return this.provideLanguageCompletionItemsForLanguageOverrides(this.getReplaceRange(location, position));182}183}184185return completions;186}187188private async provideExcludeCompletionItems(location: Location, position: vscode.Position): Promise<vscode.CompletionItem[]> {189const completions: vscode.CompletionItem[] = [];190191// Key192if (location.path.length === 1 || (location.path.length === 2 && location.path[1] === '')) {193const range = this.getReplaceRange(location, position);194195completions.push(this.newSnippetCompletionItem({196label: vscode.l10n.t("Files by Extension"),197documentation: vscode.l10n.t("Match all files of a specific file extension."),198snippet: location.path.length === 2 ? '"**/*.${1:extension}": true' : '{ "**/*.${1:extension}": true }',199range200}));201202completions.push(this.newSnippetCompletionItem({203label: vscode.l10n.t("Files with Multiple Extensions"),204documentation: vscode.l10n.t("Match all files with any of the file extensions."),205snippet: location.path.length === 2 ? '"**/*.{ext1,ext2,ext3}": true' : '{ "**/*.{ext1,ext2,ext3}": true }',206range207}));208209completions.push(this.newSnippetCompletionItem({210label: vscode.l10n.t("Files with Siblings by Name"),211documentation: vscode.l10n.t("Match files that have siblings with the same name but a different extension."),212snippet: location.path.length === 2 ? '"**/*.${1:source-extension}": { "when": "$(basename).${2:target-extension}" }' : '{ "**/*.${1:source-extension}": { "when": "$(basename).${2:target-extension}" } }',213range214}));215216completions.push(this.newSnippetCompletionItem({217label: vscode.l10n.t("Folder by Name (Top Level)"),218documentation: vscode.l10n.t("Match a top level folder with a specific name."),219snippet: location.path.length === 2 ? '"${1:name}": true' : '{ "${1:name}": true }',220range221}));222223completions.push(this.newSnippetCompletionItem({224label: vscode.l10n.t("Folders with Multiple Names (Top Level)"),225documentation: vscode.l10n.t("Match multiple top level folders."),226snippet: location.path.length === 2 ? '"{folder1,folder2,folder3}": true' : '{ "{folder1,folder2,folder3}": true }',227range228}));229230completions.push(this.newSnippetCompletionItem({231label: vscode.l10n.t("Folder by Name (Any Location)"),232documentation: vscode.l10n.t("Match a folder with a specific name in any location."),233snippet: location.path.length === 2 ? '"**/${1:name}": true' : '{ "**/${1:name}": true }',234range235}));236}237238// Value239else if (location.path.length === 2 && this.isCompletingPropertyValue(location, position)) {240const range = this.getReplaceRange(location, position);241completions.push(this.newSnippetCompletionItem({242label: vscode.l10n.t("Files with Siblings by Name"),243documentation: vscode.l10n.t("Match files that have siblings with the same name but a different extension."),244snippet: '{ "when": "$(basename).${1:extension}" }',245range246}));247}248249return completions;250}251252private async provideLanguageCompletionItems(location: Location, position: vscode.Position): Promise<vscode.CompletionItem[]> {253if (location.path.length === 1 && this.isCompletingPropertyValue(location, position)) {254const range = this.getReplaceRange(location, position);255const languages = await vscode.languages.getLanguages();256return [257this.newSimpleCompletionItem(JSON.stringify('${activeEditorLanguage}'), range, vscode.l10n.t("Use the language of the currently active text editor if any")),258...languages.map(l => this.newSimpleCompletionItem(JSON.stringify(l), range))259];260}261return [];262}263264private async provideLanguageCompletionItemsForLanguageOverrides(range: vscode.Range): Promise<vscode.CompletionItem[]> {265const languages = await vscode.languages.getLanguages();266const completionItems = [];267for (const language of languages) {268const item = new vscode.CompletionItem(JSON.stringify(language));269item.kind = vscode.CompletionItemKind.Property;270item.range = range;271completionItems.push(item);272}273return completionItems;274}275276private async provideLanguageOverridesCompletionItems(location: Location, position: vscode.Position): Promise<vscode.CompletionItem[]> {277if (location.path.length === 1 && location.isAtPropertyKey && location.previousNode && typeof location.previousNode.value === 'string' && location.previousNode.value.startsWith('[')) {278const startPosition = this.document.positionAt(location.previousNode.offset + 1);279const endPosition = startPosition.translate(undefined, location.previousNode.value.length);280const donotSuggestLanguages: string[] = [];281const languageOverridesRanges: vscode.Range[] = [];282let matches = OVERRIDE_IDENTIFIER_REGEX.exec(location.previousNode.value);283let lastLanguageOverrideRange: vscode.Range | undefined;284while (matches?.length) {285lastLanguageOverrideRange = new vscode.Range(this.document.positionAt(location.previousNode.offset + 1 + matches.index), this.document.positionAt(location.previousNode.offset + 1 + matches.index + matches[0].length));286languageOverridesRanges.push(lastLanguageOverrideRange);287/* Suggest the configured language if the position is in the match range */288if (!lastLanguageOverrideRange.contains(position)) {289donotSuggestLanguages.push(matches[1].trim());290}291matches = OVERRIDE_IDENTIFIER_REGEX.exec(location.previousNode.value);292}293const lastLanguageOverrideEndPosition = lastLanguageOverrideRange ? lastLanguageOverrideRange.end : startPosition;294if (lastLanguageOverrideEndPosition.isBefore(endPosition)) {295languageOverridesRanges.push(new vscode.Range(lastLanguageOverrideEndPosition, endPosition));296}297const languageOverrideRange = languageOverridesRanges.find(range => range.contains(position));298299/**300* Skip if suggestions are for first language override range301* Since VSCode registers language overrides to the schema, JSON language server does suggestions for first language override.302*/303if (languageOverrideRange && !languageOverrideRange.isEqual(languageOverridesRanges[0])) {304const languages = await vscode.languages.getLanguages();305const completionItems = [];306for (const language of languages) {307if (!donotSuggestLanguages.includes(language)) {308const item = new vscode.CompletionItem(`[${language}]`);309item.kind = vscode.CompletionItemKind.Property;310item.range = languageOverrideRange;311completionItems.push(item);312}313}314return completionItems;315}316}317return [];318}319320private providePortsAttributesCompletionItem(range: vscode.Range): vscode.CompletionItem[] {321return [this.newSnippetCompletionItem(322{323label: '\"3000\"',324documentation: 'Single Port Attribute',325range,326snippet: '\n \"${1:3000}\": {\n \"label\": \"${2:Application}\",\n \"onAutoForward\": \"${3:openPreview}\"\n }\n'327}),328this.newSnippetCompletionItem(329{330label: '\"5000-6000\"',331documentation: 'Ranged Port Attribute',332range,333snippet: '\n \"${1:40000-55000}\": {\n \"onAutoForward\": \"${2:ignore}\"\n }\n'334}),335this.newSnippetCompletionItem(336{337label: '\".+\\\\/server.js\"',338documentation: 'Command Match Port Attribute',339range,340snippet: '\n \"${1:.+\\\\/server.js\}\": {\n \"label\": \"${2:Application}\",\n \"onAutoForward\": \"${3:openPreview}\"\n }\n'341})342];343}344345private newSimpleCompletionItem(text: string, range: vscode.Range, description?: string, insertText?: string): vscode.CompletionItem {346const item = new vscode.CompletionItem(text);347item.kind = vscode.CompletionItemKind.Value;348item.detail = description;349item.insertText = insertText ? insertText : text;350item.range = range;351return item;352}353354private newSnippetCompletionItem(o: { label: string; documentation?: string; snippet: string; range: vscode.Range }): vscode.CompletionItem {355const item = new vscode.CompletionItem(o.label);356item.kind = vscode.CompletionItemKind.Value;357item.documentation = o.documentation;358item.insertText = new vscode.SnippetString(o.snippet);359item.range = o.range;360return item;361}362}363364365