Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
74e515c
Initial implementation
phil-allen-msft May 20, 2026
411a8d1
Please author a command in this vscode extension that will make the a…
phil-allen-msft May 20, 2026
29ad016
Change it so that #sym:csharpLspServerConfig is read from a file incl…
phil-allen-msft May 20, 2026
45ed73a
As part of the update code, check and see if the lsp-config.json file…
phil-allen-msft May 20, 2026
ecda5a5
Now refactor as necessary and add a test so that if the 'install rosl…
phil-allen-msft May 20, 2026
6f84fed
Fixup: Add tests
phil-allen-msft May 20, 2026
4140a06
Add a command to the npm build system as 'npm run ingest' which will …
phil-allen-msft May 20, 2026
684a7e6
Code review the changes that have been committed but are 'ahead' of m…
phil-allen-msft May 20, 2026
b673308
Change the name of the folder that contains lsp-config.json in this r…
phil-allen-msft May 20, 2026
db67bfb
Add a new command to uninstall the CSharp LSP for Copilot CLI use. It…
phil-allen-msft May 20, 2026
2e434ea
Rename the configure command to be 'install' so that it mirrors the n…
phil-allen-msft May 20, 2026
0efb779
Change how to determine if the lsp is installed. Instead of looking …
phil-allen-msft May 21, 2026
60b0ab3
Change the method #sym:getUninstalledCopilotLspConfigContent to also …
phil-allen-msft May 21, 2026
b92d796
Factor together both loops that look for an existing lspServer conten…
phil-allen-msft May 21, 2026
426c84a
Change all of the methods that are new in this branch as compared to …
phil-allen-msft May 21, 2026
6890590
Code review all code on the branch as compared to origin/main
phil-allen-msft May 21, 2026
878d50d
Remove "%configuration.dotnet.experiments.targetPopulation...% keys,"…
phil-allen-msft May 21, 2026
e1b403e
Fix 4 from the list above
phil-allen-msft May 21, 2026
849edb5
Change the ingest code so that if the lspServers.csharp element does …
phil-allen-msft May 21, 2026
a12608b
Local Code Review feedback taken
phil-allen-msft May 22, 2026
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
9 changes: 9 additions & 0 deletions l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,15 @@
"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",
"Failed to read packaged Copilot LSP config: {0}": "Failed to read packaged Copilot LSP config: {0}",
"Failed to read Copilot LSP config: {0}": "Failed to read Copilot LSP config: {0}",
"Failed to update Copilot LSP config: {0}": "Failed to update Copilot LSP config: {0}",
"Copilot LSP config already contains a C# LSP mapping for .cs files. No changes were made.": "Copilot LSP config already contains a C# LSP mapping for .cs files. No changes were made.",
"Updated Copilot LSP config at {0}.": "Updated Copilot LSP config at {0}.",
"Failed to write Copilot LSP config: {0}": "Failed to write Copilot LSP config: {0}",
"Failed to uninstall Copilot LSP config: {0}": "Failed to uninstall Copilot LSP config: {0}",
"Copilot LSP config does not contain a C# LSP mapping for .cs files. No changes were made.": "Copilot LSP config does not contain a C# LSP mapping for .cs files. No changes were made.",
"Removed C# LSP mapping(s) for .cs files from Copilot LSP config at {0}.": "Removed C# LSP mapping(s) for .cs files from Copilot LSP config at {0}.",
"C# LSP Trace Logs": "C# LSP Trace Logs",
"Open solution": "Open solution",
"Restart server": "Restart server",
Expand Down
35 changes: 35 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"createTags": "npx ts-node tasks/tags/createTags.ts",
"fixLocUrls": "npx ts-node tasks/debugger/fixLocUrls.ts",
"generateOptionsSchema": "npx ts-node tasks/debugger/generateOptionsSchema.ts",
"ingest": "npx ts-node tasks/components/ingestCopilotLspConfig.ts",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this something we expect is happening automatically during a build or something we need to do on a regular basis?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think this should happen automatically on build - otherwise a build could randomly fail if the config changes.

"incrementVersion": "npx ts-node tasks/snap/incrementVersion.ts",
"installDependencies": "npx ts-node tasks/packaging/installDependencies.ts",
"installDependenciesClean": "npx ts-node tasks/packaging/installDependenciesClean.ts",
Expand Down Expand Up @@ -124,6 +125,7 @@
"semver": "7.5.4",
"vscode-html-languageservice": "^5.3.1",
"vscode-languageclient": "10.0.0-next.20",
"vscode-tas-client": "^0.2.1",
"yauzl": "3.2.1"
},
"devDependencies": {
Expand Down Expand Up @@ -1870,6 +1872,18 @@
"category": ".NET",
"enablement": "isWorkspaceTrusted && dotnet.server.activationContext == 'Roslyn'"
},
{
"command": "dotnet.installCopilotLsp",
"title": "%command.dotnet.installCopilotLsp%",
"category": ".NET",
"enablement": "isWorkspaceTrusted"
},
{
"command": "dotnet.uninstallCopilotLsp",
"title": "%command.dotnet.uninstallCopilotLsp%",
"category": ".NET",
"enablement": "isWorkspaceTrusted"
},
{
"command": "o.fixAll.solution",
"title": "%command.o.fixAll.solution%",
Expand Down
2 changes: 2 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"command.o.restart": "Restart OmniSharp",
"command.o.pickProjectAndStart": "Select Project",
"command.dotnet.openSolution": "Open Solution",
"command.dotnet.installCopilotLsp": "Install Copilot CLI C# LSP",
"command.dotnet.uninstallCopilotLsp": "Uninstall Copilot CLI C# LSP",
"command.o.fixAll.solution": "Fix all occurrences of a code issue within solution",
"command.o.fixAll.project": "Fix all occurrences of a code issue within project",
"command.o.fixAll.document": "Fix all occurrences of a code issue within document",
Expand Down
22 changes: 22 additions & 0 deletions redist/lsp-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"lspServers": {
"csharp": {
"command": "dotnet",
"args": [
"dnx",
"roslyn-language-server",
"--yes",
"--prerelease",
"--",
"--stdio",
"--autoLoadProjects"
],
"fileExtensions": {
".cs": "csharp",
".razor": "aspnetcorerazor",
".cshtml": "aspnetcorerazor"
},
"warmupTimeoutMs": 120000
}
}
}
4 changes: 4 additions & 0 deletions src/activateRoslyn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { SolutionSnapshotProvider } from './lsptoolshost/solutionSnapshot/soluti
import { BuildResultDiagnostics } from './lsptoolshost/diagnostics/buildResultReporterService';
import { getComponentFolder } from './lsptoolshost/extensions/builtInComponents';
import { RazorLogger } from './razor/src/razorLogger';
import { IExperimentationService } from 'vscode-tas-client';

export function activateRoslyn(
context: vscode.ExtensionContext,
Expand All @@ -33,6 +34,7 @@ export function activateRoslyn(
eventStream: EventStream,
csharpChannel: vscode.LogOutputChannel,
reporter: TelemetryReporter,
experimentationService: IExperimentationService | undefined,
csharpDevkitExtension: vscode.Extension<CSharpDevKitExports> | undefined,
getCoreClrDebugPromise: (languageServerStarted: Promise<any>) => Promise<void>
): CSharpExtensionExports {
Expand Down Expand Up @@ -78,6 +80,8 @@ export function activateRoslyn(
logDirectory: context.logUri.fsPath,
determineBrowserType: BlazorDebugConfigurationProvider.determineBrowserType,
experimental: {
getTreatmentVariable: <T extends boolean | number | string>(name: string, configId = 'vscode') =>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What is actually using / plan to be using this export? Doesn't appear to be for tests, so maybe we delete?

experimentationService?.getTreatmentVariable<T>(configId, name),
sendServerRequest: async (t, p, ct) => await languageServerExport.sendRequest(t, p, ct),
sendServerRequestWithProgress: async (t, p, pr, ct) =>
await languageServerExport.sendRequestWithProgress(t, p, pr, ct),
Expand Down
1 change: 1 addition & 0 deletions src/csharpExtensionExports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface CSharpExtensionExports {
}

export interface CSharpExtensionExperimentalExports {
getTreatmentVariable: <T extends boolean | number | string>(name: string, configId?: string) => T | undefined;
sendServerRequest: <Params, Response, Error>(
type: RequestType<Params, Response, Error>,
params: RequestParam<Params>,
Expand Down
121 changes: 121 additions & 0 deletions src/lsptoolshost/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import * as fs from 'fs/promises';
import * as os from 'os';
import * as path from 'path';
import { RoslynLanguageServer } from './server/roslynLanguageServer';
import reportIssue from '../shared/reportIssue';
import { getDotnetInfo } from '../shared/utils/getDotnetInfo';
Expand All @@ -22,6 +25,11 @@ import { TelemetryEventNames } from '../shared/telemetryEventNames';
import { registerCollectLogsCommand } from './logging/collectLogs';
import { ObservableLogOutputChannel } from './logging/observableLogOutputChannel';
import { RazorLogger } from '../razor/src/razorLogger';
import { getUninstalledCopilotLspConfigContent, getUpdatedCopilotLspConfigContent } from './copilotLspConfig';

const installCopilotLspCommand = 'dotnet.installCopilotLsp';
const uninstallCopilotLspCommand = 'dotnet.uninstallCopilotLsp';
const packagedCopilotLspConfigPath = path.join('redist', 'lsp-config.json');

export function registerCommands(
context: vscode.ExtensionContext,
Expand Down Expand Up @@ -92,5 +100,118 @@ function registerExtensionCommands(
context.subscriptions.push(
vscode.commands.registerCommand('csharp.showOutputWindow', async () => outputChannel.show())
);
context.subscriptions.push(
vscode.commands.registerCommand(installCopilotLspCommand, async () => {
const lspConfigPath = path.join(os.homedir(), '.copilot', 'lsp-config.json');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Think we should probably also check if they have a plugin installed with an lsp.json configured for C#

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rather than doing manual configuration this way, should we just be installing one of our existing plugins that contains the registration?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit - could we extract this into a separate file & function (similar to other command registrations)

const extensionLspConfigPath = path.join(context.extension.extensionPath, packagedCopilotLspConfigPath);
let packagedContent: string;
try {
packagedContent = await fs.readFile(extensionLspConfigPath, 'utf8');
} catch (error) {
const nodeError = error as NodeJS.ErrnoException;
void vscode.window.showErrorMessage(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit - I don't necessarily think it's worth catching this error - we don't expect this to fail, and if it does the command execution should show a modal dialog if it does. Doesn't feel necessary to wrap into a special localized exception

vscode.l10n.t('Failed to read packaged Copilot LSP config: {0}', nodeError.message)
);
return;
}
let currentContent: string | undefined;

try {
currentContent = await fs.readFile(lspConfigPath, 'utf8');
} catch (error) {
const nodeError = error as NodeJS.ErrnoException;
if (nodeError.code !== 'ENOENT') {
void vscode.window.showErrorMessage(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Instead of manually showing an error message, consider throwing throw Error(msg) - the command infrastructure already seems to handle failures and throw up a modal dialog.

Image

vscode.l10n.t('Failed to read Copilot LSP config: {0}', nodeError.message)
);
return;
}
}

let updateResult: { shouldWrite: boolean; updatedContent?: string };
try {
updateResult = getUpdatedCopilotLspConfigContent(currentContent, packagedContent);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Similar to before, I don't think we expect this to throw, so imho it is fine to let this bubble up without wrapping into a nice exception

void vscode.window.showErrorMessage(vscode.l10n.t('Failed to update Copilot LSP config: {0}', message));
return;
}

if (!updateResult.shouldWrite || !updateResult.updatedContent) {
void vscode.window.showInformationMessage(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I feel like this should be a modal dialog - the user explicitly invoked a command, so it makes sense to be noisy

vscode.l10n.t(
'Copilot LSP config already contains a C# LSP mapping for .cs files. No changes were made.'
)
);
return;
}

try {
await fs.mkdir(path.dirname(lspConfigPath), { recursive: true });
await fs.writeFile(lspConfigPath, updateResult.updatedContent, 'utf8');
void vscode.window.showInformationMessage(
vscode.l10n.t('Updated Copilot LSP config at {0}.', lspConfigPath)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

would it be worth opening the config file after we write it, so the user can see the change?

);
} catch (error) {
const nodeError = error as NodeJS.ErrnoException;
void vscode.window.showErrorMessage(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

as above - could throw a nice exception and let it bubble up to the command handling

vscode.l10n.t('Failed to write Copilot LSP config: {0}', nodeError.message)
);
}
})
);
context.subscriptions.push(
vscode.commands.registerCommand(uninstallCopilotLspCommand, async () => {
const lspConfigPath = path.join(os.homedir(), '.copilot', 'lsp-config.json');

let currentContent: string | undefined;
try {
currentContent = await fs.readFile(lspConfigPath, 'utf8');
} catch (error) {
const nodeError = error as NodeJS.ErrnoException;
if (nodeError.code !== 'ENOENT') {
void vscode.window.showErrorMessage(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same nit on throwing (and below)

vscode.l10n.t('Failed to read Copilot LSP config: {0}', nodeError.message)
);
return;
}
}

let uninstallResult: { shouldWrite: boolean; updatedContent?: string };
try {
uninstallResult = getUninstalledCopilotLspConfigContent(currentContent);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
void vscode.window.showErrorMessage(
vscode.l10n.t('Failed to uninstall Copilot LSP config: {0}', message)
);
return;
}

if (!uninstallResult.shouldWrite || !uninstallResult.updatedContent) {
void vscode.window.showInformationMessage(
vscode.l10n.t(
'Copilot LSP config does not contain a C# LSP mapping for .cs files. No changes were made.'
)
);
return;
}

try {
await fs.writeFile(lspConfigPath, uninstallResult.updatedContent, 'utf8');
void vscode.window.showInformationMessage(
vscode.l10n.t(
'Removed C# LSP mapping(s) for .cs files from Copilot LSP config at {0}.',
lspConfigPath
)
);
} catch (error) {
const nodeError = error as NodeJS.ErrnoException;
void vscode.window.showErrorMessage(
vscode.l10n.t('Failed to write Copilot LSP config: {0}', nodeError.message)
);
}
})
);
registerCollectLogsCommand(context, languageServer, outputChannel, csharpTraceChannel, razorLogger);
}
Loading
Loading