Skip to content

Commit ee62554

Browse files
committed
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.
1 parent 69234b0 commit ee62554

File tree

7 files changed

+164
-12
lines changed

7 files changed

+164
-12
lines changed

l10n/bundle.l10n.json

+2
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,12 @@
187187
"Open solution": "Open solution",
188188
"Restart server": "Restart server",
189189
"C# Workspace Status": "C# Workspace Status",
190+
"Select context": "Select context",
190191
"C# Project Context Status": "C# Project Context Status",
191192
"Active File Context": "Active File Context",
192193
"Pick a fix all scope": "Pick a fix all scope",
193194
"Fix All Code Action": "Fix All Code Action",
195+
"Select project context": "Select project context",
194196
"Failed to set extension directory": "Failed to set extension directory",
195197
"Failed to set debugadpter directory": "Failed to set debugadpter directory",
196198
"Failed to set install complete file path": "Failed to set install complete file path",

package.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -1833,6 +1833,12 @@
18331833
"category": ".NET",
18341834
"enablement": "dotnet.server.activationContext == 'Roslyn' || dotnet.server.activationContext == 'OmniSharp'"
18351835
},
1836+
{
1837+
"command": "csharp.changeProjectContext",
1838+
"title": "%command.csharp.changeProjectContext%",
1839+
"category": "CSharp",
1840+
"enablement": "dotnet.server.activationContext == 'Roslyn'"
1841+
},
18361842
{
18371843
"command": "csharp.listProcess",
18381844
"title": "%command.csharp.listProcess%",
@@ -5584,4 +5590,4 @@
55845590
}
55855591
}
55865592
}
5587-
}
5593+
}

package.nls.json

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"command.dotnet.generateAssets.currentProject": "Generate Assets for Build and Debug",
1111
"command.dotnet.restore.project": "Restore Project",
1212
"command.dotnet.restore.all": "Restore All Projects",
13+
"command.csharp.changeProjectContext": "Change the active document's project context",
1314
"command.csharp.downloadDebugger": "Download .NET Core Debugger",
1415
"command.csharp.listProcess": "List process for attach",
1516
"command.csharp.listRemoteProcess": "List processes on remote connection for attach",

src/lsptoolshost/commands.ts

+52
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import reportIssue from '../shared/reportIssue';
1212
import { getDotnetInfo } from '../shared/utils/getDotnetInfo';
1313
import { IHostExecutableResolver } from '../shared/constants/IHostExecutableResolver';
1414
import { getCSharpDevKit } from '../utils/getCSharpDevKit';
15+
import { VSProjectContext, VSProjectContextList } from './roslynProtocol';
16+
import { CancellationToken } from 'vscode-languageclient/node';
1517

1618
export function registerCommands(
1719
context: vscode.ExtensionContext,
@@ -44,6 +46,11 @@ export function registerCommands(
4446
vscode.commands.registerCommand('dotnet.openSolution', async () => openSolution(languageServer))
4547
);
4648
}
49+
context.subscriptions.push(
50+
vscode.commands.registerCommand('csharp.changeProjectContext', async (options) =>
51+
changeProjectContext(languageServer, options)
52+
)
53+
);
4754
context.subscriptions.push(
4855
vscode.commands.registerCommand('csharp.reportIssue', async () =>
4956
reportIssue(
@@ -194,3 +201,48 @@ async function openSolution(languageServer: RoslynLanguageServer): Promise<vscod
194201
return uri;
195202
}
196203
}
204+
205+
async function changeProjectContext(
206+
languageServer: RoslynLanguageServer,
207+
options: ChangeProjectContextOptions | undefined
208+
): Promise<VSProjectContext | undefined> {
209+
const editor = vscode.window.activeTextEditor;
210+
if (editor === undefined) {
211+
return;
212+
}
213+
const contextList = await languageServer._projectContextService.getProjectContexts(
214+
editor.document.uri,
215+
CancellationToken.None
216+
);
217+
if (contextList === undefined) {
218+
return;
219+
}
220+
221+
let context: VSProjectContext | undefined = undefined;
222+
223+
if (options !== undefined) {
224+
const contextLabel = `${options.projectName} (${options.tfm})`;
225+
context = contextList._vs_projectContexts.find((context) => context._vs_label === contextLabel);
226+
} else {
227+
const items = contextList._vs_projectContexts.map((context) => {
228+
return { label: context._vs_label, context };
229+
});
230+
const selectedItem = await vscode.window.showQuickPick(items, {
231+
placeHolder: vscode.l10n.t('Select project context'),
232+
});
233+
context = selectedItem?.context;
234+
}
235+
236+
if (context === undefined) {
237+
return;
238+
}
239+
240+
languageServer._projectContextService.setActiveFileContext(contextList, context);
241+
// TODO: Replace this with proper server-side onDidChange notifications
242+
editor.edit(() => 0);
243+
}
244+
245+
interface ChangeProjectContextOptions {
246+
projectName: string;
247+
tfm: string;
248+
}

src/lsptoolshost/languageStatusBar.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,18 @@ class ProjectContextStatus {
6464
RazorLanguage.documentSelector
6565
);
6666
const projectContextService = languageServer._projectContextService;
67-
67+
const selectContextCommand = {
68+
command: 'csharp.changeProjectContext',
69+
title: vscode.l10n.t('Select context'),
70+
};
6871
const item = vscode.languages.createLanguageStatusItem('csharp.projectContextStatus', documentSelector);
6972
item.name = vscode.l10n.t('C# Project Context Status');
7073
item.detail = vscode.l10n.t('Active File Context');
7174
context.subscriptions.push(item);
7275

7376
projectContextService.onActiveFileContextChanged((e) => {
7477
item.text = e.context._vs_label;
78+
item.command = e.hasAdditionalContexts ? selectContextCommand : undefined;
7579

7680
// Show a warning when the active file is part of the Miscellaneous File workspace and
7781
// project initialization is complete.

src/lsptoolshost/roslynLanguageServer.ts

+27-3
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ import { registerRazorCommands } from './razorCommands';
5757
import { registerOnAutoInsert } from './onAutoInsert';
5858
import { registerCodeActionFixAllCommands } from './fixAllCodeAction';
5959
import { commonOptions, languageServerOptions, omnisharpOptions, razorOptions } from '../shared/options';
60-
import { NamedPipeInformation } from './roslynProtocol';
60+
import { NamedPipeInformation, VSTextDocumentIdentifier } from './roslynProtocol';
6161
import { IDisposable } from '../disposable';
6262
import { registerNestedCodeActionCommands } from './nestedCodeAction';
6363
import { registerRestoreCommands } from './restore';
@@ -144,7 +144,7 @@ export class RoslynLanguageServer {
144144
this._buildDiagnosticService = new BuildDiagnosticsService(diagnosticsReportedByBuild);
145145
this.registerDocumentOpenForDiagnostics();
146146

147-
this._projectContextService = new ProjectContextService(this, this._languageServerEvents);
147+
this._projectContextService = new ProjectContextService(this, _languageServerEvents);
148148

149149
// Register Razor dynamic file info handling
150150
this.registerDynamicFileInfo();
@@ -273,6 +273,7 @@ export class RoslynLanguageServer {
273273
};
274274

275275
const documentSelector = languageServerOptions.documentSelector;
276+
let server: RoslynLanguageServer | undefined = undefined;
276277

277278
// Options to control the language client
278279
const clientOptions: LanguageClientOptions = {
@@ -293,6 +294,12 @@ export class RoslynLanguageServer {
293294
protocol2Code: UriConverter.deserialize,
294295
},
295296
middleware: {
297+
async sendRequest(type, param, token, next) {
298+
if (server !== undefined && type !== RoslynProtocol.VSGetProjectContextsRequest.type) {
299+
await RoslynLanguageServer.tryAddProjectContext(param, server);
300+
}
301+
return next(type, param, token);
302+
},
296303
workspace: {
297304
configuration: (params) => readConfigurations(params),
298305
},
@@ -309,7 +316,7 @@ export class RoslynLanguageServer {
309316

310317
client.registerProposedFeatures();
311318

312-
const server = new RoslynLanguageServer(client, platformInfo, context, telemetryReporter, languageServerEvents);
319+
server = new RoslynLanguageServer(client, platformInfo, context, telemetryReporter, languageServerEvents);
313320

314321
client.registerFeature(server._onAutoInsertFeature);
315322

@@ -318,6 +325,19 @@ export class RoslynLanguageServer {
318325
return server;
319326
}
320327

328+
private static async tryAddProjectContext(param: unknown | undefined, server: RoslynLanguageServer): Promise<void> {
329+
if (!isObject(param)) {
330+
return;
331+
}
332+
333+
const textDocument = <VSTextDocumentIdentifier | undefined>(param['textDocument'] || param['_vs_textDocument']);
334+
if (!textDocument) {
335+
return;
336+
}
337+
338+
textDocument._vs_projectContext = await server._projectContextService.getDocumentContext(textDocument.uri);
339+
}
340+
321341
public async stop(): Promise<void> {
322342
await this._languageClient.stop(RoslynLanguageServer._stopTimeout);
323343
}
@@ -1215,3 +1235,7 @@ function getSessionId(): string {
12151235
export function isString(value: any): value is string {
12161236
return typeof value === 'string' || value instanceof String;
12171237
}
1238+
1239+
export function isObject(value: any): value is { [key: string]: any } {
1240+
return value !== null && typeof value === 'object';
1241+
}

src/lsptoolshost/services/projectContextService.ts

+70-7
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ import { TextDocumentIdentifier } from 'vscode-languageserver-protocol';
1010
import { UriConverter } from '../uriConverter';
1111
import { LanguageServerEvents } from '../languageServerEvents';
1212
import { ServerState } from '../serverStateChange';
13+
import { CancellationToken } from 'vscode-languageclient/node';
1314

1415
export interface ProjectContextChangeEvent {
1516
languageId: string;
1617
uri: vscode.Uri;
1718
context: VSProjectContext;
1819
isVerified: boolean;
20+
hasAdditionalContexts: boolean;
1921
}
2022

2123
const VerificationDelay = 2 * 1000;
@@ -24,6 +26,7 @@ let _verifyTimeout: NodeJS.Timeout | undefined;
2426
let _documentUriToVerify: vscode.Uri | undefined;
2527

2628
export class ProjectContextService {
29+
private readonly _projectContextMap: Map<string, VSProjectContext> = new Map();
2730
private readonly _contextChangeEmitter = new vscode.EventEmitter<ProjectContextChangeEvent>();
2831
private _source = new vscode.CancellationTokenSource();
2932
private readonly _emptyProjectContext: VSProjectContext = {
@@ -50,6 +53,53 @@ export class ProjectContextService {
5053
return this._contextChangeEmitter.event;
5154
}
5255

56+
public async getDocumentContext(uri: string | vscode.Uri): Promise<VSProjectContext | undefined>;
57+
public async getDocumentContext(
58+
uri: string | vscode.Uri,
59+
contextList?: VSProjectContextList | undefined
60+
): Promise<VSProjectContext>;
61+
public async getDocumentContext(
62+
uri: string | vscode.Uri,
63+
contextList?: VSProjectContextList | undefined
64+
): Promise<VSProjectContext | undefined> {
65+
// To find the current context for the specified document we need to know the list
66+
// of contexts that it is a part of.
67+
contextList ??= await this.getProjectContexts(uri, CancellationToken.None);
68+
if (contextList === undefined) {
69+
return undefined;
70+
}
71+
72+
const key = this.getContextKey(contextList);
73+
74+
// If this list of contexts hasn't been queried before that set the context to the default.
75+
if (!this._projectContextMap.has(key)) {
76+
this._projectContextMap.set(key, contextList._vs_projectContexts[contextList._vs_defaultIndex]);
77+
}
78+
79+
return this._projectContextMap.get(key);
80+
}
81+
82+
private getContextKey(contextList: VSProjectContextList): string {
83+
return contextList._vs_projectContexts
84+
.map((context) => context._vs_label)
85+
.sort()
86+
.join(';');
87+
}
88+
89+
public setActiveFileContext(contextList: VSProjectContextList, context: VSProjectContext): void {
90+
const textEditor = vscode.window.activeTextEditor;
91+
const uri = textEditor?.document?.uri;
92+
const languageId = textEditor?.document?.languageId;
93+
if (!uri || languageId !== 'csharp') {
94+
return;
95+
}
96+
97+
const key = this.getContextKey(contextList);
98+
this._projectContextMap.set(key, context);
99+
100+
this._contextChangeEmitter.fire({ languageId, uri, context, isVerified: true, hasAdditionalContexts: true });
101+
}
102+
53103
public async refresh() {
54104
const textEditor = vscode.window.activeTextEditor;
55105
const languageId = textEditor?.document?.languageId;
@@ -83,19 +133,32 @@ export class ProjectContextService {
83133
}
84134

85135
if (!this._languageServer.isRunning()) {
86-
this._contextChangeEmitter.fire({ languageId, uri, context: this._emptyProjectContext, isVerified: false });
136+
this._contextChangeEmitter.fire({
137+
languageId,
138+
uri,
139+
context: this._emptyProjectContext,
140+
isVerified: false,
141+
hasAdditionalContexts: false,
142+
});
87143
return;
88144
}
89145

90146
const contextList = await this.getProjectContexts(uri, this._source.token);
91147
if (!contextList) {
92-
this._contextChangeEmitter.fire({ languageId, uri, context: this._emptyProjectContext, isVerified: false });
148+
this._contextChangeEmitter.fire({
149+
languageId,
150+
uri,
151+
context: this._emptyProjectContext,
152+
isVerified: false,
153+
hasAdditionalContexts: false,
154+
});
93155
return;
94156
}
95157

96-
const context = contextList._vs_projectContexts[contextList._vs_defaultIndex];
158+
const context = await this.getDocumentContext(uri, contextList);
97159
const isVerified = !context._vs_is_miscellaneous || isVerifyPass;
98-
this._contextChangeEmitter.fire({ languageId, uri, context, isVerified });
160+
const hasAdditionalContexts = contextList._vs_projectContexts.length > 1;
161+
this._contextChangeEmitter.fire({ languageId, uri, context, isVerified, hasAdditionalContexts });
99162

100163
if (context._vs_is_miscellaneous && !isVerifyPass) {
101164
// Request the active project context be refreshed but delay the request to give
@@ -114,11 +177,11 @@ export class ProjectContextService {
114177
}
115178
}
116179

117-
private async getProjectContexts(
118-
uri: vscode.Uri,
180+
public async getProjectContexts(
181+
uri: string | vscode.Uri,
119182
token: vscode.CancellationToken
120183
): Promise<VSProjectContextList | undefined> {
121-
const uriString = UriConverter.serialize(uri);
184+
const uriString = uri instanceof vscode.Uri ? UriConverter.serialize(uri) : uri;
122185
const textDocument = TextDocumentIdentifier.create(uriString);
123186

124187
try {

0 commit comments

Comments
 (0)