From 44b576a406d852411b9e72c5c1ed6a7cc8b84b54 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Tue, 2 Jul 2024 12:14:23 -0700 Subject: [PATCH] Add ability to select a documents project context. - Adds select project context commands - Adds the select command to the Project Context status item - Updates middleware to send selected context with server requests. --- l10n/bundle.l10n.json | 2 + package.json | 6 +++ package.nls.json | 1 + src/lsptoolshost/commands.ts | 38 ++++++++++++++++ src/lsptoolshost/languageStatusBar.ts | 12 ++++-- src/lsptoolshost/roslynLanguageServer.ts | 27 ++++++++++-- .../services/projectContextService.ts | 43 ++++++++++++++++--- 7 files changed, 117 insertions(+), 12 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 2f76fc018..0221302fc 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -161,10 +161,12 @@ "Fix All: ": "Fix All: ", "C# Workspace Status": "C# Workspace Status", "Open solution": "Open solution", + "Select context": "Select context", "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", + "Select project context": "Select project context", "pipeArgs must be a string or a string array type": "pipeArgs must be a string or a string array type", "Name not defined in current configuration.": "Name not defined in current configuration.", "Configuration \"{0}\" in launch.json does not have a {1} argument with {2} for remote process listing.": "Configuration \"{0}\" in launch.json does not have a {1} argument with {2} for remote process listing.", diff --git a/package.json b/package.json index 3bbb0d0b5..3df98f5f7 100644 --- a/package.json +++ b/package.json @@ -1824,6 +1824,12 @@ "category": ".NET", "enablement": "dotnet.server.activationContext == 'Roslyn' || dotnet.server.activationContext == 'OmniSharp'" }, + { + "command": "csharp.changeDocumentContext", + "title": "%command.csharp.changeDocumentContext%", + "category": "CSharp", + "enablement": "dotnet.server.activationContext == 'Roslyn'" + }, { "command": "csharp.listProcess", "title": "%command.csharp.listProcess%", diff --git a/package.nls.json b/package.nls.json index e5885cba1..59a2fcd56 100644 --- a/package.nls.json +++ b/package.nls.json @@ -10,6 +10,7 @@ "command.dotnet.generateAssets.currentProject": "Generate Assets for Build and Debug", "command.dotnet.restore.project": "Restore Project", "command.dotnet.restore.all": "Restore All Projects", + "command.csharp.changeDocumentContext": "Change the active document's project context", "command.csharp.downloadDebugger": "Download .NET Core Debugger", "command.csharp.listProcess": "List process for attach", "command.csharp.listRemoteProcess": "List processes on remote connection for attach", diff --git a/src/lsptoolshost/commands.ts b/src/lsptoolshost/commands.ts index b5e42ad23..452c8df65 100644 --- a/src/lsptoolshost/commands.ts +++ b/src/lsptoolshost/commands.ts @@ -11,6 +11,8 @@ import { createLaunchTargetForSolution } from '../shared/launchTarget'; import reportIssue from '../shared/reportIssue'; import { getDotnetInfo } from '../shared/utils/getDotnetInfo'; import { IHostExecutableResolver } from '../shared/constants/IHostExecutableResolver'; +import { VSProjectContext } from './roslynProtocol'; +import { CancellationToken } from 'vscode-languageclient/node'; export function registerCommands( context: vscode.ExtensionContext, @@ -41,6 +43,11 @@ export function registerCommands( context.subscriptions.push( vscode.commands.registerCommand('dotnet.openSolution', async () => openSolution(languageServer)) ); + context.subscriptions.push( + vscode.commands.registerCommand('csharp.changeDocumentContext', async () => + changeDocumentContext(languageServer) + ) + ); context.subscriptions.push( vscode.commands.registerCommand('csharp.reportIssue', async () => reportIssue( @@ -191,3 +198,34 @@ async function openSolution(languageServer: RoslynLanguageServer): Promise { + const editor = vscode.window.activeTextEditor; + if (!editor) { + return; + } + const projectContexts = await languageServer._projectContextService.getProjectContexts( + editor.document.uri, + CancellationToken.None + ); + if (!projectContexts) { + return; + } + + const items = projectContexts._vs_projectContexts.map((context) => { + return { label: context._vs_label, context }; + }); + const selectedItem = await vscode.window.showQuickPick(items, { + placeHolder: vscode.l10n.t('Select project context'), + }); + + if (selectedItem) { + languageServer._projectContextService.setDocumentContext( + editor.document.uri, + selectedItem.context, + projectContexts._vs_projectContexts.length > 1 + ); + // TODO: Replace this with proper server-side onDidChange notifications + editor.edit(() => 0); + } +} diff --git a/src/lsptoolshost/languageStatusBar.ts b/src/lsptoolshost/languageStatusBar.ts index f5f2d7b50..1b760bdf1 100644 --- a/src/lsptoolshost/languageStatusBar.ts +++ b/src/lsptoolshost/languageStatusBar.ts @@ -45,7 +45,10 @@ class WorkspaceStatus { class ProjectContextStatus { static createStatusItem(context: vscode.ExtensionContext, languageServer: RoslynLanguageServer) { const projectContextService = languageServer._projectContextService; - + const selectContextCommand = { + command: 'csharp.changeDocumentContext', + title: vscode.l10n.t('Select context'), + }; const item = vscode.languages.createLanguageStatusItem( 'csharp.projectContextStatus', languageServerOptions.documentSelector @@ -54,8 +57,11 @@ class ProjectContextStatus { item.detail = vscode.l10n.t('Active File Context'); context.subscriptions.push(item); - projectContextService.onActiveFileContextChanged((e) => { - item.text = e.context._vs_label; + projectContextService.onDocumentContextChanged((e) => { + if (vscode.window.activeTextEditor?.document.uri === e.uri) { + item.text = e.context._vs_label; + item.command = e.hasAdditionalContexts ? selectContextCommand : undefined; + } }); projectContextService.refresh(); } diff --git a/src/lsptoolshost/roslynLanguageServer.ts b/src/lsptoolshost/roslynLanguageServer.ts index bff4c239d..c063fb7cc 100644 --- a/src/lsptoolshost/roslynLanguageServer.ts +++ b/src/lsptoolshost/roslynLanguageServer.ts @@ -57,7 +57,7 @@ import { registerRazorCommands } from './razorCommands'; import { registerOnAutoInsert } from './onAutoInsert'; import { registerCodeActionFixAllCommands } from './fixAllCodeAction'; import { commonOptions, languageServerOptions, omnisharpOptions, razorOptions } from '../shared/options'; -import { NamedPipeInformation } from './roslynProtocol'; +import { NamedPipeInformation, VSTextDocumentIdentifier } from './roslynProtocol'; import { IDisposable } from '../disposable'; import { registerNestedCodeActionCommands } from './nestedCodeAction'; import { registerRestoreCommands } from './restore'; @@ -127,7 +127,7 @@ export class RoslynLanguageServer { this._buildDiagnosticService = new BuildDiagnosticsService(diagnosticsReportedByBuild); this.registerDocumentOpenForDiagnostics(); - this._projectContextService = new ProjectContextService(this, this._languageServerEvents); + this._projectContextService = new ProjectContextService(this, _languageServerEvents); // Register Razor dynamic file info handling this.registerDynamicFileInfo(); @@ -220,6 +220,7 @@ export class RoslynLanguageServer { }; const documentSelector = languageServerOptions.documentSelector; + let server: RoslynLanguageServer | undefined = undefined; // Options to control the language client const clientOptions: LanguageClientOptions = { @@ -240,6 +241,22 @@ export class RoslynLanguageServer { protocol2Code: UriConverter.deserialize, }, middleware: { + async sendRequest(type, param, token, next) { + if (isObject(param)) { + if ('textDocument' in param) { + const textDocument = param.textDocument; + textDocument._vs_projectContext = server?._projectContextService.getDocumentContext( + textDocument.uri + ); + } else if ('_vs_textDocument' in param) { + const textDocument = param._vs_textDocument; + textDocument._vs_projectContext = server?._projectContextService.getDocumentContext( + textDocument.uri + ); + } + } + return next(type, param, token); + }, workspace: { configuration: (params) => readConfigurations(params), }, @@ -256,7 +273,7 @@ export class RoslynLanguageServer { client.registerProposedFeatures(); - const server = new RoslynLanguageServer(client, platformInfo, context, telemetryReporter, languageServerEvents); + server = new RoslynLanguageServer(client, platformInfo, context, telemetryReporter, languageServerEvents); client.registerFeature(server._onAutoInsertFeature); @@ -1107,3 +1124,7 @@ function getSessionId(): string { export function isString(value: any): value is string { return typeof value === 'string' || value instanceof String; } + +export function isObject(value: any): value is object { + return value !== null && typeof value === 'object'; +} diff --git a/src/lsptoolshost/services/projectContextService.ts b/src/lsptoolshost/services/projectContextService.ts index a892a81be..c0c42d792 100644 --- a/src/lsptoolshost/services/projectContextService.ts +++ b/src/lsptoolshost/services/projectContextService.ts @@ -14,9 +14,12 @@ import { ServerState } from '../serverStateChange'; export interface ProjectContextChangeEvent { uri: vscode.Uri; context: VSProjectContext; + hasAdditionalContexts: boolean; } export class ProjectContextService { + /** Track the project context for a particular document uri. */ + private readonly _documentContexts: { [uri: string]: VSProjectContext } = {}; private readonly _contextChangeEmitter = new vscode.EventEmitter(); private _source = new vscode.CancellationTokenSource(); @@ -33,10 +36,27 @@ export class ProjectContextService { vscode.window.onDidChangeActiveTextEditor(async (_) => this.refresh()); } - public get onActiveFileContextChanged(): vscode.Event { + public get onDocumentContextChanged(): vscode.Event { return this._contextChangeEmitter.event; } + public getDocumentContext(uri: string | vscode.Uri): VSProjectContext | undefined { + const uriString = uri instanceof vscode.Uri ? UriConverter.serialize(uri) : uri; + return this._documentContexts[uriString]; + } + + public setDocumentContext( + uri: string | vscode.Uri, + context: VSProjectContext, + hasAdditionalContexts: boolean + ): void { + const uriString = uri instanceof vscode.Uri ? UriConverter.serialize(uri) : uri; + uri = uri instanceof vscode.Uri ? uri : UriConverter.deserialize(uri); + + this._documentContexts[uriString] = context; + this._contextChangeEmitter.fire({ uri, context, hasAdditionalContexts }); + } + public async refresh() { const textEditor = vscode.window.activeTextEditor; if (textEditor?.document?.languageId !== 'csharp') { @@ -54,15 +74,26 @@ export class ProjectContextService { return; } - const context = contextList._vs_projectContexts[contextList._vs_defaultIndex]; - this._contextChangeEmitter.fire({ uri, context }); + // Determine if the user has selected a context for this document and whether + // it is still in the list of contexts. + const uriString = UriConverter.serialize(uri); + const selectedContext = this._documentContexts[uriString]; + const selectedContextValid = selectedContext + ? contextList._vs_projectContexts.some((c) => c._vs_id == selectedContext._vs_id) + : false; + + const defaultContext = contextList._vs_projectContexts[contextList._vs_defaultIndex]; + const context = selectedContextValid ? selectedContext : defaultContext; + const hasAdditionalContexts = contextList._vs_projectContexts.length > 1; + + this._contextChangeEmitter.fire({ uri, context, hasAdditionalContexts }); } - private async getProjectContexts( - uri: vscode.Uri, + public async getProjectContexts( + uri: string | vscode.Uri, token: vscode.CancellationToken ): Promise { - const uriString = UriConverter.serialize(uri); + const uriString = uri instanceof vscode.Uri ? UriConverter.serialize(uri) : uri; const textDocument = TextDocumentIdentifier.create(uriString); try {