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 ability to select a document's project context #7328

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,12 @@
"Open solution": "Open solution",
"Restart server": "Restart server",
"C# Workspace Status": "C# Workspace Status",
"Select context": "Select context",
"C# Project Context Status": "C# Project Context Status",
"Active File Context": "Active File Context",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering - could we merge this into the select button?
e.g.
ProjectA (net6.0) Change Active File Context

or maybe we could even put the document name in it?
ProjectA (net6.0) Change Program.cs Context

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, although the select command is only visible when there are multiple project contexts. Would it be jarring for the text to move from the item details to the command and back again as you navigate?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah hmm. I wonder if we should just always show the button even if there's only one. Not sure

"Pick a fix all scope": "Pick a fix all scope",
"Fix All Code Action": "Fix All Code Action",
"Select project context": "Select project context",
"Failed to set extension directory": "Failed to set extension directory",
"Failed to set debugadpter directory": "Failed to set debugadpter directory",
"Failed to set install complete file path": "Failed to set install complete file path",
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1833,6 +1833,12 @@
"category": ".NET",
"enablement": "dotnet.server.activationContext == 'Roslyn' || dotnet.server.activationContext == 'OmniSharp'"
},
{
"command": "csharp.changeProjectContext",
"title": "%command.csharp.changeProjectContext%",
"category": "CSharp",
"enablement": "dotnet.server.activationContext == 'Roslyn'"
},
{
"command": "csharp.listProcess",
"title": "%command.csharp.listProcess%",
Expand Down Expand Up @@ -5584,4 +5590,4 @@
}
}
}
}
}
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.changeProjectContext": "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",
Expand Down
52 changes: 52 additions & 0 deletions src/lsptoolshost/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import reportIssue from '../shared/reportIssue';
import { getDotnetInfo } from '../shared/utils/getDotnetInfo';
import { IHostExecutableResolver } from '../shared/constants/IHostExecutableResolver';
import { getCSharpDevKit } from '../utils/getCSharpDevKit';
import { VSProjectContext, VSProjectContextList } from './roslynProtocol';
import { CancellationToken } from 'vscode-languageclient/node';

export function registerCommands(
context: vscode.ExtensionContext,
Expand Down Expand Up @@ -44,6 +46,11 @@ export function registerCommands(
vscode.commands.registerCommand('dotnet.openSolution', async () => openSolution(languageServer))
);
}
context.subscriptions.push(
vscode.commands.registerCommand('csharp.changeProjectContext', async (options) =>
changeProjectContext(languageServer, options)
)
);
context.subscriptions.push(
vscode.commands.registerCommand('csharp.reportIssue', async () =>
reportIssue(
Expand Down Expand Up @@ -194,3 +201,48 @@ async function openSolution(languageServer: RoslynLanguageServer): Promise<vscod
return uri;
}
}

async function changeProjectContext(
languageServer: RoslynLanguageServer,
options: ChangeProjectContextOptions | undefined
): Promise<VSProjectContext | undefined> {
const editor = vscode.window.activeTextEditor;
if (editor === undefined) {
return;
}
const contextList = await languageServer._projectContextService.getProjectContexts(
editor.document.uri,
CancellationToken.None
);
if (contextList === undefined) {
return;
}

let context: VSProjectContext | undefined = undefined;

if (options !== undefined) {
const contextLabel = `${options.projectName} (${options.tfm})`;
context = contextList._vs_projectContexts.find((context) => context._vs_label === contextLabel);
} else {
const items = contextList._vs_projectContexts.map((context) => {
return { label: context._vs_label, context };
});
const selectedItem = await vscode.window.showQuickPick(items, {
placeHolder: vscode.l10n.t('Select project context'),
});
context = selectedItem?.context;
}

if (context === undefined) {
return;
}

languageServer._projectContextService.setActiveFileContext(contextList, context);
// TODO: Replace this with proper server-side onDidChange notifications
editor.edit(() => 0);
}

interface ChangeProjectContextOptions {
projectName: string;
tfm: string;
}
6 changes: 5 additions & 1 deletion src/lsptoolshost/languageStatusBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,18 @@ class ProjectContextStatus {
RazorLanguage.documentSelector
);
const projectContextService = languageServer._projectContextService;

const selectContextCommand = {
command: 'csharp.changeProjectContext',
title: vscode.l10n.t('Select context'),
};
const item = vscode.languages.createLanguageStatusItem('csharp.projectContextStatus', 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;
item.command = e.hasAdditionalContexts ? selectContextCommand : undefined;

// Show a warning when the active file is part of the Miscellaneous File workspace and
// project initialization is complete.
Expand Down
30 changes: 27 additions & 3 deletions src/lsptoolshost/roslynLanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -144,7 +144,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();
Expand Down Expand Up @@ -273,6 +273,7 @@ export class RoslynLanguageServer {
};

const documentSelector = languageServerOptions.documentSelector;
let server: RoslynLanguageServer | undefined = undefined;

// Options to control the language client
const clientOptions: LanguageClientOptions = {
Expand All @@ -293,6 +294,12 @@ export class RoslynLanguageServer {
protocol2Code: UriConverter.deserialize,
},
middleware: {
async sendRequest(type, param, token, next) {
if (server !== undefined && type !== RoslynProtocol.VSGetProjectContextsRequest.type) {
await RoslynLanguageServer.tryAddProjectContext(param, server);
}
return next(type, param, token);
},
workspace: {
configuration: (params) => readConfigurations(params),
},
Expand All @@ -309,7 +316,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);

Expand All @@ -318,6 +325,19 @@ export class RoslynLanguageServer {
return server;
}

private static async tryAddProjectContext(param: unknown | undefined, server: RoslynLanguageServer): Promise<void> {
if (!isObject(param)) {
return;
}

const textDocument = <VSTextDocumentIdentifier | undefined>(param['textDocument'] || param['_vs_textDocument']);
if (!textDocument) {
return;
}

textDocument._vs_projectContext = await server._projectContextService.getDocumentContext(textDocument.uri);
}

public async stop(): Promise<void> {
await this._languageClient.stop(RoslynLanguageServer._stopTimeout);
}
Expand Down Expand Up @@ -1215,3 +1235,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 { [key: string]: any } {
return value !== null && typeof value === 'object';
}
77 changes: 70 additions & 7 deletions src/lsptoolshost/services/projectContextService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import { TextDocumentIdentifier } from 'vscode-languageserver-protocol';
import { UriConverter } from '../uriConverter';
import { LanguageServerEvents } from '../languageServerEvents';
import { ServerState } from '../serverStateChange';
import { CancellationToken } from 'vscode-languageclient/node';

export interface ProjectContextChangeEvent {
languageId: string;
uri: vscode.Uri;
context: VSProjectContext;
isVerified: boolean;
hasAdditionalContexts: boolean;
}

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

export class ProjectContextService {
private readonly _projectContextMap: Map<string, VSProjectContext> = new Map();
private readonly _contextChangeEmitter = new vscode.EventEmitter<ProjectContextChangeEvent>();
private _source = new vscode.CancellationTokenSource();
private readonly _emptyProjectContext: VSProjectContext = {
Expand All @@ -50,6 +53,53 @@ export class ProjectContextService {
return this._contextChangeEmitter.event;
}

public async getDocumentContext(uri: string | vscode.Uri): Promise<VSProjectContext | undefined>;
public async getDocumentContext(
uri: string | vscode.Uri,
contextList?: VSProjectContextList | undefined
): Promise<VSProjectContext>;
public async getDocumentContext(
uri: string | vscode.Uri,
contextList?: VSProjectContextList | undefined
): Promise<VSProjectContext | undefined> {
// To find the current context for the specified document we need to know the list
// of contexts that it is a part of.
contextList ??= await this.getProjectContexts(uri, CancellationToken.None);
if (contextList === undefined) {
return undefined;
}

const key = this.getContextKey(contextList);

// If this list of contexts hasn't been queried before that set the context to the default.
if (!this._projectContextMap.has(key)) {
this._projectContextMap.set(key, contextList._vs_projectContexts[contextList._vs_defaultIndex]);
}

return this._projectContextMap.get(key);
}

private getContextKey(contextList: VSProjectContextList): string {
return contextList._vs_projectContexts
.map((context) => context._vs_label)
.sort()
.join(';');
}

public setActiveFileContext(contextList: VSProjectContextList, context: VSProjectContext): void {
const textEditor = vscode.window.activeTextEditor;
const uri = textEditor?.document?.uri;
const languageId = textEditor?.document?.languageId;
if (!uri || languageId !== 'csharp') {
return;
}

const key = this.getContextKey(contextList);
this._projectContextMap.set(key, context);

this._contextChangeEmitter.fire({ languageId, uri, context, isVerified: true, hasAdditionalContexts: true });
}

public async refresh() {
const textEditor = vscode.window.activeTextEditor;
const languageId = textEditor?.document?.languageId;
Expand Down Expand Up @@ -83,19 +133,32 @@ export class ProjectContextService {
}

if (!this._languageServer.isRunning()) {
this._contextChangeEmitter.fire({ languageId, uri, context: this._emptyProjectContext, isVerified: false });
this._contextChangeEmitter.fire({
languageId,
uri,
context: this._emptyProjectContext,
isVerified: false,
hasAdditionalContexts: false,
});
return;
}

const contextList = await this.getProjectContexts(uri, this._source.token);
if (!contextList) {
this._contextChangeEmitter.fire({ languageId, uri, context: this._emptyProjectContext, isVerified: false });
this._contextChangeEmitter.fire({
languageId,
uri,
context: this._emptyProjectContext,
isVerified: false,
hasAdditionalContexts: false,
});
return;
}

const context = contextList._vs_projectContexts[contextList._vs_defaultIndex];
const context = await this.getDocumentContext(uri, contextList);
const isVerified = !context._vs_is_miscellaneous || isVerifyPass;
this._contextChangeEmitter.fire({ languageId, uri, context, isVerified });
const hasAdditionalContexts = contextList._vs_projectContexts.length > 1;
this._contextChangeEmitter.fire({ languageId, uri, context, isVerified, hasAdditionalContexts });

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

private async getProjectContexts(
uri: vscode.Uri,
public async getProjectContexts(
uri: string | vscode.Uri,
token: vscode.CancellationToken
): Promise<VSProjectContextList | undefined> {
const uriString = UriConverter.serialize(uri);
const uriString = uri instanceof vscode.Uri ? UriConverter.serialize(uri) : uri;
const textDocument = TextDocumentIdentifier.create(uriString);

try {
Expand Down
Loading