diff --git a/extensions/html/server/npm-shrinkwrap.json b/extensions/html/server/npm-shrinkwrap.json index a1f7d09a5e9..97e582e4cdb 100644 --- a/extensions/html/server/npm-shrinkwrap.json +++ b/extensions/html/server/npm-shrinkwrap.json @@ -8,9 +8,9 @@ "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-2.0.0-next.3.tgz" }, "vscode-html-languageservice": { - "version": "1.0.1-next.3", + "version": "1.0.1-next.4", "from": "vscode-html-languageservice@next", - "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-1.0.1-next.3.tgz" + "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-1.0.1-next.4.tgz" }, "vscode-jsonrpc": { "version": "2.4.0", diff --git a/extensions/html/server/package.json b/extensions/html/server/package.json index 8dbee89cebb..0b3e859b07f 100644 --- a/extensions/html/server/package.json +++ b/extensions/html/server/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "vscode-css-languageservice": "^2.0.0-next.3", - "vscode-html-languageservice": "^1.0.1-next.3", + "vscode-html-languageservice": "^1.0.1-next.4", "vscode-languageserver": "^2.6.2-next.1", "vscode-nls": "^1.0.4", "vscode-uri": "^1.0.0" diff --git a/extensions/html/server/src/modes/cssMode.ts b/extensions/html/server/src/modes/cssMode.ts index b11f8c1568a..e23f63ab5bc 100644 --- a/extensions/html/server/src/modes/cssMode.ts +++ b/extensions/html/server/src/modes/cssMode.ts @@ -17,6 +17,9 @@ export function getCSSMode(htmlLanguageService: HTMLLanguageService, htmlDocumen let getEmbeddedCSSDocument = (document: TextDocument) => getEmbeddedDocument(htmlLanguageService, document, htmlDocuments.get(document), 'css'); return { + getId() { + return 'css'; + }, configure(options: any) { cssLanguageService.configure(options && options.css); }, diff --git a/extensions/html/server/src/modes/embeddedSupport.ts b/extensions/html/server/src/modes/embeddedSupport.ts index 9608696b271..94f2f52993d 100644 --- a/extensions/html/server/src/modes/embeddedSupport.ts +++ b/extensions/html/server/src/modes/embeddedSupport.ts @@ -5,36 +5,54 @@ 'use strict'; -import { TextDocument, Position, HTMLDocument, Node, LanguageService, TokenType, Range } from 'vscode-html-languageservice'; +import { TextDocument, Position, HTMLDocument, Node, LanguageService, TokenType, Range, Scanner } from 'vscode-html-languageservice'; export interface LanguageRange extends Range { languageId: string; } +interface EmbeddedContent { languageId: string; start: number; end: number; attributeValue?: boolean; }; + export function getLanguageAtPosition(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument, position: Position): string { let offset = document.offsetAt(position); let node = htmlDocument.findNodeAt(offset); - if (node && node.children.length === 0) { + if (node) { let embeddedContent = getEmbeddedContentForNode(languageService, document, node); - if (embeddedContent && embeddedContent.start <= offset && offset <= embeddedContent.end) { - return embeddedContent.languageId; + if (embeddedContent) { + for (let c of embeddedContent) { + if (c.start <= offset && offset <= c.end) { + return c.languageId; + } + } } } return 'html'; } export function getLanguagesInContent(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument): string[] { - let embeddedLanguageIds: { [languageId: string]: boolean } = { html: true }; + let embeddedLanguageIds = ['html']; + const maxEmbbeddedLanguages = 3; function collectEmbeddedLanguages(node: Node): void { - let c = getEmbeddedContentForNode(languageService, document, node); - if (c && !isWhitespace(document.getText().substring(c.start, c.end))) { - embeddedLanguageIds[c.languageId] = true; + if (embeddedLanguageIds.length < maxEmbbeddedLanguages) { + let embeddedContent = getEmbeddedContentForNode(languageService, document, node); + if (embeddedContent) { + for (let c of embeddedContent) { + if (!isWhitespace(document.getText(), c.start, c.end)) { + if (embeddedLanguageIds.lastIndexOf(c.languageId) === -1) { + embeddedLanguageIds.push(c.languageId); + if (embeddedLanguageIds.length === maxEmbbeddedLanguages) { + return; + } + } + } + } + } + node.children.forEach(collectEmbeddedLanguages); } - node.children.forEach(collectEmbeddedLanguages); } htmlDocument.roots.forEach(collectEmbeddedLanguages); - return Object.keys(embeddedLanguageIds); + return embeddedLanguageIds; } export function getLanguagesInRange(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument, range: Range): LanguageRange[] { @@ -44,27 +62,31 @@ export function getLanguagesInRange(languageService: LanguageService, document: let rangeEndOffset = document.offsetAt(range.end); function collectEmbeddedNodes(node: Node): void { if (node.start < rangeEndOffset && node.end > currentOffset) { - let c = getEmbeddedContentForNode(languageService, document, node); - if (c && c.start < rangeEndOffset) { - let startPos = document.positionAt(c.start); - if (currentOffset < c.start) { - ranges.push({ - start: currentPos, - end: startPos, - languageId: 'html' - }); + let embeddedContent = getEmbeddedContentForNode(languageService, document, node); + if (embeddedContent) { + for (let c of embeddedContent) { + if (c.start < rangeEndOffset) { + let startPos = document.positionAt(c.start); + if (currentOffset < c.start) { + ranges.push({ + start: currentPos, + end: startPos, + languageId: 'html' + }); + } + let end = Math.min(c.end, rangeEndOffset); + let endPos = document.positionAt(end); + if (end > c.start) { + ranges.push({ + start: startPos, + end: endPos, + languageId: c.languageId + }); + } + currentOffset = end; + currentPos = endPos; + } } - let end = Math.min(c.end, rangeEndOffset); - let endPos = document.positionAt(end); - if (end > c.start) { - ranges.push({ - start: startPos, - end: endPos, - languageId: c.languageId - }); - } - currentOffset = end; - currentPos = endPos; } } node.children.forEach(collectEmbeddedNodes); @@ -82,11 +104,15 @@ export function getLanguagesInRange(languageService: LanguageService, document: } export function getEmbeddedDocument(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument, languageId: string): TextDocument { - let contents = []; + let contents: EmbeddedContent[] = []; function collectEmbeddedNodes(node: Node): void { - let c = getEmbeddedContentForNode(languageService, document, node); - if (c && c.languageId === languageId) { - contents.push(c); + let embeddedContent = getEmbeddedContentForNode(languageService, document, node); + if (embeddedContent) { + for (let c of embeddedContent) { + if (c.languageId === languageId) { + contents.push(c); + } + } } node.children.forEach(collectEmbeddedNodes); } @@ -96,18 +122,40 @@ export function getEmbeddedDocument(languageService: LanguageService, document: let currentPos = 0; let oldContent = document.getText(); let result = ''; + let lastSuffix = ''; for (let c of contents) { - result = substituteWithWhitespace(result, currentPos, c.start, oldContent); + result = substituteWithWhitespace(result, currentPos, c.start, oldContent, lastSuffix, getPrefix(c)); result += oldContent.substring(c.start, c.end); currentPos = c.end; + lastSuffix = getSuffix(c); } - result = substituteWithWhitespace(result, currentPos, oldContent.length, oldContent); + result = substituteWithWhitespace(result, currentPos, oldContent.length, oldContent, lastSuffix, ''); return TextDocument.create(document.uri, languageId, document.version, result); } -function substituteWithWhitespace(result, start, end, oldContent) { +function getPrefix(c: EmbeddedContent) { + if (c.attributeValue) { + switch (c.languageId) { + case 'css': return 'x{'; + } + } + return ''; +} +function getSuffix(c: EmbeddedContent) { + if (c.attributeValue) { + switch (c.languageId) { + case 'css': return '}'; + case 'javascript': return ';'; + } + } + return ''; +} + + +function substituteWithWhitespace(result: string, start: number, end: number, oldContent: string, before: string, after: string) { let accumulatedWS = 0; - for (let i = start; i < end; i++) { + result += before; + for (let i = start + before.length; i < end; i++) { let ch = oldContent[i]; if (ch === '\n' || ch === '\r') { // only write new lines, skip the whitespace @@ -117,12 +165,13 @@ function substituteWithWhitespace(result, start, end, oldContent) { accumulatedWS++; } } - result = append(result, ' ', accumulatedWS); + result = append(result, ' ', accumulatedWS - after.length); + result += after; return result; } function append(result: string, str: string, n: number): string { - while (n) { + while (n > 0) { if (n & 1) { result += str; } @@ -132,13 +181,13 @@ function append(result: string, str: string, n: number): string { return result; } -function getEmbeddedContentForNode(languageService: LanguageService, document: TextDocument, node: Node): { languageId: string, start: number, end: number } { +function getEmbeddedContentForNode(languageService: LanguageService, document: TextDocument, node: Node): EmbeddedContent[] { if (node.tag === 'style') { let scanner = languageService.createScanner(document.getText().substring(node.start, node.end)); let token = scanner.scan(); while (token !== TokenType.EOS) { if (token === TokenType.Styles) { - return { languageId: 'css', start: node.start + scanner.getTokenOffset(), end: node.start + scanner.getTokenEnd() }; + return [{ languageId: 'css', start: node.start + scanner.getTokenOffset(), end: node.start + scanner.getTokenEnd() }]; } token = scanner.scan(); } @@ -160,14 +209,59 @@ function getEmbeddedContentForNode(languageService: LanguageService, document: T } isTypeAttribute = false; } else if (token === TokenType.Script) { - return { languageId, start: node.start + scanner.getTokenOffset(), end: node.start + scanner.getTokenEnd() }; + return [{ languageId, start: node.start + scanner.getTokenOffset(), end: node.start + scanner.getTokenEnd() }]; } token = scanner.scan(); } + } else if (node.attributeNames) { + let scanner: Scanner; + let result; + for (let name of node.attributeNames) { + let languageId = getAttributeLanguage(name); + if (languageId) { + if (!scanner) { + scanner = languageService.createScanner(document.getText().substring(node.start, node.end)); + } + let token = scanner.scan(); + let lastAttribute; + while (token !== TokenType.EOS) { + if (token === TokenType.AttributeName) { + lastAttribute = scanner.getTokenText(); + } else if (token === TokenType.AttributeValue && lastAttribute === name) { + let start = scanner.getTokenOffset() + node.start; + let end = scanner.getTokenEnd() + node.start; + let firstChar = document.getText()[start]; + if (firstChar === '\'' || firstChar === '"') { + start++; + end--; + } + if (!result) { + result = []; + } + result.push({ languageId, start, end, attributeValue: true }); + lastAttribute = null; + break; + } + token = scanner.scan(); + } + } + } + return result; } return void 0; } -function isWhitespace(str: string) { - return str.match(/^\s*$/); +function getAttributeLanguage(attributeName: string): string { + let match = attributeName.match(/^(style)|(on\w+)$/i); + if (!match) { + return null; + } + return match[1] ? 'css' : 'javascript'; +} + +function isWhitespace(str: string, start: number, end: number): boolean { + if (start === end) { + return true; + } + return !!str.substring(start, end).match(/^\s*$/); } \ No newline at end of file diff --git a/extensions/html/server/src/modes/htmlMode.ts b/extensions/html/server/src/modes/htmlMode.ts index 03d84ad1ecf..6a32a8c3510 100644 --- a/extensions/html/server/src/modes/htmlMode.ts +++ b/extensions/html/server/src/modes/htmlMode.ts @@ -13,6 +13,9 @@ export function getHTMLMode(htmlLanguageService: HTMLLanguageService, htmlDocume let settings: any = {}; return { + getId() { + return 'html'; + }, configure(options: any) { settings = options && options.html; }, diff --git a/extensions/html/server/src/modes/javascriptMode.ts b/extensions/html/server/src/modes/javascriptMode.ts index 957db80b184..79ecda2e8c7 100644 --- a/extensions/html/server/src/modes/javascriptMode.ts +++ b/extensions/html/server/src/modes/javascriptMode.ts @@ -53,6 +53,9 @@ export function getJavascriptMode(htmlLanguageService: HTMLLanguageService, html let settings: any = {}; return { + getId() { + return 'html'; + }, configure(options: any) { settings = options && options.javascript; }, diff --git a/extensions/html/server/src/modes/languageModes.ts b/extensions/html/server/src/modes/languageModes.ts index 4e788b7bf51..c048937034b 100644 --- a/extensions/html/server/src/modes/languageModes.ts +++ b/extensions/html/server/src/modes/languageModes.ts @@ -17,6 +17,7 @@ import { getJavascriptMode } from './javascriptMode'; import { getHTMLMode } from './htmlMode'; export interface LanguageMode { + getId(); configure?: (options: any) => void; doValidation?: (document: TextDocument) => Diagnostic[]; doComplete?: (document: TextDocument, position: Position) => CompletionList; diff --git a/extensions/html/server/src/test/embedded.test.ts b/extensions/html/server/src/test/embedded.test.ts index 11a5c1d39f1..74faa7f53f0 100644 --- a/extensions/html/server/src/test/embedded.test.ts +++ b/extensions/html/server/src/test/embedded.test.ts @@ -48,12 +48,30 @@ suite('HTML Embedded Support', () => { assertLanguageId('', 'css', ' foo { } '); assertEmbeddedLanguageContent('', 'css', ' '); assertEmbeddedLanguageContent('Hello', 'css', ' foo { } foo { } '); assertEmbeddedLanguageContent('\n \n\n', 'css', '\n \n foo { } \n \n\n'); + assertEmbeddedLanguageContent('
', 'css', ' x{color: red} '); + assertEmbeddedLanguageContent('', 'css', ' x{color:red} '); }); test('Scripts', function (): any { @@ -73,9 +91,31 @@ suite('HTML Embedded Support', () => { assertLanguageId('', 'javascript'); }); + test('Scripts in attribute', function (): any { + assertLanguageId('