[html] completion proposals for embedded CSS (for #8928)

This commit is contained in:
Martin Aeschlimann
2016-10-20 10:58:18 +02:00
parent e9d40e447b
commit f3f5435195
7 changed files with 272 additions and 8 deletions

View File

@@ -0,0 +1,91 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TextDocument, Position, HTMLDocument, Node, LanguageService, TokenType } from 'vscode-html-languageservice';
export function getEmbeddedLanguageAtPosition(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) {
let embeddedContent = getEmbeddedContentForNode(languageService, document, node);
if (embeddedContent && embeddedContent.start <= offset && offset <= embeddedContent.end) {
return embeddedContent.languageId;
}
}
return null;
}
export function getEmbeddedContent(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument, languageId: string): string {
let contents = [];
function collectEmbeddedNodes(node: Node): void {
let c = getEmbeddedContentForNode(languageService, document, node);
if (c && c.languageId === languageId) {
contents.push(c);
}
node.children.forEach(collectEmbeddedNodes);
}
htmlDocument.roots.forEach(collectEmbeddedNodes);
let currentPos = 0;
let oldContent = document.getText();
let result = '';
for (let c of contents) {
result = substituteWithWhitespace(result, currentPos, c.start, oldContent);
result += oldContent.substring(c.start, c.end);
currentPos = c.end;
}
result = substituteWithWhitespace(result, currentPos, oldContent.length, oldContent);
return result;
}
function substituteWithWhitespace(result, start, end, oldContent) {
for (let i = start; i < end; i++) {
let ch = oldContent[i];
if (ch !== '\n' && ch !== '\r') {
ch = ' ';
}
result += ch;
}
return result;
}
function getEmbeddedContentForNode(languageService: LanguageService, document: TextDocument, node: Node): { languageId: string, start: number, end: number } {
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() };
}
token = scanner.scan();
}
} else if (node.tag === 'script') {
let scanner = languageService.createScanner(document.getText().substring(node.start, node.end));
let token = scanner.scan();
let isTypeAttribute = false;
let languageId = 'javascript';
while (token !== TokenType.EOS) {
if (token === TokenType.AttributeName) {
isTypeAttribute = scanner.getTokenText() === 'type';
} else if (token === TokenType.AttributeValue) {
if (isTypeAttribute) {
if (/["'](text|application)\/(java|ecma)script["']/.test(scanner.getTokenText())) {
languageId = 'javascript';
} else {
languageId = void 0;
}
}
isTypeAttribute = false;
} else if (token === TokenType.Script) {
return { languageId, start: node.start + scanner.getTokenOffset(), end: node.start + scanner.getTokenEnd() };
}
token = scanner.scan();
}
}
return void 0;
}

View File

@@ -4,10 +4,11 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import { createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, FormattingOptions } from 'vscode-languageserver';
import { createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, FormattingOptions, RequestType, CompletionList, Position } from 'vscode-languageserver';
import { HTMLDocument, getLanguageService, CompletionConfiguration, HTMLFormatConfiguration, DocumentContext } from 'vscode-html-languageservice';
import { getLanguageModelCache } from './languageModelCache';
import { getEmbeddedContent, getEmbeddedLanguageAtPosition } from './embeddedSupport';
import * as url from 'url';
import * as path from 'path';
import uri from 'vscode-uri';
@@ -15,6 +16,25 @@ import uri from 'vscode-uri';
import * as nls from 'vscode-nls';
nls.config(process.env['VSCODE_NLS_CONFIG']);
interface EmbeddedCompletionParams {
uri: string;
embeddedLanguageId: string;
position: Position;
}
namespace EmbeddedCompletionRequest {
export const type: RequestType<EmbeddedCompletionParams, CompletionList, any> = { get method() { return 'embedded/completion'; } };
}
interface EmbeddedContentParams {
uri: string;
embeddedLanguageId: string;
}
namespace EmbeddedContentRequest {
export const type: RequestType<EmbeddedContentParams, string, any> = { get method() { return 'embedded/content'; } };
}
// Create a connection for the server
let connection: IConnection = createConnection();
@@ -79,7 +99,23 @@ connection.onCompletion(textDocumentPosition => {
let document = documents.get(textDocumentPosition.textDocument.uri);
let htmlDocument = htmlDocuments.get(document);
let options = languageSettings && languageSettings.suggest;
return languageService.doComplete(document, textDocumentPosition.position, htmlDocument, options);
let list = languageService.doComplete(document, textDocumentPosition.position, htmlDocument, options);
if (list.items.length === 0) {
let embeddedLanguageId = getEmbeddedLanguageAtPosition(languageService, document, htmlDocument, textDocumentPosition.position);
if (embeddedLanguageId) {
return connection.sendRequest(EmbeddedCompletionRequest.type, { uri: document.uri, embeddedLanguageId, position: textDocumentPosition.position });
}
}
return list;
});
connection.onRequest(EmbeddedContentRequest.type, parms => {
let document = documents.get(parms.uri);
if (document) {
let htmlDocument = htmlDocuments.get(document);
return getEmbeddedContent(languageService, document, htmlDocument, parms.embeddedLanguageId);
}
return void 0;
});
connection.onDocumentHighlight(documentHighlightParams => {

View File

@@ -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.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import * as embeddedSupport from '../embeddedSupport';
import {TextDocument} from 'vscode-languageserver-types';
import { getLanguageService } from 'vscode-html-languageservice';
suite('HTML Embedded Support', () => {
function assertEmbeddedLanguageId(value: string, expectedLanguageId: string): void {
let offset = value.indexOf('|');
value = value.substr(0, offset) + value.substr(offset + 1);
let document = TextDocument.create('test://test/test.html', 'html', 0, value);
let position = document.positionAt(offset);
let ls = getLanguageService();
let htmlDoc = ls.parseHTMLDocument(document);
let languageId = embeddedSupport.getEmbeddedLanguageAtPosition(ls, document, htmlDoc, position);
assert.equal(languageId, expectedLanguageId);
}
function assertEmbeddedLanguageContent(value: string, languageId: string, expectedContent: string): void {
let document = TextDocument.create('test://test/test.html', 'html', 0, value);
let ls = getLanguageService();
let htmlDoc = ls.parseHTMLDocument(document);
let content = embeddedSupport.getEmbeddedContent(ls, document, htmlDoc, languageId);
assert.equal(content, expectedContent);
}
test('Styles', function (): any {
assertEmbeddedLanguageId('|<html><style>foo { }</style></html>', void 0);
assertEmbeddedLanguageId('<html|><style>foo { }</style></html>', void 0);
assertEmbeddedLanguageId('<html><st|yle>foo { }</style></html>', void 0);
assertEmbeddedLanguageId('<html><style>|foo { }</style></html>', 'css');
assertEmbeddedLanguageId('<html><style>foo| { }</style></html>', 'css');
assertEmbeddedLanguageId('<html><style>foo { }|</style></html>', 'css');
assertEmbeddedLanguageId('<html><style>foo { }</sty|le></html>', void 0);
});
test('Style content', function (): any {
assertEmbeddedLanguageContent('<html><style>foo { }</style></html>', 'css', ' foo { } ');
assertEmbeddedLanguageContent('<html><script>var i = 0;</script></html>', 'css', ' ');
assertEmbeddedLanguageContent('<html><style>foo { }</style>Hello<style>foo { }</style></html>', 'css', ' foo { } foo { } ');
});
test('Scripts', function (): any {
assertEmbeddedLanguageId('|<html><script>var i = 0;</script></html>', void 0);
assertEmbeddedLanguageId('<html|><script>var i = 0;</script></html>', void 0);
assertEmbeddedLanguageId('<html><scr|ipt>var i = 0;</script></html>', void 0);
assertEmbeddedLanguageId('<html><script>|var i = 0;</script></html>', 'javascript');
assertEmbeddedLanguageId('<html><script>var| i = 0;</script></html>', 'javascript');
assertEmbeddedLanguageId('<html><script>var i = 0;|</script></html>', 'javascript');
assertEmbeddedLanguageId('<html><script>var i = 0;</scr|ipt></html>', void 0);
assertEmbeddedLanguageId('<script type="text/javascript">var| i = 0;</script>', 'javascript');
assertEmbeddedLanguageId('<script type="text/ecmascript">var| i = 0;</script>', 'javascript');
assertEmbeddedLanguageId('<script type="application/javascript">var| i = 0;</script>', 'javascript');
assertEmbeddedLanguageId('<script type="application/ecmascript">var| i = 0;</script>', 'javascript');
assertEmbeddedLanguageId('<script type="application/typescript">var| i = 0;</script>', void 0);
assertEmbeddedLanguageId('<script type=\'text/javascript\'>var| i = 0;</script>', 'javascript');
});
test('Script content', function (): any {
assertEmbeddedLanguageContent('<html><script>var i = 0;</script></html>', 'javascript', ' var i = 0; ');
assertEmbeddedLanguageContent('<script type="text/javascript">var i = 0;</script>', 'javascript', ' var i = 0; ');
});
});