Skip to content

Commit 2f4b51c

Browse files
author
Andrew Hall
committed
Update dynamic documents to store edits for closed documents
1 parent 5b414fa commit 2f4b51c

File tree

9 files changed

+150
-74
lines changed

9 files changed

+150
-74
lines changed

src/lsptoolshost/roslynLanguageServer.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import {
7878
import { registerSourceGeneratedFilesContentProvider } from './sourceGeneratedFilesContentProvider';
7979
import { registerMiscellaneousFileNotifier } from './miscellaneousFileNotifier';
8080
import { TelemetryEventNames } from '../shared/telemetryEventNames';
81+
import { RazorDynamicFileChangedParams } from '../razor/src/dynamicFile/dynamicFileUpdatedParams';
8182

8283
let _channel: vscode.LogOutputChannel;
8384
let _traceChannel: vscode.OutputChannel;
@@ -789,6 +790,11 @@ export class RoslynLanguageServer {
789790
async (notification) =>
790791
vscode.commands.executeCommand(DynamicFileInfoHandler.removeDynamicFileInfoCommand, notification)
791792
);
793+
vscode.commands.registerCommand(
794+
DynamicFileInfoHandler.dynamicFileUpdatedCommand,
795+
async (notification: RazorDynamicFileChangedParams) =>
796+
this.sendNotification<RazorDynamicFileChangedParams>('razor/dynamicFileInfoChanged', notification)
797+
);
792798
}
793799

794800
// eslint-disable-next-line @typescript-eslint/promise-function-async

src/razor/src/csharp/csharpProjectedDocument.ts

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export class CSharpProjectedDocument implements IProjectedDocument {
1919
private resolveProvisionalEditAt: number | undefined;
2020
private ProvisionalDotPosition: Position | undefined;
2121
private hostDocumentVersion: number | null = null;
22+
private edits: ServerTextChange[] | null = null;
2223

2324
public constructor(public readonly uri: vscode.Uri) {
2425
this.path = getUriPath(uri);
@@ -37,21 +38,38 @@ export class CSharpProjectedDocument implements IProjectedDocument {
3738
}
3839

3940
public update(edits: ServerTextChange[], hostDocumentVersion: number) {
41+
// Apply any stored edits if needed
42+
if (this.edits) {
43+
edits = this.edits.concat(edits);
44+
this.edits = null;
45+
}
46+
4047
this.removeProvisionalDot();
4148

4249
this.hostDocumentVersion = hostDocumentVersion;
4350

44-
if (edits.length === 0) {
45-
return;
51+
this.updateContent(edits);
52+
}
53+
54+
public storeEdits(edits: ServerTextChange[], hostDocumentVersion: number) {
55+
this.hostDocumentVersion = hostDocumentVersion;
56+
if (this.edits) {
57+
this.edits = this.edits.concat(edits);
58+
} else {
59+
this.edits = edits;
4660
}
61+
}
4762

48-
let content = this.content;
49-
for (const edit of edits.reverse()) {
50-
// TODO: Use a better data structure to represent the content, string concatenation is slow.
51-
content = this.getEditedContent(edit.newText, edit.span.start, edit.span.start + edit.span.length, content);
63+
public getAndApplyEdits() {
64+
const edits = this.edits;
65+
66+
// Make sure the internal representation of the content is updated
67+
if (edits) {
68+
this.updateContent(edits);
5269
}
5370

54-
this.setContent(content);
71+
this.edits = null;
72+
return edits;
5573
}
5674

5775
public getContent() {
@@ -150,4 +168,18 @@ export class CSharpProjectedDocument implements IProjectedDocument {
150168
private setContent(content: string) {
151169
this.content = content;
152170
}
171+
172+
private updateContent(edits: ServerTextChange[]) {
173+
if (edits.length === 0) {
174+
return;
175+
}
176+
177+
let content = this.content;
178+
for (const edit of edits.reverse()) {
179+
// TODO: Use a better data structure to represent the content, string concatenation is slow.
180+
content = this.getEditedContent(edit.newText, edit.span.start, edit.span.start + edit.span.length, content);
181+
}
182+
183+
this.setContent(content);
184+
}
153185
}

src/razor/src/document/IRazorDocument.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ export interface IRazorDocument {
1111
readonly uri: vscode.Uri;
1212
readonly csharpDocument: IProjectedDocument;
1313
readonly htmlDocument: IProjectedDocument;
14+
readonly isOpen: boolean;
1415
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 { CSharpProjectedDocument } from '../csharp/csharpProjectedDocument';
8+
import { HtmlProjectedDocument } from '../html/htmlProjectedDocument';
9+
import { getUriPath } from '../uriPaths';
10+
import { IRazorDocument } from './IRazorDocument';
11+
12+
export class RazorDocument implements IRazorDocument {
13+
public readonly path: string;
14+
15+
constructor(
16+
readonly uri: vscode.Uri,
17+
readonly csharpDocument: CSharpProjectedDocument,
18+
readonly htmlDocument: HtmlProjectedDocument
19+
) {
20+
this.path = getUriPath(uri);
21+
}
22+
23+
public get isOpen(): boolean {
24+
for (const textDocument of vscode.workspace.textDocuments) {
25+
if (textDocument.uri.fsPath == this.uri.fsPath) {
26+
return true;
27+
}
28+
}
29+
30+
return false;
31+
}
32+
}

src/razor/src/document/razorDocumentFactory.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,12 @@ import { HtmlProjectedDocumentContentProvider } from '../html/htmlProjectedDocum
1111
import { virtualCSharpSuffix, virtualHtmlSuffix } from '../razorConventions';
1212
import { getUriPath } from '../uriPaths';
1313
import { IRazorDocument } from './IRazorDocument';
14+
import { RazorDocument } from './razorDocument';
1415

15-
export function createDocument(uri: vscode.Uri) {
16+
export function createDocument(uri: vscode.Uri): IRazorDocument {
1617
const csharpDocument = createProjectedCSharpDocument(uri);
1718
const htmlDocument = createProjectedHtmlDocument(uri);
18-
const path = getUriPath(uri);
19-
20-
const document: IRazorDocument = {
21-
uri,
22-
path,
23-
csharpDocument,
24-
htmlDocument,
25-
};
19+
const document = new RazorDocument(uri, csharpDocument, htmlDocument);
2620

2721
return document;
2822
}

src/razor/src/document/razorDocumentManager.ts

Lines changed: 16 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -50,21 +50,12 @@ export class RazorDocumentManager implements IRazorDocumentManager {
5050
return Object.values(this.razorDocuments);
5151
}
5252

53-
public async getDocument(uri: vscode.Uri) {
53+
public async getDocument(uri: vscode.Uri): Promise<IRazorDocument> {
5454
const document = this._getDocument(uri);
55-
56-
// VS Code closes virtual documents after some timeout if they are not open in the IDE. Since our generated C# and Html
57-
// documents are never open in the IDE, we need to ensure that VS Code considers them open so that requests against them
58-
// succeed. Without this, even a simple diagnostics request will fail in Roslyn if the user just opens a .razor document
59-
// and leaves it open past the timeout.
60-
if (this.razorDocumentGenerationInitialized) {
61-
await this.ensureDocumentAndProjectedDocumentsOpen(document);
62-
}
63-
6455
return document;
6556
}
6657

67-
public async getActiveDocument() {
58+
public async getActiveDocument(): Promise<IRazorDocument | null> {
6859
if (!vscode.window.activeTextEditor) {
6960
return null;
7061
}
@@ -147,7 +138,7 @@ export class RazorDocumentManager implements IRazorDocumentManager {
147138
return vscode.Disposable.from(watcher, didCreateRegistration, didOpenRegistration, didCloseRegistration);
148139
}
149140

150-
private _getDocument(uri: vscode.Uri) {
141+
private _getDocument(uri: vscode.Uri): IRazorDocument {
151142
const path = getUriPath(uri);
152143
let document = this.findDocument(path);
153144

@@ -159,7 +150,7 @@ export class RazorDocumentManager implements IRazorDocumentManager {
159150
document = this.addDocument(uri);
160151
}
161152

162-
return document;
153+
return document!;
163154
}
164155

165156
private async openDocument(uri: vscode.Uri) {
@@ -182,10 +173,6 @@ export class RazorDocumentManager implements IRazorDocumentManager {
182173
await vscode.commands.executeCommand(razorInitializeCommand, pipeName);
183174
await this.serverClient.connectNamedPipe(pipeName);
184175

185-
for (const document of this.documents) {
186-
await this.ensureDocumentAndProjectedDocumentsOpen(document);
187-
}
188-
189176
this.onRazorInitializedEmitter.fire();
190177
}
191178
}
@@ -205,7 +192,7 @@ export class RazorDocumentManager implements IRazorDocumentManager {
205192
this.notifyDocumentChange(document, RazorDocumentChangeKind.closed);
206193
}
207194

208-
private addDocument(uri: vscode.Uri) {
195+
private addDocument(uri: vscode.Uri): IRazorDocument {
209196
const path = getUriPath(uri);
210197
let document = this.findDocument(path);
211198
if (document) {
@@ -261,10 +248,6 @@ export class RazorDocumentManager implements IRazorDocumentManager {
261248
) {
262249
// We allow re-setting of the updated content from the same doc sync version in the case
263250
// of project or file import changes.
264-
265-
// Make sure the document is open, because updating will cause a didChange event to fire.
266-
await vscode.workspace.openTextDocument(document.csharpDocument.uri);
267-
268251
const csharpProjectedDocument = projectedDocument as CSharpProjectedDocument;
269252

270253
// If the language server is telling us that the previous document was empty, then we should clear
@@ -275,7 +258,17 @@ export class RazorDocumentManager implements IRazorDocumentManager {
275258
csharpProjectedDocument.clear();
276259
}
277260

278-
csharpProjectedDocument.update(updateBufferRequest.changes, updateBufferRequest.hostDocumentVersion);
261+
if (document.isOpen) {
262+
// Make sure the document is open, because updating will cause a didChange event to fire.
263+
await vscode.workspace.openTextDocument(document.csharpDocument.uri);
264+
265+
csharpProjectedDocument.update(updateBufferRequest.changes, updateBufferRequest.hostDocumentVersion);
266+
} else {
267+
csharpProjectedDocument.storeEdits(
268+
updateBufferRequest.changes,
269+
updateBufferRequest.hostDocumentVersion
270+
);
271+
}
279272

280273
this.notifyDocumentChange(document, RazorDocumentChangeKind.csharpChanged);
281274
} else {
@@ -342,22 +335,4 @@ export class RazorDocumentManager implements IRazorDocumentManager {
342335

343336
this.onChangeEmitter.fire(args);
344337
}
345-
346-
private async ensureDocumentAndProjectedDocumentsOpen(document: IRazorDocument) {
347-
// vscode.workspace.openTextDocument may send a textDocument/didOpen
348-
// request to the C# language server. We need to keep track of
349-
// this to make sure we don't send a duplicate request later on.
350-
const razorUri = vscode.Uri.file(document.path);
351-
if (!this.isRazorDocumentOpenInCSharpWorkspace(razorUri)) {
352-
this.didOpenRazorCSharpDocument(razorUri);
353-
354-
// Need to tell the Razor server that the document is open, or it won't generate C# code
355-
// for it, and our projected document will always be empty, until the user manually
356-
// opens the razor file.
357-
await vscode.workspace.openTextDocument(razorUri);
358-
}
359-
360-
await vscode.workspace.openTextDocument(document.csharpDocument.uri);
361-
await vscode.workspace.openTextDocument(document.htmlDocument.uri);
362-
}
363338
}

src/razor/src/dynamicFile/dynamicFileInfoHandler.ts

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,23 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as vscode from 'vscode';
7-
import { DocumentUri } from 'vscode-languageclient/node';
87
import { UriConverter } from '../../../lsptoolshost/uriConverter';
98
import { RazorDocumentManager } from '../document/razorDocumentManager';
109
import { RazorLogger } from '../razorLogger';
1110
import { ProvideDynamicFileParams } from './provideDynamicFileParams';
1211
import { ProvideDynamicFileResponse } from './provideDynamicFileResponse';
1312
import { RemoveDynamicFileParams } from './removeDynamicFileParams';
13+
import { CSharpProjectedDocument } from '../csharp/csharpProjectedDocument';
14+
import { RazorDocumentChangeKind } from '../document/razorDocumentChangeKind';
15+
import { RazorDynamicFileChangedParams } from './dynamicFileUpdatedParams';
16+
import { TextDocumentIdentifier } from 'vscode-languageserver-protocol';
1417

1518
// Handles Razor generated doc communication between the Roslyn workspace and Razor.
1619
// didChange behavior for Razor generated docs is handled in the RazorDocumentManager.
1720
export class DynamicFileInfoHandler {
1821
public static readonly provideDynamicFileInfoCommand = 'razor.provideDynamicFileInfo';
1922
public static readonly removeDynamicFileInfoCommand = 'razor.removeDynamicFileInfo';
23+
public static readonly dynamicFileUpdatedCommand = 'razor.dynamicFileUpdated';
2024

2125
constructor(private readonly documentManager: RazorDocumentManager, private readonly logger: RazorLogger) {}
2226

@@ -33,39 +37,57 @@ export class DynamicFileInfoHandler {
3337
await this.removeDynamicFileInfo(request);
3438
}
3539
);
40+
this.documentManager.onChange(async (e) => {
41+
if (e.kind == RazorDocumentChangeKind.csharpChanged && !e.document.isOpen) {
42+
const uriString = UriConverter.serialize(e.document.uri);
43+
const identifier = TextDocumentIdentifier.create(uriString);
44+
await vscode.commands.executeCommand(
45+
DynamicFileInfoHandler.dynamicFileUpdatedCommand,
46+
new RazorDynamicFileChangedParams(identifier)
47+
);
48+
}
49+
});
3650
}
3751

3852
// Given Razor document URIs, returns associated generated doc URIs
3953
private async provideDynamicFileInfo(
4054
request: ProvideDynamicFileParams
4155
): Promise<ProvideDynamicFileResponse | null> {
42-
let virtualUri: DocumentUri | null = null;
56+
this.documentManager.roslynActivated = true;
57+
const vscodeUri = vscode.Uri.parse(request.razorDocument.uri, true);
58+
59+
// Normally we start receiving dynamic info after Razor is initialized, but if the user had a .razor file open
60+
// when they started VS Code, the order is the other way around. This no-ops if Razor is already initialized.
61+
await this.documentManager.ensureRazorInitialized();
62+
63+
const razorDocument = await this.documentManager.getDocument(vscodeUri);
4364
try {
44-
const vscodeUri = vscode.Uri.parse(request.razorDocument.uri, true);
45-
const razorDocument = await this.documentManager.getDocument(vscodeUri);
4665
if (razorDocument === undefined) {
4766
this.logger.logWarning(
4867
`Could not find Razor document ${vscodeUri.fsPath}; adding null as a placeholder in URI array.`
4968
);
50-
} else {
51-
// Retrieve generated doc URIs for each Razor URI we are given
52-
const virtualCsharpUri = UriConverter.serialize(razorDocument.csharpDocument.uri);
53-
virtualUri = virtualCsharpUri;
69+
70+
return null;
5471
}
5572

56-
this.documentManager.roslynActivated = true;
73+
const virtualCsharpUri = UriConverter.serialize(razorDocument.csharpDocument.uri);
5774

58-
// Normally we start receiving dynamic info after Razor is initialized, but if the user had a .razor file open
59-
// when they started VS Code, the order is the other way around. This no-ops if Razor is already initialized.
60-
await this.documentManager.ensureRazorInitialized();
75+
if (this.documentManager.isRazorDocumentOpenInCSharpWorkspace(vscodeUri)) {
76+
// Open documents have didOpen/didChange to update the csharp buffer. Razor
77+
// does not send edits and instead lets vscode handle them.
78+
return new ProvideDynamicFileResponse({ uri: virtualCsharpUri }, null);
79+
} else {
80+
// Closed documents provide edits since the last time they were requested since
81+
// there is no open buffer in vscode corresponding to the csharp content.
82+
const csharpDocument = razorDocument.csharpDocument as CSharpProjectedDocument;
83+
const edits = csharpDocument.getAndApplyEdits();
84+
85+
return new ProvideDynamicFileResponse({ uri: virtualCsharpUri }, edits ?? null);
86+
}
6187
} catch (error) {
6288
this.logger.logWarning(`${DynamicFileInfoHandler.provideDynamicFileInfoCommand} failed with ${error}`);
6389
}
6490

65-
if (virtualUri) {
66-
return new ProvideDynamicFileResponse({ uri: virtualUri });
67-
}
68-
6991
return null;
7092
}
7193

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 RazorDynamicFileChangedParams {
9+
constructor(public readonly razorDocument: TextDocumentIdentifier) {}
10+
}

src/razor/src/dynamicFile/provideDynamicFileResponse.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { TextDocumentIdentifier } from 'vscode-languageclient/node';
7+
import { ServerTextChange } from '../rpc/serverTextChange';
78

89
// matches https://github.com/dotnet/roslyn/blob/9e91ca6590450e66e0041ee3135bbf044ac0687a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/RazorDynamicFileInfoProvider.cs#L28
910
export class ProvideDynamicFileResponse {
10-
constructor(public readonly csharpDocument: TextDocumentIdentifier | null) {}
11+
constructor(
12+
public readonly csharpDocument: TextDocumentIdentifier | null,
13+
public readonly edits: ServerTextChange[] | null
14+
) {}
1115
}

0 commit comments

Comments
 (0)