Commands to install/uninstall roslyn-language-server for the Copilot CLI#9344
Commands to install/uninstall roslyn-language-server for the Copilot CLI#9344phil-allen-msft wants to merge 20 commits into
Conversation
…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.
…aming for the uninstall command
…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
…not exist, it reports an error
| expect(finalContent).toBe(packagedContent); | ||
| }); | ||
|
|
||
| test('does not modify config when any server maps .cs in fileExtensions', () => { |
There was a problem hiding this comment.
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?
| await fs.mkdir(path.dirname(outputPath), { recursive: true }); | ||
| await fs.writeFile(outputPath, content, 'utf8'); | ||
| console.log(`Updated ${outputPath} from ${sourceUrl}`); |
There was a problem hiding this comment.
Is there not some other npm package for downloading to a file easily?
| "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", |
There was a problem hiding this comment.
Is this something we expect is happening automatically during a build or something we need to do on a regular basis?
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
Why is this checking the sever name but the other function above is just looking for the extension?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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( |
| packagedContent = await fs.readFile(extensionLspConfigPath, 'utf8'); | ||
| } catch (error) { | ||
| const nodeError = error as NodeJS.ErrnoException; | ||
| void vscode.window.showErrorMessage( |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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` }; |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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.
| "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", |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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
| logDirectory: context.logUri.fsPath, | ||
| determineBrowserType: BlazorDebugConfigurationProvider.determineBrowserType, | ||
| experimental: { | ||
| getTreatmentVariable: <T extends boolean | number | string>(name: string, configId = 'vscode') => |
There was a problem hiding this comment.
What is actually using / plan to be using this export? Doesn't appear to be for tests, so maybe we delete?

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