diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 794a00681..2f76fc018 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", + "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 4dacead5a..f5f2d7b50 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); } class WorkspaceStatus { @@ -40,3 +41,22 @@ class WorkspaceStatus { }); } } + +class ProjectContextStatus { + static createStatusItem(context: vscode.ExtensionContext, languageServer: RoslynLanguageServer) { + 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('Active File Context'); + context.subscriptions.push(item); + + projectContextService.onActiveFileContextChanged((e) => { + item.text = e.context._vs_label; + }); + projectContextService.refresh(); + } +} diff --git a/src/lsptoolshost/roslynLanguageServer.ts b/src/lsptoolshost/roslynLanguageServer.ts index a17eb92a0..bff4c239d 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, this._languageServerEvents); + // Register Razor dynamic file info handling this.registerDynamicFileInfo(); diff --git a/src/lsptoolshost/roslynProtocol.ts b/src/lsptoolshost/roslynProtocol.ts index 0d6a8321f..7c0801886 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 000000000..a892a81be --- /dev/null +++ b/src/lsptoolshost/services/projectContextService.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; +import { LanguageServerEvents } from '../languageServerEvents'; +import { ServerState } from '../serverStateChange'; + +export interface ProjectContextChangeEvent { + uri: vscode.Uri; + context: VSProjectContext; +} + +export class ProjectContextService { + private readonly _contextChangeEmitter = new vscode.EventEmitter(); + private _source = new vscode.CancellationTokenSource(); + + 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(); + } + }); + + vscode.window.onDidChangeActiveTextEditor(async (_) => this.refresh()); + } + + public get onActiveFileContextChanged(): vscode.Event { + return this._contextChangeEmitter.event; + } + + public async refresh() { + const textEditor = vscode.window.activeTextEditor; + if (textEditor?.document?.languageId !== 'csharp') { + return; + } + + const uri = textEditor.document.uri; + + // If we have an open request, cancel it. + this._source.cancel(); + this._source = new vscode.CancellationTokenSource(); + + 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( + uri: vscode.Uri, + token: vscode.CancellationToken + ): Promise { + const uriString = UriConverter.serialize(uri); + const textDocument = TextDocumentIdentifier.create(uriString); + + try { + return this._languageServer.sendRequest( + VSGetProjectContextsRequest.type, + { _vs_textDocument: textDocument }, + token + ); + } catch (error) { + if (error instanceof vscode.CancellationError) { + return undefined; + } + + throw error; + } + } +} diff --git a/test/integrationTests/documentDiagnostics.integration.test.ts b/test/integrationTests/documentDiagnostics.integration.test.ts index 3f837e41e..a073536c2 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);