mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 21:09:43 +00:00
[html] completion proposals for embedded CSS (for #8928)
This commit is contained in:
parent
e9d40e447b
commit
f3f5435195
|
@ -6,13 +6,33 @@
|
|||
|
||||
import * as path from 'path';
|
||||
|
||||
import { languages, workspace, ExtensionContext, IndentAction } from 'vscode';
|
||||
import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient';
|
||||
import { languages, workspace, ExtensionContext, IndentAction, commands, Uri, CompletionList, EventEmitter } from 'vscode';
|
||||
import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, Position, RequestType, Protocol2Code, Code2Protocol } from 'vscode-languageclient';
|
||||
import { CompletionList as LSCompletionList } from 'vscode-languageserver-types';
|
||||
import { EMPTY_ELEMENTS } from './htmlEmptyTagsShared';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
let localize = nls.loadMessageBundle();
|
||||
|
||||
interface EmbeddedCompletionParams {
|
||||
uri: string;
|
||||
embeddedLanguageId: string;
|
||||
position: Position;
|
||||
}
|
||||
|
||||
namespace EmbeddedCompletionRequest {
|
||||
export const type: RequestType<EmbeddedCompletionParams, LSCompletionList, 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'; } };
|
||||
}
|
||||
|
||||
export function activate(context: ExtensionContext) {
|
||||
|
||||
// The server is implemented in node
|
||||
|
@ -43,12 +63,50 @@ export function activate(context: ExtensionContext) {
|
|||
// Create the language client and start the client.
|
||||
let client = new LanguageClient('html', localize('htmlserver.name', 'HTML Language Server'), serverOptions, clientOptions);
|
||||
|
||||
let embeddedContentChanged = new EventEmitter<Uri>();
|
||||
|
||||
client.onRequest(EmbeddedCompletionRequest.type, params => {
|
||||
let position = Protocol2Code.asPosition(params.position);
|
||||
let virtualURI = Uri.parse('html-embedded://' + params.embeddedLanguageId + '/' + encodeURIComponent(params.uri) + '.' + params.embeddedLanguageId);
|
||||
|
||||
embeddedContentChanged.fire(virtualURI);
|
||||
return workspace.openTextDocument(virtualURI).then(_ => {
|
||||
return commands.executeCommand<CompletionList>('vscode.executeCompletionItemProvider', virtualURI, position).then(completionList => {
|
||||
if (completionList) {
|
||||
return {
|
||||
isIncomplete: completionList.isIncomplete,
|
||||
items: completionList.items.map(Code2Protocol.asCompletionItem)
|
||||
};
|
||||
}
|
||||
return { isIncomplete: true, items: [] };
|
||||
}, error => {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}, error => {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
let disposable = client.start();
|
||||
|
||||
// Push the disposable to the context's subscriptions so that the
|
||||
// client can be deactivated on extension deactivation
|
||||
context.subscriptions.push(disposable);
|
||||
|
||||
|
||||
context.subscriptions.push(workspace.registerTextDocumentContentProvider('html-embedded', {
|
||||
provideTextDocumentContent: (uri, ct) => {
|
||||
if (uri.scheme === 'html-embedded') {
|
||||
let languageId = uri.authority;
|
||||
let path = uri.path.substring(1, uri.path.length - languageId.length - 1); // remove leading '/' and new file extension
|
||||
let documentURI = decodeURIComponent(path);
|
||||
return client.sendRequest(EmbeddedContentRequest.type, { uri: documentURI, embeddedLanguageId: languageId });
|
||||
}
|
||||
return '';
|
||||
},
|
||||
onDidChange: embeddedContentChanged.event
|
||||
}));
|
||||
|
||||
languages.setLanguageConfiguration('html', {
|
||||
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
|
||||
onEnterRules: [
|
||||
|
|
4
extensions/html/server/npm-shrinkwrap.json
generated
4
extensions/html/server/npm-shrinkwrap.json
generated
|
@ -3,9 +3,9 @@
|
|||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"vscode-html-languageservice": {
|
||||
"version": "1.0.0-next.6",
|
||||
"version": "1.0.0-next.8",
|
||||
"from": "vscode-html-languageservice@next",
|
||||
"resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-1.0.0-next.6.tgz"
|
||||
"resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-1.0.0-next.8.tgz"
|
||||
},
|
||||
"vscode-jsonrpc": {
|
||||
"version": "2.4.0",
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"node": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-html-languageservice": "^1.0.0-next.6",
|
||||
"vscode-html-languageservice": "^1.0.0-next.8",
|
||||
"vscode-languageserver": "^2.6.0-next.3",
|
||||
"vscode-nls": "^1.0.4",
|
||||
"vscode-uri": "^1.0.0"
|
||||
|
|
91
extensions/html/server/src/embeddedSupport.ts
Normal file
91
extensions/html/server/src/embeddedSupport.ts
Normal 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;
|
||||
}
|
|
@ -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 => {
|
||||
|
|
79
extensions/html/server/src/test/embedded.test.ts
Normal file
79
extensions/html/server/src/test/embedded.test.ts
Normal 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; ');
|
||||
});
|
||||
|
||||
});
|
|
@ -1,3 +1,3 @@
|
|||
--ui tdd
|
||||
--useColors true
|
||||
./out/service/test
|
||||
./out/test
|
Loading…
Reference in a new issue