Skip to content

Commit 2df11a3

Browse files
committed
[html][css] diagnistics for embedded content
1 parent 2ff7e26 commit 2df11a3

9 files changed

Lines changed: 189 additions & 41 deletions

File tree

extensions/css/server/npm-shrinkwrap.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/css/server/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
},
1010
"dependencies": {
1111
"vscode-css-languageservice": "^1.1.0",
12-
"vscode-languageserver": "^2.4.0-next.12"
12+
"vscode-languageserver": "^2.4.0-next.12",
13+
"vscode-uri": "^1.0.0"
1314
},
1415
"scripts": {
1516
"compile": "gulp compile-extension:css-server",

extensions/css/server/src/cssServerMain.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111

1212
import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageSettings, LanguageService, Stylesheet } from 'vscode-css-languageservice';
1313
import { getLanguageModelCache } from './languageModelCache';
14+
import Uri from 'vscode-uri';
15+
import { isEmbeddedContentUri, getHostDocumentUri } from './embeddedContentUri';
1416

1517
namespace ColorSymbolRequest {
1618
export const type: RequestType<string, Range[], any> = { get method() { return 'css/colorSymbols'; } };
@@ -125,7 +127,9 @@ function validateTextDocument(textDocument: TextDocument): void {
125127
let stylesheet = stylesheets.get(textDocument);
126128
let diagnostics = getLanguageService(textDocument).doValidation(textDocument, stylesheet);
127129
// Send the computed diagnostics to VSCode.
128-
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
130+
let uri = Uri.parse(textDocument.uri);
131+
let diagnosticsTarget = isEmbeddedContentUri(uri) ? getHostDocumentUri(uri) : textDocument.uri;
132+
connection.sendDiagnostics({ uri: diagnosticsTarget, diagnostics });
129133
}
130134

131135
connection.onCompletion(textDocumentPosition => {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
'use strict';
6+
7+
import Uri from 'vscode-uri';
8+
9+
export const EMBEDDED_CONTENT_SCHEME = 'embedded-content';
10+
11+
export function isEmbeddedContentUri(virtualDocumentUri: Uri): boolean {
12+
return virtualDocumentUri.scheme === EMBEDDED_CONTENT_SCHEME;
13+
}
14+
15+
export function getEmbeddedContentUri(parentDocumentUri: string, embeddedLanguageId: string): Uri {
16+
return Uri.parse(EMBEDDED_CONTENT_SCHEME + '://' + embeddedLanguageId + '/' + encodeURIComponent(parentDocumentUri) + '.' + embeddedLanguageId);
17+
};
18+
19+
export function getHostDocumentUri(virtualDocumentUri: Uri): string {
20+
let languageId = virtualDocumentUri.authority;
21+
let path = virtualDocumentUri.path.substring(1, virtualDocumentUri.path.length - languageId.length - 1); // remove leading '/' and new file extension
22+
return decodeURIComponent(path);
23+
};
24+
25+
export function getEmbeddedLanguageId(virtualDocumentUri: Uri): string {
26+
return virtualDocumentUri.authority;
27+
}

extensions/html/client/src/embeddedContentDocuments.ts

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
'use strict';
66

77
import { workspace, Uri, EventEmitter, Disposable, TextDocument } from 'vscode';
8-
import { LanguageClient, RequestType } from 'vscode-languageclient';
9-
8+
import { LanguageClient, RequestType, NotificationType } from 'vscode-languageclient';
9+
import { getEmbeddedContentUri, getEmbeddedLanguageId, getHostDocumentUri, isEmbeddedContentUri, EMBEDDED_CONTENT_SCHEME } from './embeddedContentUri';
1010

1111
interface EmbeddedContentParams {
1212
uri: string;
@@ -23,12 +23,21 @@ namespace EmbeddedContentRequest {
2323
}
2424

2525
export interface EmbeddedDocuments extends Disposable {
26-
getVirtualDocumentUri: (parentDocumentUri: string, embeddedLanguageId: string) => Uri;
27-
openVirtualDocument: (embeddedContentUri: Uri, expectedVersion: number) => Thenable<TextDocument>;
26+
getEmbeddedContentUri: (parentDocumentUri: string, embeddedLanguageId: string) => Uri;
27+
openEmbeddedContentDocument: (embeddedContentUri: Uri, expectedVersion: number) => Thenable<TextDocument>;
28+
}
29+
30+
interface EmbeddedContentChangedParams {
31+
uri: string;
32+
version: number;
33+
embeddedLanguageIds: string[];
2834
}
2935

36+
namespace EmbeddedContentChangedNotification {
37+
export const type: NotificationType<EmbeddedContentChangedParams> = { get method() { return 'embedded/contentchanged'; } };
38+
}
3039

31-
export function initializeEmbeddedContentDocuments(embeddedScheme: string, client: LanguageClient): EmbeddedDocuments {
40+
export function initializeEmbeddedContentDocuments(parentDocumentSelector: string[], embeddedLanguages: { [languageId: string]: boolean }, client: LanguageClient): EmbeddedDocuments {
3241
let toDispose: Disposable[] = [];
3342

3443
let embeddedContentChanged = new EventEmitter<Uri>();
@@ -38,16 +47,16 @@ export function initializeEmbeddedContentDocuments(embeddedScheme: string, clien
3847

3948
// documents are closed after a time out or when collected.
4049
toDispose.push(workspace.onDidCloseTextDocument(d => {
41-
if (d.uri.scheme === embeddedScheme) {
50+
if (isEmbeddedContentUri(d.uri)) {
4251
delete openVirtualDocuments[d.uri.toString()];
4352
}
4453
}));
4554

4655
// virtual document provider
47-
toDispose.push(workspace.registerTextDocumentContentProvider(embeddedScheme, {
56+
toDispose.push(workspace.registerTextDocumentContentProvider(EMBEDDED_CONTENT_SCHEME, {
4857
provideTextDocumentContent: uri => {
49-
if (uri.scheme === embeddedScheme) {
50-
let contentRequestParms = { uri: getParentDocumentUri(uri), embeddedLanguageId: getEmbeddedLanguageId(uri) };
58+
if (isEmbeddedContentUri(uri)) {
59+
let contentRequestParms = { uri: getHostDocumentUri(uri), embeddedLanguageId: getEmbeddedLanguageId(uri) };
5160
return client.sendRequest(EmbeddedContentRequest.type, contentRequestParms).then(content => {
5261
if (content) {
5362
openVirtualDocuments[uri.toString()] = content.version;
@@ -63,19 +72,16 @@ export function initializeEmbeddedContentDocuments(embeddedScheme: string, clien
6372
onDidChange: embeddedContentChanged.event
6473
}));
6574

66-
function getVirtualDocumentUri(parentDocumentUri: string, embeddedLanguageId: string) {
67-
return Uri.parse(embeddedScheme + '://' + embeddedLanguageId + '/' + encodeURIComponent(parentDocumentUri) + '.' + embeddedLanguageId);
68-
};
69-
70-
function getParentDocumentUri(virtualDocumentUri: Uri): string {
71-
let languageId = virtualDocumentUri.authority;
72-
let path = virtualDocumentUri.path.substring(1, virtualDocumentUri.path.length - languageId.length - 1); // remove leading '/' and new file extension
73-
return decodeURIComponent(path);
74-
};
75-
76-
function getEmbeddedLanguageId(virtualDocumentUri: Uri): string {
77-
return virtualDocumentUri.authority;
78-
}
75+
// diagnostics for embedded contents
76+
client.onNotification(EmbeddedContentChangedNotification.type, p => {
77+
for (let languageId in embeddedLanguages) {
78+
if (p.embeddedLanguageIds.indexOf(languageId) !== -1) {
79+
// open the document so that validation is triggered in the embedded mode
80+
let virtualUri = getEmbeddedContentUri(p.uri, languageId);
81+
openEmbeddedContentDocument(virtualUri, p.version);
82+
}
83+
}
84+
});
7985

8086
function ensureContentUpdated(virtualURI: Uri, expectedVersion: number) {
8187
let virtualURIString = virtualURI.toString();
@@ -94,7 +100,7 @@ export function initializeEmbeddedContentDocuments(embeddedScheme: string, clien
94100
return Promise.resolve();
95101
};
96102

97-
function openVirtualDocument(virtualURI: Uri, expectedVersion: number): Thenable<TextDocument> {
103+
function openEmbeddedContentDocument(virtualURI: Uri, expectedVersion: number): Thenable<TextDocument> {
98104
return ensureContentUpdated(virtualURI, expectedVersion).then(_ => {
99105
return workspace.openTextDocument(virtualURI).then(document => {
100106
if (expectedVersion === openVirtualDocuments[virtualURI.toString()]) {
@@ -106,8 +112,8 @@ export function initializeEmbeddedContentDocuments(embeddedScheme: string, clien
106112
};
107113

108114
return {
109-
getVirtualDocumentUri,
110-
openVirtualDocument,
115+
getEmbeddedContentUri,
116+
openEmbeddedContentDocument,
111117
dispose: Disposable.from(...toDispose).dispose
112118
};
113119

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
'use strict';
6+
7+
import { Uri } from 'vscode';
8+
9+
export const EMBEDDED_CONTENT_SCHEME = 'embedded-content';
10+
11+
export function isEmbeddedContentUri(virtualDocumentUri: Uri): boolean {
12+
return virtualDocumentUri.scheme === EMBEDDED_CONTENT_SCHEME;
13+
}
14+
15+
export function getEmbeddedContentUri(parentDocumentUri: string, embeddedLanguageId: string): Uri {
16+
return Uri.parse(EMBEDDED_CONTENT_SCHEME + '://' + embeddedLanguageId + '/' + encodeURIComponent(parentDocumentUri) + '.' + embeddedLanguageId);
17+
};
18+
19+
export function getHostDocumentUri(virtualDocumentUri: Uri): string {
20+
let languageId = virtualDocumentUri.authority;
21+
let path = virtualDocumentUri.path.substring(1, virtualDocumentUri.path.length - languageId.length - 1); // remove leading '/' and new file extension
22+
return decodeURIComponent(path);
23+
};
24+
25+
export function getEmbeddedLanguageId(virtualDocumentUri: Uri): string {
26+
return virtualDocumentUri.authority;
27+
}

extensions/html/client/src/htmlMain.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,32 +51,32 @@ export function activate(context: ExtensionContext) {
5151
debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }
5252
};
5353

54+
let documentSelector = ['html', 'handlebars', 'razor'];
55+
let embeddedLanguages = { 'css': true };
56+
5457
// Options to control the language client
5558
let clientOptions: LanguageClientOptions = {
56-
// Register the server for json documents
57-
documentSelector: ['html', 'handlebars', 'razor'],
59+
documentSelector,
5860
synchronize: {
59-
// Synchronize the setting section 'html' to the server
60-
configurationSection: ['html'],
61+
configurationSection: ['html'], // Synchronize the setting section 'html' to the server
6162
},
62-
6363
initializationOptions: {
64-
embeddedLanguages: { 'css': true },
64+
embeddedLanguages,
6565
['format.enable']: workspace.getConfiguration('html').get('format.enable')
6666
}
6767
};
6868

6969
// Create the language client and start the client.
7070
let client = new LanguageClient('html', localize('htmlserver.name', 'HTML Language Server'), serverOptions, clientOptions);
7171

72-
let embeddedDocuments = initializeEmbeddedContentDocuments('html-embedded', client);
72+
let embeddedDocuments = initializeEmbeddedContentDocuments(documentSelector, embeddedLanguages, client);
7373
context.subscriptions.push(embeddedDocuments);
7474

7575
client.onRequest(EmbeddedCompletionRequest.type, params => {
7676
let position = Protocol2Code.asPosition(params.position);
77-
let virtualDocumentURI = embeddedDocuments.getVirtualDocumentUri(params.uri, params.embeddedLanguageId);
77+
let virtualDocumentURI = embeddedDocuments.getEmbeddedContentUri(params.uri, params.embeddedLanguageId);
7878

79-
return embeddedDocuments.openVirtualDocument(virtualDocumentURI, params.version).then(document => {
79+
return embeddedDocuments.openEmbeddedContentDocument(virtualDocumentURI, params.version).then(document => {
8080
if (document) {
8181
return commands.executeCommand<CompletionList>('vscode.executeCompletionItemProvider', virtualDocumentURI, position).then(completionList => {
8282
if (completionList) {
@@ -94,8 +94,8 @@ export function activate(context: ExtensionContext) {
9494

9595
client.onRequest(EmbeddedHoverRequest.type, params => {
9696
let position = Protocol2Code.asPosition(params.position);
97-
let virtualDocumentURI = embeddedDocuments.getVirtualDocumentUri(params.uri, params.embeddedLanguageId);
98-
return embeddedDocuments.openVirtualDocument(virtualDocumentURI, params.version).then(document => {
97+
let virtualDocumentURI = embeddedDocuments.getEmbeddedContentUri(params.uri, params.embeddedLanguageId);
98+
return embeddedDocuments.openEmbeddedContentDocument(virtualDocumentURI, params.version).then(document => {
9999
if (document) {
100100
return commands.executeCommand<Hover[]>('vscode.executeHoverProvider', virtualDocumentURI, position).then(hover => {
101101
if (hover && hover.length > 0) {

extensions/html/server/src/embeddedSupport.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,20 @@ export function getEmbeddedLanguageAtPosition(languageService: LanguageService,
1919
return null;
2020
}
2121

22+
export function hasEmbeddedContent(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument, embeddedLanguages: { [languageId: string]: boolean }): string[] {
23+
let embeddedLanguageIds: { [languageId: string]: boolean } = {};
24+
function collectEmbeddedLanguages(node: Node): void {
25+
let c = getEmbeddedContentForNode(languageService, document, node);
26+
if (c && embeddedLanguages[c.languageId] && !isWhitespace(document.getText().substring(c.start, c.end))) {
27+
embeddedLanguageIds[c.languageId] = true;
28+
}
29+
node.children.forEach(collectEmbeddedLanguages);
30+
}
31+
32+
htmlDocument.roots.forEach(collectEmbeddedLanguages);
33+
return Object.keys(embeddedLanguageIds);
34+
}
35+
2236
export function getEmbeddedContent(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument, languageId: string): string {
2337
let contents = [];
2438
function collectEmbeddedNodes(node: Node): void {
@@ -104,4 +118,8 @@ function getEmbeddedContentForNode(languageService: LanguageService, document: T
104118
}
105119
}
106120
return void 0;
121+
}
122+
123+
function isWhitespace(str: string) {
124+
return str.match(/^\s*$/);
107125
}

extensions/html/server/src/htmlServerMain.ts

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
*--------------------------------------------------------------------------------------------*/
55
'use strict';
66

7-
import { createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, FormattingOptions, RequestType, CompletionList, Position, Hover } from 'vscode-languageserver';
7+
import {
8+
createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, FormattingOptions, RequestType, NotificationType,
9+
CompletionList, Position, Hover
10+
} from 'vscode-languageserver';
811

9-
import { HTMLDocument, getLanguageService, CompletionConfiguration, HTMLFormatConfiguration, DocumentContext } from 'vscode-html-languageservice';
12+
import { HTMLDocument, getLanguageService, CompletionConfiguration, HTMLFormatConfiguration, DocumentContext, TextDocument } from 'vscode-html-languageservice';
1013
import { getLanguageModelCache } from './languageModelCache';
11-
import { getEmbeddedContent, getEmbeddedLanguageAtPosition } from './embeddedSupport';
14+
import { getEmbeddedContent, getEmbeddedLanguageAtPosition, hasEmbeddedContent } from './embeddedSupport';
1215
import * as url from 'url';
1316
import * as path from 'path';
1417
import uri from 'vscode-uri';
@@ -52,6 +55,16 @@ namespace EmbeddedContentRequest {
5255
export const type: RequestType<EmbeddedContentParams, EmbeddedContent, any> = { get method() { return 'embedded/content'; } };
5356
}
5457

58+
interface EmbeddedContentChangedParams {
59+
uri: string;
60+
version: number;
61+
embeddedLanguageIds: string[];
62+
}
63+
64+
namespace EmbeddedContentChangedNotification {
65+
export const type: NotificationType<EmbeddedContentChangedParams> = { get method() { return 'embedded/contentchanged'; } };
66+
}
67+
5568
// Create a connection for the server
5669
let connection: IConnection = createConnection();
5770

@@ -115,6 +128,53 @@ connection.onDidChangeConfiguration((change) => {
115128
languageSettings = settings.html;
116129
});
117130

131+
let pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {};
132+
const validationDelayMs = 200;
133+
134+
// The content of a text document has changed. This event is emitted
135+
// when the text document first opened or when its content has changed.
136+
documents.onDidChangeContent(change => {
137+
triggerValidation(change.document);
138+
});
139+
140+
// a document has closed: clear all diagnostics
141+
documents.onDidClose(event => {
142+
cleanPendingValidation(event.document);
143+
//connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] });
144+
if (embeddedLanguages) {
145+
connection.sendNotification(EmbeddedContentChangedNotification.type, { uri: event.document.uri, version: event.document.version, embeddedLanguageIds: [] });
146+
}
147+
});
148+
149+
function cleanPendingValidation(textDocument: TextDocument): void {
150+
let request = pendingValidationRequests[textDocument.uri];
151+
if (request) {
152+
clearTimeout(request);
153+
delete pendingValidationRequests[textDocument.uri];
154+
}
155+
}
156+
157+
function triggerValidation(textDocument: TextDocument): void {
158+
cleanPendingValidation(textDocument);
159+
pendingValidationRequests[textDocument.uri] = setTimeout(() => {
160+
delete pendingValidationRequests[textDocument.uri];
161+
validateTextDocument(textDocument);
162+
}, validationDelayMs);
163+
}
164+
165+
function validateTextDocument(textDocument: TextDocument): void {
166+
let htmlDocument = htmlDocuments.get(textDocument);
167+
//let diagnostics = languageService.doValidation(textDocument, htmlDocument);
168+
// Send the computed diagnostics to VSCode.
169+
//connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
170+
if (embeddedLanguages) {
171+
let embeddedLanguageIds = hasEmbeddedContent(languageService, textDocument, htmlDocument, embeddedLanguages);
172+
let p = { uri: textDocument.uri, version: textDocument.version, embeddedLanguageIds };
173+
console.log(JSON.stringify(p));
174+
connection.sendNotification(EmbeddedContentChangedNotification.type, p);
175+
}
176+
}
177+
118178
connection.onCompletion(textDocumentPosition => {
119179
let document = documents.get(textDocumentPosition.textDocument.uri);
120180
let htmlDocument = htmlDocuments.get(document);

0 commit comments

Comments
 (0)