Skip to content

Commit d624719

Browse files
authored
Support Html requests in cohosting (#8210)
2 parents 05dc80b + b45dfaf commit d624719

File tree

8 files changed

+313
-32
lines changed

8 files changed

+313
-32
lines changed

src/lsptoolshost/activate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export async function activateRoslynLanguageServer(
8686
registerCodeActionFixAllCommands(context, languageServer, _channel);
8787

8888
registerRazorCommands(context, languageServer);
89-
registerRazorEndpoints(context, languageServer, razorLogger);
89+
registerRazorEndpoints(context, languageServer, razorLogger, platformInfo);
9090

9191
registerUnitTestingCommands(context, languageServer);
9292

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
6+
import * as vscode from 'vscode';
7+
import { getUriPath } from '../../razor/src/uriPaths';
8+
9+
export class HtmlDocument {
10+
public readonly path: string;
11+
private content = '';
12+
13+
public constructor(public readonly uri: vscode.Uri) {
14+
this.path = getUriPath(uri);
15+
}
16+
17+
public getContent() {
18+
return this.content;
19+
}
20+
21+
public setContent(content: string) {
22+
this.content = content;
23+
}
24+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
6+
import * as vscode from 'vscode';
7+
import { HtmlDocumentManager } from './htmlDocumentManager';
8+
import { RazorLogger } from '../../razor/src/razorLogger';
9+
import { getUriPath } from '../../razor/src/uriPaths';
10+
11+
export class HtmlDocumentContentProvider implements vscode.TextDocumentContentProvider {
12+
public static readonly scheme = 'razor-html';
13+
14+
private readonly onDidChangeEmitter: vscode.EventEmitter<vscode.Uri> = new vscode.EventEmitter<vscode.Uri>();
15+
16+
constructor(private readonly documentManager: HtmlDocumentManager, private readonly logger: RazorLogger) {}
17+
18+
public get onDidChange() {
19+
return this.onDidChangeEmitter.event;
20+
}
21+
22+
public fireDidChange(uri: vscode.Uri) {
23+
this.onDidChangeEmitter.fire(uri);
24+
}
25+
26+
public provideTextDocumentContent(uri: vscode.Uri) {
27+
const document = this.findDocument(uri);
28+
if (!document) {
29+
// Document was removed from the document manager, meaning there's no more content for this
30+
// file. Report an empty document.
31+
this.logger.logVerbose(
32+
`Could not find document '${getUriPath(
33+
uri
34+
)}' when updating the HTML buffer. This typically happens when a document is removed.`
35+
);
36+
return '';
37+
}
38+
39+
return document.getContent();
40+
}
41+
42+
private findDocument(uri: vscode.Uri) {
43+
const projectedPath = getUriPath(uri);
44+
45+
return this.documentManager.documents.find(
46+
(document) => document.path.localeCompare(projectedPath, undefined, { sensitivity: 'base' }) === 0
47+
);
48+
}
49+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
6+
import * as vscode from 'vscode';
7+
import { RazorLogger } from '../../razor/src/razorLogger';
8+
import { PlatformInformation } from '../../shared/platform';
9+
import { getUriPath } from '../../razor/src/uriPaths';
10+
import { virtualHtmlSuffix } from '../../razor/src/razorConventions';
11+
import { HtmlDocumentContentProvider } from './htmlDocumentContentProvider';
12+
import { HtmlDocument } from './htmlDocument';
13+
14+
export class HtmlDocumentManager {
15+
private readonly htmlDocuments: { [hostDocumentPath: string]: HtmlDocument } = {};
16+
private readonly contentProvider: HtmlDocumentContentProvider;
17+
18+
constructor(private readonly platformInfo: PlatformInformation, private readonly logger: RazorLogger) {
19+
this.contentProvider = new HtmlDocumentContentProvider(this, this.logger);
20+
}
21+
22+
public get documents() {
23+
return Object.values(this.htmlDocuments);
24+
}
25+
26+
public register() {
27+
const didCloseRegistration = vscode.workspace.onDidCloseTextDocument(async (document) => {
28+
// We log when a virtual document is closed just in case it helps track down future bugs
29+
if (document.uri.scheme === HtmlDocumentContentProvider.scheme) {
30+
this.logger.logVerbose(`Virtual document '${document.uri}' timed out.`);
31+
return;
32+
}
33+
34+
// When a Razor document is closed, only then can we be sure its okay to remove the virtual document.
35+
if (document.languageId === 'aspnetcorerazor') {
36+
this.logger.logVerbose(`Document '${document.uri}' was closed.`);
37+
38+
await this.closeDocument(document.uri);
39+
40+
// TODO: Send a notification back to the server so it can cancel any pending sync requests and clear its cache.
41+
}
42+
});
43+
44+
const providerRegistration = vscode.workspace.registerTextDocumentContentProvider(
45+
HtmlDocumentContentProvider.scheme,
46+
this.contentProvider
47+
);
48+
49+
return vscode.Disposable.from(didCloseRegistration, providerRegistration);
50+
}
51+
52+
public async updateDocumentText(uri: vscode.Uri, text: string) {
53+
const document = await this.getDocument(uri);
54+
55+
this.logger.logVerbose(`New content for '${uri}', updating '${document.path}'.`);
56+
57+
document.setContent(text);
58+
59+
this.contentProvider.fireDidChange(document.uri);
60+
}
61+
62+
private async closeDocument(uri: vscode.Uri) {
63+
const document = await this.findDocument(uri);
64+
65+
if (document) {
66+
this.logger.logVerbose(`Removing '${document.uri}' from the document manager.`);
67+
68+
delete this.htmlDocuments[document.path];
69+
}
70+
}
71+
72+
public async getDocument(uri: vscode.Uri): Promise<HtmlDocument> {
73+
let document = this.findDocument(uri);
74+
75+
// This might happen in the case that a file is opened outside the workspace
76+
if (!document) {
77+
this.logger.logMessage(
78+
`File '${uri}' didn't exist in the Razor document list. This is likely because it's from outside the workspace.`
79+
);
80+
document = this.addDocument(uri);
81+
}
82+
83+
await vscode.workspace.openTextDocument(document.uri);
84+
85+
return document!;
86+
}
87+
88+
private addDocument(uri: vscode.Uri): HtmlDocument {
89+
let document = this.findDocument(uri);
90+
if (document) {
91+
this.logger.logMessage(`Skipping document creation for '${document.path}' because it already exists.`);
92+
return document;
93+
}
94+
95+
document = this.createDocument(uri);
96+
this.htmlDocuments[document.path] = document;
97+
98+
return document;
99+
}
100+
101+
private findDocument(uri: vscode.Uri): HtmlDocument | undefined {
102+
let path = getUriPath(uri);
103+
104+
// We might be passed a Razor document Uri, but we store and manage Html projected documents.
105+
if (uri.scheme !== HtmlDocumentContentProvider.scheme) {
106+
path = `${path}${virtualHtmlSuffix}`;
107+
}
108+
109+
if (this.platformInfo.isLinux()) {
110+
return this.htmlDocuments[path];
111+
}
112+
113+
return Object.values(this.htmlDocuments).find(
114+
(document) => document.path.localeCompare(path, undefined, { sensitivity: 'base' }) === 0
115+
);
116+
}
117+
118+
private createDocument(uri: vscode.Uri) {
119+
uri = uri.with({
120+
scheme: HtmlDocumentContentProvider.scheme,
121+
path: `${uri.path}${virtualHtmlSuffix}`,
122+
});
123+
const projectedDocument = new HtmlDocument(uri);
124+
125+
return projectedDocument;
126+
}
127+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
6+
import { TextDocumentIdentifier } from 'vscode-languageserver-protocol';
7+
8+
export class HtmlUpdateParameters {
9+
constructor(public readonly textDocument: TextDocumentIdentifier, public readonly text: string) {}
10+
}

src/lsptoolshost/razor/razorEndpoints.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,76 @@
55

66
import { RoslynLanguageServer } from '../server/roslynLanguageServer';
77
import * as vscode from 'vscode';
8-
import { LogMessageParams, NotificationType } from 'vscode-languageclient';
8+
import {
9+
ColorInformation,
10+
ColorPresentationParams,
11+
ColorPresentationRequest,
12+
DocumentColorParams,
13+
DocumentColorRequest,
14+
LogMessageParams,
15+
NotificationType,
16+
RequestType,
17+
} from 'vscode-languageclient';
918
import { RazorLogger } from '../../razor/src/razorLogger';
19+
import { HtmlUpdateParameters } from './htmlUpdateParameters';
20+
import { UriConverter } from '../utils/uriConverter';
21+
import { PlatformInformation } from '../../shared/platform';
22+
import { HtmlDocumentManager } from './htmlDocumentManager';
23+
import { DocumentColorHandler } from '../../razor/src/documentColor/documentColorHandler';
24+
import { razorOptions } from '../../shared/options';
25+
import { ColorPresentationHandler } from '../../razor/src/colorPresentation/colorPresentationHandler';
26+
import { ColorPresentation } from 'vscode-html-languageservice';
1027

1128
export function registerRazorEndpoints(
1229
context: vscode.ExtensionContext,
1330
languageServer: RoslynLanguageServer,
14-
razorLogger: RazorLogger
31+
razorLogger: RazorLogger,
32+
platformInfo: PlatformInformation
1533
) {
1634
const logNotificationType = new NotificationType<LogMessageParams>('razor/log');
1735
languageServer.registerOnNotificationWithParams(logNotificationType, (params) =>
1836
razorLogger.log(params.message, params.type)
1937
);
38+
39+
if (!razorOptions.cohostingEnabled) {
40+
return;
41+
}
42+
43+
const documentManager = new HtmlDocumentManager(platformInfo, razorLogger);
44+
context.subscriptions.push(documentManager.register());
45+
46+
registerRequestHandler<HtmlUpdateParameters, void>('razor/updateHtml', async (params) => {
47+
const uri = UriConverter.deserialize(params.textDocument.uri);
48+
await documentManager.updateDocumentText(uri, params.text);
49+
});
50+
51+
registerRequestHandler<DocumentColorParams, ColorInformation[]>(DocumentColorRequest.method, async (params) => {
52+
const uri = UriConverter.deserialize(params.textDocument.uri);
53+
const document = await documentManager.getDocument(uri);
54+
55+
return await DocumentColorHandler.doDocumentColorRequest(document.uri);
56+
});
57+
58+
registerRequestHandler<ColorPresentationParams, ColorPresentation[]>(
59+
ColorPresentationRequest.method,
60+
async (params) => {
61+
const uri = UriConverter.deserialize(params.textDocument.uri);
62+
const document = await documentManager.getDocument(uri);
63+
64+
return await ColorPresentationHandler.doColorPresentationRequest(document.uri, params);
65+
}
66+
);
67+
68+
// Helper method that registers a request handler, and logs errors to the Razor logger.
69+
function registerRequestHandler<Params, Result>(method: string, invocation: (params: Params) => Promise<Result>) {
70+
const requestType = new RequestType<Params, Result, Error>(method);
71+
languageServer.registerOnRequest(requestType, async (params) => {
72+
try {
73+
return await invocation(params);
74+
} catch (error) {
75+
razorLogger.logError(`Error: ${error}`, error);
76+
return undefined;
77+
}
78+
});
79+
}
2080
}

src/razor/src/colorPresentation/colorPresentationHandler.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,30 +54,38 @@ export class ColorPresentationHandler {
5454
return this.emptyColorInformationResponse;
5555
}
5656

57-
const color = new vscode.Color(
58-
colorPresentationParams.color.red,
59-
colorPresentationParams.color.green,
60-
colorPresentationParams.color.blue,
61-
colorPresentationParams.color.alpha
62-
);
6357
const virtualHtmlUri = razorDocument.htmlDocument.uri;
6458

65-
const colorPresentations = await vscode.commands.executeCommand<vscode.ColorPresentation[]>(
66-
'vscode.executeColorPresentationProvider',
67-
color,
68-
new ColorPresentationContext(virtualHtmlUri, colorPresentationParams.range)
69-
);
70-
71-
const serializableColorPresentations = this.SerializeColorPresentations(colorPresentations);
72-
return serializableColorPresentations;
59+
return await ColorPresentationHandler.doColorPresentationRequest(virtualHtmlUri, colorPresentationParams);
7360
} catch (error) {
7461
this.logger.logWarning(`${ColorPresentationHandler.provideHtmlColorPresentation} failed with ${error}`);
7562
}
7663

7764
return this.emptyColorInformationResponse;
7865
}
7966

80-
private SerializeColorPresentations(colorPresentations: vscode.ColorPresentation[]) {
67+
public static async doColorPresentationRequest(
68+
virtualHtmlUri: vscode.Uri,
69+
colorPresentationParams: SerializableColorPresentationParams
70+
) {
71+
const color = new vscode.Color(
72+
colorPresentationParams.color.red,
73+
colorPresentationParams.color.green,
74+
colorPresentationParams.color.blue,
75+
colorPresentationParams.color.alpha
76+
);
77+
78+
const colorPresentations = await vscode.commands.executeCommand<vscode.ColorPresentation[]>(
79+
'vscode.executeColorPresentationProvider',
80+
color,
81+
new ColorPresentationContext(virtualHtmlUri, colorPresentationParams.range)
82+
);
83+
84+
const serializableColorPresentations = ColorPresentationHandler.SerializeColorPresentations(colorPresentations);
85+
return serializableColorPresentations;
86+
}
87+
88+
private static SerializeColorPresentations(colorPresentations: vscode.ColorPresentation[]) {
8189
const serializableColorPresentations = new Array<SerializableColorPresentation>();
8290
for (const colorPresentation of colorPresentations) {
8391
let serializedTextEdit: any = null;

src/razor/src/documentColor/documentColorHandler.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -67,24 +67,27 @@ export class DocumentColorHandler {
6767
}
6868

6969
const virtualHtmlUri = razorDocument.htmlDocument.uri;
70-
71-
const colorInformation = await vscode.commands.executeCommand<vscode.ColorInformation[]>(
72-
'vscode.executeDocumentColorProvider',
73-
virtualHtmlUri
74-
);
75-
76-
const serializableColorInformation = new Array<SerializableColorInformation>();
77-
for (const color of colorInformation) {
78-
const serializableRange = convertRangeToSerializable(color.range);
79-
const serializableColor = new SerializableColorInformation(serializableRange, color.color);
80-
serializableColorInformation.push(serializableColor);
81-
}
82-
83-
return serializableColorInformation;
70+
return await DocumentColorHandler.doDocumentColorRequest(virtualHtmlUri);
8471
} catch (error) {
8572
this.logger.logWarning(`${DocumentColorHandler.provideHtmlDocumentColorEndpoint} failed with ${error}`);
8673
}
8774

8875
return this.emptyColorInformationResponse;
8976
}
77+
78+
public static async doDocumentColorRequest(virtualHtmlUri: vscode.Uri) {
79+
const colorInformation = await vscode.commands.executeCommand<vscode.ColorInformation[]>(
80+
'vscode.executeDocumentColorProvider',
81+
virtualHtmlUri
82+
);
83+
84+
const serializableColorInformation = new Array<SerializableColorInformation>();
85+
for (const color of colorInformation) {
86+
const serializableRange = convertRangeToSerializable(color.range);
87+
const serializableColor = new SerializableColorInformation(serializableRange, color.color);
88+
serializableColorInformation.push(serializableColor);
89+
}
90+
91+
return serializableColorInformation;
92+
}
9093
}

0 commit comments

Comments
 (0)