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