Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add client side support for refreshing source generated files #7791

Merged
merged 2 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
- Debug from .csproj and .sln [#5876](https://github.com/dotnet/vscode-csharp/issues/5876)

# 2.58.x
* Add support for refreshing opened source generated files (PR: [#7791](https://github.com/dotnet/vscode-csharp/pull/7791))
* Update Roslyn to 4.13.0-2.24569.1 (PR: [#7791](https://github.com/dotnet/vscode-csharp/pull/7791))
* Support unbound generic types in 'nameof' operator. (PR: [#75368](https://github.com/dotnet/roslyn/pull/75368))
* Include list of processes that lock file in `can't write file` error message (PR: [#75946](https://github.com/dotnet/roslyn/pull/75946))
* Add server side support for refreshing source generated files (PR: [#75939](https://github.com/dotnet/roslyn/pull/75939))

# 2.57.x
* Update Razor to 9.0.0-preview.24565.1 (PR: [#7793])(https://github.com/dotnet/vscode-csharp/pull/7793)
Expand Down
1 change: 1 addition & 0 deletions l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
"Fix all issues": "Fix all issues",
"Select fix all action": "Select fix all action",
"Test run already in progress": "Test run already in progress",
"Generated document not found": "Generated document not found",
"Server stopped": "Server stopped",
"Workspace projects": "Workspace projects",
"Your workspace has multiple Visual Studio Solution files; please select one to get full IntelliSense.": "Your workspace has multiple Visual Studio Solution files; please select one to get full IntelliSense.",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
}
},
"defaults": {
"roslyn": "4.13.0-2.24565.3",
"roslyn": "4.13.0-2.24569.1",
"omniSharp": "1.39.11",
"razor": "9.0.0-preview.24565.1",
"razorOmnisharp": "7.0.0-preview.23363.1",
Expand Down
5 changes: 5 additions & 0 deletions src/lsptoolshost/roslynLanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
CancellationToken,
RequestHandler,
ResponseError,
NotificationHandler0,
} from 'vscode-languageclient/node';
import { PlatformInformation } from '../shared/platform';
import { readConfigurations } from './configurationMiddleware';
Expand Down Expand Up @@ -423,6 +424,10 @@ export class RoslynLanguageServer {
this._languageClient.addDisposable(this._languageClient.onRequest(type, handler));
}

public registerOnNotification(method: string, handler: NotificationHandler0) {
this._languageClient.addDisposable(this._languageClient.onNotification(method, handler));
}

public async registerSolutionSnapshot(token: vscode.CancellationToken): Promise<SolutionSnapshotId> {
const response = await this.sendRequest0(RoslynProtocol.RegisterSolutionSnapshotRequest.type, token);
if (response) {
Expand Down
10 changes: 9 additions & 1 deletion src/lsptoolshost/roslynProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,10 +233,12 @@ export interface CopilotRelatedDocumentsReport {

export interface SourceGeneratorGetRequestParams {
textDocument: lsp.TextDocumentIdentifier;
resultId?: string;
}

export interface SourceGeneratedDocumentText {
text: string;
text?: string;
resultId?: string;
}

export namespace WorkspaceDebugConfigurationRequest {
Expand Down Expand Up @@ -366,3 +368,9 @@ export namespace SourceGeneratorGetTextRequest {
export const messageDirection: lsp.MessageDirection = lsp.MessageDirection.clientToServer;
export const type = new lsp.RequestType<SourceGeneratorGetRequestParams, SourceGeneratedDocumentText, void>(method);
}

export namespace RefreshSourceGeneratedDocumentNotification {
export const method = 'workspace/refreshSourceGeneratedDocument';
export const messageDirection: lsp.MessageDirection = lsp.MessageDirection.serverToClient;
export const type = new lsp.NotificationType(method);
}
96 changes: 86 additions & 10 deletions src/lsptoolshost/sourceGeneratedFilesContentProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as RoslynProtocol from './roslynProtocol';
import { RoslynLanguageServer } from './roslynLanguageServer';
import { UriConverter } from './uriConverter';
import * as lsp from 'vscode-languageserver-protocol';
import { IDisposable } from '@microsoft/servicehub-framework';

export function registerSourceGeneratedFilesContentProvider(
context: vscode.ExtensionContext,
Expand All @@ -16,16 +17,91 @@ export function registerSourceGeneratedFilesContentProvider(
context.subscriptions.push(
vscode.workspace.registerTextDocumentContentProvider(
'roslyn-source-generated',
new (class implements vscode.TextDocumentContentProvider {
async provideTextDocumentContent(uri: vscode.Uri, token: vscode.CancellationToken): Promise<string> {
const result = await languageServer.sendRequest(
RoslynProtocol.SourceGeneratorGetTextRequest.type,
{ textDocument: lsp.TextDocumentIdentifier.create(UriConverter.serialize(uri)) },
token
);
return result.text;
}
})()
new RoslynSourceGeneratedContentProvider(languageServer)
)
);
}

class RoslynSourceGeneratedContentProvider implements vscode.TextDocumentContentProvider, IDisposable {
private _onDidChangeEmitter: vscode.EventEmitter<vscode.Uri> = new vscode.EventEmitter<vscode.Uri>();

// Stores all the source generated documents that we have opened so far and their up to date content.
private _openedDocuments: Map<vscode.Uri, RoslynProtocol.SourceGeneratedDocumentText> = new Map();

// Since we could potentially have multiple refresh notifications in flight at the same time,
// we use a simple queue to ensure that updates to our state map only happen serially.
private _updateQueue?: Promise<RoslynProtocol.SourceGeneratedDocumentText>;

private _cancellationSource = new vscode.CancellationTokenSource();

constructor(private languageServer: RoslynLanguageServer) {
languageServer.registerOnNotification(
RoslynProtocol.RefreshSourceGeneratedDocumentNotification.method,
async () => {
this._openedDocuments.forEach(async (_, key) => {
await this.enqueueDocumentUpdateAsync(key, this._cancellationSource.token);
this._onDidChangeEmitter.fire(key);
});
}
);
vscode.workspace.onDidCloseTextDocument((document) => {
const openedDoc = this._openedDocuments.get(document.uri);
if (openedDoc !== undefined) {
this._openedDocuments.delete(document.uri);
}
});
}

public onDidChange: vscode.Event<vscode.Uri> = this._onDidChangeEmitter.event;

async provideTextDocumentContent(uri: vscode.Uri, token: vscode.CancellationToken): Promise<string> {
let content = this._openedDocuments.get(uri);

if (!content) {
// We're being asked about this document for the first time, so we need to fetch it from the server.
content = await this.enqueueDocumentUpdateAsync(uri, token);
}

return content.text ?? vscode.l10n.t('Generated document not found');
}

private async enqueueDocumentUpdateAsync(
uri: vscode.Uri,
token: vscode.CancellationToken
): Promise<RoslynProtocol.SourceGeneratedDocumentText> {
if (!this._updateQueue) {
this._updateQueue = this.updateDocumentAsync(uri, token);
} else {
this._updateQueue = this._updateQueue.then(async () => await this.updateDocumentAsync(uri, token));
}

return await this._updateQueue;
}

private async updateDocumentAsync(
uri: vscode.Uri,
token: vscode.CancellationToken
): Promise<RoslynProtocol.SourceGeneratedDocumentText> {
const currentContent = this._openedDocuments.get(uri);
const newContent = await this.languageServer.sendRequest(
RoslynProtocol.SourceGeneratorGetTextRequest.type,
{
textDocument: lsp.TextDocumentIdentifier.create(UriConverter.serialize(uri)),
resultId: currentContent?.resultId,
},
token
);

// If we had no content before, or the resultId has changed, update the content
if (!currentContent || newContent.resultId !== currentContent?.resultId) {
this._openedDocuments.set(uri, newContent);
return newContent;
}

return currentContent;
}

dispose(): void {
this._cancellationSource.cancel();
}
}
Loading