Skip to content
Merged
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
27 changes: 17 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,30 @@
- Debug from .csproj and .sln [#5876](https://github.com/dotnet/vscode-csharp/issues/5876)

# 2.112.x
* Add ability to select a document's project context (PR: [#7328](https://github.com/dotnet/vscode-csharp/pull/7328))
* Fix TFM detection for .NET 10+ to enable launch.json generation (PR: [#8873](https://github.com/dotnet/vscode-csharp/pull/8873))
* Fix JavaScript highlighting in Razor files after C# control structures without braces (PR: [#8865](https://github.com/dotnet/vscode-csharp/pull/8865))
* Update Razor to 10.0.0-preview.26059.2 (PR: [#8877](https://github.com/dotnet/vscode-csharp/pull/8877))
* Encode double slash as underscore slash in hint names (PR: [#12597](https://github.com/dotnet/razor/pull/12597))
* Navigate to a Razor file when GTD/FAR/GTI is run in C# on the class name (PR: [#12580](https://github.com/dotnet/razor/pull/12580))
* Fix rename of components in the global namespace (PR: [#12577](https://github.com/dotnet/razor/pull/12577))
* Return a document symbol representing the "Render" method for a Razor file (PR: [#12568](https://github.com/dotnet/razor/pull/12568))
* Filter our html diagnostics when a tag helper attribute spans multiple lines (PR: [#12654](https://github.com/dotnet/razor/pull/12654))
* Handle Html indentation ourselves, rather than using the IDE formatter (PR: [#12623](https://github.com/dotnet/razor/pull/12623))
* Add new option to control attribute indent style (PR: [#12625](https://github.com/dotnet/razor/pull/12625))
* Fix formatting with adjacent C# templates (PR: [#12636](https://github.com/dotnet/razor/pull/12636))
* Emit the start of multiline implicit expressions the same as explicit. (PR: [#12624](https://github.com/dotnet/razor/pull/12624))
* Rename a .razor file when Roslyn renames the component type name (PR: [#12606](https://github.com/dotnet/razor/pull/12606))
* Rename component tags and type references when a Razor file is renamed (PR: [#12561](https://github.com/dotnet/razor/pull/12561))
* Handle conflict markers (PR: [#12642](https://github.com/dotnet/razor/pull/12642))
* Fix cross project span and edit mapping (PR: [#12614](https://github.com/dotnet/razor/pull/12614))
* Update Roslyn to 5.4.0-2.26060.1 (PR: [#8877](https://github.com/dotnet/vscode-csharp/pull/8877))
* Filter our html diagnostics when a tag helper attribute spans multiple lines (PR: [#12654](https://github.com/dotnet/razor/pull/12654))
Copy link
Member Author

Choose a reason for hiding this comment

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

Removed some trailing whitespace from Razor entries.

* Handle Html indentation ourselves, rather than using the IDE formatter (PR: [#12623](https://github.com/dotnet/razor/pull/12623))
* Add new option to control attribute indent style (PR: [#12625](https://github.com/dotnet/razor/pull/12625))
* Fix formatting with adjacent C# templates (PR: [#12636](https://github.com/dotnet/razor/pull/12636))
* Emit the start of multiline implicit expressions the same as explicit. (PR: [#12624](https://github.com/dotnet/razor/pull/12624))
* Rename a .razor file when Roslyn renames the component type name (PR: [#12606](https://github.com/dotnet/razor/pull/12606))
* Rename component tags and type references when a Razor file is renamed (PR: [#12561](https://github.com/dotnet/razor/pull/12561))
* Handle conflict markers (PR: [#12642](https://github.com/dotnet/razor/pull/12642))
* Fix cross project span and edit mapping (PR: [#12614](https://github.com/dotnet/razor/pull/12614))
* Update Roslyn to 5.4.0-2.26062.9 (PR: [#7328](https://github.com/dotnet/vscode-csharp/pull/7328))
* Add a notification handler for project context changed. (PR: [#81942](https://github.com/dotnet/roslyn/pull/81942))
* Ensure IDE does not use .editorconfig for source generated files (PR: [#81911](https://github.com/dotnet/roslyn/pull/81911))
* Send a key across identifying contents of the project context list (PR: [#81940](https://github.com/dotnet/roslyn/pull/81940))
* Include Enc diagnostics in Razor requests (PR: [#81941](https://github.com/dotnet/roslyn/pull/81941))
* Fix completions after attribute list in lambdas (PR: [#81961](https://github.com/dotnet/roslyn/pull/81961))
* Add ProjectContext refresh queue (PR: [#81938](https://github.com/dotnet/roslyn/pull/81938))
* Add back async fixers (PR: [#81835](https://github.com/dotnet/roslyn/pull/81835))
* Fix crash in convert-if-to-switch (PR: [#81724](https://github.com/dotnet/roslyn/pull/81724))
* Add code-folding/structure-guides/sticky-scroll support for extension blocks (PR: [#81667](https://github.com/dotnet/roslyn/pull/81667))
Expand Down
2 changes: 2 additions & 0 deletions l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@
"Text editor must be focused to fix all issues": "Text editor must be focused to fix all issues",
"Fix all issues": "Fix all issues",
"Select fix all action": "Select fix all action",
"Select project context": "Select project context",
"C# LSP Trace Logs": "C# LSP Trace Logs",
"Open solution": "Open solution",
"Restart server": "Restart server",
Expand All @@ -162,6 +163,7 @@
"Suppress notification": "Suppress notification",
"Restore {0}": "Restore {0}",
"Restore already in progress": "Restore already in progress",
"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

Copy link
Member Author

Choose a reason for hiding this comment

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

I think we should get it into preview in its current form and work out user experiences issues from there.

"Initializing dotnet-trace.../dotnet-trace is a command name and should not be localized": {
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"workspace"
],
"defaults": {
"roslyn": "5.4.0-2.26060.1",
"roslyn": "5.4.0-2.26062.9",
"omniSharp": "1.39.14",
"razor": "10.0.0-preview.26059.2",
"razorOmnisharp": "7.0.0-preview.23363.1",
Expand Down Expand Up @@ -1867,6 +1867,12 @@
"category": ".NET",
"enablement": "isWorkspaceTrusted && (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
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
51 changes: 50 additions & 1 deletion src/lsptoolshost/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { getDotnetInfo } from '../shared/utils/getDotnetInfo';
import { IHostExecutableResolver } from '../shared/constants/IHostExecutableResolver';
import { registerWorkspaceCommands } from './workspace/workspaceCommands';
import { registerServerCommands } from './server/serverCommands';
import { CancellationToken } from 'vscode-languageclient/node';
import { VSProjectContext } from './server/roslynProtocol';

export function registerCommands(
context: vscode.ExtensionContext,
Expand All @@ -18,7 +20,7 @@ export function registerCommands(
outputChannel: vscode.LogOutputChannel,
csharpTraceChannel: vscode.LogOutputChannel
) {
registerExtensionCommands(context, hostExecutableResolver, outputChannel, csharpTraceChannel);
registerExtensionCommands(context, languageServer, hostExecutableResolver, outputChannel, csharpTraceChannel);
registerWorkspaceCommands(context, languageServer);
registerServerCommands(context, languageServer, outputChannel);
}
Expand All @@ -28,10 +30,16 @@ export function registerCommands(
*/
function registerExtensionCommands(
context: vscode.ExtensionContext,
languageServer: RoslynLanguageServer,
hostExecutableResolver: IHostExecutableResolver,
outputChannel: vscode.LogOutputChannel,
csharpTraceChannel: vscode.LogOutputChannel
) {
context.subscriptions.push(
vscode.commands.registerCommand('csharp.changeProjectContext', async (document, options) =>
changeProjectContext(languageServer, document, options)
)
);
context.subscriptions.push(
vscode.commands.registerCommand('csharp.reportIssue', async () =>
reportIssue(
Expand All @@ -47,3 +55,44 @@ function registerExtensionCommands(
vscode.commands.registerCommand('csharp.showOutputWindow', async () => outputChannel.show())
);
}
async function changeProjectContext(
languageServer: RoslynLanguageServer,
document: vscode.TextDocument,
options: ChangeProjectContextOptions
): Promise<VSProjectContext | undefined> {
const contextList = await languageServer._projectContextService.queryServerProjectContexts(
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) ||
contextList._vs_projectContexts.find((context) => context._vs_label === options.projectName);
} 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;
}

await languageServer._projectContextService.setActiveFileContext(document, contextList, context);
}

interface ChangeProjectContextOptions {
projectName: string;
tfm: string;
}
136 changes: 124 additions & 12 deletions src/lsptoolshost/projectContext/projectContextService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,36 @@ import { LanguageServerEvents, ServerState } from '../server/languageServerEvent
import { RoslynLanguageClient } from '../server/roslynLanguageClient';

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

type ContextKey = string;
type UriString = string;

// We want to verify the project context is in a stable state before warning the user about miscellaneous files.
const VerificationDelay = 5 * 1000;

let _verifyTimeout: NodeJS.Timeout | undefined;
let _documentUriToVerify: vscode.Uri | undefined;

export class ProjectContextService {
// This map tracks which project context is active for a given context key. New entries are
// added when querying the server for project contexts for a document and when a user changes
// the active context for a document.
private readonly _keyToActiveProjectContextMap: Map<ContextKey, VSProjectContext> = new Map();

// This map tracks which context key a given document uri is associated with. New entries are
// added when querying the server for project contexts for a document.
private readonly _uriToContextKeyMap: Map<UriString, ContextKey> = new Map();

// This map tracks how many documents are associated with a given context key. New entries are
// added when querying the server for project contexts for a document. Entries are removed
// when documents are no longer associated with a context key.
private readonly _contextKeyToRefCountMap: Map<ContextKey, number> = new Map();

private readonly _contextChangeEmitter = new vscode.EventEmitter<ProjectContextChangeEvent>();
private _source = new vscode.CancellationTokenSource();
private readonly _emptyProjectContext: VSProjectContext = {
Expand Down Expand Up @@ -64,6 +81,39 @@ export class ProjectContextService {
return this._contextChangeEmitter.event;
}

public async getDocumentContext(uri: string | vscode.Uri): Promise<VSProjectContext | undefined> {
const uriString = uri instanceof vscode.Uri ? UriConverter.serialize(uri) : uri;

const key = this._uriToContextKeyMap.get(uriString);
if (key === undefined) {
return undefined;
}

return this._keyToActiveProjectContextMap.get(key);
}

public async setActiveFileContext(
document: vscode.TextDocument,
contextList: VSProjectContextList,
context: VSProjectContext
): Promise<void> {
const uri = document.uri;
const languageId = document.languageId;
if (!uri || (languageId !== 'csharp' && languageId !== 'aspnetcorerazor')) {
return;
}

this._keyToActiveProjectContextMap.set(contextList._vs_key, context);
this._contextChangeEmitter.fire({
document,
context,
isVerified: true,
hasAdditionalContexts: true,
});

await this._languageServer.refreshFeatureProviders();
}

public async refresh() {
const textEditor = vscode.window.activeTextEditor;
const languageId = textEditor?.document?.languageId;
Expand All @@ -75,7 +125,7 @@ export class ProjectContextService {
this._source.cancel();
this._source = new vscode.CancellationTokenSource();

const uri = textEditor!.document.uri;
const document = textEditor!.document;

// We verify a project context is stable by waiting for a period of time
// without any changes before sending a verified event. Changing active document
Expand All @@ -90,38 +140,53 @@ export class ProjectContextService {
}

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

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

const context = contextList._vs_projectContexts[contextList._vs_defaultIndex];
this._contextChangeEmitter.fire({ languageId, uri, context, isVerified: false });
const hasAdditionalContexts = contextList._vs_projectContexts.length > 1;
this._contextChangeEmitter.fire({ document, context, isVerified: false, hasAdditionalContexts });

// If we do not recieve a refresh even within the timout period, send a verified event.
_verifyTimeout = setTimeout(() => {
this._contextChangeEmitter.fire({ languageId, uri, context, isVerified: true });
this._contextChangeEmitter.fire({ document, context, isVerified: true, hasAdditionalContexts });
}, VerificationDelay);
}

private async getProjectContexts(
uri: vscode.Uri,
public async queryServerProjectContexts(
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 {
return await this._languageServer.sendRequest(
const contextList = await this._languageServer.sendRequest(
VSGetProjectContextsRequest.type,
{ _vs_textDocument: textDocument },
token
);

this.updateCaches(uriString, contextList);

return contextList;
} catch (error) {
if (error instanceof vscode.CancellationError) {
return undefined;
Expand All @@ -130,4 +195,51 @@ export class ProjectContextService {
throw error;
}
}

private updateCaches(uriString: UriString, contextList: VSProjectContextList) {
const oldContextKey = this._uriToContextKeyMap.get(uriString);
const newContextKey = contextList._vs_key;

if (oldContextKey === newContextKey) {
// We have already seen this context key and it hasn't changed, nothing to do.
return;
}

if (oldContextKey !== undefined) {
// The document is no longer associated with the old context key, so decrement
// the ref count. If no documents are associated with the old context key, remove it.
const oldRefCount = this._contextKeyToRefCountMap.get(oldContextKey) || 0;
if (oldRefCount <= 1) {
this._contextKeyToRefCountMap.delete(oldContextKey);
this._keyToActiveProjectContextMap.delete(oldContextKey);
} else {
this._contextKeyToRefCountMap.set(oldContextKey, oldRefCount - 1);
}
}

// Update our caches so that we can quickly lookup the active context later.
// No need to track when there is only one context because the server will use
// the default context automatically.

if (contextList._vs_projectContexts.length > 1) {
// We only get here if this is the first time we have seen this document with this
// context key. So we need to increment the ref count in order to track it.
const oldRefCount = this._contextKeyToRefCountMap.get(newContextKey) || 0;
const newRefCount = oldRefCount + 1;
this._contextKeyToRefCountMap.set(newContextKey, newRefCount);

// Track that this document uri is associated with this context key.
this._uriToContextKeyMap.set(uriString, newContextKey);

// If there is not already an active context for this key, set it to the default.
if (!this._keyToActiveProjectContextMap.has(newContextKey)) {
const defaultContext = contextList._vs_projectContexts[contextList._vs_defaultIndex];
this._keyToActiveProjectContextMap.set(newContextKey, defaultContext);
}
} else {
// We do not need to track the context key for documents with only one context. Remove any
// existing mapping.
this._uriToContextKeyMap.delete(uriString);
}
}
}
Loading
Loading