Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cache, update several Emmet commands #112597

Merged
merged 6 commits into from
Dec 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 42 additions & 42 deletions extensions/emmet/src/balance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { HtmlNode } from 'EmmetNode';
import { getHtmlNode, parseDocument, validate } from './util';
import { getHtmlNodeLS, offsetRangeToSelection, toLSTextDocument, validate } from './util';
import { parseMarkupDocument } from './parseMarkupDocument';
import { TextDocument as LSTextDocument } from 'vscode-html-languageservice';

let balanceOutStack: Array<vscode.Selection[]> = [];
let lastOut = false;
let lastBalancedSelections: vscode.Selection[] = [];

export function balanceOut() {
Expand All @@ -24,53 +24,50 @@ function balance(out: boolean) {
return;
}
const editor = vscode.window.activeTextEditor;
let rootNode = <HtmlNode>parseDocument(editor.document);
if (!rootNode) {
const document = toLSTextDocument(editor.document);
const htmlDocument = parseMarkupDocument(document);
if (!htmlDocument) {
return;
}

let getRangeFunction = out ? getRangeToBalanceOut : getRangeToBalanceIn;
const rangeFn = out ? getRangeToBalanceOut : getRangeToBalanceIn;
let newSelections: vscode.Selection[] = [];
editor.selections.forEach(selection => {
let range = getRangeFunction(editor.document, selection, rootNode);
const range = rangeFn(document, selection);
newSelections.push(range);
});

if (areSameSelections(newSelections, editor.selections)) {
return;
}

// check whether we are starting a balance elsewhere
if (areSameSelections(lastBalancedSelections, editor.selections)) {
// we are not starting elsewhere, so use the stack as-is
if (out) {
if (!balanceOutStack.length) {
// make sure we are able to expand outwards
if (!areSameSelections(editor.selections, newSelections)) {
balanceOutStack.push(editor.selections);
}
balanceOutStack.push(newSelections);
} else {
if (lastOut) {
balanceOutStack.pop();
}
newSelections = balanceOutStack.pop() || newSelections;
} else if (balanceOutStack.length) {
newSelections = balanceOutStack.pop()!;
}
} else {
balanceOutStack = out ? [editor.selections, newSelections] : [];
// we are starting elsewhere, so reset the stack
balanceOutStack = out ? [editor.selections] : [];
}

lastOut = out;
lastBalancedSelections = editor.selections = newSelections;
editor.selections = newSelections;
lastBalancedSelections = editor.selections;
}

function getRangeToBalanceOut(document: vscode.TextDocument, selection: vscode.Selection, rootNode: HtmlNode): vscode.Selection {
let nodeToBalance = getHtmlNode(document, rootNode, selection.start, false);
function getRangeToBalanceOut(document: LSTextDocument, selection: vscode.Selection): vscode.Selection {
const nodeToBalance = getHtmlNodeLS(document, selection.start, false);
if (!nodeToBalance) {
return selection;
}
if (!nodeToBalance.close) {
return new vscode.Selection(nodeToBalance.start, nodeToBalance.end);
if (!nodeToBalance.endTagStart || !nodeToBalance.startTagEnd) {
return offsetRangeToSelection(document, nodeToBalance.start, nodeToBalance.end);
}

let innerSelection = new vscode.Selection(nodeToBalance.open.end, nodeToBalance.close.start);
let outerSelection = new vscode.Selection(nodeToBalance.start, nodeToBalance.end);
const innerSelection = offsetRangeToSelection(document, nodeToBalance.startTagEnd, nodeToBalance.endTagStart);
const outerSelection = offsetRangeToSelection(document, nodeToBalance.start, nodeToBalance.end);

if (innerSelection.contains(selection) && !innerSelection.isEqual(selection)) {
return innerSelection;
Expand All @@ -81,34 +78,37 @@ function getRangeToBalanceOut(document: vscode.TextDocument, selection: vscode.S
return selection;
}

function getRangeToBalanceIn(document: vscode.TextDocument, selection: vscode.Selection, rootNode: HtmlNode): vscode.Selection {
let nodeToBalance = getHtmlNode(document, rootNode, selection.start, true);
function getRangeToBalanceIn(document: LSTextDocument, selection: vscode.Selection): vscode.Selection {
const nodeToBalance = getHtmlNodeLS(document, selection.start, true);
if (!nodeToBalance) {
return selection;
}

if (nodeToBalance.close) {
const entireNodeSelected = selection.start.isEqual(nodeToBalance.start) && selection.end.isEqual(nodeToBalance.end);
const startInOpenTag = selection.start.isAfter(nodeToBalance.open.start) && selection.start.isBefore(nodeToBalance.open.end);
const startInCloseTag = selection.start.isAfter(nodeToBalance.close.start) && selection.start.isBefore(nodeToBalance.close.end);
const selectionStart = document.offsetAt(selection.start);
const selectionEnd = document.offsetAt(selection.end);
if (nodeToBalance.endTagStart !== undefined && nodeToBalance.startTagEnd !== undefined) {
const entireNodeSelected = selectionStart === nodeToBalance.start && selectionEnd === nodeToBalance.end;
const startInOpenTag = selectionStart > nodeToBalance.start && selectionStart < nodeToBalance.startTagEnd;
const startInCloseTag = selectionStart > nodeToBalance.endTagStart && selectionStart < nodeToBalance.end;

if (entireNodeSelected || startInOpenTag || startInCloseTag) {
return new vscode.Selection(nodeToBalance.open.end, nodeToBalance.close.start);
return offsetRangeToSelection(document, nodeToBalance.startTagEnd, nodeToBalance.endTagStart);
}
}

if (!nodeToBalance.firstChild) {
if (!nodeToBalance.children.length) {
return selection;
}

if (selection.start.isEqual(nodeToBalance.firstChild.start)
&& selection.end.isEqual(nodeToBalance.firstChild.end)
&& nodeToBalance.firstChild.close) {
return new vscode.Selection(nodeToBalance.firstChild.open.end, nodeToBalance.firstChild.close.start);
const firstChild = nodeToBalance.children[0];
if (selectionStart === firstChild.start
&& selectionEnd === firstChild.end
&& firstChild.endTagStart !== undefined
&& firstChild.startTagEnd !== undefined) {
return offsetRangeToSelection(document, firstChild.startTagEnd, firstChild.endTagStart);
}

return new vscode.Selection(nodeToBalance.firstChild.start, nodeToBalance.firstChild.end);

return offsetRangeToSelection(document, firstChild.start, firstChild.end);
}

function areSameSelections(a: vscode.Selection[], b: vscode.Selection[]): boolean {
Expand All @@ -121,4 +121,4 @@ function areSameSelections(a: vscode.Selection[], b: vscode.Selection[]): boolea
}
}
return true;
}
}
17 changes: 16 additions & 1 deletion extensions/emmet/src/emmetCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ import { fetchEditPoint } from './editPoint';
import { fetchSelectItem } from './selectItem';
import { evaluateMathExpression } from './evaluateMathExpression';
import { incrementDecrement } from './incrementDecrement';
import { LANGUAGE_MODES, getMappingForIncludedLanguages, updateEmmetExtensionsPath, getPathBaseName } from './util';
import { LANGUAGE_MODES, getMappingForIncludedLanguages, updateEmmetExtensionsPath, getPathBaseName, toLSTextDocument, getSyntaxes, getEmmetMode } from './util';
import { reflectCssValue } from './reflectCssValue';
import { addFileToMarkupParseCache, removeFileFromMarkupParseCache } from './parseMarkupDocument';

export function activateEmmetExtension(context: vscode.ExtensionContext) {
registerCompletionProviders(context);
Expand Down Expand Up @@ -145,6 +146,20 @@ export function activateEmmetExtension(context: vscode.ExtensionContext) {
updateEmmetExtensionsPath(true);
}
}));

context.subscriptions.push(vscode.workspace.onDidOpenTextDocument((e) => {
const emmetMode = getEmmetMode(e.languageId, []) ?? '';
if (getSyntaxes().markup.includes(emmetMode)) {
addFileToMarkupParseCache(toLSTextDocument(e));
}
}));

context.subscriptions.push(vscode.workspace.onDidCloseTextDocument((e) => {
const emmetMode = getEmmetMode(e.languageId, []) ?? '';
if (getSyntaxes().markup.includes(emmetMode)) {
removeFileFromMarkupParseCache(toLSTextDocument(e));
}
}));
}

/**
Expand Down
32 changes: 18 additions & 14 deletions extensions/emmet/src/matchTag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,46 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { HtmlNode } from 'EmmetNode';
import { getHtmlNode, parseDocument, validate } from './util';

import { toLSTextDocument, validate, getHtmlNodeLS, offsetRangeToSelection } from './util';
import { TextDocument as LSTextDocument } from 'vscode-html-languageservice';

export function matchTag() {
if (!validate(false) || !vscode.window.activeTextEditor) {
return;
}

const editor = vscode.window.activeTextEditor;
let rootNode: HtmlNode = <HtmlNode>parseDocument(editor.document);
if (!rootNode) { return; }
const document = toLSTextDocument(editor.document);

let updatedSelections: vscode.Selection[] = [];
editor.selections.forEach(selection => {
let updatedSelection = getUpdatedSelections(editor, selection.start, rootNode);
const updatedSelection = getUpdatedSelections(document, selection.start);
if (updatedSelection) {
updatedSelections.push(updatedSelection);
}
});
if (updatedSelections.length > 0) {
if (updatedSelections.length) {
editor.selections = updatedSelections;
editor.revealRange(editor.selections[updatedSelections.length - 1]);
}
}

function getUpdatedSelections(editor: vscode.TextEditor, position: vscode.Position, rootNode: HtmlNode): vscode.Selection | undefined {
let currentNode = getHtmlNode(editor.document, rootNode, position, true);
if (!currentNode) { return; }
function getUpdatedSelections(document: LSTextDocument, position: vscode.Position): vscode.Selection | undefined {
const currentNode = getHtmlNodeLS(document, position, true);
if (!currentNode) {
return;
}

const offset = document.offsetAt(position);

// If no closing tag or cursor is between open and close tag, then no-op
if (!currentNode.close || (position.isAfter(currentNode.open.end) && position.isBefore(currentNode.close.start))) {
if (currentNode.endTagStart === undefined
|| currentNode.startTagEnd === undefined
|| (offset > currentNode.startTagEnd && offset < currentNode.endTagStart)) {
return;
}

// Place cursor inside the close tag if cursor is inside the open tag, else place it inside the open tag
let finalPosition = position.isBeforeOrEqual(currentNode.open.end) ? currentNode.close.start.translate(0, 2) : currentNode.open.start.translate(0, 1);
return new vscode.Selection(finalPosition, finalPosition);
}
const finalOffset = (offset <= currentNode.startTagEnd) ? currentNode.endTagStart + 2 : currentNode.start + 1;
return offsetRangeToSelection(document, finalOffset, finalOffset);
}
43 changes: 43 additions & 0 deletions extensions/emmet/src/parseMarkupDocument.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { HTMLDocument, TextDocument as LSTextDocument } from 'vscode-html-languageservice';
import { getLanguageService } from './util';

type Pair<K, V> = {
key: K;
value: V;
};

// Map(filename, Pair(fileVersion, parsedContent))
const _parseCache = new Map<string, Pair<number, HTMLDocument> | undefined>();

export function parseMarkupDocument(document: LSTextDocument, useCache: boolean = true): HTMLDocument {
const languageService = getLanguageService();
const key = document.uri;
const result = _parseCache.get(key);
const documentVersion = document.version;
if (useCache && result) {
if (documentVersion === result.key) {
return result.value;
}
}

const parsedDocument = languageService.parseHTMLDocument(document);
if (useCache) {
_parseCache.set(key, { key: documentVersion, value: parsedDocument });
}
return parsedDocument;
}

export function addFileToMarkupParseCache(document: LSTextDocument) {
const filename = document.uri;
_parseCache.set(filename, undefined);
}

export function removeFileFromMarkupParseCache(document: LSTextDocument) {
const filename = document.uri;
_parseCache.delete(filename);
}
47 changes: 24 additions & 23 deletions extensions/emmet/src/splitJoinTag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,51 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { HtmlNode } from 'EmmetNode';
import { getHtmlNode, parseDocument, validate, getEmmetMode, getEmmetConfiguration } from './util';
import { validate, getEmmetMode, getEmmetConfiguration, toLSTextDocument, getHtmlNodeLS, offsetRangeToVsRange } from './util';
import { Node as LSNode, TextDocument as LSTextDocument } from 'vscode-html-languageservice';

export function splitJoinTag() {
if (!validate(false) || !vscode.window.activeTextEditor) {
return;
}

const editor = vscode.window.activeTextEditor;
let rootNode = <HtmlNode>parseDocument(editor.document);
if (!rootNode) {
return;
}

const document = toLSTextDocument(editor.document);
return editor.edit(editBuilder => {
editor.selections.reverse().forEach(selection => {
let nodeToUpdate = getHtmlNode(editor.document, rootNode, selection.start, true);
const nodeToUpdate = getHtmlNodeLS(document, selection.start, true);
if (nodeToUpdate) {
let textEdit = getRangesToReplace(editor.document, nodeToUpdate);
editBuilder.replace(textEdit.range, textEdit.newText);
const textEdit = getRangesToReplace(document, nodeToUpdate);
if (textEdit) {
editBuilder.replace(textEdit.range, textEdit.newText);
}
}
});
});
}

function getRangesToReplace(document: vscode.TextDocument, nodeToUpdate: HtmlNode): vscode.TextEdit {
function getRangesToReplace(document: LSTextDocument, nodeToUpdate: LSNode): vscode.TextEdit | undefined {
let rangeToReplace: vscode.Range;
let textToReplaceWith: string;

if (!nodeToUpdate.close) {
if (!nodeToUpdate?.tag) {
return;
}

if (nodeToUpdate.endTagStart === undefined || nodeToUpdate.startTagEnd === undefined) {
// Split Tag
let nodeText = document.getText(new vscode.Range(nodeToUpdate.start, nodeToUpdate.end));
let m = nodeText.match(/(\s*\/)?>$/);
let end = <vscode.Position>nodeToUpdate.end;
let start = m ? end.translate(0, -m[0].length) : end;
const nodeText = document.getText().substring(nodeToUpdate.start, nodeToUpdate.end);
const m = nodeText.match(/(\s*\/)?>$/);
const end = nodeToUpdate.end;
const start = m ? end - m[0].length : end;

rangeToReplace = new vscode.Range(start, end);
textToReplaceWith = `></${nodeToUpdate.name}>`;
rangeToReplace = offsetRangeToVsRange(document, start, end);
textToReplaceWith = `></${nodeToUpdate.tag}>`;
} else {
// Join Tag
let start = (<vscode.Position>nodeToUpdate.open.end).translate(0, -1);
let end = <vscode.Position>nodeToUpdate.end;
rangeToReplace = new vscode.Range(start, end);
const start = nodeToUpdate.startTagEnd - 1;
const end = nodeToUpdate.end;
rangeToReplace = offsetRangeToVsRange(document, start, end);
textToReplaceWith = '/>';

const emmetMode = getEmmetMode(document.languageId, []) || '';
Expand All @@ -55,8 +57,7 @@ function getRangesToReplace(document: vscode.TextDocument, nodeToUpdate: HtmlNod
(emmetConfig.syntaxProfiles[emmetMode]['selfClosingStyle'] === 'xhtml' || emmetConfig.syntaxProfiles[emmetMode]['self_closing_tag'] === 'xhtml')) {
textToReplaceWith = ' ' + textToReplaceWith;
}

}

return new vscode.TextEdit(rangeToReplace, textToReplaceWith);
}
}
Loading