Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/snippet/browser/snippetVariables.ts
5311 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 { normalizeDriveLetter } from '../../../../base/common/labels.js';
7
import * as path from '../../../../base/common/path.js';
8
import { dirname } from '../../../../base/common/resources.js';
9
import { commonPrefixLength, getLeadingWhitespace, isFalsyOrWhitespace, splitLines } from '../../../../base/common/strings.js';
10
import { generateUuid } from '../../../../base/common/uuid.js';
11
import { Selection } from '../../../common/core/selection.js';
12
import { ITextModel } from '../../../common/model.js';
13
import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';
14
import { Text, Variable, VariableResolver } from './snippetParser.js';
15
import { OvertypingCapturer } from '../../suggest/browser/suggestOvertypingCapturer.js';
16
import * as nls from '../../../../nls.js';
17
import { ILabelService } from '../../../../platform/label/common/label.js';
18
import { WORKSPACE_EXTENSION, isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier, IWorkspaceContextService, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, isEmptyWorkspaceIdentifier } from '../../../../platform/workspace/common/workspace.js';
19
20
export const KnownSnippetVariableNames = Object.freeze<{ [key: string]: true }>({
21
'CURRENT_YEAR': true,
22
'CURRENT_YEAR_SHORT': true,
23
'CURRENT_MONTH': true,
24
'CURRENT_DATE': true,
25
'CURRENT_HOUR': true,
26
'CURRENT_MINUTE': true,
27
'CURRENT_SECOND': true,
28
'CURRENT_DAY_NAME': true,
29
'CURRENT_DAY_NAME_SHORT': true,
30
'CURRENT_MONTH_NAME': true,
31
'CURRENT_MONTH_NAME_SHORT': true,
32
'CURRENT_SECONDS_UNIX': true,
33
'CURRENT_TIMEZONE_OFFSET': true,
34
'SELECTION': true,
35
'CLIPBOARD': true,
36
'TM_SELECTED_TEXT': true,
37
'TM_CURRENT_LINE': true,
38
'TM_CURRENT_WORD': true,
39
'TM_LINE_INDEX': true,
40
'TM_LINE_NUMBER': true,
41
'TM_FILENAME': true,
42
'TM_FILENAME_BASE': true,
43
'TM_DIRECTORY': true,
44
'TM_DIRECTORY_BASE': true,
45
'TM_FILEPATH': true,
46
'CURSOR_INDEX': true, // 0-offset
47
'CURSOR_NUMBER': true, // 1-offset
48
'RELATIVE_FILEPATH': true,
49
'BLOCK_COMMENT_START': true,
50
'BLOCK_COMMENT_END': true,
51
'LINE_COMMENT': true,
52
'WORKSPACE_NAME': true,
53
'WORKSPACE_FOLDER': true,
54
'RANDOM': true,
55
'RANDOM_HEX': true,
56
'UUID': true
57
});
58
59
export class CompositeSnippetVariableResolver implements VariableResolver {
60
61
constructor(private readonly _delegates: VariableResolver[]) {
62
//
63
}
64
65
resolve(variable: Variable): string | undefined {
66
for (const delegate of this._delegates) {
67
const value = delegate.resolve(variable);
68
if (value !== undefined) {
69
return value;
70
}
71
}
72
return undefined;
73
}
74
}
75
76
export class SelectionBasedVariableResolver implements VariableResolver {
77
78
constructor(
79
private readonly _model: ITextModel,
80
private readonly _selection: Selection,
81
private readonly _selectionIdx: number,
82
private readonly _overtypingCapturer: OvertypingCapturer | undefined
83
) {
84
//
85
}
86
87
resolve(variable: Variable): string | undefined {
88
89
const { name } = variable;
90
91
if (name === 'SELECTION' || name === 'TM_SELECTED_TEXT') {
92
let value = this._model.getValueInRange(this._selection) || undefined;
93
let isMultiline = this._selection.startLineNumber !== this._selection.endLineNumber;
94
95
// If there was no selected text, try to get last overtyped text
96
if (!value && this._overtypingCapturer) {
97
const info = this._overtypingCapturer.getLastOvertypedInfo(this._selectionIdx);
98
if (info) {
99
value = info.value;
100
isMultiline = info.multiline;
101
}
102
}
103
104
if (value && isMultiline && variable.snippet) {
105
// Selection is a multiline string which we indentation we now
106
// need to adjust. We compare the indentation of this variable
107
// with the indentation at the editor position and add potential
108
// extra indentation to the value
109
110
const line = this._model.getLineContent(this._selection.startLineNumber);
111
const lineLeadingWhitespace = getLeadingWhitespace(line, 0, this._selection.startColumn - 1);
112
113
let varLeadingWhitespace = lineLeadingWhitespace;
114
variable.snippet.walk(marker => {
115
if (marker === variable) {
116
return false;
117
}
118
if (marker instanceof Text) {
119
varLeadingWhitespace = getLeadingWhitespace(splitLines(marker.value).pop()!);
120
}
121
return true;
122
});
123
const whitespaceCommonLength = commonPrefixLength(varLeadingWhitespace, lineLeadingWhitespace);
124
125
value = value.replace(
126
/(\r\n|\r|\n)(.*)/g,
127
(m, newline, rest) => `${newline}${varLeadingWhitespace.substr(whitespaceCommonLength)}${rest}`
128
);
129
}
130
return value;
131
132
} else if (name === 'TM_CURRENT_LINE') {
133
return this._model.getLineContent(this._selection.positionLineNumber);
134
135
} else if (name === 'TM_CURRENT_WORD') {
136
const info = this._model.getWordAtPosition({
137
lineNumber: this._selection.positionLineNumber,
138
column: this._selection.positionColumn
139
});
140
return info && info.word || undefined;
141
142
} else if (name === 'TM_LINE_INDEX') {
143
return String(this._selection.positionLineNumber - 1);
144
145
} else if (name === 'TM_LINE_NUMBER') {
146
return String(this._selection.positionLineNumber);
147
148
} else if (name === 'CURSOR_INDEX') {
149
return String(this._selectionIdx);
150
151
} else if (name === 'CURSOR_NUMBER') {
152
return String(this._selectionIdx + 1);
153
}
154
return undefined;
155
}
156
}
157
158
export class ModelBasedVariableResolver implements VariableResolver {
159
160
constructor(
161
private readonly _labelService: ILabelService,
162
private readonly _model: ITextModel
163
) {
164
//
165
}
166
167
resolve(variable: Variable): string | undefined {
168
169
const { name } = variable;
170
171
if (name === 'TM_FILENAME') {
172
return path.basename(this._model.uri.fsPath);
173
174
} else if (name === 'TM_FILENAME_BASE') {
175
const name = path.basename(this._model.uri.fsPath);
176
const idx = name.lastIndexOf('.');
177
if (idx <= 0) {
178
return name;
179
} else {
180
return name.slice(0, idx);
181
}
182
183
} else if (name === 'TM_DIRECTORY') {
184
if (path.dirname(this._model.uri.fsPath) === '.') {
185
return '';
186
}
187
return this._labelService.getUriLabel(dirname(this._model.uri));
188
189
} else if (name === 'TM_DIRECTORY_BASE') {
190
if (path.dirname(this._model.uri.fsPath) === '.') {
191
return '';
192
}
193
return path.basename(path.dirname(this._model.uri.fsPath));
194
195
} else if (name === 'TM_FILEPATH') {
196
return this._labelService.getUriLabel(this._model.uri);
197
} else if (name === 'RELATIVE_FILEPATH') {
198
return this._labelService.getUriLabel(this._model.uri, { relative: true, noPrefix: true });
199
}
200
201
return undefined;
202
}
203
}
204
205
export interface IReadClipboardText {
206
(): string | undefined;
207
}
208
209
export class ClipboardBasedVariableResolver implements VariableResolver {
210
211
constructor(
212
private readonly _readClipboardText: IReadClipboardText,
213
private readonly _selectionIdx: number,
214
private readonly _selectionCount: number,
215
private readonly _spread: boolean
216
) {
217
//
218
}
219
220
resolve(variable: Variable): string | undefined {
221
if (variable.name !== 'CLIPBOARD') {
222
return undefined;
223
}
224
225
const clipboardText = this._readClipboardText();
226
if (!clipboardText) {
227
return undefined;
228
}
229
230
// `spread` is assigning each cursor a line of the clipboard
231
// text whenever there the line count equals the cursor count
232
// and when enabled
233
if (this._spread) {
234
const lines = clipboardText.split(/\r\n|\n|\r/).filter(s => !isFalsyOrWhitespace(s));
235
if (lines.length === this._selectionCount) {
236
return lines[this._selectionIdx];
237
}
238
}
239
return clipboardText;
240
}
241
}
242
export class CommentBasedVariableResolver implements VariableResolver {
243
constructor(
244
private readonly _model: ITextModel,
245
private readonly _selection: Selection,
246
@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService
247
) {
248
//
249
}
250
resolve(variable: Variable): string | undefined {
251
const { name } = variable;
252
const langId = this._model.getLanguageIdAtPosition(this._selection.selectionStartLineNumber, this._selection.selectionStartColumn);
253
const config = this._languageConfigurationService.getLanguageConfiguration(langId).comments;
254
if (!config) {
255
return undefined;
256
}
257
if (name === 'LINE_COMMENT') {
258
return config.lineCommentToken || undefined;
259
} else if (name === 'BLOCK_COMMENT_START') {
260
return config.blockCommentStartToken || undefined;
261
} else if (name === 'BLOCK_COMMENT_END') {
262
return config.blockCommentEndToken || undefined;
263
}
264
return undefined;
265
}
266
}
267
export class TimeBasedVariableResolver implements VariableResolver {
268
269
private static readonly dayNames = [nls.localize('Sunday', "Sunday"), nls.localize('Monday', "Monday"), nls.localize('Tuesday', "Tuesday"), nls.localize('Wednesday', "Wednesday"), nls.localize('Thursday', "Thursday"), nls.localize('Friday', "Friday"), nls.localize('Saturday', "Saturday")];
270
private static readonly dayNamesShort = [nls.localize('SundayShort', "Sun"), nls.localize('MondayShort', "Mon"), nls.localize('TuesdayShort', "Tue"), nls.localize('WednesdayShort', "Wed"), nls.localize('ThursdayShort', "Thu"), nls.localize('FridayShort', "Fri"), nls.localize('SaturdayShort', "Sat")];
271
private static readonly monthNames = [nls.localize('January', "January"), nls.localize('February', "February"), nls.localize('March', "March"), nls.localize('April', "April"), nls.localize('May', "May"), nls.localize('June', "June"), nls.localize('July', "July"), nls.localize('August', "August"), nls.localize('September', "September"), nls.localize('October', "October"), nls.localize('November', "November"), nls.localize('December', "December")];
272
private static readonly monthNamesShort = [nls.localize('JanuaryShort', "Jan"), nls.localize('FebruaryShort', "Feb"), nls.localize('MarchShort', "Mar"), nls.localize('AprilShort', "Apr"), nls.localize('MayShort', "May"), nls.localize('JuneShort', "Jun"), nls.localize('JulyShort', "Jul"), nls.localize('AugustShort', "Aug"), nls.localize('SeptemberShort', "Sep"), nls.localize('OctoberShort', "Oct"), nls.localize('NovemberShort', "Nov"), nls.localize('DecemberShort', "Dec")];
273
274
private readonly _date = new Date();
275
276
resolve(variable: Variable): string | undefined {
277
const { name } = variable;
278
279
if (name === 'CURRENT_YEAR') {
280
return String(this._date.getFullYear());
281
} else if (name === 'CURRENT_YEAR_SHORT') {
282
return String(this._date.getFullYear()).slice(-2);
283
} else if (name === 'CURRENT_MONTH') {
284
return String(this._date.getMonth().valueOf() + 1).padStart(2, '0');
285
} else if (name === 'CURRENT_DATE') {
286
return String(this._date.getDate().valueOf()).padStart(2, '0');
287
} else if (name === 'CURRENT_HOUR') {
288
return String(this._date.getHours().valueOf()).padStart(2, '0');
289
} else if (name === 'CURRENT_MINUTE') {
290
return String(this._date.getMinutes().valueOf()).padStart(2, '0');
291
} else if (name === 'CURRENT_SECOND') {
292
return String(this._date.getSeconds().valueOf()).padStart(2, '0');
293
} else if (name === 'CURRENT_DAY_NAME') {
294
return TimeBasedVariableResolver.dayNames[this._date.getDay()];
295
} else if (name === 'CURRENT_DAY_NAME_SHORT') {
296
return TimeBasedVariableResolver.dayNamesShort[this._date.getDay()];
297
} else if (name === 'CURRENT_MONTH_NAME') {
298
return TimeBasedVariableResolver.monthNames[this._date.getMonth()];
299
} else if (name === 'CURRENT_MONTH_NAME_SHORT') {
300
return TimeBasedVariableResolver.monthNamesShort[this._date.getMonth()];
301
} else if (name === 'CURRENT_SECONDS_UNIX') {
302
return String(Math.floor(this._date.getTime() / 1000));
303
} else if (name === 'CURRENT_TIMEZONE_OFFSET') {
304
const rawTimeOffset = this._date.getTimezoneOffset();
305
const sign = rawTimeOffset > 0 ? '-' : '+';
306
const hours = Math.trunc(Math.abs(rawTimeOffset / 60));
307
const hoursString = (hours < 10 ? '0' + hours : hours);
308
const minutes = Math.abs(rawTimeOffset) - hours * 60;
309
const minutesString = (minutes < 10 ? '0' + minutes : minutes);
310
return sign + hoursString + ':' + minutesString;
311
}
312
313
return undefined;
314
}
315
}
316
317
export class WorkspaceBasedVariableResolver implements VariableResolver {
318
constructor(
319
private readonly _workspaceService: IWorkspaceContextService | undefined,
320
) {
321
//
322
}
323
324
resolve(variable: Variable): string | undefined {
325
if (!this._workspaceService) {
326
return undefined;
327
}
328
329
const workspaceIdentifier = toWorkspaceIdentifier(this._workspaceService.getWorkspace());
330
if (isEmptyWorkspaceIdentifier(workspaceIdentifier)) {
331
return undefined;
332
}
333
334
if (variable.name === 'WORKSPACE_NAME') {
335
return this._resolveWorkspaceName(workspaceIdentifier);
336
} else if (variable.name === 'WORKSPACE_FOLDER') {
337
return this._resoveWorkspacePath(workspaceIdentifier);
338
}
339
340
return undefined;
341
}
342
private _resolveWorkspaceName(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string | undefined {
343
if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) {
344
return path.basename(workspaceIdentifier.uri.path);
345
}
346
347
let filename = path.basename(workspaceIdentifier.configPath.path);
348
if (filename.endsWith(WORKSPACE_EXTENSION)) {
349
filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1);
350
}
351
return filename;
352
}
353
private _resoveWorkspacePath(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string | undefined {
354
if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) {
355
return normalizeDriveLetter(workspaceIdentifier.uri.fsPath);
356
}
357
358
const filename = path.basename(workspaceIdentifier.configPath.path);
359
let folderpath = workspaceIdentifier.configPath.fsPath;
360
if (folderpath.endsWith(filename)) {
361
folderpath = folderpath.substr(0, folderpath.length - filename.length - 1);
362
}
363
return (folderpath ? normalizeDriveLetter(folderpath) : '/');
364
}
365
}
366
367
export class RandomBasedVariableResolver implements VariableResolver {
368
resolve(variable: Variable): string | undefined {
369
const { name } = variable;
370
371
if (name === 'RANDOM') {
372
return Math.random().toString().slice(-6);
373
} else if (name === 'RANDOM_HEX') {
374
return Math.random().toString(16).slice(-6);
375
} else if (name === 'UUID') {
376
return generateUuid();
377
}
378
379
return undefined;
380
}
381
}
382
383