From bbb646f1ce90f806bf155e83abe6d325e0c38eb3 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Tue, 9 Jul 2024 15:58:48 -0700 Subject: [PATCH 1/5] Add Project Context status item --- l10n/bundle.l10n.json | 2 + src/lsptoolshost/languageStatusBar.ts | 41 ++++++++++++++++++ src/lsptoolshost/roslynLanguageServer.ts | 4 ++ src/lsptoolshost/roslynProtocol.ts | 28 ++++++++++++ .../services/projectContextService.ts | 43 +++++++++++++++++++ 5 files changed, 118 insertions(+) create mode 100644 src/lsptoolshost/services/projectContextService.ts diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 794a00681f..33d6ed860a 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -161,6 +161,8 @@ "Fix All: ": "Fix All: ", "C# Workspace Status": "C# Workspace Status", "Open solution": "Open solution", + "C# Project Context Status": "C# Project Context Status", + "Project Context": "Project Context", "Pick a fix all scope": "Pick a fix all scope", "Fix All Code Action": "Fix All Code Action", "pipeArgs must be a string or a string array type": "pipeArgs must be a string or a string array type", diff --git a/src/lsptoolshost/languageStatusBar.ts b/src/lsptoolshost/languageStatusBar.ts index 4dacead5a8..1056a72f1c 100644 --- a/src/lsptoolshost/languageStatusBar.ts +++ b/src/lsptoolshost/languageStatusBar.ts @@ -19,6 +19,7 @@ export function registerLanguageStatusItems( if (!getCSharpDevKit()) { WorkspaceStatus.createStatusItem(context, languageServerEvents); } + ProjectContextStatus.createStatusItem(context, languageServer, languageServerEvents); } class WorkspaceStatus { @@ -40,3 +41,43 @@ class WorkspaceStatus { }); } } + +class ProjectContextStatus { + static createStatusItem( + context: vscode.ExtensionContext, + languageServer: RoslynLanguageServer, + languageServerEvents: RoslynLanguageServerEvents + ) { + const projectContextService = languageServer._projectContextService; + + const item = vscode.languages.createLanguageStatusItem( + 'csharp.projectContextStatus', + languageServerOptions.documentSelector + ); + item.name = vscode.l10n.t('C# Project Context Status'); + item.detail = vscode.l10n.t('Project Context'); + context.subscriptions.push(item); + + updateItem(vscode.window.activeTextEditor); + context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(updateItem)); + + languageServerEvents.onServerStateChange((e) => { + if (e.state === ServerState.ProjectInitializationComplete) { + projectContextService.clear(); + updateItem(vscode.window.activeTextEditor); + } + }); + + async function updateItem(e: vscode.TextEditor | undefined) { + if (e?.document.languageId !== 'csharp') { + item.text = ''; + return; + } + + const projectContext = await projectContextService.getCurrentProjectContext(e.document.uri); + if (projectContext) { + item.text = projectContext._vs_label; + } + } + } +} diff --git a/src/lsptoolshost/roslynLanguageServer.ts b/src/lsptoolshost/roslynLanguageServer.ts index a17eb92a03..a8d4719bf9 100644 --- a/src/lsptoolshost/roslynLanguageServer.ts +++ b/src/lsptoolshost/roslynLanguageServer.ts @@ -65,6 +65,7 @@ import { BuildDiagnosticsService } from './buildDiagnosticsService'; import { getComponentPaths } from './builtInComponents'; import { OnAutoInsertFeature } from './onAutoInsertFeature'; import { registerLanguageStatusItems } from './languageStatusBar'; +import { ProjectContextService } from './services/projectContextService'; let _channel: vscode.OutputChannel; let _traceChannel: vscode.OutputChannel; @@ -106,6 +107,7 @@ export class RoslynLanguageServer { public readonly _onAutoInsertFeature: OnAutoInsertFeature; public _buildDiagnosticService: BuildDiagnosticsService; + public _projectContextService: ProjectContextService; constructor( private _languageClient: RoslynLanguageClient, @@ -125,6 +127,8 @@ export class RoslynLanguageServer { this._buildDiagnosticService = new BuildDiagnosticsService(diagnosticsReportedByBuild); this.registerDocumentOpenForDiagnostics(); + this._projectContextService = new ProjectContextService(this); + // Register Razor dynamic file info handling this.registerDynamicFileInfo(); diff --git a/src/lsptoolshost/roslynProtocol.ts b/src/lsptoolshost/roslynProtocol.ts index 0d6a8321f4..7c08018868 100644 --- a/src/lsptoolshost/roslynProtocol.ts +++ b/src/lsptoolshost/roslynProtocol.ts @@ -8,6 +8,21 @@ import * as lsp from 'vscode-languageserver-protocol'; import { CodeAction, TextDocumentRegistrationOptions } from 'vscode-languageserver-protocol'; import { ProjectConfigurationMessage } from '../shared/projectConfiguration'; +export interface VSProjectContextList { + _vs_projectContexts: VSProjectContext[]; + _vs_defaultIndex: number; +} + +export interface VSProjectContext { + _vs_label: string; + _vs_id: string; + _vs_kind: string; +} + +export interface VSTextDocumentIdentifier extends lsp.TextDocumentIdentifier { + _vs_projectContext: VSProjectContext | undefined; +} + export interface WorkspaceDebugConfigurationParams { /** * Workspace path containing the solution/projects to get debug information for. @@ -88,6 +103,13 @@ export interface RegisterSolutionSnapshotResponseItem { id: lsp.integer; } +export interface VSGetProjectContextParams { + /** + * The document the project context is being requested for. + */ + _vs_textDocument: lsp.TextDocumentIdentifier; +} + export interface RunTestsParams extends lsp.WorkDoneProgressParams, lsp.PartialResultParams { /** * The text document containing the tests to run. @@ -210,6 +232,12 @@ export namespace RegisterSolutionSnapshotRequest { export const type = new lsp.RequestType0(method); } +export namespace VSGetProjectContextsRequest { + export const method = 'textDocument/_vs_getProjectContexts'; + export const messageDirection: lsp.MessageDirection = lsp.MessageDirection.clientToServer; + export const type = new lsp.RequestType(method); +} + export namespace ProjectInitializationCompleteNotification { export const method = 'workspace/projectInitializationComplete'; export const messageDirection: lsp.MessageDirection = lsp.MessageDirection.serverToClient; diff --git a/src/lsptoolshost/services/projectContextService.ts b/src/lsptoolshost/services/projectContextService.ts new file mode 100644 index 0000000000..d23f27b253 --- /dev/null +++ b/src/lsptoolshost/services/projectContextService.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { RoslynLanguageServer } from '../roslynLanguageServer'; +import { VSGetProjectContextsRequest, VSProjectContext, VSProjectContextList } from '../roslynProtocol'; +import { TextDocumentIdentifier } from 'vscode-languageserver-protocol'; +import { UriConverter } from '../uriConverter'; + +export class ProjectContextService { + /** Track the project contexts for a particular document uri. */ + private _projectContexts: { [uri: string]: Promise | VSProjectContextList } = {}; + + constructor(private languageServer: RoslynLanguageServer) {} + + clear() { + this._projectContexts = {}; + } + + async getCurrentProjectContext(uri: string | vscode.Uri): Promise { + const projectContexts = await this.getProjectContexts(uri); + return projectContexts?._vs_projectContexts[projectContexts._vs_defaultIndex]; + } + + async getProjectContexts(uri: string | vscode.Uri): Promise { + const uriString = uri instanceof vscode.Uri ? UriConverter.serialize(uri) : uri; + + if (!(uriString in this._projectContexts)) { + const source = new vscode.CancellationTokenSource(); + this._projectContexts[uriString] = this.languageServer + .sendRequest( + VSGetProjectContextsRequest.type, + { _vs_textDocument: TextDocumentIdentifier.create(uriString) }, + source.token + ) + .then((contextList) => (this._projectContexts[uriString] = contextList)); + } + + return this._projectContexts[uriString]; + } +} From d8603b5f075fa0b33c347f70da72aacd49819dcd Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Wed, 10 Jul 2024 00:31:28 -0700 Subject: [PATCH 2/5] Remove caching and add onChange event. --- l10n/bundle.l10n.json | 2 +- src/lsptoolshost/languageStatusBar.ts | 33 ++------ src/lsptoolshost/roslynLanguageServer.ts | 2 +- .../services/projectContextService.ts | 77 ++++++++++++++----- 4 files changed, 64 insertions(+), 50 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 33d6ed860a..2f76fc0180 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -162,7 +162,7 @@ "C# Workspace Status": "C# Workspace Status", "Open solution": "Open solution", "C# Project Context Status": "C# Project Context Status", - "Project Context": "Project Context", + "Active File Context": "Active File Context", "Pick a fix all scope": "Pick a fix all scope", "Fix All Code Action": "Fix All Code Action", "pipeArgs must be a string or a string array type": "pipeArgs must be a string or a string array type", diff --git a/src/lsptoolshost/languageStatusBar.ts b/src/lsptoolshost/languageStatusBar.ts index 1056a72f1c..f5f2d7b502 100644 --- a/src/lsptoolshost/languageStatusBar.ts +++ b/src/lsptoolshost/languageStatusBar.ts @@ -19,7 +19,7 @@ export function registerLanguageStatusItems( if (!getCSharpDevKit()) { WorkspaceStatus.createStatusItem(context, languageServerEvents); } - ProjectContextStatus.createStatusItem(context, languageServer, languageServerEvents); + ProjectContextStatus.createStatusItem(context, languageServer); } class WorkspaceStatus { @@ -43,11 +43,7 @@ class WorkspaceStatus { } class ProjectContextStatus { - static createStatusItem( - context: vscode.ExtensionContext, - languageServer: RoslynLanguageServer, - languageServerEvents: RoslynLanguageServerEvents - ) { + static createStatusItem(context: vscode.ExtensionContext, languageServer: RoslynLanguageServer) { const projectContextService = languageServer._projectContextService; const item = vscode.languages.createLanguageStatusItem( @@ -55,29 +51,12 @@ class ProjectContextStatus { languageServerOptions.documentSelector ); item.name = vscode.l10n.t('C# Project Context Status'); - item.detail = vscode.l10n.t('Project Context'); + item.detail = vscode.l10n.t('Active File Context'); context.subscriptions.push(item); - updateItem(vscode.window.activeTextEditor); - context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(updateItem)); - - languageServerEvents.onServerStateChange((e) => { - if (e.state === ServerState.ProjectInitializationComplete) { - projectContextService.clear(); - updateItem(vscode.window.activeTextEditor); - } + projectContextService.onActiveFileContextChanged((e) => { + item.text = e.context._vs_label; }); - - async function updateItem(e: vscode.TextEditor | undefined) { - if (e?.document.languageId !== 'csharp') { - item.text = ''; - return; - } - - const projectContext = await projectContextService.getCurrentProjectContext(e.document.uri); - if (projectContext) { - item.text = projectContext._vs_label; - } - } + projectContextService.refresh(); } } diff --git a/src/lsptoolshost/roslynLanguageServer.ts b/src/lsptoolshost/roslynLanguageServer.ts index a8d4719bf9..bff4c239d1 100644 --- a/src/lsptoolshost/roslynLanguageServer.ts +++ b/src/lsptoolshost/roslynLanguageServer.ts @@ -127,7 +127,7 @@ export class RoslynLanguageServer { this._buildDiagnosticService = new BuildDiagnosticsService(diagnosticsReportedByBuild); this.registerDocumentOpenForDiagnostics(); - this._projectContextService = new ProjectContextService(this); + this._projectContextService = new ProjectContextService(this, this._languageServerEvents); // Register Razor dynamic file info handling this.registerDynamicFileInfo(); diff --git a/src/lsptoolshost/services/projectContextService.ts b/src/lsptoolshost/services/projectContextService.ts index d23f27b253..7d604aa8e1 100644 --- a/src/lsptoolshost/services/projectContextService.ts +++ b/src/lsptoolshost/services/projectContextService.ts @@ -8,36 +8,71 @@ import { RoslynLanguageServer } from '../roslynLanguageServer'; import { VSGetProjectContextsRequest, VSProjectContext, VSProjectContextList } from '../roslynProtocol'; import { TextDocumentIdentifier } from 'vscode-languageserver-protocol'; import { UriConverter } from '../uriConverter'; +import { LanguageServerEvents } from '../languageServerEvents'; +import { ServerState } from '../serverStateChange'; + +export interface ProjectContextChangeEvent { + uri: vscode.Uri; + context: VSProjectContext; +} export class ProjectContextService { - /** Track the project contexts for a particular document uri. */ - private _projectContexts: { [uri: string]: Promise | VSProjectContextList } = {}; + private readonly _contextChangeEmitter = new vscode.EventEmitter(); + private _source = new vscode.CancellationTokenSource(); - constructor(private languageServer: RoslynLanguageServer) {} + constructor(private _languageServer: RoslynLanguageServer, _languageServerEvents: LanguageServerEvents) { + _languageServerEvents.onServerStateChange((e) => { + // When the project initialization is complete, open files + // could move from the miscellaneous workspace context into + // an open project. + if (e.state === ServerState.ProjectInitializationComplete) { + this.refresh(); + } + }); - clear() { - this._projectContexts = {}; + vscode.window.onDidChangeActiveTextEditor(this.refresh); } - async getCurrentProjectContext(uri: string | vscode.Uri): Promise { - const projectContexts = await this.getProjectContexts(uri); - return projectContexts?._vs_projectContexts[projectContexts._vs_defaultIndex]; + public get onActiveFileContextChanged(): vscode.Event { + return this._contextChangeEmitter.event; } - async getProjectContexts(uri: string | vscode.Uri): Promise { - const uriString = uri instanceof vscode.Uri ? UriConverter.serialize(uri) : uri; - - if (!(uriString in this._projectContexts)) { - const source = new vscode.CancellationTokenSource(); - this._projectContexts[uriString] = this.languageServer - .sendRequest( - VSGetProjectContextsRequest.type, - { _vs_textDocument: TextDocumentIdentifier.create(uriString) }, - source.token - ) - .then((contextList) => (this._projectContexts[uriString] = contextList)); + public async refresh() { + const textEditor = vscode.window.activeTextEditor; + if (textEditor?.document?.languageId !== 'csharp') { + return; } - return this._projectContexts[uriString]; + const uri = textEditor.document.uri; + + // If we have an open request, cancel it. + this._source.cancel(); + this._source = new vscode.CancellationTokenSource(); + + try { + const contextList = await this.getProjectContexts(uri, this._source.token); + if (!contextList) { + return; + } + + const context = contextList._vs_projectContexts[contextList._vs_defaultIndex]; + this._contextChangeEmitter.fire({ uri, context }); + } catch { + // This request was cancelled + } + } + + private async getProjectContexts( + uri: vscode.Uri, + token: vscode.CancellationToken + ): Promise { + const uriString = UriConverter.serialize(uri); + const textDocument = TextDocumentIdentifier.create(uriString); + + return this._languageServer.sendRequest( + VSGetProjectContextsRequest.type, + { _vs_textDocument: textDocument }, + token + ); } } From 39cb0531bc22c07e6d6fbc0ace0a5d95096f82ce Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Wed, 10 Jul 2024 00:47:40 -0700 Subject: [PATCH 3/5] Fix tests. --- src/lsptoolshost/services/projectContextService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lsptoolshost/services/projectContextService.ts b/src/lsptoolshost/services/projectContextService.ts index 7d604aa8e1..8f5709fa0d 100644 --- a/src/lsptoolshost/services/projectContextService.ts +++ b/src/lsptoolshost/services/projectContextService.ts @@ -30,7 +30,7 @@ export class ProjectContextService { } }); - vscode.window.onDidChangeActiveTextEditor(this.refresh); + vscode.window.onDidChangeActiveTextEditor(async (_) => this.refresh()); } public get onActiveFileContextChanged(): vscode.Event { From 47b217f5f0c2fef4318531f7077351c0244b236d Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Wed, 10 Jul 2024 16:35:18 -0700 Subject: [PATCH 4/5] Handle CancellationError gracefully --- .../services/projectContextService.ts | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/lsptoolshost/services/projectContextService.ts b/src/lsptoolshost/services/projectContextService.ts index 8f5709fa0d..a892a81bef 100644 --- a/src/lsptoolshost/services/projectContextService.ts +++ b/src/lsptoolshost/services/projectContextService.ts @@ -49,17 +49,13 @@ export class ProjectContextService { this._source.cancel(); this._source = new vscode.CancellationTokenSource(); - try { - const contextList = await this.getProjectContexts(uri, this._source.token); - if (!contextList) { - return; - } - - const context = contextList._vs_projectContexts[contextList._vs_defaultIndex]; - this._contextChangeEmitter.fire({ uri, context }); - } catch { - // This request was cancelled + const contextList = await this.getProjectContexts(uri, this._source.token); + if (!contextList) { + return; } + + const context = contextList._vs_projectContexts[contextList._vs_defaultIndex]; + this._contextChangeEmitter.fire({ uri, context }); } private async getProjectContexts( @@ -69,10 +65,18 @@ export class ProjectContextService { const uriString = UriConverter.serialize(uri); const textDocument = TextDocumentIdentifier.create(uriString); - return this._languageServer.sendRequest( - VSGetProjectContextsRequest.type, - { _vs_textDocument: textDocument }, - token - ); + try { + return this._languageServer.sendRequest( + VSGetProjectContextsRequest.type, + { _vs_textDocument: textDocument }, + token + ); + } catch (error) { + if (error instanceof vscode.CancellationError) { + return undefined; + } + + throw error; + } } } From a7d58a2e2dd0ad5a38fc069eeb5469bdb682f211 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Wed, 10 Jul 2024 22:57:58 -0700 Subject: [PATCH 5/5] Fix integration test --- test/integrationTests/documentDiagnostics.integration.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/integrationTests/documentDiagnostics.integration.test.ts b/test/integrationTests/documentDiagnostics.integration.test.ts index 3f837e41e1..a073536c2b 100644 --- a/test/integrationTests/documentDiagnostics.integration.test.ts +++ b/test/integrationTests/documentDiagnostics.integration.test.ts @@ -85,8 +85,6 @@ describe(`[${testAssetWorkspace.description}] Test diagnostics`, function () { analyzer: AnalysisSetting.OpenFiles, }); - await integrationHelpers.restartLanguageServer(); - await waitForExpectedFileDiagnostics((diagnostics) => { expect(diagnostics).toHaveLength(4);