diff --git a/extensions/emmet/src/reflectCssValue.ts b/extensions/emmet/src/reflectCssValue.ts index da20b38397b..6992e629775 100644 --- a/extensions/emmet/src/reflectCssValue.ts +++ b/extensions/emmet/src/reflectCssValue.ts @@ -3,20 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Range, window, TextEditor } from 'vscode'; -import { getCssPropertyFromRule, getCssPropertyFromDocument } from './util'; -import { Property, Rule } from 'EmmetNode'; +import { window, TextEditor } from 'vscode'; +import { getCssPropertyFromRule, getCssPropertyFromDocument, offsetRangeToVsRange } from './util'; +import { Property, Rule } from 'EmmetFlatNode'; const vendorPrefixes = ['-webkit-', '-moz-', '-ms-', '-o-', '']; export function reflectCssValue(): Thenable | undefined { - let editor = window.activeTextEditor; + const editor = window.activeTextEditor; if (!editor) { window.showInformationMessage('No editor is active.'); return; } - let node = getCssPropertyFromDocument(editor, editor.selection.active); + const node = getCssPropertyFromDocument(editor, editor.selection.active); if (!node) { return; } @@ -45,10 +45,11 @@ function updateCSSNode(editor: TextEditor, property: Property): Thenable { -// teardown(closeAllEditors); +suite('Tests for Emmet actions on html tags', () => { + teardown(closeAllEditors); - // test('update image css with multiple cursors in css file', () => { - // const cssContents = ` - // .one { - // margin: 10px; - // padding: 10px; - // background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png); - // } - // .two { - // background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png); - // height: 42px; - // } - // .three { - // background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png); - // width: 42px; - // } - // `; - // const expectedContents = ` - // .one { - // margin: 10px; - // padding: 10px; - // background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png); - // width: 32px; - // height: 32px; - // } - // .two { - // background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png); - // width: 32px; - // height: 32px; - // } - // .three { - // background-image: url(https://github.com/microsoft/vscode/blob/master/resources/linux/code.png); - // height: 32px; - // width: 32px; - // } - // `; - // return withRandomFileEditor(cssContents, 'css', (editor, doc) => { - // editor.selections = [ - // new Selection(4, 50, 4, 50), - // new Selection(7, 50, 7, 50), - // new Selection(11, 50, 11, 50) - // ]; + test('update image css with multiple cursors in css file', () => { + const cssContents = ` + .one { + margin: 10px; + padding: 10px; + background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); + } + .two { + background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); + height: 42px; + } + .three { + background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); + width: 42px; + } + `; + const expectedContents = ` + .one { + margin: 10px; + padding: 10px; + background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); + width: 1024px; + height: 1024px; + } + .two { + background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); + width: 1024px; + height: 1024px; + } + .three { + background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); + height: 1024px; + width: 1024px; + } + `; + return withRandomFileEditor(cssContents, 'css', (editor, doc) => { + editor.selections = [ + new Selection(4, 50, 4, 50), + new Selection(7, 50, 7, 50), + new Selection(11, 50, 11, 50) + ]; - // return updateImageSize()!.then(() => { - // assert.equal(doc.getText(), expectedContents); - // return Promise.resolve(); - // }); - // }); - // }); + return updateImageSize()!.then(() => { + assert.equal(doc.getText(), expectedContents); + return Promise.resolve(); + }); + }); + }); - // test('update image size in css in html file with multiple cursors', () => { - // const htmlWithCssContents = ` - // - // - // - // `; - // const expectedContents = ` - // - // - // - // `; - // return withRandomFileEditor(htmlWithCssContents, 'html', (editor, doc) => { - // editor.selections = [ - // new Selection(6, 50, 6, 50), - // new Selection(9, 50, 9, 50), - // new Selection(13, 50, 13, 50) - // ]; + test('update image size in css in html file with multiple cursors', () => { + const htmlWithCssContents = ` + + + + `; + const expectedContents = ` + + + + `; + return withRandomFileEditor(htmlWithCssContents, 'html', (editor, doc) => { + editor.selections = [ + new Selection(6, 50, 6, 50), + new Selection(9, 50, 9, 50), + new Selection(13, 50, 13, 50) + ]; - // return updateImageSize()!.then(() => { - // assert.equal(doc.getText(), expectedContents); - // return Promise.resolve(); - // }); - // }); - // }); + return updateImageSize()!.then(() => { + assert.equal(doc.getText(), expectedContents); + return Promise.resolve(); + }); + }); + }); - // test('update image size in img tag in html file with multiple cursors', () => { - // const htmlwithimgtag = ` - // - // - // - // - // - // `; - // const expectedContents = ` - // - // - // - // - // - // `; - // return withRandomFileEditor(htmlwithimgtag, 'html', (editor, doc) => { - // editor.selections = [ - // new Selection(2, 50, 2, 50), - // new Selection(3, 50, 3, 50), - // new Selection(4, 50, 4, 50) - // ]; + test('update image size in img tag in html file with multiple cursors', () => { + const htmlwithimgtag = ` + + + + + + `; + const expectedContents = ` + + + + + + `; + return withRandomFileEditor(htmlwithimgtag, 'html', (editor, doc) => { + editor.selections = [ + new Selection(2, 50, 2, 50), + new Selection(3, 50, 3, 50), + new Selection(4, 50, 4, 50) + ]; - // return updateImageSize()!.then(() => { - // assert.equal(doc.getText(), expectedContents); - // return Promise.resolve(); - // }); - // }); - // }); - -// }); + return updateImageSize()!.then(() => { + assert.equal(doc.getText(), expectedContents); + return Promise.resolve(); + }); + }); + }); +}); diff --git a/extensions/emmet/src/typings/EmmetFlatNode.d.ts b/extensions/emmet/src/typings/EmmetFlatNode.d.ts index f571b25d5a1..05f5a5b3c77 100644 --- a/extensions/emmet/src/typings/EmmetFlatNode.d.ts +++ b/extensions/emmet/src/typings/EmmetFlatNode.d.ts @@ -18,7 +18,7 @@ declare module 'EmmetFlatNode' { export interface Token { start: number end: number - stream: string + stream: BufferStream toString(): string } @@ -76,4 +76,14 @@ declare module 'EmmetFlatNode' { export interface Stylesheet extends Node { comments: Token[] } + + export interface BufferStream { + peek(): number + next(): number + backUp(n: number): number + current(): string + substring(from: number, to: number): string + eat(match: any): boolean + eatWhile(match: any): boolean + } } diff --git a/extensions/emmet/src/updateImageSize.ts b/extensions/emmet/src/updateImageSize.ts index 3ac3f15a1be..b590fabb25c 100644 --- a/extensions/emmet/src/updateImageSize.ts +++ b/extensions/emmet/src/updateImageSize.ts @@ -5,26 +5,26 @@ // Based on @sergeche's work on the emmet plugin for atom -import { TextEditor, Range, Position, window, TextEdit } from 'vscode'; +import { TextEditor, Position, window, TextEdit } from 'vscode'; import * as path from 'path'; import { getImageSize } from './imageSizeHelper'; -import { parseDocument, getNode, iterateCSSToken, getCssPropertyFromRule, isStyleSheet, validate } from './util'; -import { HtmlNode, CssToken, HtmlToken, Attribute, Property } from 'EmmetNode'; +import { getFlatNode, iterateCSSToken, getCssPropertyFromRule, isStyleSheet, validate, offsetRangeToVsRange } from './util'; +import { HtmlNode, CssToken, HtmlToken, Attribute, Property } from 'EmmetFlatNode'; import { locateFile } from './locateFile'; import parseStylesheet from '@emmetio/css-parser'; -import { DocumentStreamReader } from './bufferStream'; +import { getRootNode } from './parseDocument'; /** * Updates size of context image in given editor */ -export function updateImageSize() { +export function updateImageSize(): Promise | undefined { if (!validate() || !window.activeTextEditor) { return; } const editor = window.activeTextEditor; - let allUpdatesPromise = editor.selections.reverse().map(selection => { - let position = selection.isReversed ? selection.active : selection.anchor; + const allUpdatesPromise = editor.selections.reverse().map(selection => { + const position = selection.isReversed ? selection.active : selection.anchor; if (!isStyleSheet(editor.document.languageId)) { return updateImageSizeHTML(editor, position); } else { @@ -71,15 +71,19 @@ function updateImageSizeHTML(editor: TextEditor, position: Position): Promise { const getPropertyInsiderStyleTag = (editor: TextEditor): Property | null => { - const rootNode = parseDocument(editor.document); - const currentNode = getNode(rootNode, position, true); + const document = editor.document; + const rootNode = getRootNode(document, true); + const offset = document.offsetAt(position); + const currentNode = getFlatNode(rootNode, offset, true); if (currentNode && currentNode.name === 'style' - && currentNode.open.end.isBefore(position) - && currentNode.close.start.isAfter(position)) { - let buffer = new DocumentStreamReader(editor.document, currentNode.open.end, new Range(currentNode.open.end, currentNode.close.start)); - let rootNode = parseStylesheet(buffer); - const node = getNode(rootNode, position, true); - return (node && node.type === 'property') ? node : null; + && currentNode.open && currentNode.close + && currentNode.open.end < offset + && currentNode.close.start > offset) { + const buffer = ' '.repeat(currentNode.open.end) + + document.getText().substring(currentNode.open.end, currentNode.close.start); + const innerRootNode = parseStylesheet(buffer); + const innerNode = getFlatNode(innerRootNode, offset, true); + return (innerNode && innerNode.type === 'property') ? innerNode : null; } return null; }; @@ -96,7 +100,7 @@ function updateImageSizeCSSFile(editor: TextEditor, position: Position): Promise */ function updateImageSizeCSS(editor: TextEditor, position: Position, fetchNode: (editor: TextEditor, position: Position) => Property | null): Promise { const node = fetchNode(editor, position); - const src = node && getImageSrcCSS(node, position); + const src = node && getImageSrcCSS(editor, node, position); if (!src) { return Promise.reject(new Error('No valid image source')); @@ -108,7 +112,7 @@ function updateImageSizeCSS(editor: TextEditor, position: Position, fetchNode: ( // since this action is asynchronous, we have to ensure that editor wasn’t // changed and user didn’t moved caret outside node const prop = fetchNode(editor, position); - if (prop && getImageSrcCSS(prop, position) === src) { + if (prop && getImageSrcCSS(editor, prop, position) === src) { return updateCSSNode(editor, prop, size.width, size.height); } return []; @@ -121,8 +125,10 @@ function updateImageSizeCSS(editor: TextEditor, position: Position, fetchNode: ( * be found */ function getImageHTMLNode(editor: TextEditor, position: Position): HtmlNode | null { - const rootNode = parseDocument(editor.document); - const node = getNode(rootNode, position, true); + const document = editor.document; + const rootNode = getRootNode(document, true); + const offset = document.offsetAt(position); + const node = getFlatNode(rootNode, offset, true); return node && node.name.toLowerCase() === 'img' ? node : null; } @@ -132,8 +138,10 @@ function getImageHTMLNode(editor: TextEditor, position: Position): HtmlNode | nu * be found */ function getImageCSSNode(editor: TextEditor, position: Position): Property | null { - const rootNode = parseDocument(editor.document); - const node = getNode(rootNode, position, true); + const document = editor.document; + const rootNode = getRootNode(document, true); + const offset = document.offsetAt(position); + const node = getFlatNode(rootNode, offset, true); return node && node.type === 'property' ? node : null; } @@ -152,11 +160,11 @@ function getImageSrcHTML(node: HtmlNode): string | undefined { /** * Returns image source from given `url()` token */ -function getImageSrcCSS(node: Property | undefined, position: Position): string | undefined { +function getImageSrcCSS(editor: TextEditor, node: Property | undefined, position: Position): string | undefined { if (!node) { return; } - const urlToken = findUrlToken(node, position); + const urlToken = findUrlToken(editor, node, position); if (!urlToken) { return; } @@ -174,7 +182,12 @@ function getImageSrcCSS(node: Property | undefined, position: Position): string * Updates size of given HTML node */ function updateHTMLTag(editor: TextEditor, node: HtmlNode, width: number, height: number): TextEdit[] { + const document = editor.document; const srcAttr = getAttribute(node, 'src'); + if (!srcAttr) { + return []; + } + const widthAttr = getAttribute(node, 'width'); const heightAttr = getAttribute(node, 'height'); const quote = getAttributeQuote(editor, srcAttr); @@ -186,15 +199,15 @@ function updateHTMLTag(editor: TextEditor, node: HtmlNode, width: number, height if (!widthAttr) { textToAdd += ` width=${quote}${width}${quote}`; } else { - edits.push(new TextEdit(new Range(widthAttr.value.start, widthAttr.value.end), String(width))); + edits.push(new TextEdit(offsetRangeToVsRange(document, widthAttr.value.start, widthAttr.value.end), String(width))); } if (!heightAttr) { textToAdd += ` height=${quote}${height}${quote}`; } else { - edits.push(new TextEdit(new Range(heightAttr.value.start, heightAttr.value.end), String(height))); + edits.push(new TextEdit(offsetRangeToVsRange(document, heightAttr.value.start, heightAttr.value.end), String(height))); } if (textToAdd) { - edits.push(new TextEdit(new Range(endOfAttributes, endOfAttributes), textToAdd)); + edits.push(new TextEdit(offsetRangeToVsRange(document, endOfAttributes, endOfAttributes), textToAdd)); } return edits; @@ -204,6 +217,7 @@ function updateHTMLTag(editor: TextEditor, node: HtmlNode, width: number, height * Updates size of given CSS rule */ function updateCSSNode(editor: TextEditor, srcProp: Property, width: number, height: number): TextEdit[] { + const document = editor.document; const rule = srcProp.parent; const widthProp = getCssPropertyFromRule(rule, 'width'); const heightProp = getCssPropertyFromRule(rule, 'height'); @@ -214,22 +228,22 @@ function updateCSSNode(editor: TextEditor, srcProp: Property, width: number, hei let edits: TextEdit[] = []; if (!srcProp.terminatorToken) { - edits.push(new TextEdit(new Range(srcProp.end, srcProp.end), ';')); + edits.push(new TextEdit(offsetRangeToVsRange(document, srcProp.end, srcProp.end), ';')); } let textToAdd = ''; if (!widthProp) { textToAdd += `${before}width${separator}${width}px;`; } else { - edits.push(new TextEdit(new Range(widthProp.valueToken.start, widthProp.valueToken.end), `${width}px`)); + edits.push(new TextEdit(offsetRangeToVsRange(document, widthProp.valueToken.start, widthProp.valueToken.end), `${width}px`)); } if (!heightProp) { textToAdd += `${before}height${separator}${height}px;`; } else { - edits.push(new TextEdit(new Range(heightProp.valueToken.start, heightProp.valueToken.end), `${height}px`)); + edits.push(new TextEdit(offsetRangeToVsRange(document, heightProp.valueToken.start, heightProp.valueToken.end), `${height}px`)); } if (textToAdd) { - edits.push(new TextEdit(new Range(srcProp.end, srcProp.end), textToAdd)); + edits.push(new TextEdit(offsetRangeToVsRange(document, srcProp.end, srcProp.end), textToAdd)); } return edits; @@ -238,9 +252,9 @@ function updateCSSNode(editor: TextEditor, srcProp: Property, width: number, hei /** * Returns attribute object with `attrName` name from given HTML node */ -function getAttribute(node: HtmlNode, attrName: string): Attribute { +function getAttribute(node: HtmlNode, attrName: string): Attribute | undefined { attrName = attrName.toLowerCase(); - return node && (node.open as any).attributes.find((attr: any) => attr.name.value.toLowerCase() === attrName); + return node && node.attributes.find(attr => attr.name.toString().toLowerCase() === attrName); } /** @@ -248,18 +262,20 @@ function getAttribute(node: HtmlNode, attrName: string): Attribute { * string if attribute wasn’t quoted */ -function getAttributeQuote(editor: TextEditor, attr: any): string { - const range = new Range(attr.value ? attr.value.end : attr.end, attr.end); - return range.isEmpty ? '' : editor.document.getText(range); +function getAttributeQuote(editor: TextEditor, attr: Attribute): string { + const begin = attr.value ? attr.value.end : attr.end; + const end = attr.end; + return begin === end ? '' : editor.document.getText().substring(begin, end); } /** * Finds 'url' token for given `pos` point in given CSS property `node` */ -function findUrlToken(node: Property, pos: Position): CssToken | undefined { +function findUrlToken(editor: TextEditor, node: Property, pos: Position): CssToken | undefined { + const offset = editor.document.offsetAt(pos); for (let i = 0, il = (node as any).parsedValue.length, url; i < il; i++) { iterateCSSToken((node as any).parsedValue[i], (token: CssToken) => { - if (token.type === 'url' && token.start.isBeforeOrEqual(pos) && token.end.isAfterOrEqual(pos)) { + if (token.type === 'url' && token.start <= offset && token.end >= offset) { url = token; return false; } @@ -279,9 +295,9 @@ function findUrlToken(node: Property, pos: Position): CssToken | undefined { function getPropertyDelimitor(editor: TextEditor, node: Property): string { let anchor; if (anchor = (node.previousSibling || node.parent.contentStartToken)) { - return editor.document.getText(new Range(anchor.end, node.start)); + return editor.document.getText().substring(anchor.end, node.start); } else if (anchor = (node.nextSibling || node.parent.contentEndToken)) { - return editor.document.getText(new Range(node.end, anchor.start)); + return editor.document.getText().substring(node.end, anchor.start); } return ''; diff --git a/extensions/emmet/src/util.ts b/extensions/emmet/src/util.ts index d3476dc0d91..e83aad0820e 100644 --- a/extensions/emmet/src/util.ts +++ b/extensions/emmet/src/util.ts @@ -6,11 +6,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 { Node, HtmlNode, Stylesheet } from 'EmmetNode'; +import { Node as FlatNode, HtmlNode as HtmlFlatNode, Property as FlatProperty, Rule as FlatRule, CssToken as FlatCssToken } from 'EmmetFlatNode'; import { DocumentStreamReader } from './bufferStream'; import * as EmmetHelper from 'vscode-emmet-helper'; import { TextDocument as LSTextDocument } from 'vscode-languageserver-textdocument'; +import { getRootNode } from './parseDocument'; let _emmetHelper: typeof EmmetHelper; let _currentExtensionsPath: string | undefined = undefined; @@ -652,7 +653,7 @@ export function getEmmetConfiguration(syntax: string) { * Itereates by each child, as well as nested child's children, in their order * and invokes `fn` for each. If `fn` function returns `false`, iteration stops */ -export function iterateCSSToken(token: CssToken, fn: (x: any) => any): boolean { +export function iterateCSSToken(token: FlatCssToken, fn: (x: any) => any): boolean { for (let i = 0, il = token.size; i < il; i++) { if (fn(token.item(i)) === false || iterateCSSToken(token.item(i), fn) === false) { return false; @@ -664,31 +665,35 @@ export function iterateCSSToken(token: CssToken, fn: (x: any) => any): boolean { /** * Returns `name` CSS property from given `rule` */ -export function getCssPropertyFromRule(rule: Rule, name: string): Property | undefined { - return rule.children.find(node => node.type === 'property' && node.name === name) as Property; +export function getCssPropertyFromRule(rule: FlatRule, name: string): FlatProperty | undefined { + return rule.children.find(node => node.type === 'property' && node.name === name) as FlatProperty; } /** * Returns css property under caret in given editor or `null` if such node cannot * be found */ -export function getCssPropertyFromDocument(editor: vscode.TextEditor, position: vscode.Position): Property | null { - const rootNode = parseDocument(editor.document); - const node = getNode(rootNode, position, true); +export function getCssPropertyFromDocument(editor: vscode.TextEditor, position: vscode.Position): FlatProperty | null { + const document = editor.document; + const rootNode = getRootNode(document, true); + const offset = document.offsetAt(position); + const node = getFlatNode(rootNode, offset, true); if (isStyleSheet(editor.document.languageId)) { - return node && node.type === 'property' ? node : null; + return node && node.type === 'property' ? node : null; } - let htmlNode = node; + const htmlNode = node; if (htmlNode && htmlNode.name === 'style' - && htmlNode.open.end.isBefore(position) - && htmlNode.close.start.isAfter(position)) { - let buffer = new DocumentStreamReader(editor.document, htmlNode.start, new vscode.Range(htmlNode.start, htmlNode.end)); - let rootNode = parseStylesheet(buffer); - const node = getNode(rootNode, position, true); - return (node && node.type === 'property') ? node : null; + && htmlNode.open && htmlNode.close + && htmlNode.open.end < offset + && htmlNode.close.start > offset) { + const buffer = ' '.repeat(htmlNode.start) + + document.getText().substring(htmlNode.start, htmlNode.end); + const innerRootNode = parseStylesheet(buffer); + const innerNode = getFlatNode(innerRootNode, offset, true); + return (innerNode && innerNode.type === 'property') ? innerNode : null; } return null; @@ -705,9 +710,7 @@ export function getEmbeddedCssNodeIfAny(document: vscode.TextDocument, currentNo if (innerRange && innerRange.contains(position)) { if (currentHtmlNode.name === 'style' && currentHtmlNode.open.end.isBefore(position) - && currentHtmlNode.close.start.isAfter(position) - - ) { + && currentHtmlNode.close.start.isAfter(position)) { let buffer = new DocumentStreamReader(document, currentHtmlNode.open.end, new vscode.Range(currentHtmlNode.open.end, currentHtmlNode.close.start)); return parseStylesheet(buffer); }