From 8a85fc03993bab693d9ae071fea11467d1a8797c Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 14 Nov 2016 11:34:33 +0100 Subject: [PATCH] [html] add css support to html extension as library --- extensions/css/client/src/colorDecorators.ts | 4 +- .../css/client/src/embeddedContentUri.ts | 27 --- extensions/css/server/package.json | 3 +- extensions/css/server/src/cssServerMain.ts | 6 +- .../css/server/src/embeddedContentUri.ts | 27 --- extensions/html/client/src/colorDecorators.ts | 95 +++++++++ .../client/src/embeddedContentDocuments.ts | 124 ------------ .../html/client/src/embeddedContentUri.ts | 27 --- extensions/html/client/src/htmlMain.ts | 81 ++------ extensions/html/package.json | 2 +- extensions/html/server/package.json | 8 +- extensions/html/server/src/embeddedSupport.ts | 10 +- extensions/html/server/src/htmlServerMain.ts | 189 ++++++------------ extensions/html/server/src/languageModes.ts | 164 +++++++++++++++ .../html/server/src/test/embedded.test.ts | 28 +-- .../src/test/embeddedContentUri.test.ts | 27 --- .../server/src/test/embeddedContentUri.ts | 27 --- 17 files changed, 357 insertions(+), 492 deletions(-) delete mode 100644 extensions/css/client/src/embeddedContentUri.ts delete mode 100644 extensions/css/server/src/embeddedContentUri.ts create mode 100644 extensions/html/client/src/colorDecorators.ts delete mode 100644 extensions/html/client/src/embeddedContentDocuments.ts delete mode 100644 extensions/html/client/src/embeddedContentUri.ts create mode 100644 extensions/html/server/src/languageModes.ts delete mode 100644 extensions/html/server/src/test/embeddedContentUri.test.ts delete mode 100644 extensions/html/server/src/test/embeddedContentUri.ts diff --git a/extensions/css/client/src/colorDecorators.ts b/extensions/css/client/src/colorDecorators.ts index cca08522d69..5afe66b6c5d 100644 --- a/extensions/css/client/src/colorDecorators.ts +++ b/extensions/css/client/src/colorDecorators.ts @@ -5,7 +5,6 @@ 'use strict'; import { window, workspace, DecorationOptions, DecorationRenderOptions, Disposable, Range, TextDocument, TextEditor } from 'vscode'; -import { isEmbeddedContentUri, getHostDocumentUri } from './embeddedContentUri'; const MAX_DECORATORS = 500; @@ -64,9 +63,8 @@ export function activateColorDecorations(decoratorProvider: (uri: string) => The if (triggerUpdate) { pendingUpdateRequests[documentUriStr] = setTimeout(() => { // check if the document is in use by an active editor - let contentHostUri = isEmbeddedContentUri(documentUri) ? getHostDocumentUri(documentUri) : documentUriStr; window.visibleTextEditors.forEach(editor => { - if (editor.document && contentHostUri === editor.document.uri.toString()) { + if (editor.document && documentUriStr === editor.document.uri.toString()) { updateDecorationForEditor(editor, documentUriStr); } }); diff --git a/extensions/css/client/src/embeddedContentUri.ts b/extensions/css/client/src/embeddedContentUri.ts deleted file mode 100644 index 3d7b012a0fd..00000000000 --- a/extensions/css/client/src/embeddedContentUri.ts +++ /dev/null @@ -1,27 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { Uri } from 'vscode'; - -export const EMBEDDED_CONTENT_SCHEME = 'embedded-content'; - -export function isEmbeddedContentUri(virtualDocumentUri: Uri): boolean { - return virtualDocumentUri.scheme === EMBEDDED_CONTENT_SCHEME; -} - -export function getEmbeddedContentUri(parentDocumentUri: string, embeddedLanguageId: string): Uri { - return new Uri().with({ scheme: EMBEDDED_CONTENT_SCHEME, authority: embeddedLanguageId, path: '/' + encodeURIComponent(parentDocumentUri) + '.' + embeddedLanguageId }); -}; - -export function getHostDocumentUri(virtualDocumentUri: Uri): string { - let languageId = virtualDocumentUri.authority; - let path = virtualDocumentUri.path.substring(1, virtualDocumentUri.path.length - languageId.length - 1); // remove leading '/' and new file extension - return decodeURIComponent(path); -}; - -export function getEmbeddedLanguageId(virtualDocumentUri: Uri): string { - return virtualDocumentUri.authority; -} \ No newline at end of file diff --git a/extensions/css/server/package.json b/extensions/css/server/package.json index b008f217cb0..6c562f095da 100644 --- a/extensions/css/server/package.json +++ b/extensions/css/server/package.json @@ -9,8 +9,7 @@ }, "dependencies": { "vscode-css-languageservice": "^1.1.0", - "vscode-languageserver": "^2.4.0-next.12", - "vscode-uri": "^1.0.0" + "vscode-languageserver": "^2.4.0-next.12" }, "scripts": { "compile": "gulp compile-extension:css-server", diff --git a/extensions/css/server/src/cssServerMain.ts b/extensions/css/server/src/cssServerMain.ts index 14b17a4f9cf..e1711ffaa9e 100644 --- a/extensions/css/server/src/cssServerMain.ts +++ b/extensions/css/server/src/cssServerMain.ts @@ -11,8 +11,6 @@ import { import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageSettings, LanguageService, Stylesheet } from 'vscode-css-languageservice'; import { getLanguageModelCache } from './languageModelCache'; -import Uri from 'vscode-uri'; -import { isEmbeddedContentUri, getHostDocumentUri } from './embeddedContentUri'; namespace ColorSymbolRequest { export const type: RequestType = { get method() { return 'css/colorSymbols'; } }; @@ -127,9 +125,7 @@ function validateTextDocument(textDocument: TextDocument): void { let stylesheet = stylesheets.get(textDocument); let diagnostics = getLanguageService(textDocument).doValidation(textDocument, stylesheet); // Send the computed diagnostics to VSCode. - let uri = Uri.parse(textDocument.uri); - let diagnosticsTarget = isEmbeddedContentUri(uri) ? getHostDocumentUri(uri) : textDocument.uri; - connection.sendDiagnostics({ uri: diagnosticsTarget, diagnostics }); + connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); } connection.onCompletion(textDocumentPosition => { diff --git a/extensions/css/server/src/embeddedContentUri.ts b/extensions/css/server/src/embeddedContentUri.ts deleted file mode 100644 index 8a305a85908..00000000000 --- a/extensions/css/server/src/embeddedContentUri.ts +++ /dev/null @@ -1,27 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 Uri from 'vscode-uri'; - -export const EMBEDDED_CONTENT_SCHEME = 'embedded-content'; - -export function isEmbeddedContentUri(virtualDocumentUri: Uri): boolean { - return virtualDocumentUri.scheme === EMBEDDED_CONTENT_SCHEME; -} - -export function getEmbeddedContentUri(parentDocumentUri: string, embeddedLanguageId: string): Uri { - return Uri.from({ scheme: EMBEDDED_CONTENT_SCHEME, authority: embeddedLanguageId, path: '/' + encodeURIComponent(parentDocumentUri) + '.' + embeddedLanguageId }); -}; - -export function getHostDocumentUri(virtualDocumentUri: Uri): string { - let languageId = virtualDocumentUri.authority; - let path = virtualDocumentUri.path.substring(1, virtualDocumentUri.path.length - languageId.length - 1); // remove leading '/' and new file extension - return decodeURIComponent(path); -}; - -export function getEmbeddedLanguageId(virtualDocumentUri: Uri): string { - return virtualDocumentUri.authority; -} \ No newline at end of file diff --git a/extensions/html/client/src/colorDecorators.ts b/extensions/html/client/src/colorDecorators.ts new file mode 100644 index 00000000000..5afe66b6c5d --- /dev/null +++ b/extensions/html/client/src/colorDecorators.ts @@ -0,0 +1,95 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { window, workspace, DecorationOptions, DecorationRenderOptions, Disposable, Range, TextDocument, TextEditor } from 'vscode'; + +const MAX_DECORATORS = 500; + +let decorationType: DecorationRenderOptions = { + before: { + contentText: ' ', + border: 'solid 0.1em #000', + margin: '0.1em 0.2em 0 0.2em', + width: '0.8em', + height: '0.8em' + }, + dark: { + before: { + border: 'solid 0.1em #eee' + } + } +}; + +export function activateColorDecorations(decoratorProvider: (uri: string) => Thenable, supportedLanguages: { [id: string]: boolean }): Disposable { + + let disposables: Disposable[] = []; + + let colorsDecorationType = window.createTextEditorDecorationType(decorationType); + disposables.push(colorsDecorationType); + + let pendingUpdateRequests: { [key: string]: NodeJS.Timer; } = {}; + + // we care about all visible editors + window.visibleTextEditors.forEach(editor => { + if (editor.document) { + triggerUpdateDecorations(editor.document); + } + }); + // to get visible one has to become active + window.onDidChangeActiveTextEditor(editor => { + if (editor) { + triggerUpdateDecorations(editor.document); + } + }, null, disposables); + + workspace.onDidChangeTextDocument(event => triggerUpdateDecorations(event.document), null, disposables); + workspace.onDidOpenTextDocument(triggerUpdateDecorations, null, disposables); + workspace.onDidCloseTextDocument(triggerUpdateDecorations, null, disposables); + + workspace.textDocuments.forEach(triggerUpdateDecorations); + + function triggerUpdateDecorations(document: TextDocument) { + let triggerUpdate = supportedLanguages[document.languageId]; + let documentUri = document.uri; + let documentUriStr = documentUri.toString(); + let timeout = pendingUpdateRequests[documentUriStr]; + if (typeof timeout !== 'undefined') { + clearTimeout(timeout); + triggerUpdate = true; // force update, even if languageId is not supported (anymore) + } + if (triggerUpdate) { + pendingUpdateRequests[documentUriStr] = setTimeout(() => { + // check if the document is in use by an active editor + window.visibleTextEditors.forEach(editor => { + if (editor.document && documentUriStr === editor.document.uri.toString()) { + updateDecorationForEditor(editor, documentUriStr); + } + }); + delete pendingUpdateRequests[documentUriStr]; + }, 500); + } + } + + function updateDecorationForEditor(editor: TextEditor, contentUri: string) { + let document = editor.document; + decoratorProvider(contentUri).then(ranges => { + let decorations = ranges.slice(0, MAX_DECORATORS).map(range => { + let color = document.getText(range); + return { + range: range, + renderOptions: { + before: { + backgroundColor: color + } + } + }; + }); + editor.setDecorations(colorsDecorationType, decorations); + }); + } + + return Disposable.from(...disposables); +} diff --git a/extensions/html/client/src/embeddedContentDocuments.ts b/extensions/html/client/src/embeddedContentDocuments.ts deleted file mode 100644 index 6db5fe3b5db..00000000000 --- a/extensions/html/client/src/embeddedContentDocuments.ts +++ /dev/null @@ -1,124 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { workspace, Uri, EventEmitter, Disposable, TextDocument } from 'vscode'; -import { LanguageClient, RequestType, NotificationType } from 'vscode-languageclient'; -import { getEmbeddedContentUri, getEmbeddedLanguageId, getHostDocumentUri, isEmbeddedContentUri, EMBEDDED_CONTENT_SCHEME } from './embeddedContentUri'; - -interface EmbeddedContentParams { - uri: string; - embeddedLanguageId: string; -} - -interface EmbeddedContent { - content: string; - version: number; -} - -namespace EmbeddedContentRequest { - export const type: RequestType = { get method() { return 'embedded/content'; } }; -} - -export interface EmbeddedDocuments extends Disposable { - getEmbeddedContentUri: (parentDocumentUri: string, embeddedLanguageId: string) => Uri; - openEmbeddedContentDocument: (embeddedContentUri: Uri, expectedVersion: number) => Thenable; -} - -interface EmbeddedContentChangedParams { - uri: string; - version: number; - embeddedLanguageIds: string[]; -} - -namespace EmbeddedContentChangedNotification { - export const type: NotificationType = { get method() { return 'embedded/contentchanged'; } }; -} - -export function initializeEmbeddedContentDocuments(parentDocumentSelector: string[], embeddedLanguages: { [languageId: string]: boolean }, client: LanguageClient): EmbeddedDocuments { - let toDispose: Disposable[] = []; - - let embeddedContentChanged = new EventEmitter(); - - // remember all open virtual documents with the version of the content - let openVirtualDocuments: { [uri: string]: number } = {}; - - // documents are closed after a time out or when collected. - toDispose.push(workspace.onDidCloseTextDocument(d => { - if (isEmbeddedContentUri(d.uri)) { - delete openVirtualDocuments[d.uri.toString()]; - } - })); - - // virtual document provider - toDispose.push(workspace.registerTextDocumentContentProvider(EMBEDDED_CONTENT_SCHEME, { - provideTextDocumentContent: uri => { - if (isEmbeddedContentUri(uri)) { - let contentRequestParms = { uri: getHostDocumentUri(uri), embeddedLanguageId: getEmbeddedLanguageId(uri) }; - return client.sendRequest(EmbeddedContentRequest.type, contentRequestParms).then(content => { - if (content) { - openVirtualDocuments[uri.toString()] = content.version; - return content.content; - } else { - delete openVirtualDocuments[uri.toString()]; - return ''; - } - }); - } - return ''; - }, - onDidChange: embeddedContentChanged.event - })); - - // diagnostics for embedded contents - client.onNotification(EmbeddedContentChangedNotification.type, p => { - for (let languageId in embeddedLanguages) { - if (p.embeddedLanguageIds.indexOf(languageId) !== -1) { - // open the document so that validation is triggered in the embedded mode - let virtualUri = getEmbeddedContentUri(p.uri, languageId); - openEmbeddedContentDocument(virtualUri, p.version); - } - } - }); - - function ensureContentUpdated(virtualURI: Uri, expectedVersion: number) { - let virtualURIString = virtualURI.toString(); - let virtualDocVersion = openVirtualDocuments[virtualURIString]; - if (isDefined(virtualDocVersion) && virtualDocVersion !== expectedVersion) { - return new Promise((resolve, reject) => { - let subscription = workspace.onDidChangeTextDocument(d => { - if (d.document.uri.toString() === virtualURIString) { - subscription.dispose(); - resolve(); - } - }); - embeddedContentChanged.fire(virtualURI); - }); - } - return Promise.resolve(); - }; - - function openEmbeddedContentDocument(virtualURI: Uri, expectedVersion: number): Thenable { - return ensureContentUpdated(virtualURI, expectedVersion).then(_ => { - return workspace.openTextDocument(virtualURI).then(document => { - if (expectedVersion === openVirtualDocuments[virtualURI.toString()]) { - return document; - } - return void 0; - }); - }); - }; - - return { - getEmbeddedContentUri, - openEmbeddedContentDocument, - dispose: Disposable.from(...toDispose).dispose - }; - -} - -function isDefined(o: any) { - return typeof o !== 'undefined'; -} \ No newline at end of file diff --git a/extensions/html/client/src/embeddedContentUri.ts b/extensions/html/client/src/embeddedContentUri.ts deleted file mode 100644 index 3d7b012a0fd..00000000000 --- a/extensions/html/client/src/embeddedContentUri.ts +++ /dev/null @@ -1,27 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { Uri } from 'vscode'; - -export const EMBEDDED_CONTENT_SCHEME = 'embedded-content'; - -export function isEmbeddedContentUri(virtualDocumentUri: Uri): boolean { - return virtualDocumentUri.scheme === EMBEDDED_CONTENT_SCHEME; -} - -export function getEmbeddedContentUri(parentDocumentUri: string, embeddedLanguageId: string): Uri { - return new Uri().with({ scheme: EMBEDDED_CONTENT_SCHEME, authority: embeddedLanguageId, path: '/' + encodeURIComponent(parentDocumentUri) + '.' + embeddedLanguageId }); -}; - -export function getHostDocumentUri(virtualDocumentUri: Uri): string { - let languageId = virtualDocumentUri.authority; - let path = virtualDocumentUri.path.substring(1, virtualDocumentUri.path.length - languageId.length - 1); // remove leading '/' and new file extension - return decodeURIComponent(path); -}; - -export function getEmbeddedLanguageId(virtualDocumentUri: Uri): string { - return virtualDocumentUri.authority; -} \ No newline at end of file diff --git a/extensions/html/client/src/htmlMain.ts b/extensions/html/client/src/htmlMain.ts index 2d815dedc7a..06a5faaea49 100644 --- a/extensions/html/client/src/htmlMain.ts +++ b/extensions/html/client/src/htmlMain.ts @@ -6,35 +6,16 @@ import * as path from 'path'; -import { languages, workspace, ExtensionContext, IndentAction, commands, CompletionList, Hover } from 'vscode'; -import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, Position, RequestType, Protocol2Code, Code2Protocol } from 'vscode-languageclient'; -import { CompletionList as LSCompletionList, Hover as LSHover } from 'vscode-languageserver-types'; +import { languages, workspace, ExtensionContext, IndentAction } from 'vscode'; +import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, Range, RequestType, Protocol2Code } from 'vscode-languageclient'; import { EMPTY_ELEMENTS } from './htmlEmptyTagsShared'; -import { initializeEmbeddedContentDocuments } from './embeddedContentDocuments'; +import { activateColorDecorations } from './colorDecorators'; import * as nls from 'vscode-nls'; let localize = nls.loadMessageBundle(); -interface EmbeddedCompletionParams { - uri: string; - version: number; - embeddedLanguageId: string; - position: Position; -} - -namespace EmbeddedCompletionRequest { - export const type: RequestType = { get method() { return 'embedded/completion'; } }; -} - -interface EmbeddedHoverParams { - uri: string; - version: number; - embeddedLanguageId: string; - position: Position; -} - -namespace EmbeddedHoverRequest { - export const type: RequestType = { get method() { return 'embedded/hover'; } }; +namespace ColorSymbolRequest { + export const type: RequestType = { get method() { return 'css/colorSymbols'; } }; } export function activate(context: ExtensionContext) { @@ -67,56 +48,20 @@ 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 embeddedDocuments = initializeEmbeddedContentDocuments(documentSelector, embeddedLanguages, client); - context.subscriptions.push(embeddedDocuments); - - client.onRequest(EmbeddedCompletionRequest.type, params => { - let position = Protocol2Code.asPosition(params.position); - let virtualDocumentURI = embeddedDocuments.getEmbeddedContentUri(params.uri, params.embeddedLanguageId); - - return embeddedDocuments.openEmbeddedContentDocument(virtualDocumentURI, params.version).then(document => { - if (document) { - return commands.executeCommand('vscode.executeCompletionItemProvider', virtualDocumentURI, position).then(completionList => { - if (completionList) { - return { - isIncomplete: completionList.isIncomplete, - items: completionList.items.map(Code2Protocol.asCompletionItem) - }; - } - return { isIncomplete: true, items: [] }; - }); - } - return { isIncomplete: true, items: [] }; - }); - }); - - client.onRequest(EmbeddedHoverRequest.type, params => { - let position = Protocol2Code.asPosition(params.position); - let virtualDocumentURI = embeddedDocuments.getEmbeddedContentUri(params.uri, params.embeddedLanguageId); - return embeddedDocuments.openEmbeddedContentDocument(virtualDocumentURI, params.version).then(document => { - if (document) { - return commands.executeCommand('vscode.executeHoverProvider', virtualDocumentURI, position).then(hover => { - if (hover && hover.length > 0) { - return { - contents: hover[0].contents, - range: Code2Protocol.asRange(hover[0].range) - }; - } - return void 0; - }); - } - return void 0; - }); - }); - + let client = new LanguageClient('html', localize('htmlserver.name', 'HTML Language Server'), serverOptions, clientOptions, true); 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); + let colorRequestor = (uri: string) => { + return client.sendRequest(ColorSymbolRequest.type, uri).then(ranges => ranges.map(Protocol2Code.asRange)); + }; + disposable = activateColorDecorations(colorRequestor, { html: true, handlebars: true, razor: true }); + context.subscriptions.push(disposable); + + languages.setLanguageConfiguration('html', { wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g, onEnterRules: [ diff --git a/extensions/html/package.json b/extensions/html/package.json index fad83066a81..10065b96869 100644 --- a/extensions/html/package.json +++ b/extensions/html/package.json @@ -143,7 +143,7 @@ } }, "dependencies": { - "vscode-languageclient": "^2.6.0-next.1", + "vscode-languageclient": "^2.6.2", "vscode-languageserver-types": "^1.0.4", "vscode-nls": "^1.0.7" } diff --git a/extensions/html/server/package.json b/extensions/html/server/package.json index 71847c642be..7761a81fbd9 100644 --- a/extensions/html/server/package.json +++ b/extensions/html/server/package.json @@ -8,8 +8,9 @@ "node": "*" }, "dependencies": { - "vscode-html-languageservice": "^1.0.0-next.9", - "vscode-languageserver": "^2.6.0-next.3", + "vscode-html-languageservice": "^1.0.0", + "vscode-languageserver": "^2.6.1", + "vscode-css-languageservice": "^1.1.0", "vscode-nls": "^1.0.4", "vscode-uri": "^1.0.0" }, @@ -19,6 +20,7 @@ "install-service-next": "npm install vscode-html-languageservice@next -f -S", "install-service-local": "npm install ../../../../vscode-html-languageservice -f -S", "install-server-next": "npm install vscode-languageserver@next -f -S", - "install-server-local": "npm install ../../../../vscode-languageserver-node/server -f -S" + "install-server-local": "npm install ../../../../vscode-languageserver-node/server -f -S", + "test": "mocha" } } diff --git a/extensions/html/server/src/embeddedSupport.ts b/extensions/html/server/src/embeddedSupport.ts index cccef3399d1..90907100a52 100644 --- a/extensions/html/server/src/embeddedSupport.ts +++ b/extensions/html/server/src/embeddedSupport.ts @@ -16,14 +16,14 @@ export function getEmbeddedLanguageAtPosition(languageService: LanguageService, return embeddedContent.languageId; } } - return null; + return 'html'; } -export function hasEmbeddedContent(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument, embeddedLanguages: { [languageId: string]: boolean }): string[] { +export function hasEmbeddedContent(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument): string[] { let embeddedLanguageIds: { [languageId: string]: boolean } = {}; function collectEmbeddedLanguages(node: Node): void { let c = getEmbeddedContentForNode(languageService, document, node); - if (c && embeddedLanguages[c.languageId] && !isWhitespace(document.getText().substring(c.start, c.end))) { + if (c && !isWhitespace(document.getText().substring(c.start, c.end))) { embeddedLanguageIds[c.languageId] = true; } node.children.forEach(collectEmbeddedLanguages); @@ -33,7 +33,7 @@ export function hasEmbeddedContent(languageService: LanguageService, document: T return Object.keys(embeddedLanguageIds); } -export function getEmbeddedContent(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument, languageId: string): string { +export function getEmbeddedDocument(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument, languageId: string): TextDocument { let contents = []; function collectEmbeddedNodes(node: Node): void { let c = getEmbeddedContentForNode(languageService, document, node); @@ -54,7 +54,7 @@ export function getEmbeddedContent(languageService: LanguageService, document: T currentPos = c.end; } result = substituteWithWhitespace(result, currentPos, oldContent.length, oldContent); - return result; + return TextDocument.create(document.uri, languageId, document.version, result); } function substituteWithWhitespace(result, start, end, oldContent) { diff --git a/extensions/html/server/src/htmlServerMain.ts b/extensions/html/server/src/htmlServerMain.ts index 5471c064e28..6264c0734e1 100644 --- a/extensions/html/server/src/htmlServerMain.ts +++ b/extensions/html/server/src/htmlServerMain.ts @@ -4,14 +4,10 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { - createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, FormattingOptions, RequestType, NotificationType, - CompletionList, Position, Hover -} from 'vscode-languageserver'; +import { createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, RequestType } from 'vscode-languageserver'; +import { DocumentContext, TextDocument, Diagnostic, DocumentLink, Range } from 'vscode-html-languageservice'; +import { getLanguageModes } from './languageModes'; -import { HTMLDocument, getLanguageService, CompletionConfiguration, HTMLFormatConfiguration, DocumentContext, TextDocument } from 'vscode-html-languageservice'; -import { getLanguageModelCache } from './languageModelCache'; -import { getEmbeddedContent, getEmbeddedLanguageAtPosition, hasEmbeddedContent } from './embeddedSupport'; import * as url from 'url'; import * as path from 'path'; import uri from 'vscode-uri'; @@ -19,50 +15,8 @@ import uri from 'vscode-uri'; import * as nls from 'vscode-nls'; nls.config(process.env['VSCODE_NLS_CONFIG']); -interface EmbeddedCompletionParams { - uri: string; - version: number; - embeddedLanguageId: string; - position: Position; -} - -namespace EmbeddedCompletionRequest { - export const type: RequestType = { get method() { return 'embedded/completion'; } }; -} - -interface EmbeddedHoverParams { - uri: string; - version: number; - embeddedLanguageId: string; - position: Position; -} - -namespace EmbeddedHoverRequest { - export const type: RequestType = { get method() { return 'embedded/hover'; } }; -} - -interface EmbeddedContentParams { - uri: string; - embeddedLanguageId: string; -} - -interface EmbeddedContent { - content: string; - version: number; -} - -namespace EmbeddedContentRequest { - export const type: RequestType = { get method() { return 'embedded/content'; } }; -} - -interface EmbeddedContentChangedParams { - uri: string; - version: number; - embeddedLanguageIds: string[]; -} - -namespace EmbeddedContentChangedNotification { - export const type: NotificationType = { get method() { return 'embedded/contentchanged'; } }; +namespace ColorSymbolRequest { + export const type: RequestType = { get method() { return 'css/colorSymbols'; } }; } // Create a connection for the server @@ -78,22 +32,22 @@ let documents: TextDocuments = new TextDocuments(); // for open, change and close text document events documents.listen(connection); -let htmlDocuments = getLanguageModelCache(10, 60, document => getLanguageService().parseHTMLDocument(document)); +let languageModes = getLanguageModes({ 'css': true }); + documents.onDidClose(e => { - htmlDocuments.onDocumentRemoved(e.document); + languageModes.getAllModes().forEach(m => m.onDocumentRemoved(e.document)); }); connection.onShutdown(() => { - htmlDocuments.dispose(); + languageModes.getAllModes().forEach(m => m.dispose()); }); let workspacePath: string; -let embeddedLanguages: { [languageId: string]: boolean }; + // After the server has started the client sends an initilize request. The server receives // in the passed params the rootPath of the workspace plus the client capabilites connection.onInitialize((params: InitializeParams): InitializeResult => { workspacePath = params.rootPath; - embeddedLanguages = params.initializationOptions.embeddedLanguages; return { capabilities: { // Tell the client that the server works in FULL text document sync mode @@ -107,25 +61,10 @@ connection.onInitialize((params: InitializeParams): InitializeResult => { }; }); -// create the JSON language service -var languageService = getLanguageService(); - -// The settings interface describes the server relevant settings part -interface Settings { - html: LanguageSettings; -} - -interface LanguageSettings { - suggest: CompletionConfiguration; - format: HTMLFormatConfiguration; -} - -let languageSettings: LanguageSettings; // The settings have changed. Is send on server activation as well. connection.onDidChangeConfiguration((change) => { - var settings = change.settings; - languageSettings = settings.html; + languageModes.configure(change.settings); }); let pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {}; @@ -140,10 +79,7 @@ documents.onDidChangeContent(change => { // a document has closed: clear all diagnostics documents.onDidClose(event => { cleanPendingValidation(event.document); - //connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); - if (embeddedLanguages) { - connection.sendNotification(EmbeddedContentChangedNotification.type, { uri: event.document.uri, version: event.document.version, embeddedLanguageIds: [] }); - } + connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); }); function cleanPendingValidation(textDocument: TextDocument): void { @@ -163,79 +99,50 @@ function triggerValidation(textDocument: TextDocument): void { } function validateTextDocument(textDocument: TextDocument): void { - let htmlDocument = htmlDocuments.get(textDocument); - //let diagnostics = languageService.doValidation(textDocument, htmlDocument); - // Send the computed diagnostics to VSCode. - //connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); - if (embeddedLanguages) { - let embeddedLanguageIds = hasEmbeddedContent(languageService, textDocument, htmlDocument, embeddedLanguages); - let p = { uri: textDocument.uri, version: textDocument.version, embeddedLanguageIds }; - connection.sendNotification(EmbeddedContentChangedNotification.type, p); - } + let diagnostics: Diagnostic[] = []; + languageModes.getAllModesInDocument(textDocument).forEach(mode => { + if (mode.doValidation) { + diagnostics = diagnostics.concat(mode.doValidation(textDocument)); + } + }); + connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); } connection.onCompletion(textDocumentPosition => { let document = documents.get(textDocumentPosition.textDocument.uri); - let htmlDocument = htmlDocuments.get(document); - let options = languageSettings && languageSettings.suggest; - let list = languageService.doComplete(document, textDocumentPosition.position, htmlDocument, options); - if (list.items.length === 0 && embeddedLanguages) { - let embeddedLanguageId = getEmbeddedLanguageAtPosition(languageService, document, htmlDocument, textDocumentPosition.position); - if (embeddedLanguageId && embeddedLanguages[embeddedLanguageId]) { - return connection.sendRequest(EmbeddedCompletionRequest.type, { uri: document.uri, version: document.version, embeddedLanguageId, position: textDocumentPosition.position }); - } + let mode = languageModes.getModeAtPosition(document, textDocumentPosition.position); + if (mode && mode.doComplete) { + return mode.doComplete(document, textDocumentPosition.position); } - return list; + return { isIncomplete: true, items: [] }; }); connection.onHover(textDocumentPosition => { let document = documents.get(textDocumentPosition.textDocument.uri); - let htmlDocument = htmlDocuments.get(document); - let hover = languageService.doHover(document, textDocumentPosition.position, htmlDocument); - if (!hover && embeddedLanguages) { - let embeddedLanguageId = getEmbeddedLanguageAtPosition(languageService, document, htmlDocument, textDocumentPosition.position); - if (embeddedLanguageId && embeddedLanguages[embeddedLanguageId]) { - return connection.sendRequest(EmbeddedHoverRequest.type, { uri: document.uri, version: document.version, embeddedLanguageId, position: textDocumentPosition.position }); - } - } - return hover; -}); - -connection.onRequest(EmbeddedContentRequest.type, parms => { - let document = documents.get(parms.uri); - if (document) { - let htmlDocument = htmlDocuments.get(document); - return { content: getEmbeddedContent(languageService, document, htmlDocument, parms.embeddedLanguageId), version: document.version }; + let mode = languageModes.getModeAtPosition(document, textDocumentPosition.position); + if (mode && mode.doHover) { + return mode.doHover(document, textDocumentPosition.position); } - return void 0; + return null; }); connection.onDocumentHighlight(documentHighlightParams => { let document = documents.get(documentHighlightParams.textDocument.uri); - let htmlDocument = htmlDocuments.get(document); - return languageService.findDocumentHighlights(document, documentHighlightParams.position, htmlDocument); -}); - -function merge(src: any, dst: any): any { - for (var key in src) { - if (src.hasOwnProperty(key)) { - dst[key] = src[key]; - } + let mode = languageModes.getModeAtPosition(document, documentHighlightParams.position); + if (mode && mode.findDocumentHighlight) { + return mode.findDocumentHighlight(document, documentHighlightParams.position); } - return dst; -} - -function getFormattingOptions(formatParams: FormattingOptions) { - let formatSettings = languageSettings && languageSettings.format; - if (!formatSettings) { - return formatParams; - } - return merge(formatParams, merge(formatSettings, {})); -} + return []; +}); connection.onDocumentRangeFormatting(formatParams => { let document = documents.get(formatParams.textDocument.uri); - return languageService.format(document, formatParams.range, getFormattingOptions(formatParams.options)); + let startMode = languageModes.getModeAtPosition(document, formatParams.range.start); + let endMode = languageModes.getModeAtPosition(document, formatParams.range.end); + if (startMode && startMode === endMode && startMode.format) { + return startMode.format(document, formatParams.range, formatParams.options); + } + return null; }); connection.onDocumentLinks(documentLinkParam => { @@ -248,9 +155,27 @@ connection.onDocumentLinks(documentLinkParam => { return url.resolve(document.uri, ref); } }; - return languageService.findDocumentLinks(document, documentContext); + let links: DocumentLink[] = []; + languageModes.getAllModesInDocument(document).forEach(m => { + if (m.findDocumentLinks) { + links = links.concat(m.findDocumentLinks(document, documentContext)); + } + }); + return links; }); +connection.onRequest(ColorSymbolRequest.type, uri => { + let ranges: Range[] = []; + let document = documents.get(uri); + if (document) { + languageModes.getAllModesInDocument(document).forEach(m => { + if (m.findColorSymbols) { + ranges = ranges.concat(m.findColorSymbols(document)); + } + }); + } + return ranges; +}); // Listen on the connection connection.listen(); \ No newline at end of file diff --git a/extensions/html/server/src/languageModes.ts b/extensions/html/server/src/languageModes.ts new file mode 100644 index 00000000000..c5e1887c4cc --- /dev/null +++ b/extensions/html/server/src/languageModes.ts @@ -0,0 +1,164 @@ +/*--------------------------------------------------------------------------------------------- + * 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, Diagnostic, CompletionList, Hover, getLanguageService as getHTMLLanguageService, + DocumentHighlight, DocumentLink, DocumentContext, Range, TextEdit, FormattingOptions, CompletionConfiguration, HTMLFormatConfiguration +} from 'vscode-html-languageservice'; +import { getCSSLanguageService, Stylesheet } from 'vscode-css-languageservice'; + +import { getLanguageModelCache } from './languageModelCache'; +import { getEmbeddedDocument, getEmbeddedLanguageAtPosition, hasEmbeddedContent } from './embeddedSupport'; + +export interface LanguageMode { + doValidation?: (document: TextDocument) => Diagnostic[]; + doComplete?: (document: TextDocument, position: Position) => CompletionList; + doHover?: (document: TextDocument, position: Position) => Hover; + findDocumentHighlight?: (document: TextDocument, position: Position) => DocumentHighlight[]; + findDocumentLinks?: (document: TextDocument, documentContext: DocumentContext) => DocumentLink[]; + format?: (document: TextDocument, range: Range, options: FormattingOptions) => TextEdit[]; + findColorSymbols?: (document: TextDocument) => Range[]; + onDocumentRemoved(document: TextDocument): void; + dispose(): void; +} + +// The settings interface describes the server relevant settings part +export interface Settings { + html: HTMLLanguageSettings; + css: any; +} + +export interface HTMLLanguageSettings { + suggest: CompletionConfiguration; + format: HTMLFormatConfiguration; +} + +export interface LanguageModes { + getModeAtPosition(document: TextDocument, position: Position): LanguageMode; + getAllModesInDocument(document: TextDocument): LanguageMode[]; + getAllModes(): LanguageMode[]; + getMode(languageId: string): LanguageMode; + configure(options: Settings): void; +} + +export function getLanguageModes(supportedLanguages: { [languageId: string]: boolean; }): LanguageModes { + + var htmlLanguageService = getHTMLLanguageService(); + let htmlDocuments = getLanguageModelCache(10, 60, document => htmlLanguageService.parseHTMLDocument(document)); + + let modes: { [id: string]: LanguageMode } = {}; + let settings: any = {}; + + supportedLanguages['html'] = true; + modes['html'] = { + doComplete(document: TextDocument, position: Position) { + let options = settings && settings.html && settings.html.suggest; + return htmlLanguageService.doComplete(document, position, htmlDocuments.get(document), options); + }, + doHover(document: TextDocument, position: Position) { + return htmlLanguageService.doHover(document, position, htmlDocuments.get(document)); + }, + findDocumentHighlight(document: TextDocument, position: Position) { + return htmlLanguageService.findDocumentHighlights(document, position, htmlDocuments.get(document)); + }, + findDocumentLinks(document: TextDocument, documentContext: DocumentContext) { + return htmlLanguageService.findDocumentLinks(document, documentContext); + }, + format(document: TextDocument, range: Range, formatParams: FormattingOptions) { + let formatSettings = settings && settings.html && settings.html.format; + if (!formatSettings) { + formatSettings = formatParams; + } else { + formatSettings = merge(formatParams, merge(formatSettings, {})); + } + return htmlLanguageService.format(document, range, formatSettings); + }, + onDocumentRemoved(document: TextDocument) { + htmlDocuments.onDocumentRemoved(document); + }, + dispose() { + htmlDocuments.dispose(); + } + }; + if (supportedLanguages['css']) { + let cssLanguageService = getCSSLanguageService(); + let cssStylesheets = getLanguageModelCache(10, 60, document => cssLanguageService.parseStylesheet(document)); + let getEmbeddedCSSDocument = (document: TextDocument) => getEmbeddedDocument(htmlLanguageService, document, htmlDocuments.get(document), 'css'); + + modes['css'] = { + doValidation(document: TextDocument) { + let embedded = getEmbeddedCSSDocument(document); + return cssLanguageService.doValidation(embedded, cssStylesheets.get(embedded)); + }, + doComplete(document: TextDocument, position: Position) { + let embedded = getEmbeddedCSSDocument(document); + return cssLanguageService.doComplete(embedded, position, cssStylesheets.get(embedded)); + }, + doHover(document: TextDocument, position: Position) { + let embedded = getEmbeddedCSSDocument(document); + return cssLanguageService.doHover(embedded, position, cssStylesheets.get(embedded)); + }, + findDocumentHighlight(document: TextDocument, position: Position) { + let embedded = getEmbeddedCSSDocument(document); + return cssLanguageService.findDocumentHighlights(embedded, position, cssStylesheets.get(embedded)); + }, + findColorSymbols(document: TextDocument) { + let embedded = getEmbeddedCSSDocument(document); + return cssLanguageService.findColorSymbols(embedded, cssStylesheets.get(embedded)); + }, + onDocumentRemoved(document: TextDocument) { + cssStylesheets.onDocumentRemoved(document); + }, + dispose() { + cssStylesheets.dispose(); + } + }; + }; + + return { + getModeAtPosition(document: TextDocument, position: Position): LanguageMode { + let languageId = getEmbeddedLanguageAtPosition(htmlLanguageService, document, htmlDocuments.get(document), position); + if (supportedLanguages[languageId]) { + return modes[languageId]; + } + return null; + }, + getAllModesInDocument(document: TextDocument): LanguageMode[] { + let result = [modes['html']]; + let embeddedLanguageIds = hasEmbeddedContent(htmlLanguageService, document, htmlDocuments.get(document)); + for (let languageId of embeddedLanguageIds) { + if (supportedLanguages[languageId]) { + result.push(modes[languageId]); + } + } + return result; + }, + getAllModes(): LanguageMode[] { + let result = []; + for (let languageId in modes) { + if (supportedLanguages[languageId]) { + result.push(modes[languageId]); + } + } + return result; + }, + getMode(languageId: string): LanguageMode { + return modes[languageId]; + }, + configure(options: any): void { + settings = options; + } + }; +} + +function merge(src: any, dst: any): any { + for (var key in src) { + if (src.hasOwnProperty(key)) { + dst[key] = src[key]; + } + } + return dst; +} diff --git a/extensions/html/server/src/test/embedded.test.ts b/extensions/html/server/src/test/embedded.test.ts index 3bdbec2eb1b..c95685f4fea 100644 --- a/extensions/html/server/src/test/embedded.test.ts +++ b/extensions/html/server/src/test/embedded.test.ts @@ -7,11 +7,11 @@ import * as assert from 'assert'; import * as embeddedSupport from '../embeddedSupport'; import {TextDocument} from 'vscode-languageserver-types'; - -import { getLanguageService } from 'vscode-html-languageservice'; +import { getLanguageService} from 'vscode-html-languageservice'; suite('HTML Embedded Support', () => { + var htmlLanguageService = getLanguageService(); function assertEmbeddedLanguageId(value: string, expectedLanguageId: string): void { let offset = value.indexOf('|'); @@ -23,7 +23,7 @@ suite('HTML Embedded Support', () => { let ls = getLanguageService(); let htmlDoc = ls.parseHTMLDocument(document); - let languageId = embeddedSupport.getEmbeddedLanguageAtPosition(ls, document, htmlDoc, position); + let languageId = embeddedSupport.getEmbeddedLanguageAtPosition(htmlLanguageService, document, htmlDoc, position); assert.equal(languageId, expectedLanguageId); } @@ -34,18 +34,18 @@ suite('HTML Embedded Support', () => { let ls = getLanguageService(); let htmlDoc = ls.parseHTMLDocument(document); - let content = embeddedSupport.getEmbeddedContent(ls, document, htmlDoc, languageId); - assert.equal(content, expectedContent); + let content = embeddedSupport.getEmbeddedDocument(ls, document, htmlDoc, languageId); + assert.equal(content.getText(), expectedContent); } test('Styles', function (): any { - assertEmbeddedLanguageId('|', void 0); - assertEmbeddedLanguageId('', void 0); - assertEmbeddedLanguageId('foo { }', void 0); + assertEmbeddedLanguageId('|', 'html'); + assertEmbeddedLanguageId('', 'html'); + assertEmbeddedLanguageId('foo { }', 'html'); assertEmbeddedLanguageId('', 'css'); assertEmbeddedLanguageId('', 'css'); assertEmbeddedLanguageId('', 'css'); - assertEmbeddedLanguageId('