Skip to content

Commands to install/uninstall roslyn-language-server for the Copilot CLI#9344

Open
phil-allen-msft wants to merge 20 commits into
mainfrom
dev/phil-allen-msft/installLsp
Open

Commands to install/uninstall roslyn-language-server for the Copilot CLI#9344
phil-allen-msft wants to merge 20 commits into
mainfrom
dev/phil-allen-msft/installLsp

Conversation

@phil-allen-msft
Copy link
Copy Markdown
Member

2 new commands (invokable from Command Palette/F1) to install and uninstall, which is really are edits on $HOME/.copilot/lsp-config.json

Reasonable error handling and notifications are present. Initial integration with microsoft-tas-client as well.

To update the 'redist' copy of lsp-config.json which is merged in as part of install, run npm run ingest

Unittests added; manually tested with no file (file created, content added), existing file which is C# (no changes), and existing file which has C++-looking bindings (content added).

…bove fullConfig changes to the lsp-config.json file if it exists, and write a new lsp-config.json file if it doesn't exist, assuming the .copilot folder does exist.
…uded in the vsix. Copy the file from the dotnet/skills repo now.
… on the user's machine already contains content for the roslyn-language-server. If so, make no changes.
…yn-language-server for Copilot CLI' runs multiple times, the resulting lsp-config.json has exactly the same contents as the copy of the file shipped with the product initially.
…do the same copy that you did earlier of the lsp-config.json from the dotnet/skills repo, but do it again when 'npm run ingest' is specifically run.
…ain.

Change the Roslyn detection logic such that if there is a lspServer.csharp object in the read lsp-config.json, that counts as Roslyn detection = true
…epo and in the eventual extension to be 'redist'. Update all references.
… should find the lsp-config.json in the customer's HOME/.copilot directory; if it exists it should parse the file, and if the "lspServers.csharp" element exists, remove the "lspServers.csharp" element and leave all others.
…or an object named 'csharp', look for an object that lists ".cs" in the "fileExtensions" collection. Use lsp-config.json in the redist folder as a guide.
…search for content to delete by looking for an lspServer object that has a "fileExtensions" with ".cs"
…t into one method and use from both existing implementation sites.

Change how the callers of getCSharpServerNames works so that 'contains Roslyn Language Server' need only find one instance before eventually returns true.
…origin/main such that if they contained #sym:RoslynLanguageServer , they instead contain CSharpLsp
Fix element 1 in the above list. (create .copilot file if necessary)
… from the package.json file

Now remove the "dotnet.experiments.targetPopulation" property from package.json
Update message text to reflect current behavior
@phil-allen-msft phil-allen-msft requested a review from a team as a code owner May 22, 2026 17:08
expect(finalContent).toBe(packagedContent);
});

test('does not modify config when any server maps .cs in fileExtensions', () => {
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 the behavior we want? If somebody is wanting to run us to upgrade from some other LSP to our own, don't we want to remove the other one?

Comment on lines +20 to +22
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, content, 'utf8');
console.log(`Updated ${outputPath} from ${sourceUrl}`);
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 there not some other npm package for downloading to a file easily?

Comment thread package.json
"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.

);
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.

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


function containsCSharpServer(lspServers: { [key: string]: unknown }): boolean {
for (const [serverName, server] of Object.entries(lspServers)) {
if (serverName === 'csharp' || serverContainsCSharpFileExtension(server)) {
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.

Why is this checking the sever name but the other function above is just looking for the extension?

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.

Feels like they should both check for extension - I believe server name could be anything.

Also curious what happens if there is already a c# server configured that doesn't use roslyn-language-server.

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 do also wonder if we could install the lsp config as a plugin (e.g. ship the plugin in the extension, then use CLI commands to install the plugin). And rely on the cli for de-dupe? Not sure if thats any better though

);
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#

);
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.

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

} 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

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

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

};

updatedConfig.lspServers!.csharp = packagedCsharpConfig;
return { shouldWrite: true, updatedContent: `${JSON.stringify(updatedConfig, null, 2)}\n` };
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.

Could this change the formatting of their file? Maybe not a huge issue, but we've done things like this - https://github.com/dotnet/vscode-csharp/blob/main/src/shared/assets.ts#L1017 before to ensure in place updates


function containsCSharpServer(lspServers: { [key: string]: unknown }): boolean {
for (const [serverName, server] of Object.entries(lspServers)) {
if (serverName === 'csharp' || serverContainsCSharpFileExtension(server)) {
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.

Feels like they should both check for extension - I believe server name could be anything.

Also curious what happens if there is already a c# server configured that doesn't use roslyn-language-server.

Comment thread package.json
"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.

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


function containsCSharpServer(lspServers: { [key: string]: unknown }): boolean {
for (const [serverName, server] of Object.entries(lspServers)) {
if (serverName === 'csharp' || serverContainsCSharpFileExtension(server)) {
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 do also wonder if we could install the lsp config as a plugin (e.g. ship the plugin in the extension, then use CLI commands to install the plugin). And rely on the cli for de-dupe? Not sure if thats any better though

Comment thread src/activateRoslyn.ts
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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants