Skip to content

Commit 4d50c24

Browse files
authored
Storage account for Copilot language server (#8408)
This is primarily an implementation of David Barbet's suggestion that helps with our dev inner-loop reducing the amount of dependencies across repos. With the roslyn copilot language server coming in from the storage account, changes are no longer required in C# Dev Kit to point to the right language server.
2 parents 8bfd417 + 27caf6d commit 4d50c24

File tree

9 files changed

+100
-78
lines changed

9 files changed

+100
-78
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ out
1515
.razorDevKit/
1616
.razorExtension/
1717
.vscode-test/
18+
.roslynCopilot/
1819
msbuild/signing/signJs/*.log
1920
msbuild/signing/signVsix/*.log
2021
dist/

package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,20 @@
422422
"isFramework": false,
423423
"integrity": "9944EBD6EE06BD595BCADD3057CD9BEF4105C3A3952DAE03E54F3114E2E6661F"
424424
},
425+
{
426+
"id": "RoslynCopilot",
427+
"description": "Language server for Roslyn Copilot integration",
428+
"url": "https://roslyn.blob.core.windows.net/releases/Microsoft.VisualStudio.Copilot.Roslyn.LanguageServer-18.0.479-alpha.zip",
429+
"installPath": ".roslynCopilot",
430+
"platforms": [
431+
"neutral"
432+
],
433+
"architectures": [
434+
"neutral"
435+
],
436+
"installTestPath": "./.roslynCopilot/Microsoft.VisualStudio.Copilot.Roslyn.LanguageServer.dll",
437+
"integrity": "1D16E555AEFB581F6090D66A20FA5B3DD367EFA0D33BC97EF176285F60E02FEF"
438+
},
425439
{
426440
"id": "Debugger",
427441
"description": ".NET Core Debugger (Windows / x64)",
@@ -1441,6 +1455,10 @@
14411455
"xamlTools": {
14421456
"description": "%configuration.dotnet.server.componentPaths.xamlTools%",
14431457
"type": "string"
1458+
},
1459+
"roslynCopilot": {
1460+
"description": "%configuration.dotnet.server.componentPaths.roslynCopilot%",
1461+
"type": "string"
14441462
}
14451463
},
14461464
"default": {}

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"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)",
3232
"configuration.dotnet.server.componentPaths.roslynDevKit": "Overrides the folder path for the .roslynDevKit component of the language server",
3333
"configuration.dotnet.server.componentPaths.xamlTools": "Overrides the folder path for the .xamlTools component of the language server",
34+
"configuration.dotnet.server.componentPaths.roslynCopilot": "Overrides the folder path for the .roslynCopilot component of the language server",
3435
"configuration.dotnet.server.startTimeout": "Specifies a timeout (in ms) for the client to successfully start and connect to the language server.",
3536
"configuration.dotnet.server.waitForDebugger": "Passes the --debug flag when launching the server to allow a debugger to be attached. (Previously `omnisharp.waitForDebugger`)",
3637
"configuration.dotnet.server.extensionPaths": "Override for path to language server --extension arguments",

src/lsptoolshost/copilot/contextProviders.ts

Lines changed: 10 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,7 @@ import * as vscode from 'vscode';
77
import * as lsp from 'vscode-languageserver-protocol';
88
import { RoslynLanguageServer } from '../server/roslynLanguageServer';
99
import { CSharpExtensionId } from '../../constants/csharpExtensionId';
10-
import { csharpDevkitExtensionId, getCSharpDevKit } from '../../utils/getCSharpDevKit';
11-
import path from 'path';
12-
import { readJsonSync } from 'fs-extra';
13-
14-
export const copilotLanguageServerExtensionComponentName = '@microsoft/visualstudio.copilot.roslyn.languageserver';
15-
export const copilotLanguageServerExtensionAssemblyName = 'Microsoft.VisualStudio.Copilot.Roslyn.LanguageServer.dll';
16-
const copilotLanguageServerExtensionCapabilitiesFileName = 'capabilities.json';
10+
import { getCSharpDevKit } from '../../utils/getCSharpDevKit';
1711

1812
type ActiveExperiments = { [name: string]: string | number | boolean | string[] };
1913

@@ -29,17 +23,9 @@ export interface ContextResolveParam {
2923
data?: any;
3024
activeExperiments: ActiveExperiments;
3125
}
32-
33-
const oldResolveContextMethodName = 'roslyn/resolveContext';
34-
const oldresolveContextMethodSupportedVersion = '1';
35-
const newResolveContextMethodName = 'roslyn/resolveContext@2';
36-
const newResolveContextMethodSupportedVersion = '1';
37-
const oldResolveContextRequest = new lsp.RequestType<ContextResolveParam, SupportedContextItem[], void>(
38-
oldResolveContextMethodName,
39-
lsp.ParameterStructures.auto
40-
);
41-
const newResolveContextRequest = new lsp.RequestType<ContextResolveParam, SupportedContextItem[], void>(
42-
newResolveContextMethodName,
26+
const resolveContextMethodName = 'roslyn/resolveContext@2';
27+
const resolveContextRequest = new lsp.RequestType<ContextResolveParam, SupportedContextItem[], void>(
28+
resolveContextMethodName,
4329
lsp.ParameterStructures.auto
4430
);
4531

@@ -85,43 +71,8 @@ export function registerCopilotContextProviders(
8571
return;
8672
}
8773

88-
devkit.activate().then(async (devKitExports) => {
74+
devkit.activate().then(async () => {
8975
try {
90-
let resolveMethod: lsp.RequestType<ContextResolveParam, SupportedContextItem[], void> | undefined =
91-
undefined;
92-
const copilotServerExtensionfolder = devKitExports.components[copilotLanguageServerExtensionComponentName];
93-
if (copilotServerExtensionfolder) {
94-
const capabilitiesFilePath = path.join(
95-
copilotServerExtensionfolder,
96-
copilotLanguageServerExtensionCapabilitiesFileName
97-
);
98-
const capabilitiesContent = await readJsonSync(capabilitiesFilePath);
99-
for (const capability of capabilitiesContent?.capabilities ?? []) {
100-
if (
101-
capability.method === oldResolveContextMethodName &&
102-
capability.version === oldresolveContextMethodSupportedVersion
103-
) {
104-
resolveMethod = oldResolveContextRequest;
105-
channel.debug(`supported 'roslyn/resolveContext' method found in capabilities.json`);
106-
break;
107-
} else if (
108-
capability.method === newResolveContextMethodName &&
109-
capability.version === newResolveContextMethodSupportedVersion
110-
) {
111-
resolveMethod = newResolveContextRequest;
112-
channel.debug(`supported 'roslyn/resolveContext@2' method found in capabilities.json`);
113-
break;
114-
}
115-
}
116-
}
117-
118-
if (!resolveMethod) {
119-
channel.debug(
120-
`Failed to find compatible version of context provider from installed version of ${csharpDevkitExtensionId}.`
121-
);
122-
return;
123-
}
124-
12576
const copilotApi = vscode.extensions.getExtension<CopilotApi>('github.copilot');
12677
if (!copilotApi) {
12778
channel.debug(
@@ -150,7 +101,11 @@ export function registerCopilotContextProviders(
150101
if (!contextResolveParam) {
151102
return [];
152103
}
153-
const items = await languageServer.sendRequest(resolveMethod, contextResolveParam, token);
104+
const items = await languageServer.sendRequest(
105+
resolveContextRequest,
106+
contextResolveParam,
107+
token
108+
);
154109
channel.trace(`Copilot context provider resolved ${items.length} items`);
155110
return items;
156111
},

src/lsptoolshost/extensions/builtInComponents.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ export const componentInfo: { [key: string]: ComponentInfo } = {
3737
optionName: 'razorExtension',
3838
componentDllPaths: ['Microsoft.VisualStudioCode.RazorExtension.dll'],
3939
},
40+
roslynCopilot: {
41+
defaultFolderName: '.roslynCopilot',
42+
optionName: 'roslynCopilot',
43+
componentDllPaths: ['Microsoft.VisualStudio.Copilot.Roslyn.LanguageServer.dll'],
44+
},
4045
};
4146

4247
export function getComponentPaths(componentName: string, options: LanguageServerOptions | undefined): string[] {

src/lsptoolshost/server/roslynLanguageServer.ts

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,6 @@ import { getProfilingEnvVars } from '../profiling/profiling';
7474
import { isString } from '../utils/isString';
7575
import { getServerPath } from '../activate';
7676
import { UriConverter } from '../utils/uriConverter';
77-
import {
78-
copilotLanguageServerExtensionAssemblyName,
79-
copilotLanguageServerExtensionComponentName,
80-
} from '../copilot/contextProviders';
8177

8278
// Flag indicating if C# Devkit was installed the last time we activated.
8379
// Used to determine if we need to restart the server on extension changes.
@@ -689,7 +685,7 @@ export class RoslynLanguageServer {
689685
const csharpDevKitArgs = this.getCSharpDevKitExportArgs(additionalExtensionPaths);
690686
args = args.concat(csharpDevKitArgs);
691687

692-
await this.setupDevKitEnvironment(dotnetInfo.env, csharpDevkitExtension, additionalExtensionPaths, channel);
688+
await this.setupDevKitEnvironment(dotnetInfo.env, csharpDevkitExtension, additionalExtensionPaths);
693689
} else {
694690
// C# Dev Kit is not installed - continue C#-only activation.
695691
channel.info('Activating C# standalone...');
@@ -1068,8 +1064,7 @@ export class RoslynLanguageServer {
10681064
private static async setupDevKitEnvironment(
10691065
env: NodeJS.ProcessEnv,
10701066
csharpDevkitExtension: vscode.Extension<CSharpDevKitExports>,
1071-
additionalExtensionPaths: string[],
1072-
channel: vscode.LogOutputChannel
1067+
additionalExtensionPaths: string[]
10731068
): Promise<void> {
10741069
const exports: CSharpDevKitExports = await csharpDevkitExtension.activate();
10751070

@@ -1079,17 +1074,9 @@ export class RoslynLanguageServer {
10791074
await exports.setupTelemetryEnvironmentAsync(env);
10801075
}
10811076

1082-
const copilotServerExtensionfolder = exports.components[copilotLanguageServerExtensionComponentName];
1083-
if (copilotServerExtensionfolder) {
1084-
const copilotServerExtensionFullPath = path.join(
1085-
copilotServerExtensionfolder,
1086-
copilotLanguageServerExtensionAssemblyName
1087-
);
1088-
additionalExtensionPaths.push(copilotServerExtensionFullPath);
1089-
channel.trace(
1090-
`CSharp DevKit contributes Copilot langauge server extension: ${copilotServerExtensionFullPath}`
1091-
);
1092-
}
1077+
getComponentPaths('roslynCopilot', languageServerOptions).forEach((extPath) => {
1078+
additionalExtensionPaths.push(extPath);
1079+
});
10931080
}
10941081

10951082
/**

src/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ export async function activate(
7777
if (useOmnisharpServer) {
7878
requiredPackageIds.push('OmniSharp');
7979
}
80+
if (csharpDevkitExtension) {
81+
requiredPackageIds.push('RoslynCopilot');
82+
}
8083

8184
const networkSettingsProvider = vscodeNetworkSettingsProvider(vscode);
8285
const useFramework = useOmnisharpServer && omnisharpOptions.useModernNet !== true;

src/packageManager/packageFilterer.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { PlatformInformation } from '../shared/platform';
77
import * as util from '../common';
88
import { AbsolutePathPackage } from './absolutePathPackage';
99

10+
const NEUTRAL = 'neutral';
11+
1012
export async function getNotInstalledPackagesForPlatform(
1113
packages: AbsolutePathPackage[],
1214
platformInfo: PlatformInformation
@@ -17,7 +19,11 @@ export async function getNotInstalledPackagesForPlatform(
1719

1820
export function filterPlatformPackages(packages: AbsolutePathPackage[], platformInfo: PlatformInformation) {
1921
return packages.filter(
20-
(pkg) => pkg.architectures.includes(platformInfo.architecture) && pkg.platforms.includes(platformInfo.platform)
22+
(pkg) =>
23+
// Match architecture, packages declared neutral are included as well.
24+
(pkg.architectures.includes(NEUTRAL) || pkg.architectures.includes(platformInfo.architecture)) &&
25+
// Match platform, packages declared neutral are included as well.
26+
(pkg.platforms.includes(NEUTRAL) || pkg.platforms.includes(platformInfo.platform))
2127
);
2228
}
2329

test/omnisharp/omnisharpUnitTests/packages/packageFilterer.test.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,24 @@ describe(`${getNotInstalledPackagesForPlatform.name}`, () => {
5555
architectures: ['architecture2'],
5656
installPath: 'path3',
5757
},
58+
{
59+
description: 'neutral platform and architecture uninstalled package',
60+
platforms: ['neutral'],
61+
architectures: ['neutral'],
62+
installPath: 'path6',
63+
},
64+
{
65+
description: 'neutral platform but specific architecture package',
66+
platforms: ['neutral'],
67+
architectures: ['architecture1'],
68+
installPath: 'path7',
69+
},
70+
{
71+
description: 'specific platform but neutral architecture package',
72+
platforms: ['linux'],
73+
architectures: ['neutral'],
74+
installPath: 'path8',
75+
},
5876
];
5977

6078
beforeEach(async () => {
@@ -79,18 +97,46 @@ describe(`${getNotInstalledPackagesForPlatform.name}`, () => {
7997
test('Filters the packages based on Platform Information', async () => {
8098
const platformInfo = new PlatformInformation('win32', 'architecture2');
8199
const filteredPackages = await getNotInstalledPackagesForPlatform(absolutePathPackages, platformInfo);
82-
expect(filteredPackages.length).toEqual(1);
100+
expect(filteredPackages.length).toEqual(2);
83101
expect(filteredPackages[0].description).toEqual('win32-Architecture2 uninstalled package');
84102
expect(filteredPackages[0].platforms[0]).toEqual('win32');
85103
expect(filteredPackages[0].architectures[0]).toEqual('architecture2');
104+
105+
expect(filteredPackages[1].description).toEqual('neutral platform and architecture uninstalled package');
106+
expect(filteredPackages[1].platforms[0]).toEqual('neutral');
107+
expect(filteredPackages[1].architectures[0]).toEqual('neutral');
86108
});
87109

88110
test('Returns only the packages where install.Lock is not present', async () => {
89111
const platformInfo = new PlatformInformation('linux', 'architecture1');
90112
const filteredPackages = await getNotInstalledPackagesForPlatform(absolutePathPackages, platformInfo);
113+
// Should include linux-Architecture1 package + neutral package (both uninstalled)
114+
expect(filteredPackages.length).toEqual(4);
115+
116+
const descriptions = filteredPackages.map((pkg) => pkg.description);
117+
expect(descriptions).toContain('linux-Architecture1 uninstalled package');
118+
expect(descriptions).toContain('neutral platform and architecture uninstalled package');
119+
expect(descriptions).toContain('neutral platform but specific architecture package');
120+
expect(descriptions).toContain('specific platform but neutral architecture package');
121+
});
122+
123+
test('Returns only neutral packages when no platform-specific packages match', async () => {
124+
const platformInfo = new PlatformInformation('darwin', 'arm64'); // Non-existent platform/arch combo
125+
const filteredPackages = await getNotInstalledPackagesForPlatform(absolutePathPackages, platformInfo);
126+
127+
// Should only include neutral package (uninstalled one)
128+
expect(filteredPackages.length).toEqual(1);
129+
expect(filteredPackages[0].description).toEqual('neutral platform and architecture uninstalled package');
130+
expect(filteredPackages[0].platforms[0]).toEqual('neutral');
131+
expect(filteredPackages[0].architectures[0]).toEqual('neutral');
132+
});
133+
134+
test('Filters out installed neutral packages', async () => {
135+
const platformInfo = new PlatformInformation('darwin', 'arm64'); // Only neutral packages should match
136+
const filteredPackages = await getNotInstalledPackagesForPlatform(absolutePathPackages, platformInfo);
137+
138+
// Should only return uninstalled neutral package, not the installed one
91139
expect(filteredPackages.length).toEqual(1);
92-
expect(filteredPackages[0].description).toEqual('linux-Architecture1 uninstalled package');
93-
expect(filteredPackages[0].platforms[0]).toEqual('linux');
94-
expect(filteredPackages[0].architectures[0]).toEqual('architecture1');
140+
expect(filteredPackages[0].description).toEqual('neutral platform and architecture uninstalled package');
95141
});
96142
});

0 commit comments

Comments
 (0)