diff --git a/extensions/emmet/src/balance.ts b/extensions/emmet/src/balance.ts index 0862b20a60f..9cce9593b04 100644 --- a/extensions/emmet/src/balance.ts +++ b/extensions/emmet/src/balance.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { getHtmlNodeLS, offsetRangeToSelection, toLSTextDocument, validate } from './util'; -import { parseMarkupDocument } from './parseMarkupDocument'; -import { TextDocument as LSTextDocument } from 'vscode-html-languageservice'; +import { getHtmlFlatNode, offsetRangeToSelection, validate } from './util'; +import { getRootNode } from './parseDocument'; +import { HtmlNode as HtmlFlatNode } from 'EmmetFlatNode'; let balanceOutStack: Array = []; let lastBalancedSelections: vscode.Selection[] = []; @@ -24,16 +24,16 @@ function balance(out: boolean) { return; } const editor = vscode.window.activeTextEditor; - const document = toLSTextDocument(editor.document); - const htmlDocument = parseMarkupDocument(document); - if (!htmlDocument) { + const document = editor.document; + const rootNode = getRootNode(document, true); + if (!rootNode) { return; } const rangeFn = out ? getRangeToBalanceOut : getRangeToBalanceIn; let newSelections: vscode.Selection[] = []; editor.selections.forEach(selection => { - const range = rangeFn(document, selection); + const range = rangeFn(document, rootNode, selection); newSelections.push(range); }); @@ -57,17 +57,18 @@ function balance(out: boolean) { lastBalancedSelections = editor.selections; } -function getRangeToBalanceOut(document: LSTextDocument, selection: vscode.Selection): vscode.Selection { - const nodeToBalance = getHtmlNodeLS(document, selection.start, false); +function getRangeToBalanceOut(document: vscode.TextDocument, rootNode: HtmlFlatNode, selection: vscode.Selection): vscode.Selection { + const offset = document.offsetAt(selection.start); + const nodeToBalance = getHtmlFlatNode(document.getText(), rootNode, offset, false); if (!nodeToBalance) { return selection; } - if (!nodeToBalance.endTagStart || !nodeToBalance.startTagEnd) { + if (!nodeToBalance.open || !nodeToBalance.close) { return offsetRangeToSelection(document, nodeToBalance.start, nodeToBalance.end); } - const innerSelection = offsetRangeToSelection(document, nodeToBalance.startTagEnd, nodeToBalance.endTagStart); - const outerSelection = offsetRangeToSelection(document, nodeToBalance.start, nodeToBalance.end); + const innerSelection = offsetRangeToSelection(document, nodeToBalance.open.end, nodeToBalance.close.start); + const outerSelection = offsetRangeToSelection(document, nodeToBalance.open.start, nodeToBalance.close.end); if (innerSelection.contains(selection) && !innerSelection.isEqual(selection)) { return innerSelection; @@ -78,34 +79,35 @@ function getRangeToBalanceOut(document: LSTextDocument, selection: vscode.Select return selection; } -function getRangeToBalanceIn(document: LSTextDocument, selection: vscode.Selection): vscode.Selection { - const nodeToBalance = getHtmlNodeLS(document, selection.start, true); +function getRangeToBalanceIn(document: vscode.TextDocument, rootNode: HtmlFlatNode, selection: vscode.Selection): vscode.Selection { + const offset = document.offsetAt(selection.start); + const nodeToBalance = getHtmlFlatNode(document.getText(), rootNode, offset, true); if (!nodeToBalance) { return selection; } const selectionStart = document.offsetAt(selection.start); const selectionEnd = document.offsetAt(selection.end); - if (nodeToBalance.endTagStart !== undefined && nodeToBalance.startTagEnd !== undefined) { + if (nodeToBalance.open && nodeToBalance.close) { const entireNodeSelected = selectionStart === nodeToBalance.start && selectionEnd === nodeToBalance.end; - const startInOpenTag = selectionStart > nodeToBalance.start && selectionStart < nodeToBalance.startTagEnd; - const startInCloseTag = selectionStart > nodeToBalance.endTagStart && selectionStart < nodeToBalance.end; + const startInOpenTag = selectionStart > nodeToBalance.open.start && selectionStart < nodeToBalance.open.end; + const startInCloseTag = selectionStart > nodeToBalance.close.start && selectionStart < nodeToBalance.close.end; if (entireNodeSelected || startInOpenTag || startInCloseTag) { - return offsetRangeToSelection(document, nodeToBalance.startTagEnd, nodeToBalance.endTagStart); + return offsetRangeToSelection(document, nodeToBalance.open.end, nodeToBalance.close.start); } } - if (!nodeToBalance.children.length) { + if (!nodeToBalance.firstChild) { return selection; } - const firstChild = nodeToBalance.children[0]; + const firstChild = nodeToBalance.firstChild; if (selectionStart === firstChild.start && selectionEnd === firstChild.end - && firstChild.endTagStart !== undefined - && firstChild.startTagEnd !== undefined) { - return offsetRangeToSelection(document, firstChild.startTagEnd, firstChild.endTagStart); + && firstChild.open + && firstChild.close) { + return offsetRangeToSelection(document, firstChild.open.end, firstChild.close.start); } return offsetRangeToSelection(document, firstChild.start, firstChild.end); diff --git a/extensions/emmet/src/emmetCommon.ts b/extensions/emmet/src/emmetCommon.ts index 56e02a3b1aa..d0474286216 100644 --- a/extensions/emmet/src/emmetCommon.ts +++ b/extensions/emmet/src/emmetCommon.ts @@ -17,9 +17,9 @@ import { fetchEditPoint } from './editPoint'; import { fetchSelectItem } from './selectItem'; import { evaluateMathExpression } from './evaluateMathExpression'; import { incrementDecrement } from './incrementDecrement'; -import { LANGUAGE_MODES, getMappingForIncludedLanguages, updateEmmetExtensionsPath, getPathBaseName, toLSTextDocument, getSyntaxes, getEmmetMode } from './util'; +import { LANGUAGE_MODES, getMappingForIncludedLanguages, updateEmmetExtensionsPath, getPathBaseName, getSyntaxes, getEmmetMode } from './util'; import { reflectCssValue } from './reflectCssValue'; -import { addFileToMarkupParseCache, removeFileFromMarkupParseCache } from './parseMarkupDocument'; +import { addFileToParseCache, removeFileFromParseCache } from './parseDocument'; export function activateEmmetExtension(context: vscode.ExtensionContext) { registerCompletionProviders(context); @@ -149,15 +149,17 @@ export function activateEmmetExtension(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.workspace.onDidOpenTextDocument((e) => { const emmetMode = getEmmetMode(e.languageId, []) ?? ''; - if (getSyntaxes().markup.includes(emmetMode)) { - addFileToMarkupParseCache(toLSTextDocument(e)); + const syntaxes = getSyntaxes(); + if (syntaxes.markup.includes(emmetMode) || syntaxes.stylesheet.includes(emmetMode)) { + addFileToParseCache(e); } })); context.subscriptions.push(vscode.workspace.onDidCloseTextDocument((e) => { const emmetMode = getEmmetMode(e.languageId, []) ?? ''; - if (getSyntaxes().markup.includes(emmetMode)) { - removeFileFromMarkupParseCache(toLSTextDocument(e)); + const syntaxes = getSyntaxes(); + if (syntaxes.markup.includes(emmetMode) || syntaxes.stylesheet.includes(emmetMode)) { + removeFileFromParseCache(e); } })); } diff --git a/extensions/emmet/src/matchTag.ts b/extensions/emmet/src/matchTag.ts index f2b707d4457..d7331a465b2 100644 --- a/extensions/emmet/src/matchTag.ts +++ b/extensions/emmet/src/matchTag.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { toLSTextDocument, validate, getHtmlNodeLS, offsetRangeToSelection } from './util'; -import { TextDocument as LSTextDocument } from 'vscode-html-languageservice'; +import { validate, getHtmlFlatNode, offsetRangeToSelection } from './util'; +import { getRootNode } from './parseDocument'; +import { HtmlNode as HtmlFlatNode } from 'EmmetFlatNode'; export function matchTag() { if (!validate(false) || !vscode.window.activeTextEditor) { @@ -13,11 +14,15 @@ export function matchTag() { } const editor = vscode.window.activeTextEditor; - const document = toLSTextDocument(editor.document); + const document = editor.document; + const rootNode = getRootNode(document, true); + if (!rootNode) { + return; + } let updatedSelections: vscode.Selection[] = []; editor.selections.forEach(selection => { - const updatedSelection = getUpdatedSelections(document, selection.start); + const updatedSelection = getUpdatedSelections(document, rootNode, selection.start); if (updatedSelection) { updatedSelections.push(updatedSelection); } @@ -28,22 +33,21 @@ export function matchTag() { } } -function getUpdatedSelections(document: LSTextDocument, position: vscode.Position): vscode.Selection | undefined { - const currentNode = getHtmlNodeLS(document, position, true); +function getUpdatedSelections(document: vscode.TextDocument, rootNode: HtmlFlatNode, position: vscode.Position): vscode.Selection | undefined { + const offset = document.offsetAt(position); + const currentNode = getHtmlFlatNode(document.getText(), rootNode, offset, 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.endTagStart === undefined - || currentNode.startTagEnd === undefined - || (offset > currentNode.startTagEnd && offset < currentNode.endTagStart)) { + // If no opening/closing tag or cursor is between open and close tag, then no-op + if (!currentNode.open + || !currentNode.close + || (offset > currentNode.open.end && offset < currentNode.close.start)) { return; } // Place cursor inside the close tag if cursor is inside the open tag, else place it inside the open tag - const finalOffset = (offset <= currentNode.startTagEnd) ? currentNode.endTagStart + 2 : currentNode.start + 1; + const finalOffset = (offset <= currentNode.open.end) ? currentNode.close.start + 2 : currentNode.start + 1; return offsetRangeToSelection(document, finalOffset, finalOffset); } diff --git a/extensions/emmet/src/parseDocument.ts b/extensions/emmet/src/parseDocument.ts new file mode 100644 index 00000000000..c062e9d89ab --- /dev/null +++ b/extensions/emmet/src/parseDocument.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TextDocument } from 'vscode'; +import { Node as FlatNode } from 'EmmetFlatNode'; +import parse from '@emmetio/html-matcher'; +import parseStylesheet from '@emmetio/css-parser'; +import { isStyleSheet } from './util'; + +type Pair = { + key: K; + value: V; +}; + +// Map(filename, Pair(fileVersion, rootNodeOfParsedContent)) +const _parseCache = new Map | undefined>(); + +export function getRootNode(document: TextDocument, useCache: boolean): FlatNode { + const key = document.uri.toString(); + const result = _parseCache.get(key); + const documentVersion = document.version; + if (useCache && result) { + if (documentVersion === result.key) { + return result.value; + } + } + + const parseContent = isStyleSheet(document.languageId) ? parseStylesheet : parse; + const rootNode = parseContent(document.getText()); + if (useCache) { + _parseCache.set(key, { key: documentVersion, value: rootNode }); + } + return rootNode; +} + +export function addFileToParseCache(document: TextDocument) { + const filename = document.uri.toString(); + _parseCache.set(filename, undefined); +} + +export function removeFileFromParseCache(document: TextDocument) { + const filename = document.uri.toString(); + _parseCache.delete(filename); +} diff --git a/extensions/emmet/src/parseMarkupDocument.ts b/extensions/emmet/src/parseMarkupDocument.ts deleted file mode 100644 index a1e526056c8..00000000000 --- a/extensions/emmet/src/parseMarkupDocument.ts +++ /dev/null @@ -1,43 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 = { - key: K; - value: V; -}; - -// Map(filename, Pair(fileVersion, parsedContent)) -const _parseCache = new Map | 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); -} diff --git a/extensions/emmet/src/removeTag.ts b/extensions/emmet/src/removeTag.ts index 263aa2248d8..89f91e6b026 100644 --- a/extensions/emmet/src/removeTag.ts +++ b/extensions/emmet/src/removeTag.ts @@ -4,16 +4,24 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { validate, getHtmlNodeLS, toLSTextDocument, offsetRangeToVsRange } from './util'; +import { getRootNode } from './parseDocument'; +import { validate, getHtmlFlatNode, offsetRangeToVsRange } from './util'; +import { HtmlNode as HtmlFlatNode } from 'EmmetFlatNode'; export function removeTag() { if (!validate(false) || !vscode.window.activeTextEditor) { return; } const editor = vscode.window.activeTextEditor; + const document = editor.document; + const rootNode = getRootNode(document, true); + if (!rootNode) { + return; + } + let finalRangesToRemove = editor.selections.reverse() .reduce((prev, selection) => - prev.concat(getRangesToRemove(editor.document, selection)), []); + prev.concat(getRangesToRemove(editor.document, rootNode, selection)), []); return editor.edit(editBuilder => { finalRangesToRemove.forEach(range => { @@ -27,26 +35,32 @@ export function removeTag() { * It finds the node to remove based on the selection's start position * and then removes that node, reindenting the content in between. */ -function getRangesToRemove(document: vscode.TextDocument, selection: vscode.Selection): vscode.Range[] { - const lsDocument = toLSTextDocument(document); - const nodeToUpdate = getHtmlNodeLS(lsDocument, selection.start, true); +function getRangesToRemove(document: vscode.TextDocument, rootNode: HtmlFlatNode, selection: vscode.Selection): vscode.Range[] { + const offset = document.offsetAt(selection.start); + const nodeToUpdate = getHtmlFlatNode(document.getText(), rootNode, offset, true); if (!nodeToUpdate) { return []; } - const openTagRange = offsetRangeToVsRange(lsDocument, nodeToUpdate.start, nodeToUpdate.startTagEnd ?? nodeToUpdate.end); + let openTagRange: vscode.Range | undefined; + if (nodeToUpdate.open) { + openTagRange = offsetRangeToVsRange(document, nodeToUpdate.open.start, nodeToUpdate.open.end); + } let closeTagRange: vscode.Range | undefined; - if (nodeToUpdate.endTagStart !== undefined) { - closeTagRange = offsetRangeToVsRange(lsDocument, nodeToUpdate.endTagStart, nodeToUpdate.end); + if (nodeToUpdate.close) { + closeTagRange = offsetRangeToVsRange(document, nodeToUpdate.close.start, nodeToUpdate.close.end); } - let rangesToRemove = [openTagRange]; - if (closeTagRange) { - const indentAmountToRemove = calculateIndentAmountToRemove(document, openTagRange, closeTagRange); - for (let i = openTagRange.start.line + 1; i < closeTagRange.start.line; i++) { - rangesToRemove.push(new vscode.Range(i, 0, i, indentAmountToRemove)); + let rangesToRemove = []; + if (openTagRange) { + rangesToRemove.push(openTagRange); + if (closeTagRange) { + const indentAmountToRemove = calculateIndentAmountToRemove(document, openTagRange, closeTagRange); + for (let i = openTagRange.start.line + 1; i < closeTagRange.start.line; i++) { + rangesToRemove.push(new vscode.Range(i, 0, i, indentAmountToRemove)); + } + rangesToRemove.push(closeTagRange); } - rangesToRemove.push(closeTagRange); } return rangesToRemove; } diff --git a/extensions/emmet/src/splitJoinTag.ts b/extensions/emmet/src/splitJoinTag.ts index d2df5d65078..3d1df021999 100644 --- a/extensions/emmet/src/splitJoinTag.ts +++ b/extensions/emmet/src/splitJoinTag.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { validate, getEmmetMode, getEmmetConfiguration, toLSTextDocument, getHtmlNodeLS, offsetRangeToVsRange } from './util'; -import { Node as LSNode, TextDocument as LSTextDocument } from 'vscode-html-languageservice'; +import { validate, getEmmetMode, getEmmetConfiguration, getHtmlFlatNode, offsetRangeToVsRange } from './util'; +import { HtmlNode as HtmlFlatNode } from 'EmmetFlatNode'; +import { getRootNode } from './parseDocument'; export function splitJoinTag() { if (!validate(false) || !vscode.window.activeTextEditor) { @@ -13,29 +14,30 @@ export function splitJoinTag() { } const editor = vscode.window.activeTextEditor; - const document = toLSTextDocument(editor.document); + const document = editor.document; + const rootNode = getRootNode(editor.document, true); + if (!rootNode) { + return; + } + return editor.edit(editBuilder => { editor.selections.reverse().forEach(selection => { - const nodeToUpdate = getHtmlNodeLS(document, selection.start, true); + const documentText = document.getText(); + const offset = document.offsetAt(selection.start); + const nodeToUpdate = getHtmlFlatNode(documentText, rootNode, offset, true); if (nodeToUpdate) { const textEdit = getRangesToReplace(document, nodeToUpdate); - if (textEdit) { - editBuilder.replace(textEdit.range, textEdit.newText); - } + editBuilder.replace(textEdit.range, textEdit.newText); } }); }); } -function getRangesToReplace(document: LSTextDocument, nodeToUpdate: LSNode): vscode.TextEdit | undefined { +function getRangesToReplace(document: vscode.TextDocument, nodeToUpdate: HtmlFlatNode): vscode.TextEdit { let rangeToReplace: vscode.Range; let textToReplaceWith: string; - if (!nodeToUpdate?.tag) { - return; - } - - if (nodeToUpdate.endTagStart === undefined || nodeToUpdate.startTagEnd === undefined) { + if (!nodeToUpdate.open || !nodeToUpdate.close) { // Split Tag const nodeText = document.getText().substring(nodeToUpdate.start, nodeToUpdate.end); const m = nodeText.match(/(\s*\/)?>$/); @@ -43,10 +45,10 @@ function getRangesToReplace(document: LSTextDocument, nodeToUpdate: LSNode): vsc const start = m ? end - m[0].length : end; rangeToReplace = offsetRangeToVsRange(document, start, end); - textToReplaceWith = `>`; + textToReplaceWith = `>`; } else { // Join Tag - const start = nodeToUpdate.startTagEnd - 1; + const start = nodeToUpdate.open.end - 1; const end = nodeToUpdate.end; rangeToReplace = offsetRangeToVsRange(document, start, end); textToReplaceWith = '/>'; diff --git a/extensions/emmet/src/typings/EmmetFlatNode.d.ts b/extensions/emmet/src/typings/EmmetFlatNode.d.ts new file mode 100644 index 00000000000..f571b25d5a1 --- /dev/null +++ b/extensions/emmet/src/typings/EmmetFlatNode.d.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +declare module 'EmmetFlatNode' { + export interface Node { + start: number + end: number + type: string + parent: Node | undefined + firstChild: Node | undefined + nextSibling: Node | undefined + previousSibling: Node | undefined + children: Node[] + } + + export interface Token { + start: number + end: number + stream: string + toString(): string + } + + export interface CssToken extends Token { + size: number + item(number: number): any + type: string + } + + export interface HtmlToken extends Token { + value: string + } + + export interface Attribute extends Token { + name: Token + value: Token + } + + export interface HtmlNode extends Node { + name: string + open: Token | undefined + close: Token | undefined + parent: HtmlNode | undefined + firstChild: HtmlNode | undefined + nextSibling: HtmlNode | undefined + previousSibling: HtmlNode | undefined + children: HtmlNode[] + attributes: Attribute[] + } + + export interface CssNode extends Node { + name: string + parent: CssNode | undefined + firstChild: CssNode | undefined + nextSibling: CssNode | undefined + previousSibling: CssNode | undefined + children: CssNode[] + } + + export interface Rule extends CssNode { + selectorToken: Token + contentStartToken: Token + contentEndToken: Token + } + + export interface Property extends CssNode { + valueToken: Token + separator: string + parent: Rule + terminatorToken: Token + separatorToken: Token + value: string + } + + export interface Stylesheet extends Node { + comments: Token[] + } +} diff --git a/extensions/emmet/src/typings/emmetio__css-parser.d.ts b/extensions/emmet/src/typings/emmetio__css-parser.d.ts index 65e7ac6ecdd..e02b94709e5 100644 --- a/extensions/emmet/src/typings/emmetio__css-parser.d.ts +++ b/extensions/emmet/src/typings/emmetio__css-parser.d.ts @@ -5,8 +5,10 @@ declare module '@emmetio/css-parser' { import { BufferStream, Stylesheet } from 'EmmetNode'; + import { Stylesheet as FlatStylesheet } from 'EmmetFlatNode'; function parseStylesheet(stream: BufferStream): Stylesheet; + function parseStylesheet(stream: string): FlatStylesheet; export default parseStylesheet; } diff --git a/extensions/emmet/src/typings/emmetio__html-matcher.d.ts b/extensions/emmet/src/typings/emmetio__html-matcher.d.ts index 2b425c88e3b..1c525f2d02c 100644 --- a/extensions/emmet/src/typings/emmetio__html-matcher.d.ts +++ b/extensions/emmet/src/typings/emmetio__html-matcher.d.ts @@ -5,8 +5,10 @@ declare module '@emmetio/html-matcher' { import { BufferStream, HtmlNode } from 'EmmetNode'; + import { HtmlNode as HtmlFlatNode } from 'EmmetFlatNode'; function parse(stream: BufferStream): HtmlNode; + function parse(stream: string): HtmlFlatNode; export default parse; } diff --git a/extensions/emmet/src/updateTag.ts b/extensions/emmet/src/updateTag.ts index a2f9b5a66e0..5dcbe2ffb7f 100644 --- a/extensions/emmet/src/updateTag.ts +++ b/extensions/emmet/src/updateTag.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { getHtmlNodeLS, toLSTextDocument, validate } from './util'; -import { TextDocument as LSTextDocument, Node as LSNode } from 'vscode-html-languageservice'; +import { getHtmlFlatNode, validate } from './util'; +import { HtmlNode as HtmlFlatNode } from 'EmmetFlatNode'; +import { getRootNode } from './parseDocument'; export function updateTag(tagName: string): Thenable | undefined { if (!validate(false) || !vscode.window.activeTextEditor) { @@ -13,9 +14,15 @@ export function updateTag(tagName: string): Thenable | undefined { } const editor = vscode.window.activeTextEditor; + const document = editor.document; + const rootNode = getRootNode(document, true); + if (!rootNode) { + return; + } + const rangesToUpdate = editor.selections.reverse() .reduce((prev, selection) => - prev.concat(getRangesToUpdate(editor, selection)), []); + prev.concat(getRangesToUpdate(document, selection, rootNode)), []); return editor.edit(editBuilder => { rangesToUpdate.forEach(range => { @@ -24,34 +31,25 @@ export function updateTag(tagName: string): Thenable | undefined { }); } -function getPositionFromOffset(offset: number | undefined, document: LSTextDocument): vscode.Position | undefined { - if (offset === undefined) { - return undefined; - } - const pos = document.positionAt(offset); - return new vscode.Position(pos.line, pos.character); -} - -function getRangesFromNode(node: LSNode, document: LSTextDocument): vscode.Range[] { - const start = getPositionFromOffset(node.start, document)!; - const startTagEnd = getPositionFromOffset(node.startTagEnd, document); - const end = getPositionFromOffset(node.end, document)!; - const endTagStart = getPositionFromOffset(node.endTagStart, document); - +function getRangesFromNode(node: HtmlFlatNode, document: vscode.TextDocument): vscode.Range[] { let ranges: vscode.Range[] = []; - if (startTagEnd) { + if (node.open) { + const start = document.positionAt(node.open.start); ranges.push(new vscode.Range(start.translate(0, 1), - start.translate(0, 1).translate(0, node.tag!.length ?? 0))); + start.translate(0, 1).translate(0, node.name.length))); } - if (endTagStart) { + if (node.close) { + const endTagStart = document.positionAt(node.close.start); + const end = document.positionAt(node.close.end); ranges.push(new vscode.Range(endTagStart.translate(0, 2), end.translate(0, -1))); } return ranges; } -function getRangesToUpdate(editor: vscode.TextEditor, selection: vscode.Selection): vscode.Range[] { - const document = toLSTextDocument(editor.document); - const nodeToUpdate = getHtmlNodeLS(document, selection.start, true); +function getRangesToUpdate(document: vscode.TextDocument, selection: vscode.Selection, rootNode: HtmlFlatNode): vscode.Range[] { + const documentText = document.getText(); + const offset = document.offsetAt(selection.start); + const nodeToUpdate = getHtmlFlatNode(documentText, rootNode, offset, true); if (!nodeToUpdate) { return []; } diff --git a/extensions/emmet/src/util.ts b/extensions/emmet/src/util.ts index 17cac93bd4f..d47c00ad67b 100644 --- a/extensions/emmet/src/util.ts +++ b/extensions/emmet/src/util.ts @@ -7,13 +7,12 @@ import * as vscode from 'vscode'; import parse from '@emmetio/html-matcher'; import parseStylesheet from '@emmetio/css-parser'; import { Node, HtmlNode, CssToken, Property, Rule, Stylesheet } from 'EmmetNode'; +import { Node as FlatNode, HtmlNode as HtmlFlatNode } from 'EmmetFlatNode'; import { DocumentStreamReader } from './bufferStream'; import * as EmmetHelper from 'vscode-emmet-helper'; -import { Position as LSPosition, getLanguageService as getLanguageServiceInternal, LanguageService, LanguageServiceOptions, TextDocument as LSTextDocument, Node as LSNode } from 'vscode-html-languageservice'; -import { parseMarkupDocument } from './parseMarkupDocument'; +import { TextDocument as LSTextDocument } from 'vscode-languageserver-textdocument'; let _emmetHelper: typeof EmmetHelper; -let _languageService: LanguageService; let _currentExtensionsPath: string | undefined = undefined; let _homeDir: vscode.Uri | undefined; @@ -33,16 +32,6 @@ export function getEmmetHelper() { return _emmetHelper; } -export function getLanguageService(options?: LanguageServiceOptions): LanguageService { - if (!options) { - if (!_languageService) { - _languageService = getLanguageServiceInternal(); - } - return _languageService; - } - return getLanguageServiceInternal(options); -} - /** * Update Emmet Helper to use user snippets from the extensionsPath setting */ @@ -323,7 +312,7 @@ export function parsePartialStylesheet(document: vscode.TextDocument, position: /** * Returns node corresponding to given position in the given root node */ -export function getNode(root: Node | undefined, position: vscode.Position, includeNodeBoundary: boolean) { +export function getNode(root: Node | undefined, position: vscode.Position, includeNodeBoundary: boolean): Node | null { if (!root) { return null; } @@ -348,6 +337,46 @@ export function getNode(root: Node | undefined, position: vscode.Position, inclu return foundNode; } +export function getFlatNode(root: FlatNode | undefined, offset: number, includeNodeBoundary: boolean): FlatNode | undefined { + if (!root) { + return; + } + + function getFlatNodeChild(child: FlatNode | undefined): FlatNode | undefined { + if (!child) { + return; + } + const nodeStart = child.start; + const nodeEnd = child.end; + if ((nodeStart < offset && nodeEnd > offset) + || (includeNodeBoundary && nodeStart <= offset && nodeEnd >= offset)) { + return getFlatNodeChildren(child.children) ?? child; + } + else if ('close' in child) { + // We have an HTML node in this case. + // In case this node is an invalid unpaired HTML node, + // we still want to search its children + const htmlChild = child; + if (htmlChild.open && !htmlChild.close) { + return getFlatNodeChildren(htmlChild.children); + } + } + return; + } + + function getFlatNodeChildren(children: FlatNode[]): FlatNode | undefined { + for (let i = 0; i < children.length; i++) { + const foundChild = getFlatNodeChild(children[i]); + if (foundChild) { + return foundChild; + } + } + return; + } + + return getFlatNodeChildren(root.children); +} + export const allowedMimeTypesInScriptTag = ['text/html', 'text/plain', 'text/x-template', 'text/template', 'text/ng-template']; /** @@ -380,37 +409,24 @@ export function getHtmlNode(document: vscode.TextDocument, root: Node | undefine /** * Finds the HTML node within an HTML document at a given position */ -export function getHtmlNodeLS(document: LSTextDocument, position: vscode.Position, includeNodeBoundary: boolean): LSNode | undefined { - const documentText = document.getText(); - const offset = document.offsetAt(position); - let selectionStartOffset = offset; - if (includeNodeBoundary && documentText.charAt(offset) === '<') { - selectionStartOffset++; - } - else if (includeNodeBoundary && documentText.charAt(offset) === '>') { - selectionStartOffset--; - } - return getHtmlNodeLSInternal(document, selectionStartOffset); -} +export function getHtmlFlatNode(documentText: string, root: FlatNode | undefined, offset: number, includeNodeBoundary: boolean): HtmlFlatNode | undefined { + const currentNode: HtmlFlatNode | undefined = getFlatNode(root, offset, includeNodeBoundary); + if (!currentNode) { return; } -function getHtmlNodeLSInternal(document: LSTextDocument, offset: number, isInTemplateNode: boolean = false): LSNode | undefined { - const useCache = !isInTemplateNode; - const parsedDocument = parseMarkupDocument(document, useCache); - - const currentNode: LSNode = parsedDocument.findNodeAt(offset); - if (!currentNode.tag) { return; } - - const isTemplateScript = isNodeTemplateScriptLS(currentNode); + const isTemplateScript = currentNode.name === 'script' && + (currentNode.attributes && + currentNode.attributes.some(x => x.name.toString() === 'type' + && allowedMimeTypesInScriptTag.includes(x.value.toString()))); if (isTemplateScript - && currentNode.startTagEnd - && offset > currentNode.startTagEnd - && (!currentNode.endTagStart || offset < currentNode.endTagStart)) { + && currentNode.open + && offset > currentNode.open.end + && (!currentNode.close || offset < currentNode.close.start)) { // blank out the rest of the document and search for the node within - const documentText = document.getText(); - const beforePadding = ' '.repeat(currentNode.startTagEnd); - const scriptBodyText = beforePadding + documentText.substring(currentNode.startTagEnd, currentNode.endTagStart ?? currentNode.end); - const scriptBodyDocument = LSTextDocument.create(document.uri, document.languageId, document.version, scriptBodyText); - const scriptBodyNode = getHtmlNodeLSInternal(scriptBodyDocument, offset, true); + const beforePadding = ' '.repeat(currentNode.open.end); + const endToUse = currentNode.close ? currentNode.close.start : currentNode.end; + const scriptBodyText = beforePadding + documentText.substring(currentNode.open.end, endToUse); + const innerRoot: HtmlFlatNode = parse(scriptBodyText); + const scriptBodyNode = getHtmlFlatNode(scriptBodyText, innerRoot, offset, includeNodeBoundary); if (scriptBodyNode) { scriptBodyNode.parent = currentNode; currentNode.children.push(scriptBodyNode); @@ -420,33 +436,16 @@ function getHtmlNodeLSInternal(document: LSTextDocument, offset: number, isInTem return currentNode; } -/** - * Returns whether the node is a