diff --git a/.gitignore b/.gitignore index ebd2fc124e..291b7b01bb 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ out .razorDevKit/ .razorExtension/ .vscode-test/ +.roslynCopilot/ msbuild/signing/signJs/*.log msbuild/signing/signVsix/*.log dist/ diff --git a/package.json b/package.json index 18cb41a4e1..ab59396dbd 100644 --- a/package.json +++ b/package.json @@ -422,6 +422,20 @@ "isFramework": false, "integrity": "9944EBD6EE06BD595BCADD3057CD9BEF4105C3A3952DAE03E54F3114E2E6661F" }, + { + "id": "RoslynCopilot", + "description": "Language server for Roslyn Copilot integration", + "url": "https://roslyn.blob.core.windows.net/releases/Microsoft.VisualStudio.Copilot.Roslyn.LanguageServer-18.0.479-alpha.zip", + "installPath": ".roslynCopilot", + "platforms": [ + "neutral" + ], + "architectures": [ + "neutral" + ], + "installTestPath": "./.roslynCopilot/Microsoft.VisualStudio.Copilot.Roslyn.LanguageServer.dll", + "integrity": "1D16E555AEFB581F6090D66A20FA5B3DD367EFA0D33BC97EF176285F60E02FEF" + }, { "id": "Debugger", "description": ".NET Core Debugger (Windows / x64)", @@ -1441,6 +1455,10 @@ "xamlTools": { "description": "%configuration.dotnet.server.componentPaths.xamlTools%", "type": "string" + }, + "roslynCopilot": { + "description": "%configuration.dotnet.server.componentPaths.roslynCopilot%", + "type": "string" } }, "default": {} diff --git a/package.nls.json b/package.nls.json index 9bd6325377..8ed486b10c 100644 --- a/package.nls.json +++ b/package.nls.json @@ -31,6 +31,7 @@ "configuration.dotnet.server.componentPaths": "Allows overriding the folder path for built in components of the language server (for example, override the .roslynDevKit path in the extension directory to use locally built components)", "configuration.dotnet.server.componentPaths.roslynDevKit": "Overrides the folder path for the .roslynDevKit component of the language server", "configuration.dotnet.server.componentPaths.xamlTools": "Overrides the folder path for the .xamlTools component of the language server", + "configuration.dotnet.server.componentPaths.roslynCopilot": "Overrides the folder path for the .roslynCopilot component of the language server", "configuration.dotnet.server.startTimeout": "Specifies a timeout (in ms) for the client to successfully start and connect to the language server.", "configuration.dotnet.server.waitForDebugger": "Passes the --debug flag when launching the server to allow a debugger to be attached. (Previously `omnisharp.waitForDebugger`)", "configuration.dotnet.server.extensionPaths": "Override for path to language server --extension arguments", diff --git a/src/lsptoolshost/copilot/contextProviders.ts b/src/lsptoolshost/copilot/contextProviders.ts index b4ba8fe0c7..ac1ceebadf 100644 --- a/src/lsptoolshost/copilot/contextProviders.ts +++ b/src/lsptoolshost/copilot/contextProviders.ts @@ -7,13 +7,7 @@ import * as vscode from 'vscode'; import * as lsp from 'vscode-languageserver-protocol'; import { RoslynLanguageServer } from '../server/roslynLanguageServer'; import { CSharpExtensionId } from '../../constants/csharpExtensionId'; -import { csharpDevkitExtensionId, getCSharpDevKit } from '../../utils/getCSharpDevKit'; -import path from 'path'; -import { readJsonSync } from 'fs-extra'; - -export const copilotLanguageServerExtensionComponentName = '@microsoft/visualstudio.copilot.roslyn.languageserver'; -export const copilotLanguageServerExtensionAssemblyName = 'Microsoft.VisualStudio.Copilot.Roslyn.LanguageServer.dll'; -const copilotLanguageServerExtensionCapabilitiesFileName = 'capabilities.json'; +import { getCSharpDevKit } from '../../utils/getCSharpDevKit'; type ActiveExperiments = { [name: string]: string | number | boolean | string[] }; @@ -29,17 +23,9 @@ export interface ContextResolveParam { data?: any; activeExperiments: ActiveExperiments; } - -const oldResolveContextMethodName = 'roslyn/resolveContext'; -const oldresolveContextMethodSupportedVersion = '1'; -const newResolveContextMethodName = 'roslyn/resolveContext@2'; -const newResolveContextMethodSupportedVersion = '1'; -const oldResolveContextRequest = new lsp.RequestType( - oldResolveContextMethodName, - lsp.ParameterStructures.auto -); -const newResolveContextRequest = new lsp.RequestType( - newResolveContextMethodName, +const resolveContextMethodName = 'roslyn/resolveContext@2'; +const resolveContextRequest = new lsp.RequestType( + resolveContextMethodName, lsp.ParameterStructures.auto ); @@ -85,43 +71,8 @@ export function registerCopilotContextProviders( return; } - devkit.activate().then(async (devKitExports) => { + devkit.activate().then(async () => { try { - let resolveMethod: lsp.RequestType | undefined = - undefined; - const copilotServerExtensionfolder = devKitExports.components[copilotLanguageServerExtensionComponentName]; - if (copilotServerExtensionfolder) { - const capabilitiesFilePath = path.join( - copilotServerExtensionfolder, - copilotLanguageServerExtensionCapabilitiesFileName - ); - const capabilitiesContent = await readJsonSync(capabilitiesFilePath); - for (const capability of capabilitiesContent?.capabilities ?? []) { - if ( - capability.method === oldResolveContextMethodName && - capability.version === oldresolveContextMethodSupportedVersion - ) { - resolveMethod = oldResolveContextRequest; - channel.debug(`supported 'roslyn/resolveContext' method found in capabilities.json`); - break; - } else if ( - capability.method === newResolveContextMethodName && - capability.version === newResolveContextMethodSupportedVersion - ) { - resolveMethod = newResolveContextRequest; - channel.debug(`supported 'roslyn/resolveContext@2' method found in capabilities.json`); - break; - } - } - } - - if (!resolveMethod) { - channel.debug( - `Failed to find compatible version of context provider from installed version of ${csharpDevkitExtensionId}.` - ); - return; - } - const copilotApi = vscode.extensions.getExtension('github.copilot'); if (!copilotApi) { channel.debug( @@ -150,7 +101,11 @@ export function registerCopilotContextProviders( if (!contextResolveParam) { return []; } - const items = await languageServer.sendRequest(resolveMethod, contextResolveParam, token); + const items = await languageServer.sendRequest( + resolveContextRequest, + contextResolveParam, + token + ); channel.trace(`Copilot context provider resolved ${items.length} items`); return items; }, diff --git a/src/lsptoolshost/extensions/builtInComponents.ts b/src/lsptoolshost/extensions/builtInComponents.ts index dd14e71448..af657fa165 100644 --- a/src/lsptoolshost/extensions/builtInComponents.ts +++ b/src/lsptoolshost/extensions/builtInComponents.ts @@ -37,6 +37,11 @@ export const componentInfo: { [key: string]: ComponentInfo } = { optionName: 'razorExtension', componentDllPaths: ['Microsoft.VisualStudioCode.RazorExtension.dll'], }, + roslynCopilot: { + defaultFolderName: '.roslynCopilot', + optionName: 'roslynCopilot', + componentDllPaths: ['Microsoft.VisualStudio.Copilot.Roslyn.LanguageServer.dll'], + }, }; export function getComponentPaths(componentName: string, options: LanguageServerOptions | undefined): string[] { diff --git a/src/lsptoolshost/server/roslynLanguageServer.ts b/src/lsptoolshost/server/roslynLanguageServer.ts index e187ebf970..dc7c0250e4 100644 --- a/src/lsptoolshost/server/roslynLanguageServer.ts +++ b/src/lsptoolshost/server/roslynLanguageServer.ts @@ -74,10 +74,6 @@ import { getProfilingEnvVars } from '../profiling/profiling'; import { isString } from '../utils/isString'; import { getServerPath } from '../activate'; import { UriConverter } from '../utils/uriConverter'; -import { - copilotLanguageServerExtensionAssemblyName, - copilotLanguageServerExtensionComponentName, -} from '../copilot/contextProviders'; // Flag indicating if C# Devkit was installed the last time we activated. // Used to determine if we need to restart the server on extension changes. @@ -689,7 +685,7 @@ export class RoslynLanguageServer { const csharpDevKitArgs = this.getCSharpDevKitExportArgs(additionalExtensionPaths); args = args.concat(csharpDevKitArgs); - await this.setupDevKitEnvironment(dotnetInfo.env, csharpDevkitExtension, additionalExtensionPaths, channel); + await this.setupDevKitEnvironment(dotnetInfo.env, csharpDevkitExtension, additionalExtensionPaths); } else { // C# Dev Kit is not installed - continue C#-only activation. channel.info('Activating C# standalone...'); @@ -1068,8 +1064,7 @@ export class RoslynLanguageServer { private static async setupDevKitEnvironment( env: NodeJS.ProcessEnv, csharpDevkitExtension: vscode.Extension, - additionalExtensionPaths: string[], - channel: vscode.LogOutputChannel + additionalExtensionPaths: string[] ): Promise { const exports: CSharpDevKitExports = await csharpDevkitExtension.activate(); @@ -1079,17 +1074,9 @@ export class RoslynLanguageServer { await exports.setupTelemetryEnvironmentAsync(env); } - const copilotServerExtensionfolder = exports.components[copilotLanguageServerExtensionComponentName]; - if (copilotServerExtensionfolder) { - const copilotServerExtensionFullPath = path.join( - copilotServerExtensionfolder, - copilotLanguageServerExtensionAssemblyName - ); - additionalExtensionPaths.push(copilotServerExtensionFullPath); - channel.trace( - `CSharp DevKit contributes Copilot langauge server extension: ${copilotServerExtensionFullPath}` - ); - } + getComponentPaths('roslynCopilot', languageServerOptions).forEach((extPath) => { + additionalExtensionPaths.push(extPath); + }); } /** diff --git a/src/main.ts b/src/main.ts index 71a40187fd..5808a2d872 100644 --- a/src/main.ts +++ b/src/main.ts @@ -77,6 +77,9 @@ export async function activate( if (useOmnisharpServer) { requiredPackageIds.push('OmniSharp'); } + if (csharpDevkitExtension) { + requiredPackageIds.push('RoslynCopilot'); + } const networkSettingsProvider = vscodeNetworkSettingsProvider(vscode); const useFramework = useOmnisharpServer && omnisharpOptions.useModernNet !== true; diff --git a/src/packageManager/packageFilterer.ts b/src/packageManager/packageFilterer.ts index 71b32fe7fc..80d22fe4a8 100644 --- a/src/packageManager/packageFilterer.ts +++ b/src/packageManager/packageFilterer.ts @@ -7,6 +7,8 @@ import { PlatformInformation } from '../shared/platform'; import * as util from '../common'; import { AbsolutePathPackage } from './absolutePathPackage'; +const NEUTRAL = 'neutral'; + export async function getNotInstalledPackagesForPlatform( packages: AbsolutePathPackage[], platformInfo: PlatformInformation @@ -17,7 +19,11 @@ export async function getNotInstalledPackagesForPlatform( export function filterPlatformPackages(packages: AbsolutePathPackage[], platformInfo: PlatformInformation) { return packages.filter( - (pkg) => pkg.architectures.includes(platformInfo.architecture) && pkg.platforms.includes(platformInfo.platform) + (pkg) => + // Match architecture, packages declared neutral are included as well. + (pkg.architectures.includes(NEUTRAL) || pkg.architectures.includes(platformInfo.architecture)) && + // Match platform, packages declared neutral are included as well. + (pkg.platforms.includes(NEUTRAL) || pkg.platforms.includes(platformInfo.platform)) ); } diff --git a/test/omnisharp/omnisharpUnitTests/packages/packageFilterer.test.ts b/test/omnisharp/omnisharpUnitTests/packages/packageFilterer.test.ts index eb4c7173da..e3a5c29426 100644 --- a/test/omnisharp/omnisharpUnitTests/packages/packageFilterer.test.ts +++ b/test/omnisharp/omnisharpUnitTests/packages/packageFilterer.test.ts @@ -55,6 +55,24 @@ describe(`${getNotInstalledPackagesForPlatform.name}`, () => { architectures: ['architecture2'], installPath: 'path3', }, + { + description: 'neutral platform and architecture uninstalled package', + platforms: ['neutral'], + architectures: ['neutral'], + installPath: 'path6', + }, + { + description: 'neutral platform but specific architecture package', + platforms: ['neutral'], + architectures: ['architecture1'], + installPath: 'path7', + }, + { + description: 'specific platform but neutral architecture package', + platforms: ['linux'], + architectures: ['neutral'], + installPath: 'path8', + }, ]; beforeEach(async () => { @@ -79,18 +97,46 @@ describe(`${getNotInstalledPackagesForPlatform.name}`, () => { test('Filters the packages based on Platform Information', async () => { const platformInfo = new PlatformInformation('win32', 'architecture2'); const filteredPackages = await getNotInstalledPackagesForPlatform(absolutePathPackages, platformInfo); - expect(filteredPackages.length).toEqual(1); + expect(filteredPackages.length).toEqual(2); expect(filteredPackages[0].description).toEqual('win32-Architecture2 uninstalled package'); expect(filteredPackages[0].platforms[0]).toEqual('win32'); expect(filteredPackages[0].architectures[0]).toEqual('architecture2'); + + expect(filteredPackages[1].description).toEqual('neutral platform and architecture uninstalled package'); + expect(filteredPackages[1].platforms[0]).toEqual('neutral'); + expect(filteredPackages[1].architectures[0]).toEqual('neutral'); }); test('Returns only the packages where install.Lock is not present', async () => { const platformInfo = new PlatformInformation('linux', 'architecture1'); const filteredPackages = await getNotInstalledPackagesForPlatform(absolutePathPackages, platformInfo); + // Should include linux-Architecture1 package + neutral package (both uninstalled) + expect(filteredPackages.length).toEqual(4); + + const descriptions = filteredPackages.map((pkg) => pkg.description); + expect(descriptions).toContain('linux-Architecture1 uninstalled package'); + expect(descriptions).toContain('neutral platform and architecture uninstalled package'); + expect(descriptions).toContain('neutral platform but specific architecture package'); + expect(descriptions).toContain('specific platform but neutral architecture package'); + }); + + test('Returns only neutral packages when no platform-specific packages match', async () => { + const platformInfo = new PlatformInformation('darwin', 'arm64'); // Non-existent platform/arch combo + const filteredPackages = await getNotInstalledPackagesForPlatform(absolutePathPackages, platformInfo); + + // Should only include neutral package (uninstalled one) + expect(filteredPackages.length).toEqual(1); + expect(filteredPackages[0].description).toEqual('neutral platform and architecture uninstalled package'); + expect(filteredPackages[0].platforms[0]).toEqual('neutral'); + expect(filteredPackages[0].architectures[0]).toEqual('neutral'); + }); + + test('Filters out installed neutral packages', async () => { + const platformInfo = new PlatformInformation('darwin', 'arm64'); // Only neutral packages should match + const filteredPackages = await getNotInstalledPackagesForPlatform(absolutePathPackages, platformInfo); + + // Should only return uninstalled neutral package, not the installed one expect(filteredPackages.length).toEqual(1); - expect(filteredPackages[0].description).toEqual('linux-Architecture1 uninstalled package'); - expect(filteredPackages[0].platforms[0]).toEqual('linux'); - expect(filteredPackages[0].architectures[0]).toEqual('architecture1'); + expect(filteredPackages[0].description).toEqual('neutral platform and architecture uninstalled package'); }); });