Skip to content

Commit

Permalink
Add ability to select a documents project context.
Browse files Browse the repository at this point in the history
- Adds select project context commands
- Adds the select command to the Project Context status item
- Updates middleware to send selected context with server requests.
  • Loading branch information
JoeRobich committed Jul 11, 2024
1 parent ff3e0b2 commit 6ca46de
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 12 deletions.
2 changes: 2 additions & 0 deletions l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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%",
Expand Down
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.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",
Expand Down
38 changes: 38 additions & 0 deletions src/lsptoolshost/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -191,3 +198,34 @@ async function openSolution(languageServer: RoslynLanguageServer): Promise<vscod
return uri;
}
}

async function changeDocumentContext(languageServer: RoslynLanguageServer): Promise<VSProjectContext | undefined> {
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);
}
}
12 changes: 9 additions & 3 deletions src/lsptoolshost/languageStatusBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
}
Expand Down
27 changes: 24 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 @@ -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();
Expand Down Expand Up @@ -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 = {
Expand All @@ -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 = <VSTextDocumentIdentifier>param.textDocument;
textDocument._vs_projectContext = server?._projectContextService.getDocumentContext(
textDocument.uri
);
} else if ('_vs_textDocument' in param) {
const textDocument = <VSTextDocumentIdentifier>param._vs_textDocument;
textDocument._vs_projectContext = server?._projectContextService.getDocumentContext(
textDocument.uri
);
}
}
return next(type, param, token);
},
workspace: {
configuration: (params) => readConfigurations(params),
},
Expand All @@ -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);

Expand Down Expand Up @@ -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';
}
43 changes: 37 additions & 6 deletions src/lsptoolshost/services/projectContextService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProjectContextChangeEvent>();
private _source = new vscode.CancellationTokenSource();

Expand All @@ -33,10 +36,27 @@ export class ProjectContextService {
vscode.window.onDidChangeActiveTextEditor(async (_) => this.refresh());
}

public get onActiveFileContextChanged(): vscode.Event<ProjectContextChangeEvent> {
public get onDocumentContextChanged(): vscode.Event<ProjectContextChangeEvent> {
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') {
Expand All @@ -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<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

0 comments on commit 6ca46de

Please sign in to comment.