diff --git a/.gitignore b/.gitignore index 917a12b71..0acd9146a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules out .roslyn/ .roslynDevKit/ +.xamlTools/ .omnisharp/ .omnisharp-*/ .vs/ diff --git a/package.json b/package.json index 7f3647ed6..9a0874316 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "omniSharp": "1.39.11", "razor": "7.0.0-preview.24178.4", "razorOmnisharp": "7.0.0-preview.23363.1", - "razorTelemetry": "7.0.0-preview.24178.4" + "razorTelemetry": "7.0.0-preview.24178.4", + "xamlTools": "17.11.34816.20" }, "main": "./dist/extension", "l10n": "./l10n", @@ -1621,6 +1622,21 @@ "scope": "machine-overridable", "description": "%configuration.dotnet.server.path%" }, + "dotnet.server.componentPaths": { + "type": "object", + "description": "%configuration.dotnet.server.componentPaths%", + "properties": { + "roslynDevKit": { + "description": "%configuration.dotnet.server.componentPaths.roslynDevKit%", + "type": "string" + }, + "xamlTools": { + "description": "%configuration.dotnet.server.componentPaths.xamlTools%", + "type": "string" + } + }, + "default": {} + }, "dotnet.server.startTimeout": { "type": "number", "scope": "machine-overridable", @@ -1666,6 +1682,12 @@ "default": null, "description": "%configuration.dotnet.server.crashDumpPath%" }, + "dotnet.enableXamlToolsPreview": { + "scope": "machine-overridable", + "type": "boolean", + "default": false, + "description": "%configuration.dotnet.enableXamlToolsPreview%" + }, "dotnet.projects.binaryLogPath": { "scope": "machine-overridable", "type": "string", diff --git a/package.nls.json b/package.nls.json index 9ced594ca..8f91131cf 100644 --- a/package.nls.json +++ b/package.nls.json @@ -26,11 +26,15 @@ "configuration.dotnet.defaultSolution.description": "The path of the default solution to be opened in the workspace, or set to 'disable' to skip it. (Previously `omnisharp.defaultLaunchSolution`)", "configuration.dotnet.dotnetPath": "Specifies the path to a dotnet installation directory to use instead of the default system one. This only influences the dotnet installation to use for hosting the language server itself. Example: \"/home/username/mycustomdotnetdirectory\".", "configuration.dotnet.server.path": "Specifies the absolute path to the server (LSP or O#) executable. When left empty the version pinned to the C# Extension is used. (Previously `omnisharp.path`)", + "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.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.trace": "Sets the logging level for the language server", "configuration.dotnet.server.extensionPaths": "Override for path to language server --extension arguments", "configuration.dotnet.server.crashDumpPath": "Sets a folder path where crash dumps are written to if the language server crashes. Must be writeable by the user.", + "configuration.dotnet.enableXamlToolsPreview": "[Experimental] Enables XAML tools when using C# Dev Kit", "configuration.dotnet.projects.enableAutomaticRestore": "Enables automatic NuGet restore if the extension detects assets are missing.", "configuration.dotnet.preferCSharpExtension": "Forces projects to load with the C# extension only. This can be useful when using legacy project types that are not supported by C# Dev Kit. (Requires window reload)", "configuration.dotnet.implementType.insertionBehavior": "The insertion location of properties, events, and methods When implement interface or abstract class.", diff --git a/src/csharpExtensionExports.ts b/src/csharpExtensionExports.ts index 35e85b615..a6a717a2f 100644 --- a/src/csharpExtensionExports.ts +++ b/src/csharpExtensionExports.ts @@ -25,6 +25,7 @@ export interface CSharpExtensionExports { profferBrokeredServices: (container: GlobalBrokeredServiceContainer) => void; determineBrowserType: () => Promise; experimental: CSharpExtensionExperimentalExports; + getComponentFolder: (componentName: string) => string; } export interface CSharpExtensionExperimentalExports { diff --git a/src/lsptoolshost/builtInComponents.ts b/src/lsptoolshost/builtInComponents.ts new file mode 100644 index 000000000..220d5ba34 --- /dev/null +++ b/src/lsptoolshost/builtInComponents.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; +import * as path from 'path'; +import { LanguageServerOptions } from '../shared/options'; + +interface ComponentInfo { + defaultFolderName: string; + optionName: string; + componentDllPaths: string[]; +} + +export const componentInfo: { [key: string]: ComponentInfo } = { + roslynDevKit: { + defaultFolderName: '.roslynDevKit', + optionName: 'roslynDevKit', + componentDllPaths: ['Microsoft.VisualStudio.LanguageServices.DevKit.dll'], + }, + xamlTools: { + defaultFolderName: '.xamlTools', + optionName: 'xamlTools', + componentDllPaths: [ + 'Microsoft.VisualStudio.DesignTools.CodeAnalysis.dll', + 'Microsoft.VisualStudio.DesignTools.CodeAnalysis.Diagnostics.dll', + ], + }, +}; + +export function getComponentPaths(componentName: string, options: LanguageServerOptions): string[] { + const component = componentInfo[componentName]; + const baseFolder = getComponentFolderPath(component, options); + const paths = component.componentDllPaths.map((dllPath) => path.join(baseFolder, dllPath)); + for (const dllPath of paths) { + if (!fs.existsSync(dllPath)) { + throw new Error(`Component DLL not found: ${dllPath}`); + } + } + + return paths; +} + +export function getComponentFolder(componentName: string, options: LanguageServerOptions): string { + const component = componentInfo[componentName]; + return getComponentFolderPath(component, options); +} + +function getComponentFolderPath(component: ComponentInfo, options: LanguageServerOptions): string { + if (options.componentPaths) { + const optionValue = options.componentPaths[component.optionName]; + if (optionValue) { + return optionValue; + } + } + + return path.join(__dirname, '..', component.defaultFolderName); +} diff --git a/src/lsptoolshost/roslynLanguageServer.ts b/src/lsptoolshost/roslynLanguageServer.ts index e37e0ff31..0cd6a58aa 100644 --- a/src/lsptoolshost/roslynLanguageServer.ts +++ b/src/lsptoolshost/roslynLanguageServer.ts @@ -62,6 +62,7 @@ import { IDisposable } from '../disposable'; import { registerNestedCodeActionCommands } from './nestedCodeAction'; import { registerRestoreCommands } from './restore'; import { BuildDiagnosticsService } from './buildDiagnosticsService'; +import { getComponentPaths } from './builtInComponents'; let _channel: vscode.OutputChannel; let _traceChannel: vscode.OutputChannel; @@ -506,10 +507,6 @@ export class RoslynLanguageServer { args.push('--logLevel', logLevel); } - for (const extensionPath of additionalExtensionPaths) { - args.push('--extension', extensionPath); - } - args.push( '--razorSourceGenerator', path.join(context.extension.extensionPath, '.razor', 'Microsoft.CodeAnalysis.Razor.Compiler.dll') @@ -540,7 +537,7 @@ export class RoslynLanguageServer { // Set command enablement as soon as we know devkit is available. vscode.commands.executeCommand('setContext', 'dotnet.server.activationContext', 'RoslynDevKit'); - const csharpDevKitArgs = this.getCSharpDevKitExportArgs(); + const csharpDevKitArgs = this.getCSharpDevKitExportArgs(additionalExtensionPaths); args = args.concat(csharpDevKitArgs); await this.setupDevKitEnvironment(dotnetInfo.env, csharpDevkitExtension); @@ -553,6 +550,10 @@ export class RoslynLanguageServer { _wasActivatedWithCSharpDevkit = false; } + for (const extensionPath of additionalExtensionPaths) { + args.push('--extension', extensionPath); + } + if (logLevel && [Trace.Messages, Trace.Verbose].includes(this.GetTraceLevel(logLevel))) { _channel.appendLine(`Starting server at ${serverPath}`); } @@ -806,19 +807,24 @@ export class RoslynLanguageServer { ); } - private static getCSharpDevKitExportArgs(): string[] { + private static getCSharpDevKitExportArgs(additionalExtensionPaths: string[]): string[] { const args: string[] = []; - const clientRoot = __dirname; - const devKitDepsPath = path.join( - clientRoot, - '..', - '.roslynDevKit', - 'Microsoft.VisualStudio.LanguageServices.DevKit.dll' - ); - args.push('--devKitDependencyPath', devKitDepsPath); + const devKitDepsPath = getComponentPaths('roslynDevKit', languageServerOptions); + if (devKitDepsPath.length > 1) { + throw new Error('Expected only one devkit deps path'); + } + + args.push('--devKitDependencyPath', devKitDepsPath[0]); args.push('--sessionId', getSessionId()); + + // Also include the Xaml Dev Kit extensions, if enabled. + if (languageServerOptions.enableXamlToolsPreview) { + getComponentPaths('xamlTools', languageServerOptions).forEach((path) => + additionalExtensionPaths.push(path) + ); + } return args; } diff --git a/src/main.ts b/src/main.ts index a0989a0c7..781ee99a1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -56,9 +56,10 @@ import { RoslynLanguageServerEvents } from './lsptoolshost/languageServerEvents' import { ServerStateChange } from './lsptoolshost/serverStateChange'; import { SolutionSnapshotProvider } from './lsptoolshost/services/solutionSnapshotProvider'; import { RazorTelemetryDownloader } from './razor/razorTelemetryDownloader'; -import { commonOptions, omnisharpOptions, razorOptions } from './shared/options'; +import { commonOptions, languageServerOptions, omnisharpOptions, razorOptions } from './shared/options'; import { BuildResultDiagnostics } from './lsptoolshost/services/buildResultReporterService'; import { debugSessionTracker } from './coreclrDebug/provisionalDebugSessionTracker'; +import { getComponentFolder } from './lsptoolshost/builtInComponents'; export async function activate( context: vscode.ExtensionContext @@ -367,6 +368,9 @@ export async function activate( sendServerRequest: async (t, p, ct) => await languageServerExport.sendRequest(t, p, ct), languageServerEvents: roslynLanguageServerEvents, }, + getComponentFolder: (componentName) => { + return getComponentFolder(componentName, languageServerOptions); + }, }; } else { return { diff --git a/src/shared/options.ts b/src/shared/options.ts index 2fb34f869..9e2c3012a 100644 --- a/src/shared/options.ts +++ b/src/shared/options.ts @@ -77,6 +77,8 @@ export interface LanguageServerOptions { readonly crashDumpPath: string | undefined; readonly analyzerDiagnosticScope: string; readonly compilerDiagnosticScope: string; + readonly componentPaths: { [key: string]: string } | null; + readonly enableXamlToolsPreview: boolean; } export interface RazorOptions { @@ -397,6 +399,12 @@ class LanguageServerOptionsImpl implements LanguageServerOptions { public get compilerDiagnosticScope() { return readOption('dotnet.backgroundAnalysis.compilerDiagnosticsScope', 'openFiles'); } + public get componentPaths() { + return readOption<{ [key: string]: string }>('dotnet.server.componentPaths', {}); + } + public get enableXamlToolsPreview() { + return readOption('dotnet.enableXamlToolsPreview', false); + } } class RazorOptionsImpl implements RazorOptions { @@ -490,4 +498,6 @@ export const LanguageServerOptionsThatTriggerReload: ReadonlyArray string; + packageJsonName: string; + getPackageContentPath: (platformInfo: VSIXPlatformInfo | undefined) => string; + vsixOutputPath: string; +} + +// Set of NuGet packages that we need to download and install. +export const nugetPackageInfo: { [key: string]: NugetPackageInfo } = { + roslyn: { + getPackageName: (platformInfo) => `Microsoft.CodeAnalysis.LanguageServer.${platformInfo?.rid ?? 'neutral'}`, + packageJsonName: 'roslyn', + getPackageContentPath: (platformInfo) => path.join('content', 'LanguageServer', platformInfo?.rid ?? 'neutral'), + vsixOutputPath: languageServerDirectory, + }, + roslynDevKit: { + getPackageName: (_platformInfo) => 'Microsoft.VisualStudio.LanguageServices.DevKit', + packageJsonName: 'roslyn', + getPackageContentPath: (_platformInfo) => 'content', + vsixOutputPath: devKitDependenciesDirectory, + }, + xamlTools: { + getPackageName: (_platformInfo) => 'Microsoft.VisualStudio.DesignToolsBase', + packageJsonName: 'xamlTools', + getPackageContentPath: (_platformInfo) => 'content', + vsixOutputPath: xamlToolsDirectory, + }, +}; + const vsixTasks: string[] = []; for (const p of platformSpecificPackages) { let platformName: string; @@ -100,11 +130,12 @@ gulp.task('installDependencies', async () => { const packageJSON = getPackageJSON(); const platform = await PlatformInformation.GetCurrent(); + const vsixPlatformInfo = platformSpecificPackages.find( + (p) => p.platformInfo.platform === platform.platform && p.platformInfo.architecture === platform.architecture + )!; try { - await installRoslyn(packageJSON, platform); - await installDebugger(packageJSON, platform); - await installRazor(packageJSON, platform); + acquireAndInstallAllNugetPackages(vsixPlatformInfo, packageJSON, true); } catch (err) { const message = (err instanceof Error ? err.stack : err) ?? ''; // NOTE: Extra `\n---` at the end is because gulp will print this message following by the @@ -113,6 +144,10 @@ gulp.task('installDependencies', async () => { } }); +// Defines a special task to acquire all the platform specific Roslyn packages. +// All packages need to be saved to the consumption AzDo artifacts feed, for non-platform +// specific packages this only requires running the installDependencies tasks. However for Roslyn packages +// we need to acquire the nuget packages once for each platform to ensure they get saved to the feed. gulp.task( 'updateRoslynVersion', // Run the fetch of all packages, and then also installDependencies after @@ -120,45 +155,64 @@ gulp.task( const packageJSON = getPackageJSON(); // Fetch the neutral package that we don't otherwise have in our platform list - await acquireRoslyn(packageJSON, undefined, true); + await acquireNugetPackage(nugetPackageInfo.roslyn, undefined, packageJSON, true); // And now fetch each platform specific for (const p of platformSpecificPackages) { - await acquireRoslyn(packageJSON, p.platformInfo, true); + await acquireNugetPackage(nugetPackageInfo.roslyn, p, packageJSON, true); } // Also pull in the Roslyn DevKit dependencies nuget package. - await acquireRoslynDevKit(packageJSON, true); + await acquireNugetPackage(nugetPackageInfo.RoslynDevKit, undefined, packageJSON, true); }, 'installDependencies') ); -// Install Tasks -async function installRoslyn(packageJSON: any, platformInfo?: PlatformInformation) { - // Install the Roslyn language server bits. - const { packagePath, serverPlatform } = await acquireRoslyn(packageJSON, platformInfo, false); - await installNuGetPackage( - packagePath, - path.join('content', 'LanguageServer', serverPlatform), - languageServerDirectory - ); +async function acquireAndInstallAllNugetPackages( + platformInfo: VSIXPlatformInfo | undefined, + packageJSON: any, + interactive: boolean +) { + for (const key in nugetPackageInfo) { + const nugetPackage = nugetPackageInfo[key]; + const packagePath = await acquireNugetPackage(nugetPackage, platformInfo, packageJSON, interactive); + await installNuGetPackage(packagePath, nugetPackage, platformInfo); + } +} - // Install Roslyn DevKit dependencies. - const roslynDevKitPackagePath = await acquireRoslynDevKit(packageJSON, false); - await installNuGetPackage(roslynDevKitPackagePath, 'content', devKitDependenciesDirectory); +async function acquireNugetPackage( + nugetPackageInfo: NugetPackageInfo, + platformInfo: VSIXPlatformInfo | undefined, + packageJSON: any, + interactive: boolean +) { + const packageVersion = packageJSON.defaults[nugetPackageInfo.packageJsonName]; + const packageName = nugetPackageInfo.getPackageName(platformInfo); + const packagePath = await restoreNugetPackage(packageName, packageVersion, interactive); + return packagePath; } -async function installNuGetPackage(pathToPackage: string, contentPath: string, outputPath: string) { +async function installNuGetPackage( + pathToPackage: string, + nugetPackageInfo: NugetPackageInfo, + platformInfo: VSIXPlatformInfo | undefined +) { // Get the directory containing the content. - const contentDirectory = path.join(pathToPackage, contentPath); + const pathToContentInNugetPackage = nugetPackageInfo.getPackageContentPath(platformInfo); + const contentDirectory = path.join(pathToPackage, pathToContentInNugetPackage); if (!fs.existsSync(contentDirectory)) { - throw new Error(`Failed to find NuGet package content at ${contentDirectory}`); + throw new Error( + `Failed to find NuGet package content at ${contentDirectory} for ${nugetPackageInfo.getPackageName( + platformInfo + )}` + ); } const numFilesToCopy = fs.readdirSync(contentDirectory).length; console.log(`Extracting content from ${contentDirectory}`); - // Copy the files to the language server directory. + // Copy the files to the specified output directory. + const outputPath = nugetPackageInfo.vsixOutputPath; fs.mkdirSync(outputPath); fsextra.copySync(contentDirectory, outputPath); const numCopiedFiles = fs.readdirSync(outputPath).length; @@ -169,43 +223,6 @@ async function installNuGetPackage(pathToPackage: string, contentPath: string, o } } -async function acquireRoslyn( - packageJSON: any, - platformInfo: PlatformInformation | undefined, - interactive: boolean -): Promise<{ packagePath: string; serverPlatform: string }> { - const roslynVersion = packageJSON.defaults.roslyn; - - // Find the matching server RID for the current platform. - let serverPlatform: string; - if (platformInfo === undefined) { - serverPlatform = 'neutral'; - } else { - serverPlatform = platformSpecificPackages.find( - (p) => - p.platformInfo.platform === platformInfo.platform && - p.platformInfo.architecture === platformInfo.architecture - )!.rid; - } - - const packagePath = await acquireNugetPackage( - `Microsoft.CodeAnalysis.LanguageServer.${serverPlatform}`, - roslynVersion, - interactive - ); - return { packagePath, serverPlatform }; -} - -async function acquireRoslynDevKit(packageJSON: any, interactive: boolean): Promise { - const roslynVersion = packageJSON.defaults.roslyn; - const packagePath = await acquireNugetPackage( - `Microsoft.VisualStudio.LanguageServices.DevKit`, - roslynVersion, - interactive - ); - return packagePath; -} - async function installRazor(packageJSON: any, platformInfo: PlatformInformation) { if (platformInfo === undefined) { const platformNeutral = new PlatformInformation('neutral', 'neutral'); @@ -243,7 +260,7 @@ async function installPackageJsonDependency( } } -async function acquireNugetPackage(packageName: string, packageVersion: string, interactive: boolean): Promise { +async function restoreNugetPackage(packageName: string, packageVersion: string, interactive: boolean): Promise { packageName = packageName.toLocaleLowerCase(); const packageOutputPath = path.join(nugetTempPath, packageName, packageVersion); if (fs.existsSync(packageOutputPath)) { @@ -310,13 +327,7 @@ async function doPackageOffline(vsixPlatform: VSIXPlatformInfo | undefined) { if (vsixPlatform === undefined) { await buildVsix(packageJSON, packedVsixOutputRoot, prerelease); } else { - await buildVsix( - packageJSON, - packedVsixOutputRoot, - prerelease, - vsixPlatform.vsceTarget, - vsixPlatform.platformInfo - ); + await buildVsix(packageJSON, packedVsixOutputRoot, prerelease, vsixPlatform); } } catch (err) { const message = (err instanceof Error ? err.stack : err) ?? ''; @@ -330,32 +341,24 @@ async function doPackageOffline(vsixPlatform: VSIXPlatformInfo | undefined) { } async function cleanAsync() { - await del([ - 'install.*', - '.omnisharp*', - '.debugger', - '.razor', - languageServerDirectory, - devKitDependenciesDirectory, - ]); + const directoriesToDelete = ['install.*', '.omnisharp*', '.debugger', '.razor']; + for (const key in nugetPackageInfo) { + directoriesToDelete.push(nugetPackageInfo[key].vsixOutputPath); + } + + await del(directoriesToDelete); } -async function buildVsix( - packageJSON: any, - outputFolder: string, - prerelease: boolean, - vsceTarget?: string, - platformInfo?: PlatformInformation -) { - await installRoslyn(packageJSON, platformInfo); +async function buildVsix(packageJSON: any, outputFolder: string, prerelease: boolean, platformInfo?: VSIXPlatformInfo) { + await acquireAndInstallAllNugetPackages(platformInfo, packageJSON, false); if (platformInfo != null) { - await installRazor(packageJSON, platformInfo); - await installDebugger(packageJSON, platformInfo); + await installRazor(packageJSON, platformInfo.platformInfo); + await installDebugger(packageJSON, platformInfo.platformInfo); } - const packageFileName = getPackageName(packageJSON, vsceTarget); - await createPackageAsync(outputFolder, prerelease, packageFileName, vsceTarget); + const packageFileName = getPackageName(packageJSON, platformInfo?.vsceTarget); + await createPackageAsync(outputFolder, prerelease, packageFileName, platformInfo?.vsceTarget); } function getPackageName(packageJSON: any, vscodePlatformId?: string) { diff --git a/tasks/projectPaths.ts b/tasks/projectPaths.ts index 8c409ed98..894649f8b 100644 --- a/tasks/projectPaths.ts +++ b/tasks/projectPaths.ts @@ -5,6 +5,7 @@ import * as path from 'path'; import { commandLineOptions } from './commandLineArguments'; +import { componentInfo } from '../src/lsptoolshost/builtInComponents'; export const rootPath = path.resolve(__dirname, '..'); @@ -15,7 +16,8 @@ export const jestPath = path.join(nodeModulesPath, 'jest', 'bin', 'jest'); export const packedVsixOutputRoot = commandLineOptions.outputFolder || path.join(rootPath, 'vsix'); export const nugetTempPath = path.join(rootPath, 'out', '.nuget'); export const languageServerDirectory = path.join(rootPath, '.roslyn'); -export const devKitDependenciesDirectory = path.join(rootPath, '.roslynDevKit'); +export const devKitDependenciesDirectory = path.join(rootPath, componentInfo.roslynDevKit.defaultFolderName); +export const xamlToolsDirectory = path.join(rootPath, componentInfo.xamlTools.defaultFolderName); export const codeExtensionPath = commandLineOptions.codeExtensionPath || rootPath;