Consolidate wrapWithAbbreviation and wrapIndividualLinesWithAbbreviation in one command (#116603)

fixes #109191, fixes #116738
This commit is contained in:
Jean Pierre 2021-02-19 14:33:47 -05:00 committed by GitHub
parent e66f74e0c2
commit 5d2c9bf299
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 179 additions and 223 deletions

View file

@ -238,11 +238,6 @@
} }
}, },
"commands": [ "commands": [
{
"command": "editor.emmet.action.wrapIndividualLinesWithAbbreviation",
"title": "%command.wrapIndividualLinesWithAbbreviation%",
"category": "Emmet"
},
{ {
"command": "editor.emmet.action.wrapWithAbbreviation", "command": "editor.emmet.action.wrapWithAbbreviation",
"title": "%command.wrapWithAbbreviation%", "title": "%command.wrapWithAbbreviation%",
@ -361,9 +356,6 @@
], ],
"menus": { "menus": {
"commandPalette": [ "commandPalette": [
{
"command": "editor.emmet.action.wrapIndividualLinesWithAbbreviation"
},
{ {
"command": "editor.emmet.action.wrapWithAbbreviation" "command": "editor.emmet.action.wrapWithAbbreviation"
}, },

View file

@ -1,7 +1,6 @@
{ {
"description": "Emmet support for VS Code", "description": "Emmet support for VS Code",
"command.wrapWithAbbreviation": "Wrap with Abbreviation", "command.wrapWithAbbreviation": "Wrap with Abbreviation",
"command.wrapIndividualLinesWithAbbreviation": "Wrap Individual Lines with Abbreviation",
"command.removeTag": "Remove Tag", "command.removeTag": "Remove Tag",
"command.updateTag": "Update Tag", "command.updateTag": "Update Tag",
"command.matchTag": "Go to Matching Pair", "command.matchTag": "Go to Matching Pair",

View file

@ -6,19 +6,12 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import { Node, HtmlNode, Rule, Property, Stylesheet } from 'EmmetFlatNode'; import { Node, HtmlNode, Rule, Property, Stylesheet } from 'EmmetFlatNode';
import { getEmmetHelper, getFlatNode, getMappingForIncludedLanguages, validate, getEmmetConfiguration, isStyleSheet, getEmmetMode, parsePartialStylesheet, isStyleAttribute, getEmbeddedCssNodeIfAny, allowedMimeTypesInScriptTag, toLSTextDocument } from './util'; import { getEmmetHelper, getFlatNode, getMappingForIncludedLanguages, validate, getEmmetConfiguration, isStyleSheet, getEmmetMode, parsePartialStylesheet, isStyleAttribute, getEmbeddedCssNodeIfAny, allowedMimeTypesInScriptTag, toLSTextDocument, isOffsetInsideOpenOrCloseTag } from './util';
import { getRootNode as parseDocument } from './parseDocument'; import { getRootNode as parseDocument } from './parseDocument';
import { MarkupAbbreviation } from 'emmet';
// import { AbbreviationNode } from '@emmetio/abbreviation';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
const trimRegex = /[\u00a0]*[\d#\-\*\u2022]+\.?/; const trimRegex = /[\u00a0]*[\d#\-\*\u2022]+\.?/;
const hexColorRegex = /^#[\da-fA-F]{0,6}$/; const hexColorRegex = /^#[\da-fA-F]{0,6}$/;
// const inlineElements = ['a', 'abbr', 'acronym', 'applet', 'b', 'basefont', 'bdo',
// 'big', 'br', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i',
// 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'map', 'object', 'q',
// 's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup',
// 'textarea', 'tt', 'u', 'var'];
interface ExpandAbbreviationInput { interface ExpandAbbreviationInput {
syntax: string; syntax: string;
@ -26,6 +19,8 @@ interface ExpandAbbreviationInput {
rangeToReplace: vscode.Range; rangeToReplace: vscode.Range;
textToWrap?: string[]; textToWrap?: string[];
filter?: string; filter?: string;
indent?: string;
baseIndent?: string;
} }
interface PreviewRangesWithContent { interface PreviewRangesWithContent {
@ -33,22 +28,15 @@ interface PreviewRangesWithContent {
originalRange: vscode.Range; originalRange: vscode.Range;
originalContent: string; originalContent: string;
textToWrapInPreview: string[]; textToWrapInPreview: string[];
baseIndent: string;
} }
export function wrapWithAbbreviation(args: any) { export async function wrapWithAbbreviation(args: any): Promise<boolean> {
return doWrapping(true, args); if (!validate(false)) {
} return false;
export function wrapIndividualLinesWithAbbreviation(args: any) {
return doWrapping(true, args);
}
function doWrapping(_: boolean, args: any) {
if (!validate(false) || !vscode.window.activeTextEditor) {
return;
} }
const editor = vscode.window.activeTextEditor; const editor = vscode.window.activeTextEditor!;
const document = editor.document; const document = editor.document;
args = args || {}; args = args || {};
@ -59,44 +47,68 @@ function doWrapping(_: boolean, args: any) {
const syntax = getSyntaxFromArgs(args) || 'html'; const syntax = getSyntaxFromArgs(args) || 'html';
const rootNode = parseDocument(document, true); const rootNode = parseDocument(document, true);
let inPreview = false;
let currentValue = '';
const helper = getEmmetHelper(); const helper = getEmmetHelper();
// Fetch general information for the succesive expansions. i.e. the ranges to replace and its contents const operationRanges = editor.selections.sort((a, b) => a.start.compareTo(b.start)).map(selection => {
const rangesToReplace: PreviewRangesWithContent[] = editor.selections.sort((a: vscode.Selection, b: vscode.Selection) => { return a.start.compareTo(b.start); }).map(selection => { let rangeToReplace: vscode.Range = selection;
let rangeToReplace: vscode.Range = selection.isReversed ? new vscode.Range(selection.active, selection.anchor) : selection; // wrap around the node if the selection falls inside its open or close tag
if (!rangeToReplace.isSingleLine && rangeToReplace.end.character === 0) { {
// in case of multi-line, exclude last empty line from rangeToReplace let { start, end } = rangeToReplace;
const previousLine = rangeToReplace.end.line - 1;
const lastChar = document.lineAt(previousLine).text.length; const startOffset = document.offsetAt(start);
rangeToReplace = new vscode.Range(rangeToReplace.start, new vscode.Position(previousLine, lastChar)); const startNode = getFlatNode(rootNode, startOffset, true);
} else if (rangeToReplace.isEmpty) { if (startNode && isOffsetInsideOpenOrCloseTag(startNode, startOffset)) {
const { active } = selection; start = document.positionAt(startNode.start);
const activeOffset = document.offsetAt(active); const nodeEndPosition = document.positionAt(startNode.end);
const currentNode = getFlatNode(rootNode, activeOffset, true); end = nodeEndPosition.isAfter(end) ? nodeEndPosition : end;
if (currentNode) {
const currentNodeStart = document.positionAt(currentNode.start);
const currentNodeEnd = document.positionAt(currentNode.end);
if (currentNodeStart.line === active.line || currentNodeEnd.line === active.line) {
// wrap around entire node
rangeToReplace = new vscode.Range(currentNodeStart, currentNodeEnd);
}
else {
// wrap line that cursor is on
rangeToReplace = new vscode.Range(rangeToReplace.start.line, 0, rangeToReplace.start.line, document.lineAt(rangeToReplace.start.line).text.length);
}
} else {
// wrap line that cursor is on
rangeToReplace = new vscode.Range(rangeToReplace.start.line, 0, rangeToReplace.start.line, document.lineAt(rangeToReplace.start.line).text.length);
} }
const endOffset = document.offsetAt(end);
const endNode = getFlatNode(rootNode, endOffset, true);
if (endNode && isOffsetInsideOpenOrCloseTag(endNode, endOffset)) {
const nodeStartPosition = document.positionAt(endNode.start);
start = nodeStartPosition.isBefore(start) ? nodeStartPosition : start;
const nodeEndPosition = document.positionAt(endNode.end);
end = nodeEndPosition.isAfter(end) ? nodeEndPosition : end;
}
rangeToReplace = new vscode.Range(start, end);
}
// in case of multi-line, exclude last empty line from rangeToReplace
if (!rangeToReplace.isSingleLine && rangeToReplace.end.character === 0) {
const previousLine = rangeToReplace.end.line - 1;
rangeToReplace = new vscode.Range(rangeToReplace.start, document.lineAt(previousLine).range.end);
}
// wrap line the cursor is on
if (rangeToReplace.isEmpty) {
rangeToReplace = document.lineAt(rangeToReplace.start).range;
} }
const firstLineOfSelection = document.lineAt(rangeToReplace.start).text.substr(rangeToReplace.start.character); // ignore whitespace on the first line
const matches = firstLineOfSelection.match(/^(\s*)/); const firstLineOfRange = document.lineAt(rangeToReplace.start);
const extraWhitespaceSelected = matches ? matches[1].length : 0; if (!firstLineOfRange.isEmptyOrWhitespace && firstLineOfRange.firstNonWhitespaceCharacterIndex > rangeToReplace.start.character) {
rangeToReplace = new vscode.Range(rangeToReplace.start.line, rangeToReplace.start.character + extraWhitespaceSelected, rangeToReplace.end.line, rangeToReplace.end.character); rangeToReplace = rangeToReplace.with(new vscode.Position(rangeToReplace.start.line, firstLineOfRange.firstNonWhitespaceCharacterIndex));
}
return rangeToReplace;
}).reduce((mergedRanges, range) => {
// Merge overlapping ranges
if (mergedRanges.length > 0 && range.intersection(mergedRanges[mergedRanges.length - 1])) {
mergedRanges.push(range.union(mergedRanges.pop()!));
} else {
mergedRanges.push(range);
}
return mergedRanges;
}, [] as vscode.Range[]);
// Backup orginal selections and update selections
// Also helps with https://github.com/microsoft/vscode/issues/113930 by avoiding `editor.linkedEditing`
// execution if selection is inside an open or close tag
const oldSelections = editor.selections;
editor.selections = operationRanges.map(range => new vscode.Selection(range.start, range.end));
// Fetch general information for the succesive expansions. i.e. the ranges to replace and its contents
const rangesToReplace: PreviewRangesWithContent[] = operationRanges.map(rangeToReplace => {
let textToWrapInPreview: string[]; let textToWrapInPreview: string[];
const textToReplace = document.getText(rangeToReplace); const textToReplace = document.getText(rangeToReplace);
@ -104,10 +116,10 @@ function doWrapping(_: boolean, args: any) {
// this assumption helps with applyPreview later // this assumption helps with applyPreview later
const wholeFirstLine = document.lineAt(rangeToReplace.start).text; const wholeFirstLine = document.lineAt(rangeToReplace.start).text;
const otherMatches = wholeFirstLine.match(/^(\s*)/); const otherMatches = wholeFirstLine.match(/^(\s*)/);
const precedingWhitespace = otherMatches ? otherMatches[1] : ''; const baseIndent = otherMatches ? otherMatches[1] : '';
textToWrapInPreview = rangeToReplace.isSingleLine ? textToWrapInPreview = rangeToReplace.isSingleLine ?
[textToReplace] : [textToReplace] :
textToReplace.split('\n' + precedingWhitespace).map(x => x.trimEnd()); textToReplace.split('\n' + baseIndent).map(x => x.trimEnd());
// escape $ characters, fixes #52640 // escape $ characters, fixes #52640
textToWrapInPreview = textToWrapInPreview.map(e => e.replace(/(\$\d)/g, '\\$1')); textToWrapInPreview = textToWrapInPreview.map(e => e.replace(/(\$\d)/g, '\\$1'));
@ -116,31 +128,13 @@ function doWrapping(_: boolean, args: any) {
previewRange: rangeToReplace, previewRange: rangeToReplace,
originalRange: rangeToReplace, originalRange: rangeToReplace,
originalContent: textToReplace, originalContent: textToReplace,
textToWrapInPreview textToWrapInPreview,
baseIndent
}; };
}); });
// if a selection falls on a node, it could interfere with linked editing, const { tabSize, insertSpaces } = editor.options;
// so back up the selections, and change selections to wrap around the node const indent = insertSpaces ? ' '.repeat(tabSize as number) : '\t';
const oldSelections = editor.selections;
const newSelections: vscode.Selection[] = [];
editor.selections.forEach(selection => {
let { start, end } = selection;
const startOffset = document.offsetAt(start);
const startNode = <HtmlNode>getFlatNode(rootNode, startOffset, true);
const endOffset = document.offsetAt(end);
const endNode = <HtmlNode>getFlatNode(rootNode, endOffset, true);
if (startNode) {
start = document.positionAt(startNode.start);
}
if (endNode) {
end = document.positionAt(endNode.end);
}
// don't need to preserve active/anchor order since the selection changes
// after wrapping anyway
newSelections.push(new vscode.Selection(start, end));
});
editor.selections = newSelections;
function revertPreview(): Thenable<boolean> { function revertPreview(): Thenable<boolean> {
return editor.edit(builder => { return editor.edit(builder => {
@ -168,16 +162,10 @@ function doWrapping(_: boolean, args: any) {
// get the current preview range, format the new wrapped text, and then replace // get the current preview range, format the new wrapped text, and then replace
// the text in the preview range with that new text // the text in the preview range with that new text
const oldPreviewRange = rangesToReplace[i].previewRange; const oldPreviewRange = rangesToReplace[i].previewRange;
const preceedingText = editor.document.getText(new vscode.Range(oldPreviewRange.start.line, 0, oldPreviewRange.start.line, oldPreviewRange.start.character)); const newText = expandedText
const indentPrefix = (preceedingText.match(/^(\s*)/) || ['', ''])[1]; .replace(/\$\{[\d]*\}/g, '|') // Removing Tabstops
.replace(/\$\{[\d]*:([^}]*)\}/g, (_, placeholder) => placeholder) // Replacing Placeholders
let newText = expandedText; .replace(/\\\$/g, '$'); // Remove backslashes before $
newText = newText.replace(/\n/g, '\n' + indentPrefix); // Adding indentation on each line of expanded text
newText = newText.replace(/\$\{[\d]*\}/g, '|'); // Removing Tabstops
newText = newText.replace(/\$\{[\d]*(:[^}]*)?\}/g, (match) => { // Replacing Placeholders
return match.replace(/^\$\{[\d]*:/, '').replace('}', '');
});
newText = newText.replace(/\\\$/g, '$'); // Remove backslashes before $
builder.replace(oldPreviewRange, newText); builder.replace(oldPreviewRange, newText);
// calculate the new preview range to use for future previews // calculate the new preview range to use for future previews
@ -198,12 +186,10 @@ function doWrapping(_: boolean, args: any) {
// plus the number of characters between both selections. // plus the number of characters between both selections.
newPreviewStart = lastNewPreviewRange.end.character + (oldPreviewRange.start.character - lastOldPreviewRange.end.character); newPreviewStart = lastNewPreviewRange.end.character + (oldPreviewRange.start.character - lastOldPreviewRange.end.character);
newPreviewEnd += newPreviewStart; newPreviewEnd += newPreviewStart;
} } else if (i > 0 && newPreviewLineStart === lastNewPreviewRange.end.line) {
else if (i > 0 && newPreviewLineStart === lastNewPreviewRange.end.line) {
// Same as above but expandedTextLines.length > 1 so newPreviewEnd keeps its value. // Same as above but expandedTextLines.length > 1 so newPreviewEnd keeps its value.
newPreviewStart = lastNewPreviewRange.end.character + (oldPreviewRange.start.character - lastOldPreviewRange.end.character); newPreviewStart = lastNewPreviewRange.end.character + (oldPreviewRange.start.character - lastOldPreviewRange.end.character);
} } else if (expandedTextLines.length === 1) {
else if (expandedTextLines.length === 1) {
// If the expandedText is single line, add the length of preceeding text as it will not be included in line length. // If the expandedText is single line, add the length of preceeding text as it will not be included in line length.
newPreviewEnd += oldPreviewRange.start.character; newPreviewEnd += oldPreviewRange.start.character;
} }
@ -216,66 +202,64 @@ function doWrapping(_: boolean, args: any) {
}, { undoStopBefore: false, undoStopAfter: false }); }, { undoStopBefore: false, undoStopAfter: false });
} }
function makeChanges(inputAbbreviation: string | undefined, definitive: boolean): Thenable<boolean> { let inPreviewMode = false;
if (!inputAbbreviation || !inputAbbreviation.trim() || !helper.isAbbreviationValid(syntax, inputAbbreviation)) { async function makeChanges(inputAbbreviation: string | undefined, previewChanges: boolean): Promise<boolean> {
return inPreview ? revertPreview().then(() => { return false; }) : Promise.resolve(inPreview); const isAbbreviationValid = !!inputAbbreviation && !!inputAbbreviation.trim() && helper.isAbbreviationValid(syntax, inputAbbreviation);
} const extractedResults = isAbbreviationValid ? helper.extractAbbreviationFromText(inputAbbreviation!) : undefined;
const extractedResults = helper.extractAbbreviationFromText(inputAbbreviation);
if (!extractedResults) { if (!extractedResults) {
return Promise.resolve(inPreview); if (inPreviewMode) {
} else if (extractedResults.abbreviation !== inputAbbreviation) { inPreviewMode = false;
// Not clear what should we do in this case. Warn the user? How? await revertPreview();
}
return false;
} }
const { abbreviation, filter } = extractedResults; const { abbreviation, filter } = extractedResults;
if (definitive) { if (abbreviation !== inputAbbreviation) {
const revertPromise = inPreview ? revertPreview() : Promise.resolve(true); // Not clear what should we do in this case. Warn the user? How?
return revertPromise.then(() => {
const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent => {
const rangeToReplace = rangesAndContent.originalRange;
let textToWrap: string[];
// if (individualLines) {
textToWrap = rangesAndContent.textToWrapInPreview;
// } else {
// // use the p tag as a dummy element to get Emmet to wrap the expression properly
// textToWrap = rangeToReplace.isSingleLine ?
// ['$TM_SELECTED_TEXT'] : ['<p>$TM_SELECTED_TEXT</p>'];
// }
return { syntax: syntax || '', abbreviation, rangeToReplace, textToWrap, filter };
});
return expandAbbreviationInRange(editor, expandAbbrList, false).then(() => { return true; });
});
} }
const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent => { if (previewChanges) {
return { syntax: syntax || '', abbreviation, rangeToReplace: rangesAndContent.originalRange, textToWrap: rangesAndContent.textToWrapInPreview, filter }; const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent =>
}); ({ syntax, abbreviation, rangeToReplace: rangesAndContent.originalRange, textToWrap: rangesAndContent.textToWrapInPreview, filter, indent, baseIndent: rangesAndContent.baseIndent })
);
return applyPreview(expandAbbrList); inPreviewMode = true;
return applyPreview(expandAbbrList);
}
const expandAbbrList: ExpandAbbreviationInput[] = rangesToReplace.map(rangesAndContent =>
({ syntax, abbreviation, rangeToReplace: rangesAndContent.originalRange, textToWrap: rangesAndContent.textToWrapInPreview, filter, indent })
);
if (inPreviewMode) {
inPreviewMode = false;
await revertPreview();
}
return expandAbbreviationInRange(editor, expandAbbrList, false);
} }
let currentValue = '';
function inputChanged(value: string): string { function inputChanged(value: string): string {
if (value !== currentValue) { if (value !== currentValue) {
currentValue = value; currentValue = value;
makeChanges(value, false).then((out) => { makeChanges(value, true);
inPreview = out;
});
} }
return ''; return '';
} }
const prompt = localize('wrapWithAbbreviationPrompt', "Enter Abbreviation"); const prompt = localize('wrapWithAbbreviationPrompt', "Enter Abbreviation");
const abbreviationPromise: Thenable<string | undefined> = (args && args['abbreviation']) ? const inputAbbreviation = (args && args['abbreviation'])
Promise.resolve(args['abbreviation']) : ? (args['abbreviation'] as string)
vscode.window.showInputBox({ prompt, validateInput: inputChanged }); : await vscode.window.showInputBox({ prompt, validateInput: inputChanged });
return abbreviationPromise.then(async (inputAbbreviation) => {
const changesWereMade = await makeChanges(inputAbbreviation, true); const changesWereMade = await makeChanges(inputAbbreviation, false);
if (!changesWereMade) { if (!changesWereMade) {
editor.selections = oldSelections; editor.selections = oldSelections;
} }
return changesWereMade;
}); return changesWereMade;
} }
export function expandEmmetAbbreviation(args: any): Thenable<boolean | undefined> { export function expandEmmetAbbreviation(args: any): Thenable<boolean | undefined> {
@ -654,36 +638,13 @@ function expandAbbreviationInRange(editor: vscode.TextEditor, expandAbbrList: Ex
// all cursors are maintained after snippet insertion // all cursors are maintained after snippet insertion
const anyExpandAbbrInput = expandAbbrList[0]; const anyExpandAbbrInput = expandAbbrList[0];
const expandedText = expandAbbr(anyExpandAbbrInput); const expandedText = expandAbbr(anyExpandAbbrInput);
const allRanges = expandAbbrList.map(value => { const allRanges = expandAbbrList.map(value => value.rangeToReplace);
return new vscode.Range(value.rangeToReplace.start.line, value.rangeToReplace.start.character, value.rangeToReplace.end.line, value.rangeToReplace.end.character);
});
if (expandedText) { if (expandedText) {
return editor.insertSnippet(new vscode.SnippetString(expandedText), allRanges); return editor.insertSnippet(new vscode.SnippetString(expandedText), allRanges);
} }
return Promise.resolve(false); return Promise.resolve(false);
} }
// /*
// * Walks the tree rooted at root and apply function fn on each node.
// * if fn return false at any node, the further processing of tree is stopped.
// */
// function walk(root: AbbreviationNode, fn: ((node: AbbreviationNode) => boolean)): boolean {
// if (fn(root) === false || walkChildren(root.children, fn) === false) {
// return false;
// }
// return true;
// }
// function walkChildren(children: AbbreviationNode[], fn: ((node: AbbreviationNode) => boolean)): boolean {
// for (let i = 0; i < children.length; i++) {
// const child = children[i];
// if (walk(child, fn) === false) {
// return false;
// }
// }
// return true;
// }
/** /**
* Expands abbreviation as detailed in given input. * Expands abbreviation as detailed in given input.
*/ */
@ -699,54 +660,26 @@ function expandAbbr(input: ExpandAbbreviationInput): string | undefined {
} }
expandOptions['text'] = input.textToWrap; expandOptions['text'] = input.textToWrap;
// Below fixes https://github.com/microsoft/vscode/issues/29898 if (expandOptions.options) {
// With this, Emmet formats inline elements as block elements // Below fixes https://github.com/microsoft/vscode/issues/29898
// ensuring the wrapped multi line text does not get merged to a single line // With this, Emmet formats inline elements as block elements
if (!input.rangeToReplace.isSingleLine && expandOptions.options) { // ensuring the wrapped multi line text does not get merged to a single line
expandOptions.options['output.inlineBreak'] = 1; if (!input.rangeToReplace.isSingleLine) {
expandOptions.options['output.inlineBreak'] = 1;
}
if (input.indent) {
expandOptions.options['output.indent'] = input.indent;
}
if (input.baseIndent) {
expandOptions.options['output.baseIndent'] = input.baseIndent;
}
} }
} }
let expandedText; let expandedText: string | undefined;
try { try {
// Expand the abbreviation expandedText = helper.expandAbbreviation(input.abbreviation, expandOptions);
if (input.textToWrap && !isStyleSheet(input.syntax)) {
const parsedAbbr = <MarkupAbbreviation>helper.parseAbbreviation(input.abbreviation, expandOptions);
// if (input.rangeToReplace.isSingleLine && input.textToWrap.length === 1) {
// // Fetch rightmost element in the parsed abbreviation (i.e the element that will contain the wrapped text).
// const wrappingNodeChildren = parsedAbbr.children;
// let wrappingNode = wrappingNodeChildren[wrappingNodeChildren.length - 1];
// while (wrappingNode && wrappingNode.children && wrappingNode.children.length > 0) {
// wrappingNode = wrappingNode.children[wrappingNode.children.length - 1];
// }
// // If wrapping with a block element, insert newline in the text to wrap.
// // const format = expandOptions.options ? (expandOptions.options['output.format'] ?? true) : true;
// // if (wrappingNode && wrappingNode.name && wrappingNode.value
// // && inlineElements.indexOf(wrappingNode.name) === -1
// // && format) {
// // wrappingNode.value[0] = '\n\t' + wrappingNode.value[0] + '\n';
// // }
// }
// Below fixes https://github.com/microsoft/vscode/issues/78219
// walk the tree and remove tags for empty values
// walkChildren(parsedAbbr.children, node => {
// if (node.name !== null && node.value && node.value[0] === '' && !node.selfClosing && node.children.length === 0) {
// node.name = '';
// node.value[0] = '\n';
// }
// return true;
// });
expandedText = helper.expandAbbreviation(parsedAbbr, expandOptions);
// All $anyword would have been escaped by the emmet helper.
// Remove the escaping backslash from $TM_SELECTED_TEXT so that VS Code Snippet controller can treat it as a variable
expandedText = expandedText.replace('<p>\\$TM_SELECTED_TEXT</p>', '$TM_SELECTED_TEXT');
} else {
expandedText = helper.expandAbbreviation(input.abbreviation, expandOptions);
}
} catch (e) { } catch (e) {
vscode.window.showErrorMessage('Failed to expand abbreviation'); vscode.window.showErrorMessage('Failed to expand abbreviation');
} }

View file

@ -5,7 +5,7 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { DefaultCompletionItemProvider } from './defaultCompletionProvider'; import { DefaultCompletionItemProvider } from './defaultCompletionProvider';
import { expandEmmetAbbreviation, wrapWithAbbreviation, wrapIndividualLinesWithAbbreviation } from './abbreviationActions'; import { expandEmmetAbbreviation, wrapWithAbbreviation } from './abbreviationActions';
import { removeTag } from './removeTag'; import { removeTag } from './removeTag';
import { updateTag } from './updateTag'; import { updateTag } from './updateTag';
import { matchTag } from './matchTag'; import { matchTag } from './matchTag';
@ -28,10 +28,6 @@ export function activateEmmetExtension(context: vscode.ExtensionContext) {
wrapWithAbbreviation(args); wrapWithAbbreviation(args);
})); }));
context.subscriptions.push(vscode.commands.registerCommand('editor.emmet.action.wrapIndividualLinesWithAbbreviation', (args) => {
wrapIndividualLinesWithAbbreviation(args);
}));
context.subscriptions.push(vscode.commands.registerCommand('emmet.expandAbbreviation', (args) => { context.subscriptions.push(vscode.commands.registerCommand('emmet.expandAbbreviation', (args) => {
expandEmmetAbbreviation(args); expandEmmetAbbreviation(args);
})); }));

View file

@ -7,7 +7,7 @@ import 'mocha';
import * as assert from 'assert'; import * as assert from 'assert';
import { Selection, workspace, ConfigurationTarget } from 'vscode'; import { Selection, workspace, ConfigurationTarget } from 'vscode';
import { withRandomFileEditor, closeAllEditors } from './testUtils'; import { withRandomFileEditor, closeAllEditors } from './testUtils';
import { wrapWithAbbreviation, wrapIndividualLinesWithAbbreviation } from '../abbreviationActions'; import { wrapWithAbbreviation } from '../abbreviationActions';
const htmlContentsForBlockWrapTests = ` const htmlContentsForBlockWrapTests = `
<ul class="nav main"> <ul class="nav main">
@ -167,7 +167,7 @@ suite('Tests for Wrap with Abbreviations', () => {
</div> </div>
</a> </a>
`; `;
return testWrapWithAbbreviation([new Selection(1, 1, 1, 1)], 'a[href="https://example.com"]>div', expectedContents, contents); return testWrapWithAbbreviation([new Selection(1, 2, 1, 2)], 'a[href="https://example.com"]>div', expectedContents, contents);
}); });
test('Wrap with abbreviation entire node when cursor is on opening tag', () => { test('Wrap with abbreviation entire node when cursor is on opening tag', () => {
@ -183,7 +183,7 @@ suite('Tests for Wrap with Abbreviations', () => {
</div> </div>
</div> </div>
`; `;
return testWrapWithAbbreviation([new Selection(1, 1, 1, 1)], 'div', expectedContents, contents); return testWrapWithAbbreviation([new Selection(1, 2, 1, 2)], 'div', expectedContents, contents);
}); });
test('Wrap with abbreviation entire node when cursor is on closing tag', () => { test('Wrap with abbreviation entire node when cursor is on closing tag', () => {
@ -199,7 +199,7 @@ suite('Tests for Wrap with Abbreviations', () => {
</div> </div>
</div> </div>
`; `;
return testWrapWithAbbreviation([new Selection(3, 1, 3, 1)], 'div', expectedContents, contents); return testWrapWithAbbreviation([new Selection(3, 2, 3, 2)], 'div', expectedContents, contents);
}); });
test('Wrap with multiline abbreviation doesnt add extra spaces', () => { test('Wrap with multiline abbreviation doesnt add extra spaces', () => {
@ -302,7 +302,7 @@ suite('Tests for Wrap with Abbreviations', () => {
}); });
test('Wrap with abbreviation and format set to false', () => { test('Wrap with abbreviation and format set to false', () => {
return workspace.getConfiguration('emmet').update('syntaxProfiles', { 'html' : { 'format': false } }, ConfigurationTarget.Global).then(() => { return workspace.getConfiguration('emmet').update('syntaxProfiles', { 'html': { 'format': false } }, ConfigurationTarget.Global).then(() => {
return testWrapWithAbbreviation(multiCursors, 'h1', wrapInlineElementExpectedFormatFalse, htmlContentsForBlockWrapTests).then(() => { return testWrapWithAbbreviation(multiCursors, 'h1', wrapInlineElementExpectedFormatFalse, htmlContentsForBlockWrapTests).then(() => {
return workspace.getConfiguration('emmet').update('syntaxProfiles', oldValueForSyntaxProfiles ? oldValueForSyntaxProfiles.globalValue : undefined, ConfigurationTarget.Global); return workspace.getConfiguration('emmet').update('syntaxProfiles', oldValueForSyntaxProfiles ? oldValueForSyntaxProfiles.globalValue : undefined, ConfigurationTarget.Global);
}); });
@ -347,7 +347,7 @@ suite('Tests for Wrap with Abbreviations', () => {
</ul> </ul>
`; `;
return testWrapWithAbbreviation([new Selection(2,2,3,33)], '.hello', wrapMultiLineJsxExpected, htmlContentsForBlockWrapTests, 'jsx'); return testWrapWithAbbreviation([new Selection(2, 2, 3, 33)], '.hello', wrapMultiLineJsxExpected, htmlContentsForBlockWrapTests, 'jsx');
}); });
test('Wrap individual line with abbreviation uses className for jsx files', () => { test('Wrap individual line with abbreviation uses className for jsx files', () => {
@ -362,8 +362,34 @@ suite('Tests for Wrap with Abbreviations', () => {
</ul> </ul>
`; `;
return testWrapIndividualLinesWithAbbreviation([new Selection(2,2,3,33)], '.hello$*', wrapIndividualLinesJsxExpected, htmlContentsForBlockWrapTests, 'jsx'); return testWrapIndividualLinesWithAbbreviation([new Selection(2, 2, 3, 33)], '.hello$*', wrapIndividualLinesJsxExpected, htmlContentsForBlockWrapTests, 'jsx');
}); });
test('Wrap with abbreviation merge overlapping computed ranges', () => {
const contents = `
<div class="nav main">
hello
</div>
`;
const expectedContents = `
<div>
<div class="nav main">
hello
</div>
</div>
`;
return testWrapWithAbbreviation([new Selection(1, 2, 1, 2), new Selection(1, 10, 1, 10)], 'div', expectedContents, contents);
});
test('Wrap with abbreviation ignore invalid abbreviation', () => {
const contents = `
<div class="nav main">
hello
</div>
`;
return testWrapWithAbbreviation([new Selection(1, 2, 1, 2)], 'div]', contents, contents);
});
}); });
@ -386,7 +412,7 @@ function testWrapWithAbbreviation(selections: Selection[], abbreviation: string,
function testWrapIndividualLinesWithAbbreviation(selections: Selection[], abbreviation: string, expectedContents: string, input: string, fileExtension: string = 'html'): Thenable<any> { function testWrapIndividualLinesWithAbbreviation(selections: Selection[], abbreviation: string, expectedContents: string, input: string, fileExtension: string = 'html'): Thenable<any> {
return withRandomFileEditor(input, fileExtension, (editor, _) => { return withRandomFileEditor(input, fileExtension, (editor, _) => {
editor.selections = selections; editor.selections = selections;
const promise = wrapIndividualLinesWithAbbreviation({ abbreviation }); const promise = wrapWithAbbreviation({ abbreviation });
if (!promise) { if (!promise) {
assert.equal(1, 2, 'Wrap individual lines with Abbreviation returned undefined.'); assert.equal(1, 2, 'Wrap individual lines with Abbreviation returned undefined.');
return Promise.resolve(); return Promise.resolve();

View file

@ -374,6 +374,16 @@ export function getHtmlFlatNode(documentText: string, root: FlatNode | undefined
return currentNode; return currentNode;
} }
export function isOffsetInsideOpenOrCloseTag(node: FlatNode, offset: number): boolean {
const htmlNode = node as HtmlFlatNode;
if ((htmlNode.open && offset > htmlNode.open.start && offset < htmlNode.open.end)
|| (htmlNode.close && offset > htmlNode.close.start && offset < htmlNode.close.end)) {
return true;
}
return false;
}
export function offsetRangeToSelection(document: vscode.TextDocument, start: number, end: number): vscode.Selection { export function offsetRangeToSelection(document: vscode.TextDocument, start: number, end: number): vscode.Selection {
const startPos = document.positionAt(start); const startPos = document.positionAt(start);
const endPos = document.positionAt(end); const endPos = document.positionAt(end);