diff --git a/extensions/css/server/npm-shrinkwrap.json b/extensions/css/server/npm-shrinkwrap.json index 3de9c5957ee..e2b7c2a7082 100644 --- a/extensions/css/server/npm-shrinkwrap.json +++ b/extensions/css/server/npm-shrinkwrap.json @@ -33,6 +33,11 @@ "version": "1.0.7", "from": "vscode-nls@>=1.0.4 <2.0.0", "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-1.0.7.tgz" + }, + "vscode-uri": { + "version": "1.0.0", + "from": "vscode-uri@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.0.tgz" } } } diff --git a/extensions/css/server/package.json b/extensions/css/server/package.json index 6c562f095da..b008f217cb0 100644 --- a/extensions/css/server/package.json +++ b/extensions/css/server/package.json @@ -9,7 +9,8 @@ }, "dependencies": { "vscode-css-languageservice": "^1.1.0", - "vscode-languageserver": "^2.4.0-next.12" + "vscode-languageserver": "^2.4.0-next.12", + "vscode-uri": "^1.0.0" }, "scripts": { "compile": "gulp compile-extension:css-server", diff --git a/extensions/css/server/src/cssServerMain.ts b/extensions/css/server/src/cssServerMain.ts index 32a6523a751..275a40750d4 100644 --- a/extensions/css/server/src/cssServerMain.ts +++ b/extensions/css/server/src/cssServerMain.ts @@ -11,6 +11,8 @@ 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'; } }; @@ -125,7 +127,9 @@ function validateTextDocument(textDocument: TextDocument): void { let stylesheet = stylesheets.get(textDocument); let diagnostics = getLanguageService(textDocument).doValidation(textDocument, stylesheet); // Send the computed diagnostics to VSCode. - connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); + let uri = Uri.parse(textDocument.uri); + let diagnosticsTarget = isEmbeddedContentUri(uri) ? getHostDocumentUri(uri) : textDocument.uri; + connection.sendDiagnostics({ uri: diagnosticsTarget, diagnostics }); } connection.onCompletion(textDocumentPosition => { diff --git a/extensions/css/server/src/embeddedContentUri.ts b/extensions/css/server/src/embeddedContentUri.ts new file mode 100644 index 00000000000..c2751a0825f --- /dev/null +++ b/extensions/css/server/src/embeddedContentUri.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * 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.parse(EMBEDDED_CONTENT_SCHEME + '://' + embeddedLanguageId + '/' + 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/embeddedContentDocuments.ts b/extensions/html/client/src/embeddedContentDocuments.ts index 71e641cbdaa..6db5fe3b5db 100644 --- a/extensions/html/client/src/embeddedContentDocuments.ts +++ b/extensions/html/client/src/embeddedContentDocuments.ts @@ -5,8 +5,8 @@ 'use strict'; import { workspace, Uri, EventEmitter, Disposable, TextDocument } from 'vscode'; -import { LanguageClient, RequestType } from 'vscode-languageclient'; - +import { LanguageClient, RequestType, NotificationType } from 'vscode-languageclient'; +import { getEmbeddedContentUri, getEmbeddedLanguageId, getHostDocumentUri, isEmbeddedContentUri, EMBEDDED_CONTENT_SCHEME } from './embeddedContentUri'; interface EmbeddedContentParams { uri: string; @@ -23,12 +23,21 @@ namespace EmbeddedContentRequest { } export interface EmbeddedDocuments extends Disposable { - getVirtualDocumentUri: (parentDocumentUri: string, embeddedLanguageId: string) => Uri; - openVirtualDocument: (embeddedContentUri: Uri, expectedVersion: number) => Thenable; + getEmbeddedContentUri: (parentDocumentUri: string, embeddedLanguageId: string) => Uri; + openEmbeddedContentDocument: (embeddedContentUri: Uri, expectedVersion: number) => Thenable; } +interface EmbeddedContentChangedParams { + uri: string; + version: number; + embeddedLanguageIds: string[]; +} -export function initializeEmbeddedContentDocuments(embeddedScheme: string, client: LanguageClient): EmbeddedDocuments { +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(); @@ -38,16 +47,16 @@ export function initializeEmbeddedContentDocuments(embeddedScheme: string, clien // documents are closed after a time out or when collected. toDispose.push(workspace.onDidCloseTextDocument(d => { - if (d.uri.scheme === embeddedScheme) { + if (isEmbeddedContentUri(d.uri)) { delete openVirtualDocuments[d.uri.toString()]; } })); // virtual document provider - toDispose.push(workspace.registerTextDocumentContentProvider(embeddedScheme, { + toDispose.push(workspace.registerTextDocumentContentProvider(EMBEDDED_CONTENT_SCHEME, { provideTextDocumentContent: uri => { - if (uri.scheme === embeddedScheme) { - let contentRequestParms = { uri: getParentDocumentUri(uri), embeddedLanguageId: getEmbeddedLanguageId(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; @@ -63,19 +72,16 @@ export function initializeEmbeddedContentDocuments(embeddedScheme: string, clien onDidChange: embeddedContentChanged.event })); - function getVirtualDocumentUri(parentDocumentUri: string, embeddedLanguageId: string) { - return Uri.parse(embeddedScheme + '://' + embeddedLanguageId + '/' + encodeURIComponent(parentDocumentUri) + '.' + embeddedLanguageId); - }; - - function getParentDocumentUri(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); - }; - - function getEmbeddedLanguageId(virtualDocumentUri: Uri): string { - return virtualDocumentUri.authority; - } + // 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(); @@ -94,7 +100,7 @@ export function initializeEmbeddedContentDocuments(embeddedScheme: string, clien return Promise.resolve(); }; - function openVirtualDocument(virtualURI: Uri, expectedVersion: number): Thenable { + function openEmbeddedContentDocument(virtualURI: Uri, expectedVersion: number): Thenable { return ensureContentUpdated(virtualURI, expectedVersion).then(_ => { return workspace.openTextDocument(virtualURI).then(document => { if (expectedVersion === openVirtualDocuments[virtualURI.toString()]) { @@ -106,8 +112,8 @@ export function initializeEmbeddedContentDocuments(embeddedScheme: string, clien }; return { - getVirtualDocumentUri, - openVirtualDocument, + getEmbeddedContentUri, + openEmbeddedContentDocument, dispose: Disposable.from(...toDispose).dispose }; diff --git a/extensions/html/client/src/embeddedContentUri.ts b/extensions/html/client/src/embeddedContentUri.ts new file mode 100644 index 00000000000..8fccadaf83e --- /dev/null +++ b/extensions/html/client/src/embeddedContentUri.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * 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 Uri.parse(EMBEDDED_CONTENT_SCHEME + '://' + embeddedLanguageId + '/' + 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 e7e1363cab0..2d815dedc7a 100644 --- a/extensions/html/client/src/htmlMain.ts +++ b/extensions/html/client/src/htmlMain.ts @@ -51,17 +51,17 @@ export function activate(context: ExtensionContext) { debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } }; + let documentSelector = ['html', 'handlebars', 'razor']; + let embeddedLanguages = { 'css': true }; + // Options to control the language client let clientOptions: LanguageClientOptions = { - // Register the server for json documents - documentSelector: ['html', 'handlebars', 'razor'], + documentSelector, synchronize: { - // Synchronize the setting section 'html' to the server - configurationSection: ['html'], + configurationSection: ['html'], // Synchronize the setting section 'html' to the server }, - initializationOptions: { - embeddedLanguages: { 'css': true }, + embeddedLanguages, ['format.enable']: workspace.getConfiguration('html').get('format.enable') } }; @@ -69,14 +69,14 @@ 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('html-embedded', client); + let embeddedDocuments = initializeEmbeddedContentDocuments(documentSelector, embeddedLanguages, client); context.subscriptions.push(embeddedDocuments); client.onRequest(EmbeddedCompletionRequest.type, params => { let position = Protocol2Code.asPosition(params.position); - let virtualDocumentURI = embeddedDocuments.getVirtualDocumentUri(params.uri, params.embeddedLanguageId); + let virtualDocumentURI = embeddedDocuments.getEmbeddedContentUri(params.uri, params.embeddedLanguageId); - return embeddedDocuments.openVirtualDocument(virtualDocumentURI, params.version).then(document => { + return embeddedDocuments.openEmbeddedContentDocument(virtualDocumentURI, params.version).then(document => { if (document) { return commands.executeCommand('vscode.executeCompletionItemProvider', virtualDocumentURI, position).then(completionList => { if (completionList) { @@ -94,8 +94,8 @@ export function activate(context: ExtensionContext) { client.onRequest(EmbeddedHoverRequest.type, params => { let position = Protocol2Code.asPosition(params.position); - let virtualDocumentURI = embeddedDocuments.getVirtualDocumentUri(params.uri, params.embeddedLanguageId); - return embeddedDocuments.openVirtualDocument(virtualDocumentURI, params.version).then(document => { + 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) { diff --git a/extensions/html/server/src/embeddedSupport.ts b/extensions/html/server/src/embeddedSupport.ts index 8ef212403ab..cccef3399d1 100644 --- a/extensions/html/server/src/embeddedSupport.ts +++ b/extensions/html/server/src/embeddedSupport.ts @@ -19,6 +19,20 @@ export function getEmbeddedLanguageAtPosition(languageService: LanguageService, return null; } +export function hasEmbeddedContent(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument, embeddedLanguages: { [languageId: string]: boolean }): 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))) { + embeddedLanguageIds[c.languageId] = true; + } + node.children.forEach(collectEmbeddedLanguages); + } + + htmlDocument.roots.forEach(collectEmbeddedLanguages); + return Object.keys(embeddedLanguageIds); +} + export function getEmbeddedContent(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument, languageId: string): string { let contents = []; function collectEmbeddedNodes(node: Node): void { @@ -104,4 +118,8 @@ function getEmbeddedContentForNode(languageService: LanguageService, document: T } } return void 0; +} + +function isWhitespace(str: string) { + return str.match(/^\s*$/); } \ No newline at end of file diff --git a/extensions/html/server/src/htmlServerMain.ts b/extensions/html/server/src/htmlServerMain.ts index a914e1b3e05..95a35bda32f 100644 --- a/extensions/html/server/src/htmlServerMain.ts +++ b/extensions/html/server/src/htmlServerMain.ts @@ -4,11 +4,14 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, FormattingOptions, RequestType, CompletionList, Position, Hover } from 'vscode-languageserver'; +import { + createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, FormattingOptions, RequestType, NotificationType, + CompletionList, Position, Hover +} from 'vscode-languageserver'; -import { HTMLDocument, getLanguageService, CompletionConfiguration, HTMLFormatConfiguration, DocumentContext } from 'vscode-html-languageservice'; +import { HTMLDocument, getLanguageService, CompletionConfiguration, HTMLFormatConfiguration, DocumentContext, TextDocument } from 'vscode-html-languageservice'; import { getLanguageModelCache } from './languageModelCache'; -import { getEmbeddedContent, getEmbeddedLanguageAtPosition } from './embeddedSupport'; +import { getEmbeddedContent, getEmbeddedLanguageAtPosition, hasEmbeddedContent } from './embeddedSupport'; import * as url from 'url'; import * as path from 'path'; import uri from 'vscode-uri'; @@ -52,6 +55,16 @@ 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'; } }; +} + // Create a connection for the server let connection: IConnection = createConnection(); @@ -115,6 +128,53 @@ connection.onDidChangeConfiguration((change) => { languageSettings = settings.html; }); +let pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {}; +const validationDelayMs = 200; + +// The content of a text document has changed. This event is emitted +// when the text document first opened or when its content has changed. +documents.onDidChangeContent(change => { + triggerValidation(change.document); +}); + +// 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: [] }); + } +}); + +function cleanPendingValidation(textDocument: TextDocument): void { + let request = pendingValidationRequests[textDocument.uri]; + if (request) { + clearTimeout(request); + delete pendingValidationRequests[textDocument.uri]; + } +} + +function triggerValidation(textDocument: TextDocument): void { + cleanPendingValidation(textDocument); + pendingValidationRequests[textDocument.uri] = setTimeout(() => { + delete pendingValidationRequests[textDocument.uri]; + validateTextDocument(textDocument); + }, validationDelayMs); +} + +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 }; + console.log(JSON.stringify(p)); + connection.sendNotification(EmbeddedContentChangedNotification.type, p); + } +} + connection.onCompletion(textDocumentPosition => { let document = documents.get(textDocumentPosition.textDocument.uri); let htmlDocument = htmlDocuments.get(document);