Upgrade Emmet removetag perf + behaviour, fixes #104173

This commit is contained in:
Raymond Zhao 2020-12-17 11:56:53 -08:00 committed by GitHub
parent bcef72ddd0
commit dadb18c39e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 95 additions and 37 deletions

View file

@ -4,8 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { parseDocument, validate, getHtmlNode } from './util';
import { HtmlNode } from 'EmmetNode';
import { validate, getHtmlNodeLS, toLSTextDocument, offsetRangeToVsRange } from './util';
export function removeTag() {
if (!validate(false) || !vscode.window.activeTextEditor) {
@ -13,54 +12,113 @@ export function removeTag() {
}
const editor = vscode.window.activeTextEditor;
let rootNode = <HtmlNode>parseDocument(editor.document);
if (!rootNode) {
return;
}
let indentInSpaces = '';
const tabSize: number = editor.options.tabSize ? +editor.options.tabSize : 0;
for (let i = 0; i < tabSize || 0; i++) {
indentInSpaces += ' ';
}
let rangesToRemove: vscode.Range[] = [];
editor.selections.reverse().forEach(selection => {
rangesToRemove = rangesToRemove.concat(getRangeToRemove(editor, rootNode, selection, indentInSpaces));
});
const tabSize: number = +editor.options.tabSize!;
let finalRangesToRemove = editor.selections.reverse()
.reduce<vscode.Range[]>((prev, selection) =>
prev.concat(getRangesToRemove(editor.document, selection, tabSize)), []);
return editor.edit(editBuilder => {
rangesToRemove.forEach(range => {
finalRangesToRemove.forEach(range => {
editBuilder.replace(range, '');
});
});
}
function getRangeToRemove(editor: vscode.TextEditor, rootNode: HtmlNode, selection: vscode.Selection, indentInSpaces: string): vscode.Range[] {
let nodeToUpdate = getHtmlNode(editor.document, rootNode, selection.start, true);
/**
* Calculates the ranges to remove, along with what to replace those ranges with.
* It finds the node to remove based on the selection's start position
* and then removes that node, reindenting the content in between.
* Assumption: The document indents consist of only tabs or only spaces.
*/
function getRangesToRemove(document: vscode.TextDocument, selection: vscode.Selection, tabSize: number): vscode.Range[] {
const lsDocument = toLSTextDocument(document);
const nodeToUpdate = getHtmlNodeLS(lsDocument, selection.start, true);
if (!nodeToUpdate) {
return [];
}
let openRange = new vscode.Range(nodeToUpdate.open.start, nodeToUpdate.open.end);
let closeRange: vscode.Range | null = null;
if (nodeToUpdate.close) {
closeRange = new vscode.Range(nodeToUpdate.close.start, nodeToUpdate.close.end);
const openTagRange = offsetRangeToVsRange(lsDocument, nodeToUpdate.start, nodeToUpdate.startTagEnd ?? nodeToUpdate.end);
let closeTagRange: vscode.Range | undefined;
if (nodeToUpdate.endTagStart !== undefined) {
closeTagRange = offsetRangeToVsRange(lsDocument, nodeToUpdate.endTagStart, nodeToUpdate.end);
}
let ranges = [openRange];
if (closeRange) {
for (let i = openRange.start.line + 1; i <= closeRange.start.line; i++) {
let lineContent = editor.document.lineAt(i).text;
if (lineContent.startsWith('\t')) {
ranges.push(new vscode.Range(i, 0, i, 1));
} else if (lineContent.startsWith(indentInSpaces)) {
ranges.push(new vscode.Range(i, 0, i, indentInSpaces.length));
}
let rangesToRemove = [openTagRange];
if (closeTagRange) {
const indentAmountToRemove = calculateIndentAmountToRemove(document, openTagRange, closeTagRange, tabSize);
for (let i = openTagRange.start.line + 1; i < closeTagRange.start.line; i++) {
rangesToRemove.push(new vscode.Range(i, 0, i, indentAmountToRemove));
}
ranges.push(closeRange);
rangesToRemove.push(closeTagRange);
}
return ranges;
return rangesToRemove;
}
type IndentInfo = {
indentAmount: number,
tabsOnly: boolean
};
/**
* Calculates the amount of indent to remove for getRangesToRemove.
*/
function calculateIndentAmountToRemove(document: vscode.TextDocument, openRange: vscode.Range, closeRange: vscode.Range, tabSize: number): number {
const startLine = openRange.start.line;
const endLine = closeRange.start.line;
const startLineIndent = calculateLineIndentInSpaces(document.lineAt(startLine).text, tabSize);
const endLineIndent = calculateLineIndentInSpaces(document.lineAt(endLine).text, tabSize);
let contentIndent: IndentInfo | undefined;
for (let i = startLine + 1; i <= endLine - 1; i++) {
const lineContent = document.lineAt(i).text;
const indent = calculateLineIndentInSpaces(lineContent, tabSize);
contentIndent = !contentIndent ? indent :
{
indentAmount: Math.min(contentIndent.indentAmount, indent.indentAmount),
tabsOnly: contentIndent.tabsOnly && indent.tabsOnly
};
}
let indentAmountSpaces = 0;
let tabsOnly = startLineIndent.tabsOnly && endLineIndent.tabsOnly;
if (contentIndent) {
if (contentIndent.indentAmount < startLineIndent.indentAmount
|| contentIndent.indentAmount < endLineIndent.indentAmount) {
indentAmountSpaces = 0;
}
else {
indentAmountSpaces = Math.min(
contentIndent.indentAmount - startLineIndent.indentAmount,
contentIndent.indentAmount - endLineIndent.indentAmount
);
}
tabsOnly = tabsOnly && contentIndent.tabsOnly;
}
return tabsOnly ? Math.trunc(indentAmountSpaces / tabSize) : indentAmountSpaces;
}
function calculateLineIndentInSpaces(line: string, tabSize: number): IndentInfo {
const whiteSpaceMatch = line.match(/^\s+/);
const whiteSpaceContent = whiteSpaceMatch ? whiteSpaceMatch[0] : '';
if (!whiteSpaceContent) {
return { indentAmount: 0, tabsOnly: true };
}
let numSpaces = 0;
let numTabs = 0;
let tabsOnly = true;
for (const c of whiteSpaceContent) {
if (c === '\t') {
numTabs++;
}
else {
numSpaces++;
tabsOnly = false;
}
}
return { indentAmount: numTabs * tabSize + numSpaces, tabsOnly };
}

View file

@ -175,7 +175,7 @@ suite('Tests for Emmet actions on html tags', () => {
<li><span>Hello</span></li>
<li><span>There</span></li>
<div><li><span>Bye</span></li></div>
\t
\t\t
<span/>
</script>
`;