From 6547ce09e82651cdc43d7bc1fbdda1c262cea5e2 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 8 Feb 2017 20:51:33 +0100 Subject: [PATCH] #19149 Implement intellisense for language specific editor configurations in settings editor --- .../configuration-editing/src/extension.ts | 127 +---------- .../src/settingsDocumentHelper.ts | 211 ++++++++++++++++++ 2 files changed, 213 insertions(+), 125 deletions(-) create mode 100644 extensions/configuration-editing/src/settingsDocumentHelper.ts diff --git a/extensions/configuration-editing/src/extension.ts b/extensions/configuration-editing/src/extension.ts index a72531b63f6..e33fb47afcd 100644 --- a/extensions/configuration-editing/src/extension.ts +++ b/extensions/configuration-editing/src/extension.ts @@ -8,9 +8,7 @@ import * as vscode from 'vscode'; import { getLocation, visit } from 'jsonc-parser'; import * as path from 'path'; -import * as nls from 'vscode-nls'; - -const localize = nls.loadMessageBundle(); +import { SettingsDocument } from './settingsDocumentHelper'; const decoration = vscode.window.createTextEditorDecorationType({ color: '#b1b1b1' @@ -52,119 +50,8 @@ function registerKeybindingsCompletions(): vscode.Disposable { function registerSettingsCompletions(): vscode.Disposable { return vscode.languages.registerCompletionItemProvider({ language: 'json', pattern: '**/settings.json' }, { - provideCompletionItems(document, position, token) { - const completions: vscode.CompletionItem[] = []; - const location = getLocation(document.getText(), document.offsetAt(position)); - - // window.title - if (location.path[0] === 'window.title') { - const range = document.getWordRangeAtPosition(position) || new vscode.Range(position, position); - - completions.push(newSimpleCompletionItem('${activeEditorName}', range, localize('activeEditorName', "e.g. myFile.txt"))); - completions.push(newSimpleCompletionItem('${activeFilePath}', range, localize('activeFilePath', "e.g. /Users/Development/myProject/myFile.txt"))); - completions.push(newSimpleCompletionItem('${rootName}', range, localize('rootName', "e.g. myProject"))); - completions.push(newSimpleCompletionItem('${rootPath}', range, localize('rootPath', "e.g. /Users/Development/myProject"))); - completions.push(newSimpleCompletionItem('${appName}', range, localize('appName', "e.g. VS Code"))); - completions.push(newSimpleCompletionItem('${dirty}', range, localize('dirty', "a dirty indicator if the active editor is dirty"))); - completions.push(newSimpleCompletionItem('${separator}', range, localize('separator', "a conditional separator (' - ') that only shows when surrounded by variables with values"))); - } - - // files.association - else if (location.path[0] === 'files.associations') { - const range = document.getWordRangeAtPosition(position) || new vscode.Range(position, position); - - // Key - if (location.path.length === 1) { - completions.push(newSnippetCompletionItem({ - label: localize('assocLabelFile', "Files with Extension"), - documentation: localize('assocDescriptionFile', "Map all files matching the glob pattern in their filename to the language with the given identifier."), - snippet: location.isAtPropertyKey ? '"*.${1:extension}": "${2:language}"' : '{ "*.${1:extension}": "${2:language}" }', - range - })); - - completions.push(newSnippetCompletionItem({ - label: localize('assocLabelPath', "Files with Path"), - documentation: localize('assocDescriptionPath', "Map all files matching the absolute path glob pattern in their path to the language with the given identifier."), - snippet: location.isAtPropertyKey ? '"/${1:path to file}/*.${2:extension}": "${3:language}"' : '{ "/${1:path to file}/*.${2:extension}": "${3:language}" }', - range - })); - } - - // Value - else if (location.path.length === 2 && !location.isAtPropertyKey) { - return vscode.languages.getLanguages().then(languages => { - return languages.map(l => { - return newSimpleCompletionItem(JSON.stringify(l), range); - }); - }); - } - } - - // files.exclude, search.exclude - else if (location.path[0] === 'files.exclude' || location.path[0] === 'search.exclude') { - const range = document.getWordRangeAtPosition(position) || new vscode.Range(position, position); - - // Key - if (location.path.length === 1) { - completions.push(newSnippetCompletionItem({ - label: localize('fileLabel', "Files by Extension"), - documentation: localize('fileDescription', "Match all files of a specific file extension."), - snippet: location.isAtPropertyKey ? '"**/*.${1:extension}": true' : '{ "**/*.${1:extension}": true }', - range - })); - - completions.push(newSnippetCompletionItem({ - label: localize('filesLabel', "Files with Multiple Extensions"), - documentation: localize('filesDescription', "Match all files with any of the file extensions."), - snippet: location.isAtPropertyKey ? '"**/*.{ext1,ext2,ext3}": true' : '{ "**/*.{ext1,ext2,ext3}": true }', - range - })); - - completions.push(newSnippetCompletionItem({ - label: localize('derivedLabel', "Files with Siblings by Name"), - documentation: localize('derivedDescription', "Match files that have siblings with the same name but a different extension."), - snippet: location.isAtPropertyKey ? '"**/*.${1:source-extension}": { "when": "$(basename).${2:target-extension}" }' : '{ "**/*.${1:source-extension}": { "when": "$(basename).${2:target-extension}" } }', - range - })); - - completions.push(newSnippetCompletionItem({ - label: localize('topFolderLabel', "Folder by Name (Top Level)"), - documentation: localize('topFolderDescription', "Match a top level folder with a specific name."), - snippet: location.isAtPropertyKey ? '"${1:name}": true' : '{ "${1:name}": true }', - range - })); - - completions.push(newSnippetCompletionItem({ - label: localize('topFoldersLabel', "Folders with Multiple Names (Top Level)"), - documentation: localize('topFoldersDescription', "Match multiple top level folders."), - snippet: location.isAtPropertyKey ? '"{folder1,folder2,folder3}": true' : '{ "{folder1,folder2,folder3}": true }', - range - })); - - completions.push(newSnippetCompletionItem({ - label: localize('folderLabel', "Folder by Name (Any Location)"), - documentation: localize('folderDescription', "Match a folder with a specific name in any location."), - snippet: location.isAtPropertyKey ? '"**/${1:name}": true' : '{ "**/${1:name}": true }', - range - })); - } - - // Value - else { - completions.push(newSimpleCompletionItem('false', range, localize('falseDescription', "Disable the pattern."))); - completions.push(newSimpleCompletionItem('true', range, localize('trueDescription', "Enable the pattern."))); - - completions.push(newSnippetCompletionItem({ - label: localize('derivedLabel', "Files with Siblings by Name"), - documentation: localize('siblingsDescription', "Match files that have siblings with the same name but a different extension."), - snippet: '{ "when": "$(basename).${1:extension}" }', - range - })); - } - } - - return Promise.resolve(completions); + return new SettingsDocument(document).provideCompletionItems(position, token); } }); } @@ -179,16 +66,6 @@ function newSimpleCompletionItem(text: string, range: vscode.Range, description? return item; } -function newSnippetCompletionItem(o: { label: string; documentation?: string; snippet: string; range: vscode.Range; }): vscode.CompletionItem { - const item = new vscode.CompletionItem(o.label); - item.kind = vscode.CompletionItemKind.Value; - item.documentation = o.documentation; - item.insertText = new vscode.SnippetString(o.snippet); - item.range = o.range; - - return item; -} - function updateLaunchJsonDecorations(editor: vscode.TextEditor | undefined): void { if (!editor || path.basename(editor.document.fileName) !== 'launch.json') { return; diff --git a/extensions/configuration-editing/src/settingsDocumentHelper.ts b/extensions/configuration-editing/src/settingsDocumentHelper.ts new file mode 100644 index 00000000000..8157bd0d483 --- /dev/null +++ b/extensions/configuration-editing/src/settingsDocumentHelper.ts @@ -0,0 +1,211 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { getLocation, Location } from 'jsonc-parser'; +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); + +export class SettingsDocument { + + constructor(private document: vscode.TextDocument) { } + + public provideCompletionItems(position: vscode.Position, token: vscode.CancellationToken): vscode.ProviderResult { + const location = getLocation(this.document.getText(), this.document.offsetAt(position)); + const range = this.document.getWordRangeAtPosition(position) || new vscode.Range(position, position); + + // window.title + if (location.path[0] === 'window.title') { + return this.provideWindowTitleCompletionItems(location, range); + } + + // files.association + if (location.path[0] === 'files.associations') { + return this.provideFilesAssociationsCompletionItems(location, range); + } + + // files.exclude, search.exclude + if (location.path[0] === 'files.exclude' || location.path[0] === 'search.exclude') { + return this.provideExcludeCompletionItems(location, range); + } + + return this.provideLanguageOverridesCompletionItems(location, position); + } + + private provideWindowTitleCompletionItems(location: Location, range: vscode.Range): vscode.ProviderResult { + const completions: vscode.CompletionItem[] = []; + + completions.push(this.newSimpleCompletionItem('${activeEditorName}', range, localize('activeEditorName', "e.g. myFile.txt"))); + completions.push(this.newSimpleCompletionItem('${activeFilePath}', range, localize('activeFilePath', "e.g. /Users/Development/myProject/myFile.txt"))); + completions.push(this.newSimpleCompletionItem('${rootName}', range, localize('rootName', "e.g. myProject"))); + completions.push(this.newSimpleCompletionItem('${rootPath}', range, localize('rootPath', "e.g. /Users/Development/myProject"))); + completions.push(this.newSimpleCompletionItem('${appName}', range, localize('appName', "e.g. VS Code"))); + completions.push(this.newSimpleCompletionItem('${dirty}', range, localize('dirty', "a dirty indicator if the active editor is dirty"))); + completions.push(this.newSimpleCompletionItem('${separator}', range, localize('separator', "a conditional separator (' - ') that only shows when surrounded by variables with values"))); + + return Promise.resolve(completions); + } + + private provideFilesAssociationsCompletionItems(location: Location, range: vscode.Range): vscode.ProviderResult { + const completions: vscode.CompletionItem[] = []; + + // Key + if (location.path.length === 1) { + completions.push(this.newSnippetCompletionItem({ + label: localize('assocLabelFile', "Files with Extension"), + documentation: localize('assocDescriptionFile', "Map all files matching the glob pattern in their filename to the language with the given identifier."), + snippet: location.isAtPropertyKey ? '"*.${1:extension}": "${2:language}"' : '{ "*.${1:extension}": "${2:language}" }', + range + })); + + completions.push(this.newSnippetCompletionItem({ + label: localize('assocLabelPath', "Files with Path"), + documentation: localize('assocDescriptionPath', "Map all files matching the absolute path glob pattern in their path to the language with the given identifier."), + snippet: location.isAtPropertyKey ? '"/${1:path to file}/*.${2:extension}": "${3:language}"' : '{ "/${1:path to file}/*.${2:extension}": "${3:language}" }', + range + })); + } + + // Value + else if (location.path.length === 2 && !location.isAtPropertyKey) { + return this.provideLanguageCompletionItems(location, range); + } + + return Promise.resolve(completions); + } + + private provideExcludeCompletionItems(location: Location, range: vscode.Range): vscode.ProviderResult { + const completions: vscode.CompletionItem[] = []; + + // Key + if (location.path.length === 1) { + completions.push(this.newSnippetCompletionItem({ + label: localize('fileLabel', "Files by Extension"), + documentation: localize('fileDescription', "Match all files of a specific file extension."), + snippet: location.isAtPropertyKey ? '"**/*.${1:extension}": true' : '{ "**/*.${1:extension}": true }', + range + })); + + completions.push(this.newSnippetCompletionItem({ + label: localize('filesLabel', "Files with Multiple Extensions"), + documentation: localize('filesDescription', "Match all files with any of the file extensions."), + snippet: location.isAtPropertyKey ? '"**/*.{ext1,ext2,ext3}": true' : '{ "**/*.{ext1,ext2,ext3}": true }', + range + })); + + completions.push(this.newSnippetCompletionItem({ + label: localize('derivedLabel', "Files with Siblings by Name"), + documentation: localize('derivedDescription', "Match files that have siblings with the same name but a different extension."), + snippet: location.isAtPropertyKey ? '"**/*.${1:source-extension}": { "when": "$(basename).${2:target-extension}" }' : '{ "**/*.${1:source-extension}": { "when": "$(basename).${2:target-extension}" } }', + range + })); + + completions.push(this.newSnippetCompletionItem({ + label: localize('topFolderLabel', "Folder by Name (Top Level)"), + documentation: localize('topFolderDescription', "Match a top level folder with a specific name."), + snippet: location.isAtPropertyKey ? '"${1:name}": true' : '{ "${1:name}": true }', + range + })); + + completions.push(this.newSnippetCompletionItem({ + label: localize('topFoldersLabel', "Folders with Multiple Names (Top Level)"), + documentation: localize('topFoldersDescription', "Match multiple top level folders."), + snippet: location.isAtPropertyKey ? '"{folder1,folder2,folder3}": true' : '{ "{folder1,folder2,folder3}": true }', + range + })); + + completions.push(this.newSnippetCompletionItem({ + label: localize('folderLabel', "Folder by Name (Any Location)"), + documentation: localize('folderDescription', "Match a folder with a specific name in any location."), + snippet: location.isAtPropertyKey ? '"**/${1:name}": true' : '{ "**/${1:name}": true }', + range + })); + } + + // Value + else { + completions.push(this.newSimpleCompletionItem('false', range, localize('falseDescription', "Disable the pattern."))); + completions.push(this.newSimpleCompletionItem('true', range, localize('trueDescription', "Enable the pattern."))); + + completions.push(this.newSnippetCompletionItem({ + label: localize('derivedLabel', "Files with Siblings by Name"), + documentation: localize('siblingsDescription', "Match files that have siblings with the same name but a different extension."), + snippet: '{ "when": "$(basename).${1:extension}" }', + range + })); + } + + return Promise.resolve(completions); + } + + private provideLanguageCompletionItems(location: Location, range: vscode.Range, stringify: boolean = true): vscode.ProviderResult { + return vscode.languages.getLanguages().then(languages => { + return languages.map(l => { + return this.newSimpleCompletionItem(stringify ? JSON.stringify(l) : l, range); + }); + }); + } + + private provideLanguageOverridesCompletionItems(location: Location, position: vscode.Position): vscode.ProviderResult { + let range = this.document.getWordRangeAtPosition(position) || new vscode.Range(position, position); + const text = this.document.getText(range); + + if (location.path.length === 0) { + + let snippet = '"[${1:language}]": {\n\t"$0"\n}'; + + // Suggestion model word matching includes quotes, + // hence exclude the starting quote from the snippet and the range + // ending quote gets replaced + if (text.startsWith('"')) { + range = new vscode.Range(new vscode.Position(range.start.line, range.start.character + 1), range.end); + snippet = snippet.substring(1); + } + + return Promise.resolve([this.newSnippetCompletionItem({ + label: localize('languageSpecificEditorSettings', "Language specific editor settings"), + documentation: localize('languageSpecificEditorSettingsDescription', "Override editor settings for language"), + snippet, + range + })]); + } + + if (location.path.length === 1 && location.previousNode && location.previousNode.value.startsWith('[')) { + + // Suggestion model word matching includes starting quote and open sqaure bracket + // Hence exclude them from the proposal range + range = new vscode.Range(new vscode.Position(range.start.line, range.start.character + 2), range.end); + + return vscode.languages.getLanguages().then(languages => { + return languages.map(l => { + + // Suggestion model word matching includes closed sqaure bracket and ending quote + // Hence include them in the proposal to replace + return this.newSimpleCompletionItem(l, range, '', l + ']"'); + }); + }); + } + return Promise.resolve([]); + } + + private newSimpleCompletionItem(text: string, range: vscode.Range, description?: string, insertText?: string): vscode.CompletionItem { + const item = new vscode.CompletionItem(text); + item.kind = vscode.CompletionItemKind.Value; + item.detail = description; + item.insertText = insertText ? insertText : text; + item.range = range; + return item; + } + + private newSnippetCompletionItem(o: { label: string; documentation?: string; snippet: string; range: vscode.Range; }): vscode.CompletionItem { + const item = new vscode.CompletionItem(o.label); + item.kind = vscode.CompletionItemKind.Value; + item.documentation = o.documentation; + item.insertText = new vscode.SnippetString(o.snippet); + item.range = o.range; + return item; + } +} -- GitLab