Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/configuration-editing/src/settingsDocumentHelper.ts
3291 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as vscode from 'vscode';
7
import { getLocation, Location, parse } from 'jsonc-parser';
8
import { provideInstalledExtensionProposals } from './extensionsProposals';
9
10
const OVERRIDE_IDENTIFIER_REGEX = /\[([^\[\]]*)\]/g;
11
12
export class SettingsDocument {
13
14
constructor(private document: vscode.TextDocument) { }
15
16
public async provideCompletionItems(position: vscode.Position, _token: vscode.CancellationToken): Promise<vscode.CompletionItem[] | vscode.CompletionList> {
17
const location = getLocation(this.document.getText(), this.document.offsetAt(position));
18
19
// window.title
20
if (location.path[0] === 'window.title') {
21
return this.provideWindowTitleCompletionItems(location, position);
22
}
23
24
// files.association
25
if (location.path[0] === 'files.associations') {
26
return this.provideFilesAssociationsCompletionItems(location, position);
27
}
28
29
// files.exclude, search.exclude, explorer.autoRevealExclude
30
if (location.path[0] === 'files.exclude' || location.path[0] === 'search.exclude' || location.path[0] === 'explorer.autoRevealExclude') {
31
return this.provideExcludeCompletionItems(location, position);
32
}
33
34
// files.defaultLanguage
35
if (location.path[0] === 'files.defaultLanguage') {
36
return this.provideLanguageCompletionItems(location, position);
37
}
38
39
// workbench.editor.label
40
if (location.path[0] === 'workbench.editor.label.patterns') {
41
return this.provideEditorLabelCompletionItems(location, position);
42
}
43
44
// settingsSync.ignoredExtensions
45
if (location.path[0] === 'settingsSync.ignoredExtensions') {
46
let ignoredExtensions = [];
47
try {
48
ignoredExtensions = parse(this.document.getText())['settingsSync.ignoredExtensions'];
49
} catch (e) {/* ignore error */ }
50
const range = this.getReplaceRange(location, position);
51
return provideInstalledExtensionProposals(ignoredExtensions, '', range, true);
52
}
53
54
// remote.extensionKind
55
if (location.path[0] === 'remote.extensionKind' && location.path.length === 2 && location.isAtPropertyKey) {
56
let alreadyConfigured: string[] = [];
57
try {
58
alreadyConfigured = Object.keys(parse(this.document.getText())['remote.extensionKind']);
59
} catch (e) {/* ignore error */ }
60
const range = this.getReplaceRange(location, position);
61
return provideInstalledExtensionProposals(alreadyConfigured, location.previousNode ? '' : `: [\n\t"ui"\n]`, range, true);
62
}
63
64
// remote.portsAttributes
65
if (location.path[0] === 'remote.portsAttributes' && location.path.length === 2 && location.isAtPropertyKey) {
66
return this.providePortsAttributesCompletionItem(this.getReplaceRange(location, position));
67
}
68
69
return this.provideLanguageOverridesCompletionItems(location, position);
70
}
71
72
private getReplaceRange(location: Location, position: vscode.Position) {
73
const node = location.previousNode;
74
if (node) {
75
const nodeStart = this.document.positionAt(node.offset), nodeEnd = this.document.positionAt(node.offset + node.length);
76
if (nodeStart.isBeforeOrEqual(position) && nodeEnd.isAfterOrEqual(position)) {
77
return new vscode.Range(nodeStart, nodeEnd);
78
}
79
}
80
return new vscode.Range(position, position);
81
}
82
83
private isCompletingPropertyValue(location: Location, pos: vscode.Position) {
84
if (location.isAtPropertyKey) {
85
return false;
86
}
87
const previousNode = location.previousNode;
88
if (previousNode) {
89
const offset = this.document.offsetAt(pos);
90
return offset >= previousNode.offset && offset <= previousNode.offset + previousNode.length;
91
}
92
return true;
93
}
94
95
private async provideWindowTitleCompletionItems(location: Location, pos: vscode.Position): Promise<vscode.CompletionItem[]> {
96
const completions: vscode.CompletionItem[] = [];
97
98
if (!this.isCompletingPropertyValue(location, pos)) {
99
return completions;
100
}
101
102
let range = this.document.getWordRangeAtPosition(pos, /\$\{[^"\}]*\}?/);
103
if (!range || range.start.isEqual(pos) || range.end.isEqual(pos) && this.document.getText(range).endsWith('}')) {
104
range = new vscode.Range(pos, pos);
105
}
106
107
const getText = (variable: string) => {
108
const text = '${' + variable + '}';
109
return location.previousNode ? text : JSON.stringify(text);
110
};
111
112
113
completions.push(this.newSimpleCompletionItem(getText('activeEditorShort'), range, vscode.l10n.t("the file name (e.g. myFile.txt)")));
114
completions.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)")));
115
completions.push(this.newSimpleCompletionItem(getText('activeEditorLong'), range, vscode.l10n.t("the full path of the file (e.g. /Users/Development/myFolder/myFileFolder/myFile.txt)")));
116
completions.push(this.newSimpleCompletionItem(getText('activeFolderShort'), range, vscode.l10n.t("the name of the folder the file is contained in (e.g. myFileFolder)")));
117
completions.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)")));
118
completions.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)")));
119
completions.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))")));
120
completions.push(this.newSimpleCompletionItem(getText('rootNameShort'), range, vscode.l10n.t("shortened name of the workspace without suffixes (e.g. myFolder or myWorkspace)")));
121
completions.push(this.newSimpleCompletionItem(getText('rootPath'), range, vscode.l10n.t("file path of the workspace (e.g. /Users/Development/myWorkspace)")));
122
completions.push(this.newSimpleCompletionItem(getText('folderName'), range, vscode.l10n.t("name of the workspace folder the file is contained in (e.g. myFolder)")));
123
completions.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)")));
124
completions.push(this.newSimpleCompletionItem(getText('appName'), range, vscode.l10n.t("e.g. VS Code")));
125
completions.push(this.newSimpleCompletionItem(getText('remoteName'), range, vscode.l10n.t("e.g. SSH")));
126
completions.push(this.newSimpleCompletionItem(getText('dirty'), range, vscode.l10n.t("an indicator for when the active editor has unsaved changes")));
127
completions.push(this.newSimpleCompletionItem(getText('separator'), range, vscode.l10n.t("a conditional separator (' - ') that only shows when surrounded by variables with values")));
128
completions.push(this.newSimpleCompletionItem(getText('activeRepositoryName'), range, vscode.l10n.t("the name of the active repository (e.g. vscode)")));
129
completions.push(this.newSimpleCompletionItem(getText('activeRepositoryBranchName'), range, vscode.l10n.t("the name of the active branch in the active repository (e.g. main)")));
130
completions.push(this.newSimpleCompletionItem(getText('activeEditorState'), range, vscode.l10n.t("the state of the active editor (e.g. modified).")));
131
return completions;
132
}
133
134
private async provideEditorLabelCompletionItems(location: Location, pos: vscode.Position): Promise<vscode.CompletionItem[]> {
135
const completions: vscode.CompletionItem[] = [];
136
137
if (!this.isCompletingPropertyValue(location, pos)) {
138
return completions;
139
}
140
141
let range = this.document.getWordRangeAtPosition(pos, /\$\{[^"\}]*\}?/);
142
if (!range || range.start.isEqual(pos) || range.end.isEqual(pos) && this.document.getText(range).endsWith('}')) {
143
range = new vscode.Range(pos, pos);
144
}
145
146
const getText = (variable: string) => {
147
const text = '${' + variable + '}';
148
return location.previousNode ? text : JSON.stringify(text);
149
};
150
151
152
completions.push(this.newSimpleCompletionItem(getText('dirname'), range, vscode.l10n.t("The parent folder name of the editor (e.g. myFileFolder)")));
153
completions.push(this.newSimpleCompletionItem(getText('dirname(1)'), range, vscode.l10n.t("The nth parent folder name of the editor")));
154
completions.push(this.newSimpleCompletionItem(getText('filename'), range, vscode.l10n.t("The file name of the editor without its directory or extension (e.g. myFile)")));
155
completions.push(this.newSimpleCompletionItem(getText('extname'), range, vscode.l10n.t("The file extension of the editor (e.g. txt)")));
156
return completions;
157
}
158
159
private async provideFilesAssociationsCompletionItems(location: Location, position: vscode.Position): Promise<vscode.CompletionItem[]> {
160
const completions: vscode.CompletionItem[] = [];
161
162
if (location.path.length === 2) {
163
// Key
164
if (location.path[1] === '') {
165
const range = this.getReplaceRange(location, position);
166
167
completions.push(this.newSnippetCompletionItem({
168
label: vscode.l10n.t("Files with Extension"),
169
documentation: vscode.l10n.t("Map all files matching the glob pattern in their filename to the language with the given identifier."),
170
snippet: location.isAtPropertyKey ? '"*.${1:extension}": "${2:language}"' : '{ "*.${1:extension}": "${2:language}" }',
171
range
172
}));
173
174
completions.push(this.newSnippetCompletionItem({
175
label: vscode.l10n.t("Files with Path"),
176
documentation: vscode.l10n.t("Map all files matching the absolute path glob pattern in their path to the language with the given identifier."),
177
snippet: location.isAtPropertyKey ? '"/${1:path to file}/*.${2:extension}": "${3:language}"' : '{ "/${1:path to file}/*.${2:extension}": "${3:language}" }',
178
range
179
}));
180
} else if (this.isCompletingPropertyValue(location, position)) {
181
// Value
182
return this.provideLanguageCompletionItemsForLanguageOverrides(this.getReplaceRange(location, position));
183
}
184
}
185
186
return completions;
187
}
188
189
private async provideExcludeCompletionItems(location: Location, position: vscode.Position): Promise<vscode.CompletionItem[]> {
190
const completions: vscode.CompletionItem[] = [];
191
192
// Key
193
if (location.path.length === 1 || (location.path.length === 2 && location.path[1] === '')) {
194
const range = this.getReplaceRange(location, position);
195
196
completions.push(this.newSnippetCompletionItem({
197
label: vscode.l10n.t("Files by Extension"),
198
documentation: vscode.l10n.t("Match all files of a specific file extension."),
199
snippet: location.path.length === 2 ? '"**/*.${1:extension}": true' : '{ "**/*.${1:extension}": true }',
200
range
201
}));
202
203
completions.push(this.newSnippetCompletionItem({
204
label: vscode.l10n.t("Files with Multiple Extensions"),
205
documentation: vscode.l10n.t("Match all files with any of the file extensions."),
206
snippet: location.path.length === 2 ? '"**/*.{ext1,ext2,ext3}": true' : '{ "**/*.{ext1,ext2,ext3}": true }',
207
range
208
}));
209
210
completions.push(this.newSnippetCompletionItem({
211
label: vscode.l10n.t("Files with Siblings by Name"),
212
documentation: vscode.l10n.t("Match files that have siblings with the same name but a different extension."),
213
snippet: location.path.length === 2 ? '"**/*.${1:source-extension}": { "when": "$(basename).${2:target-extension}" }' : '{ "**/*.${1:source-extension}": { "when": "$(basename).${2:target-extension}" } }',
214
range
215
}));
216
217
completions.push(this.newSnippetCompletionItem({
218
label: vscode.l10n.t("Folder by Name (Top Level)"),
219
documentation: vscode.l10n.t("Match a top level folder with a specific name."),
220
snippet: location.path.length === 2 ? '"${1:name}": true' : '{ "${1:name}": true }',
221
range
222
}));
223
224
completions.push(this.newSnippetCompletionItem({
225
label: vscode.l10n.t("Folders with Multiple Names (Top Level)"),
226
documentation: vscode.l10n.t("Match multiple top level folders."),
227
snippet: location.path.length === 2 ? '"{folder1,folder2,folder3}": true' : '{ "{folder1,folder2,folder3}": true }',
228
range
229
}));
230
231
completions.push(this.newSnippetCompletionItem({
232
label: vscode.l10n.t("Folder by Name (Any Location)"),
233
documentation: vscode.l10n.t("Match a folder with a specific name in any location."),
234
snippet: location.path.length === 2 ? '"**/${1:name}": true' : '{ "**/${1:name}": true }',
235
range
236
}));
237
}
238
239
// Value
240
else if (location.path.length === 2 && this.isCompletingPropertyValue(location, position)) {
241
const range = this.getReplaceRange(location, position);
242
completions.push(this.newSnippetCompletionItem({
243
label: vscode.l10n.t("Files with Siblings by Name"),
244
documentation: vscode.l10n.t("Match files that have siblings with the same name but a different extension."),
245
snippet: '{ "when": "$(basename).${1:extension}" }',
246
range
247
}));
248
}
249
250
return completions;
251
}
252
253
private async provideLanguageCompletionItems(location: Location, position: vscode.Position): Promise<vscode.CompletionItem[]> {
254
if (location.path.length === 1 && this.isCompletingPropertyValue(location, position)) {
255
const range = this.getReplaceRange(location, position);
256
const languages = await vscode.languages.getLanguages();
257
return [
258
this.newSimpleCompletionItem(JSON.stringify('${activeEditorLanguage}'), range, vscode.l10n.t("Use the language of the currently active text editor if any")),
259
...languages.map(l => this.newSimpleCompletionItem(JSON.stringify(l), range))
260
];
261
}
262
return [];
263
}
264
265
private async provideLanguageCompletionItemsForLanguageOverrides(range: vscode.Range): Promise<vscode.CompletionItem[]> {
266
const languages = await vscode.languages.getLanguages();
267
const completionItems = [];
268
for (const language of languages) {
269
const item = new vscode.CompletionItem(JSON.stringify(language));
270
item.kind = vscode.CompletionItemKind.Property;
271
item.range = range;
272
completionItems.push(item);
273
}
274
return completionItems;
275
}
276
277
private async provideLanguageOverridesCompletionItems(location: Location, position: vscode.Position): Promise<vscode.CompletionItem[]> {
278
if (location.path.length === 1 && location.isAtPropertyKey && location.previousNode && typeof location.previousNode.value === 'string' && location.previousNode.value.startsWith('[')) {
279
const startPosition = this.document.positionAt(location.previousNode.offset + 1);
280
const endPosition = startPosition.translate(undefined, location.previousNode.value.length);
281
const donotSuggestLanguages: string[] = [];
282
const languageOverridesRanges: vscode.Range[] = [];
283
let matches = OVERRIDE_IDENTIFIER_REGEX.exec(location.previousNode.value);
284
let lastLanguageOverrideRange: vscode.Range | undefined;
285
while (matches?.length) {
286
lastLanguageOverrideRange = new vscode.Range(this.document.positionAt(location.previousNode.offset + 1 + matches.index), this.document.positionAt(location.previousNode.offset + 1 + matches.index + matches[0].length));
287
languageOverridesRanges.push(lastLanguageOverrideRange);
288
/* Suggest the configured language if the position is in the match range */
289
if (!lastLanguageOverrideRange.contains(position)) {
290
donotSuggestLanguages.push(matches[1].trim());
291
}
292
matches = OVERRIDE_IDENTIFIER_REGEX.exec(location.previousNode.value);
293
}
294
const lastLanguageOverrideEndPosition = lastLanguageOverrideRange ? lastLanguageOverrideRange.end : startPosition;
295
if (lastLanguageOverrideEndPosition.isBefore(endPosition)) {
296
languageOverridesRanges.push(new vscode.Range(lastLanguageOverrideEndPosition, endPosition));
297
}
298
const languageOverrideRange = languageOverridesRanges.find(range => range.contains(position));
299
300
/**
301
* Skip if suggestions are for first language override range
302
* Since VSCode registers language overrides to the schema, JSON language server does suggestions for first language override.
303
*/
304
if (languageOverrideRange && !languageOverrideRange.isEqual(languageOverridesRanges[0])) {
305
const languages = await vscode.languages.getLanguages();
306
const completionItems = [];
307
for (const language of languages) {
308
if (!donotSuggestLanguages.includes(language)) {
309
const item = new vscode.CompletionItem(`[${language}]`);
310
item.kind = vscode.CompletionItemKind.Property;
311
item.range = languageOverrideRange;
312
completionItems.push(item);
313
}
314
}
315
return completionItems;
316
}
317
}
318
return [];
319
}
320
321
private providePortsAttributesCompletionItem(range: vscode.Range): vscode.CompletionItem[] {
322
return [this.newSnippetCompletionItem(
323
{
324
label: '\"3000\"',
325
documentation: 'Single Port Attribute',
326
range,
327
snippet: '\n \"${1:3000}\": {\n \"label\": \"${2:Application}\",\n \"onAutoForward\": \"${3:openPreview}\"\n }\n'
328
}),
329
this.newSnippetCompletionItem(
330
{
331
label: '\"5000-6000\"',
332
documentation: 'Ranged Port Attribute',
333
range,
334
snippet: '\n \"${1:40000-55000}\": {\n \"onAutoForward\": \"${2:ignore}\"\n }\n'
335
}),
336
this.newSnippetCompletionItem(
337
{
338
label: '\".+\\\\/server.js\"',
339
documentation: 'Command Match Port Attribute',
340
range,
341
snippet: '\n \"${1:.+\\\\/server.js\}\": {\n \"label\": \"${2:Application}\",\n \"onAutoForward\": \"${3:openPreview}\"\n }\n'
342
})
343
];
344
}
345
346
private newSimpleCompletionItem(text: string, range: vscode.Range, description?: string, insertText?: string): vscode.CompletionItem {
347
const item = new vscode.CompletionItem(text);
348
item.kind = vscode.CompletionItemKind.Value;
349
item.detail = description;
350
item.insertText = insertText ? insertText : text;
351
item.range = range;
352
return item;
353
}
354
355
private newSnippetCompletionItem(o: { label: string; documentation?: string; snippet: string; range: vscode.Range }): vscode.CompletionItem {
356
const item = new vscode.CompletionItem(o.label);
357
item.kind = vscode.CompletionItemKind.Value;
358
item.documentation = o.documentation;
359
item.insertText = new vscode.SnippetString(o.snippet);
360
item.range = o.range;
361
return item;
362
}
363
}
364
365