Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/emmet/src/abbreviationActions.ts
4772 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 { Node, HtmlNode, Rule, Property, Stylesheet } from 'EmmetFlatNode';
8
import { getEmmetHelper, getFlatNode, getHtmlFlatNode, getMappingForIncludedLanguages, validate, getEmmetConfiguration, isStyleSheet, getEmmetMode, parsePartialStylesheet, isStyleAttribute, getEmbeddedCssNodeIfAny, allowedMimeTypesInScriptTag, toLSTextDocument, isOffsetInsideOpenOrCloseTag } from './util';
9
import { getRootNode as parseDocument } from './parseDocument';
10
11
const trimRegex = /[\u00a0]*[\d#\-\*\u2022]+\.?/;
12
const hexColorRegex = /^#[\da-fA-F]{0,6}$/;
13
14
interface ExpandAbbreviationInput {
15
syntax: string;
16
abbreviation: string;
17
rangeToReplace: vscode.Range;
18
textToWrap?: string[];
19
filter?: string;
20
indent?: string;
21
baseIndent?: string;
22
}
23
24
interface PreviewRangesWithContent {
25
previewRange: vscode.Range;
26
originalRange: vscode.Range;
27
originalContent: string;
28
textToWrapInPreview: string[];
29
baseIndent: string;
30
}
31
32
export async function wrapWithAbbreviation(args: any): Promise<boolean> {
33
if (!validate(false)) {
34
return false;
35
}
36
37
const editor = vscode.window.activeTextEditor!;
38
const document = editor.document;
39
40
args = args || {};
41
if (!args['language']) {
42
args['language'] = document.languageId;
43
}
44
// we know it's not stylesheet due to the validate(false) call above
45
const syntax = getSyntaxFromArgs(args) || 'html';
46
const rootNode = parseDocument(document, true);
47
48
const helper = getEmmetHelper();
49
50
const operationRanges = Array.from(editor.selections).sort((a, b) => a.start.compareTo(b.start)).map(selection => {
51
let rangeToReplace: vscode.Range = selection;
52
// wrap around the node if the selection falls inside its open or close tag
53
{
54
let { start, end } = rangeToReplace;
55
56
const startOffset = document.offsetAt(start);
57
const documentText = document.getText();
58
const startNode = getHtmlFlatNode(documentText, rootNode, startOffset, true);
59
if (startNode && isOffsetInsideOpenOrCloseTag(startNode, startOffset)) {
60
start = document.positionAt(startNode.start);
61
const nodeEndPosition = document.positionAt(startNode.end);
62
end = nodeEndPosition.isAfter(end) ? nodeEndPosition : end;
63
}
64
65
const endOffset = document.offsetAt(end);
66
const endNode = getHtmlFlatNode(documentText, rootNode, endOffset, true);
67
if (endNode && isOffsetInsideOpenOrCloseTag(endNode, endOffset)) {
68
const nodeStartPosition = document.positionAt(endNode.start);
69
start = nodeStartPosition.isBefore(start) ? nodeStartPosition : start;
70
const nodeEndPosition = document.positionAt(endNode.end);
71
end = nodeEndPosition.isAfter(end) ? nodeEndPosition : end;
72
}
73
74
rangeToReplace = new vscode.Range(start, end);
75
}
76
// in case of multi-line, exclude last empty line from rangeToReplace
77
if (!rangeToReplace.isSingleLine && rangeToReplace.end.character === 0) {
78
const previousLine = rangeToReplace.end.line - 1;
79
rangeToReplace = new vscode.Range(rangeToReplace.start, document.lineAt(previousLine).range.end);
80
}
81
// wrap line the cursor is on
82
if (rangeToReplace.isEmpty) {
83
rangeToReplace = document.lineAt(rangeToReplace.start).range;
84
}
85
86
// ignore whitespace on the first line
87
const firstLineOfRange = document.lineAt(rangeToReplace.start);
88
if (!firstLineOfRange.isEmptyOrWhitespace && firstLineOfRange.firstNonWhitespaceCharacterIndex > rangeToReplace.start.character) {
89
rangeToReplace = rangeToReplace.with(new vscode.Position(rangeToReplace.start.line, firstLineOfRange.firstNonWhitespaceCharacterIndex));
90
}
91
92
return rangeToReplace;
93
}).reduce((mergedRanges, range) => {
94
// Merge overlapping ranges
95
if (mergedRanges.length > 0 && range.intersection(mergedRanges[mergedRanges.length - 1])) {
96
mergedRanges.push(range.union(mergedRanges.pop()!));
97
} else {
98
mergedRanges.push(range);
99
}
100
return mergedRanges;
101
}, [] as vscode.Range[]);
102
103
// Backup orginal selections and update selections
104
// Also helps with https://github.com/microsoft/vscode/issues/113930 by avoiding `editor.linkedEditing`
105
// execution if selection is inside an open or close tag
106
const oldSelections = editor.selections;
107
editor.selections = operationRanges.map(range => new vscode.Selection(range.start, range.end));
108
109
// Fetch general information for the succesive expansions. i.e. the ranges to replace and its contents
110
const rangesToReplace: PreviewRangesWithContent[] = operationRanges.map(rangeToReplace => {
111
let textToWrapInPreview: string[];
112
const textToReplace = document.getText(rangeToReplace);
113
114
// the following assumes all the lines are indented the same way as the first
115
// this assumption helps with applyPreview later
116
const wholeFirstLine = document.lineAt(rangeToReplace.start).text;
117
const otherMatches = wholeFirstLine.match(/^(\s*)/);
118
const baseIndent = otherMatches ? otherMatches[1] : '';
119
textToWrapInPreview = rangeToReplace.isSingleLine ?
120
[textToReplace] :
121
textToReplace.split('\n' + baseIndent).map(x => x.trimEnd());
122
123
// escape $ characters, fixes #52640
124
textToWrapInPreview = textToWrapInPreview.map(e => e.replace(/(\$\d)/g, '\\$1'));
125
126
return {
127
previewRange: rangeToReplace,
128
originalRange: rangeToReplace,
129
originalContent: textToReplace,
130
textToWrapInPreview,
131
baseIndent
132
};
133
});
134
135
const { tabSize, insertSpaces } = editor.options;
136
const indent = insertSpaces ? ' '.repeat(tabSize as number) : '\t';
137
138
function revertPreview(): Thenable<boolean> {
139
return editor.edit(builder => {
140
for (const rangeToReplace of rangesToReplace) {
141
builder.replace(rangeToReplace.previewRange, rangeToReplace.originalContent);
142
rangeToReplace.previewRange = rangeToReplace.originalRange;
143
}
144
}, { undoStopBefore: false, undoStopAfter: false });
145
}
146
147
function applyPreview(expandAbbrList: ExpandAbbreviationInput[]): Thenable<boolean> {
148
let lastOldPreviewRange = new vscode.Range(0, 0, 0, 0);
149
let lastNewPreviewRange = new vscode.Range(0, 0, 0, 0);
150
let totalNewLinesInserted = 0;
151
152
return editor.edit(builder => {
153
// the edits are applied in order top-down
154
for (let i = 0; i < rangesToReplace.length; i++) {
155
const expandedText = expandAbbr(expandAbbrList[i]) || '';
156
if (!expandedText) {
157
// Failed to expand text. We already showed an error inside expandAbbr.
158
break;
159
}
160
161
// get the current preview range, format the new wrapped text, and then replace
162
// the text in the preview range with that new text
163
const oldPreviewRange = rangesToReplace[i].previewRange;
164
const newText = expandedText
165
.replace(/\$\{[\d]*\}/g, '|') // Removing Tabstops
166
.replace(/\$\{[\d]*:([^}]*)\}/g, (_, placeholder) => placeholder) // Replacing Placeholders
167
.replace(/\\\$/g, '$'); // Remove backslashes before $
168
builder.replace(oldPreviewRange, newText);
169
170
// calculate the new preview range to use for future previews
171
// we also have to take into account that the previous expansions could:
172
// - cause new lines to appear
173
// - be on the same line as other expansions
174
const expandedTextLines = newText.split('\n');
175
const oldPreviewLines = oldPreviewRange.end.line - oldPreviewRange.start.line + 1;
176
const newLinesInserted = expandedTextLines.length - oldPreviewLines;
177
178
const newPreviewLineStart = oldPreviewRange.start.line + totalNewLinesInserted;
179
let newPreviewStart = oldPreviewRange.start.character;
180
const newPreviewLineEnd = oldPreviewRange.end.line + totalNewLinesInserted + newLinesInserted;
181
let newPreviewEnd = expandedTextLines[expandedTextLines.length - 1].length;
182
if (i > 0 && newPreviewLineEnd === lastNewPreviewRange.end.line) {
183
// If newPreviewLineEnd is equal to the previous expandedText lineEnd,
184
// set newPreviewStart to the length of the previous expandedText in that line
185
// plus the number of characters between both selections.
186
newPreviewStart = lastNewPreviewRange.end.character + (oldPreviewRange.start.character - lastOldPreviewRange.end.character);
187
newPreviewEnd += newPreviewStart;
188
} else if (i > 0 && newPreviewLineStart === lastNewPreviewRange.end.line) {
189
// Same as above but expandedTextLines.length > 1 so newPreviewEnd keeps its value.
190
newPreviewStart = lastNewPreviewRange.end.character + (oldPreviewRange.start.character - lastOldPreviewRange.end.character);
191
} else if (expandedTextLines.length === 1) {
192
// If the expandedText is single line, add the length of preceeding text as it will not be included in line length.
193
newPreviewEnd += oldPreviewRange.start.character;
194
}
195
196
lastOldPreviewRange = rangesToReplace[i].previewRange;
197
lastNewPreviewRange = new vscode.Range(newPreviewLineStart, newPreviewStart, newPreviewLineEnd, newPreviewEnd);
198
rangesToReplace[i].previewRange = lastNewPreviewRange;
199
totalNewLinesInserted += newLinesInserted;
200
}
201
}, { undoStopBefore: false, undoStopAfter: false });
202
}
203
204
let inPreviewMode = false;
205
async function makeChanges(inputAbbreviation: string | undefined, previewChanges: boolean): Promise<boolean> {
206
const isAbbreviationValid = !!inputAbbreviation && !!inputAbbreviation.trim() && helper.isAbbreviationValid(syntax, inputAbbreviation);
207
const extractedResults = isAbbreviationValid ? helper.extractAbbreviationFromText(inputAbbreviation!, syntax) : undefined;
208
if (!extractedResults) {
209
if (inPreviewMode) {
210
inPreviewMode = false;
211
await revertPreview();
212
}
213
return false;
214
}
215
216
const { abbreviation, filter } = extractedResults;
217
if (abbreviation !== inputAbbreviation) {
218
// Not clear what should we do in this case. Warn the user? How?
219
}
220
221
if (previewChanges) {
222
const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent =>
223
({ syntax, abbreviation, rangeToReplace: rangesAndContent.originalRange, textToWrap: rangesAndContent.textToWrapInPreview, filter, indent, baseIndent: rangesAndContent.baseIndent })
224
);
225
226
inPreviewMode = true;
227
return applyPreview(expandAbbrList);
228
}
229
230
const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent =>
231
({ syntax, abbreviation, rangeToReplace: rangesAndContent.originalRange, textToWrap: rangesAndContent.textToWrapInPreview, filter, indent })
232
);
233
234
if (inPreviewMode) {
235
inPreviewMode = false;
236
await revertPreview();
237
}
238
239
return expandAbbreviationInRange(editor, expandAbbrList, false);
240
}
241
242
let currentValue = '';
243
async function inputChanged(value: string): Promise<string> {
244
if (value !== currentValue) {
245
currentValue = value;
246
await makeChanges(value, true);
247
}
248
return '';
249
}
250
251
const prompt = vscode.l10n.t("Enter Abbreviation");
252
const inputAbbreviation = (args && args['abbreviation'])
253
? (args['abbreviation'] as string)
254
: await vscode.window.showInputBox({ prompt, validateInput: inputChanged });
255
256
const changesWereMade = await makeChanges(inputAbbreviation, false);
257
if (!changesWereMade) {
258
editor.selections = oldSelections;
259
}
260
261
return changesWereMade;
262
}
263
264
export function expandEmmetAbbreviation(args: any): Thenable<boolean | undefined> {
265
if (!validate() || !vscode.window.activeTextEditor) {
266
return fallbackTab();
267
}
268
269
/**
270
* Short circuit the parsing. If previous character is space, do not expand.
271
*/
272
if (vscode.window.activeTextEditor.selections.length === 1 &&
273
vscode.window.activeTextEditor.selection.isEmpty
274
) {
275
const anchor = vscode.window.activeTextEditor.selection.anchor;
276
if (anchor.character === 0) {
277
return fallbackTab();
278
}
279
280
const prevPositionAnchor = anchor.translate(0, -1);
281
const prevText = vscode.window.activeTextEditor.document.getText(new vscode.Range(prevPositionAnchor, anchor));
282
if (prevText === ' ' || prevText === '\t') {
283
return fallbackTab();
284
}
285
}
286
287
args = args || {};
288
if (!args['language']) {
289
args['language'] = vscode.window.activeTextEditor.document.languageId;
290
} else {
291
const excludedLanguages = vscode.workspace.getConfiguration('emmet')['excludeLanguages'] ? vscode.workspace.getConfiguration('emmet')['excludeLanguages'] : [];
292
if (excludedLanguages.includes(vscode.window.activeTextEditor.document.languageId)) {
293
return fallbackTab();
294
}
295
}
296
const syntax = getSyntaxFromArgs(args);
297
if (!syntax) {
298
return fallbackTab();
299
}
300
301
const editor = vscode.window.activeTextEditor;
302
303
// When tabbed on a non empty selection, do not treat it as an emmet abbreviation, and fallback to tab instead
304
if (vscode.workspace.getConfiguration('emmet')['triggerExpansionOnTab'] === true && editor.selections.find(x => !x.isEmpty)) {
305
return fallbackTab();
306
}
307
308
const abbreviationList: ExpandAbbreviationInput[] = [];
309
let firstAbbreviation: string;
310
let allAbbreviationsSame: boolean = true;
311
const helper = getEmmetHelper();
312
313
const getAbbreviation = (document: vscode.TextDocument, selection: vscode.Selection, position: vscode.Position, syntax: string): [vscode.Range | null, string, string | undefined] => {
314
position = document.validatePosition(position);
315
let rangeToReplace: vscode.Range = selection;
316
let abbr = document.getText(rangeToReplace);
317
if (!rangeToReplace.isEmpty) {
318
const extractedResults = helper.extractAbbreviationFromText(abbr, syntax);
319
if (extractedResults) {
320
return [rangeToReplace, extractedResults.abbreviation, extractedResults.filter];
321
}
322
return [null, '', ''];
323
}
324
325
const currentLine = editor.document.lineAt(position.line).text;
326
const textTillPosition = currentLine.substr(0, position.character);
327
328
// Expand cases like <div to <div></div> explicitly
329
// else we will end up with <<div></div>
330
if (syntax === 'html') {
331
const matches = textTillPosition.match(/<(\w+)$/);
332
if (matches) {
333
abbr = matches[1];
334
rangeToReplace = new vscode.Range(position.translate(0, -(abbr.length + 1)), position);
335
return [rangeToReplace, abbr, ''];
336
}
337
}
338
const extractedResults = helper.extractAbbreviation(toLSTextDocument(editor.document), position, { lookAhead: false });
339
if (!extractedResults) {
340
return [null, '', ''];
341
}
342
343
const { abbreviationRange, abbreviation, filter } = extractedResults;
344
return [new vscode.Range(abbreviationRange.start.line, abbreviationRange.start.character, abbreviationRange.end.line, abbreviationRange.end.character), abbreviation, filter];
345
};
346
347
const selectionsInReverseOrder = editor.selections.slice(0);
348
selectionsInReverseOrder.sort((a, b) => {
349
const posA = a.isReversed ? a.anchor : a.active;
350
const posB = b.isReversed ? b.anchor : b.active;
351
return posA.compareTo(posB) * -1;
352
});
353
354
let rootNode: Node | undefined;
355
function getRootNode() {
356
if (rootNode) {
357
return rootNode;
358
}
359
360
const usePartialParsing = vscode.workspace.getConfiguration('emmet')['optimizeStylesheetParsing'] === true;
361
if (editor.selections.length === 1 && isStyleSheet(editor.document.languageId) && usePartialParsing && editor.document.lineCount > 1000) {
362
rootNode = parsePartialStylesheet(editor.document, editor.selection.isReversed ? editor.selection.anchor : editor.selection.active);
363
} else {
364
rootNode = parseDocument(editor.document, true);
365
}
366
367
return rootNode;
368
}
369
370
selectionsInReverseOrder.forEach(selection => {
371
const position = selection.isReversed ? selection.anchor : selection.active;
372
const [rangeToReplace, abbreviation, filter] = getAbbreviation(editor.document, selection, position, syntax);
373
if (!rangeToReplace) {
374
return;
375
}
376
if (!helper.isAbbreviationValid(syntax, abbreviation)) {
377
return;
378
}
379
if (isStyleSheet(syntax) && abbreviation.endsWith(':')) {
380
// Fix for https://github.com/Microsoft/vscode/issues/1623
381
return;
382
}
383
384
const offset = editor.document.offsetAt(position);
385
let currentNode = getFlatNode(getRootNode(), offset, true);
386
let validateLocation = true;
387
let syntaxToUse = syntax;
388
389
if (editor.document.languageId === 'html') {
390
if (isStyleAttribute(currentNode, offset)) {
391
syntaxToUse = 'css';
392
validateLocation = false;
393
} else {
394
const embeddedCssNode = getEmbeddedCssNodeIfAny(editor.document, currentNode, position);
395
if (embeddedCssNode) {
396
currentNode = getFlatNode(embeddedCssNode, offset, true);
397
syntaxToUse = 'css';
398
}
399
}
400
}
401
402
if (validateLocation && !isValidLocationForEmmetAbbreviation(editor.document, getRootNode(), currentNode, syntaxToUse, offset, rangeToReplace)) {
403
return;
404
}
405
406
if (!firstAbbreviation) {
407
firstAbbreviation = abbreviation;
408
} else if (allAbbreviationsSame && firstAbbreviation !== abbreviation) {
409
allAbbreviationsSame = false;
410
}
411
412
abbreviationList.push({ syntax: syntaxToUse, abbreviation, rangeToReplace, filter });
413
});
414
415
return expandAbbreviationInRange(editor, abbreviationList, allAbbreviationsSame).then(success => {
416
return success ? Promise.resolve(undefined) : fallbackTab();
417
});
418
}
419
420
function fallbackTab(): Thenable<boolean | undefined> {
421
if (vscode.workspace.getConfiguration('emmet')['triggerExpansionOnTab'] === true) {
422
return vscode.commands.executeCommand('tab');
423
}
424
return Promise.resolve(true);
425
}
426
/**
427
* Checks if given position is a valid location to expand emmet abbreviation.
428
* Works only on html and css/less/scss syntax
429
* @param document current Text Document
430
* @param rootNode parsed document
431
* @param currentNode current node in the parsed document
432
* @param syntax syntax of the abbreviation
433
* @param position position to validate
434
* @param abbreviationRange The range of the abbreviation for which given position is being validated
435
*/
436
export function isValidLocationForEmmetAbbreviation(document: vscode.TextDocument, rootNode: Node | undefined, currentNode: Node | undefined, syntax: string, offset: number, abbreviationRange: vscode.Range): boolean {
437
if (isStyleSheet(syntax)) {
438
const stylesheet = <Stylesheet>rootNode;
439
if (stylesheet && (stylesheet.comments || []).some(x => offset >= x.start && offset <= x.end)) {
440
return false;
441
}
442
// Continue validation only if the file was parse-able and the currentNode has been found
443
if (!currentNode) {
444
return true;
445
}
446
447
// Get the abbreviation right now
448
// Fixes https://github.com/microsoft/vscode/issues/74505
449
// Stylesheet abbreviations starting with @ should bring up suggestions
450
// even at outer-most level
451
const abbreviation = document.getText(new vscode.Range(abbreviationRange.start.line, abbreviationRange.start.character, abbreviationRange.end.line, abbreviationRange.end.character));
452
if (abbreviation.startsWith('@')) {
453
return true;
454
}
455
456
// Fix for https://github.com/microsoft/vscode/issues/34162
457
// Other than sass, stylus, we can make use of the terminator tokens to validate position
458
if (syntax !== 'sass' && syntax !== 'stylus' && currentNode.type === 'property') {
459
// Fix for upstream issue https://github.com/emmetio/css-parser/issues/3
460
if (currentNode.parent
461
&& currentNode.parent.type !== 'rule'
462
&& currentNode.parent.type !== 'at-rule') {
463
return false;
464
}
465
466
const propertyNode = <Property>currentNode;
467
if (propertyNode.terminatorToken
468
&& propertyNode.separator
469
&& offset >= propertyNode.separatorToken.end
470
&& offset <= propertyNode.terminatorToken.start
471
&& !abbreviation.includes(':')) {
472
return hexColorRegex.test(abbreviation) || abbreviation === '!';
473
}
474
if (!propertyNode.terminatorToken
475
&& propertyNode.separator
476
&& offset >= propertyNode.separatorToken.end
477
&& !abbreviation.includes(':')) {
478
return hexColorRegex.test(abbreviation) || abbreviation === '!';
479
}
480
if (hexColorRegex.test(abbreviation) || abbreviation === '!') {
481
return false;
482
}
483
}
484
485
// If current node is a rule or at-rule, then perform additional checks to ensure
486
// emmet suggestions are not provided in the rule selector
487
if (currentNode.type !== 'rule' && currentNode.type !== 'at-rule') {
488
return true;
489
}
490
491
const currentCssNode = <Rule>currentNode;
492
493
// Position is valid if it occurs after the `{` that marks beginning of rule contents
494
if (offset > currentCssNode.contentStartToken.end) {
495
return true;
496
}
497
498
// Workaround for https://github.com/microsoft/vscode/30188
499
// The line above the rule selector is considered as part of the selector by the css-parser
500
// But we should assume it is a valid location for css properties under the parent rule
501
if (currentCssNode.parent
502
&& (currentCssNode.parent.type === 'rule' || currentCssNode.parent.type === 'at-rule')
503
&& currentCssNode.selectorToken) {
504
const position = document.positionAt(offset);
505
const tokenStartPos = document.positionAt(currentCssNode.selectorToken.start);
506
const tokenEndPos = document.positionAt(currentCssNode.selectorToken.end);
507
if (position.line !== tokenEndPos.line
508
&& tokenStartPos.character === abbreviationRange.start.character
509
&& tokenStartPos.line === abbreviationRange.start.line
510
) {
511
return true;
512
}
513
}
514
515
return false;
516
}
517
518
const startAngle = '<';
519
const endAngle = '>';
520
const escape = '\\';
521
const question = '?';
522
const currentHtmlNode = <HtmlNode>currentNode;
523
let start = 0;
524
525
if (currentHtmlNode) {
526
if (currentHtmlNode.name === 'script') {
527
const typeAttribute = (currentHtmlNode.attributes || []).filter(x => x.name.toString() === 'type')[0];
528
const typeValue = typeAttribute ? typeAttribute.value.toString() : '';
529
530
if (allowedMimeTypesInScriptTag.includes(typeValue)) {
531
return true;
532
}
533
534
const isScriptJavascriptType = !typeValue || typeValue === 'application/javascript' || typeValue === 'text/javascript';
535
if (isScriptJavascriptType) {
536
return !!getSyntaxFromArgs({ language: 'javascript' });
537
}
538
return false;
539
}
540
541
// Fix for https://github.com/microsoft/vscode/issues/28829
542
if (!currentHtmlNode.open || !currentHtmlNode.close ||
543
!(currentHtmlNode.open.end <= offset && offset <= currentHtmlNode.close.start)) {
544
return false;
545
}
546
547
// Fix for https://github.com/microsoft/vscode/issues/35128
548
// Find the position up till where we will backtrack looking for unescaped < or >
549
// to decide if current position is valid for emmet expansion
550
start = currentHtmlNode.open.end;
551
let lastChildBeforePosition = currentHtmlNode.firstChild;
552
while (lastChildBeforePosition) {
553
if (lastChildBeforePosition.end > offset) {
554
break;
555
}
556
start = lastChildBeforePosition.end;
557
lastChildBeforePosition = lastChildBeforePosition.nextSibling;
558
}
559
}
560
const startPos = document.positionAt(start);
561
let textToBackTrack = document.getText(new vscode.Range(startPos.line, startPos.character, abbreviationRange.start.line, abbreviationRange.start.character));
562
563
// Worse case scenario is when cursor is inside a big chunk of text which needs to backtracked
564
// Backtrack only 500 offsets to ensure we dont waste time doing this
565
if (textToBackTrack.length > 500) {
566
textToBackTrack = textToBackTrack.substr(textToBackTrack.length - 500);
567
}
568
569
if (!textToBackTrack.trim()) {
570
return true;
571
}
572
573
let valid = true;
574
let foundSpace = false; // If < is found before finding whitespace, then its valid abbreviation. E.g.: <div|
575
let i = textToBackTrack.length - 1;
576
if (textToBackTrack[i] === startAngle) {
577
return false;
578
}
579
580
while (i >= 0) {
581
const char = textToBackTrack[i];
582
i--;
583
if (!foundSpace && /\s/.test(char)) {
584
foundSpace = true;
585
continue;
586
}
587
if (char === question && textToBackTrack[i] === startAngle) {
588
i--;
589
continue;
590
}
591
// Fix for https://github.com/microsoft/vscode/issues/55411
592
// A space is not a valid character right after < in a tag name.
593
if (/\s/.test(char) && textToBackTrack[i] === startAngle) {
594
i--;
595
continue;
596
}
597
if (char !== startAngle && char !== endAngle) {
598
continue;
599
}
600
if (i >= 0 && textToBackTrack[i] === escape) {
601
i--;
602
continue;
603
}
604
if (char === endAngle) {
605
if (i >= 0 && textToBackTrack[i] === '=') {
606
continue; // False alarm of cases like =>
607
} else {
608
break;
609
}
610
}
611
if (char === startAngle) {
612
valid = !foundSpace;
613
break;
614
}
615
}
616
617
return valid;
618
}
619
620
/**
621
* Expands abbreviations as detailed in expandAbbrList in the editor
622
*
623
* @returns false if no snippet can be inserted.
624
*/
625
async function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrList: ExpandAbbreviationInput[], insertSameSnippet: boolean): Promise<boolean> {
626
if (!expandAbbrList || expandAbbrList.length === 0) {
627
return false;
628
}
629
630
// Snippet to replace at multiple cursors are not the same
631
// `editor.insertSnippet` will have to be called for each instance separately
632
// We will not be able to maintain multiple cursors after snippet insertion
633
let insertedSnippetsCount = 0;
634
if (!insertSameSnippet) {
635
expandAbbrList.sort((a: ExpandAbbreviationInput, b: ExpandAbbreviationInput) => { return b.rangeToReplace.start.compareTo(a.rangeToReplace.start); });
636
for (const expandAbbrInput of expandAbbrList) {
637
const expandedText = expandAbbr(expandAbbrInput);
638
if (expandedText) {
639
await editor.insertSnippet(new vscode.SnippetString(expandedText), expandAbbrInput.rangeToReplace, { undoStopBefore: false, undoStopAfter: false });
640
insertedSnippetsCount++;
641
}
642
}
643
return insertedSnippetsCount > 0;
644
}
645
646
// Snippet to replace at all cursors are the same
647
// We can pass all ranges to `editor.insertSnippet` in a single call so that
648
// all cursors are maintained after snippet insertion
649
const anyExpandAbbrInput = expandAbbrList[0];
650
const expandedText = expandAbbr(anyExpandAbbrInput);
651
const allRanges = expandAbbrList.map(value => value.rangeToReplace);
652
if (expandedText) {
653
return editor.insertSnippet(new vscode.SnippetString(expandedText), allRanges);
654
}
655
return false;
656
}
657
658
/**
659
* Expands abbreviation as detailed in given input.
660
*/
661
function expandAbbr(input: ExpandAbbreviationInput): string | undefined {
662
const helper = getEmmetHelper();
663
const expandOptions = helper.getExpandOptions(input.syntax, getEmmetConfiguration(input.syntax), input.filter);
664
665
if (input.textToWrap) {
666
// escape ${ sections, fixes #122231
667
input.textToWrap = input.textToWrap.map(e => e.replace(/\$\{/g, '\\\$\{'));
668
if (input.filter && input.filter.includes('t')) {
669
input.textToWrap = input.textToWrap.map(line => {
670
return line.replace(trimRegex, '').trim();
671
});
672
}
673
expandOptions['text'] = input.textToWrap;
674
675
if (expandOptions.options) {
676
// Below fixes https://github.com/microsoft/vscode/issues/29898
677
// With this, Emmet formats inline elements as block elements
678
// ensuring the wrapped multi line text does not get merged to a single line
679
if (!input.rangeToReplace.isSingleLine) {
680
expandOptions.options['output.inlineBreak'] = 1;
681
}
682
683
if (input.indent) {
684
expandOptions.options['output.indent'] = input.indent;
685
}
686
if (input.baseIndent) {
687
expandOptions.options['output.baseIndent'] = input.baseIndent;
688
}
689
}
690
}
691
692
let expandedText: string | undefined;
693
try {
694
expandedText = helper.expandAbbreviation(input.abbreviation, expandOptions);
695
} catch (e) {
696
void vscode.window.showErrorMessage('Failed to expand abbreviation');
697
}
698
699
return expandedText;
700
}
701
702
export function getSyntaxFromArgs(args: { [x: string]: string }): string | undefined {
703
const mappedModes = getMappingForIncludedLanguages();
704
const language: string = args['language'];
705
const parentMode: string = args['parentMode'];
706
const excludedLanguages = vscode.workspace.getConfiguration('emmet')['excludeLanguages'] ? vscode.workspace.getConfiguration('emmet')['excludeLanguages'] : [];
707
if (excludedLanguages.includes(language)) {
708
return;
709
}
710
711
let syntax = getEmmetMode(mappedModes[language] ?? language, mappedModes, excludedLanguages);
712
if (!syntax) {
713
syntax = getEmmetMode(mappedModes[parentMode] ?? parentMode, mappedModes, excludedLanguages);
714
}
715
716
return syntax;
717
}
718
719