From b3e2f53a083bb8a28afca445111a180ea9a2f5b7 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Tue, 1 Apr 2025 14:45:24 -0400 Subject: [PATCH 1/7] Support different toolchains per folder With the introduction of swiftly's `.swift-version` file the active toolchain can now be per-folder instead of global. If the user has multiple different packages open they may each be using a different toolchain as defined by their `.swift-version` file. In order to support this new paradigm the `toolchain` has been moved from `WorkspaceContext` to `FolderContext`. Each time a folder (package) is added to the workspace a new toolchain is created, as it might be different from folder to folder. The toolchain created respects the `.swift-version` file. If the toolchain specified in the `.swift-version` file is not installed an error message is shown prompting the user to install the version with swiftly. There is still a `globalToolchain` on the `WorkspaceContext` which refers to the globally available toolchain. This would be the toolchain used when you run `swift` outside of a workspace folder. This is mainly used as a fallback toolchain for when there are no workspace folders. It is generally advisable to use the toolchain provided on the `FolderContext` to ensure you don't end up using mismatched versions. This PR also refactors the `LanguageClientManager` so that one instance of sourcekit-lsp is started per-toolchain, coordinating startup so that the server from a given toolchain starts up when a folder using that toolchain is added to the workspace. While this PR adds support for .swift-version files, there is still quite a bit of work to do to make using swiftly with the VS Code Swift extension a nicer experience including: Installing swiftly directly from the extension, downloading missing toolchains automatically, listing/picking/downloading toolchains via `swiftly list`, a smoother toolchain switching experience that would optionally write the `.swift-version` file, and more. --- src/FolderContext.ts | 20 +- src/PackageWatcher.ts | 46 ++- src/SwiftSnippets.ts | 6 +- src/TestExplorer/TestExplorer.ts | 16 +- src/TestExplorer/TestRunner.ts | 6 +- src/WorkspaceContext.ts | 42 ++- src/commands.ts | 12 +- src/commands/build.ts | 2 +- src/commands/captureDiagnostics.ts | 54 ++- src/commands/dependencies/edit.ts | 2 +- src/commands/dependencies/resolve.ts | 2 +- src/commands/dependencies/unedit.ts | 2 +- src/commands/dependencies/update.ts | 2 +- src/commands/dependencies/useLocal.ts | 4 +- src/commands/reindexProject.ts | 13 +- src/commands/resetPackage.ts | 9 +- src/commands/runSwiftScript.ts | 8 +- src/configuration.ts | 2 +- src/coverage/LcovResults.ts | 4 +- src/debugger/buildConfig.ts | 44 +-- src/debugger/debugAdapterFactory.ts | 23 +- .../DocumentationPreviewEditor.ts | 11 +- src/extension.ts | 2 +- .../LanguageClientConfiguration.ts | 260 +++++++++++++ src/sourcekit-lsp/LanguageClientManager.ts | 356 +++--------------- .../LanguageClientToolchainCoordinator.ts | 131 +++++++ src/sourcekit-lsp/inlayHints.ts | 4 +- src/tasks/SwiftPluginTaskProvider.ts | 22 +- src/tasks/SwiftTaskProvider.ts | 22 +- src/tasks/TaskQueue.ts | 2 +- src/toolchain/toolchain.ts | 59 +-- src/ui/LanguageStatusItems.ts | 46 ++- src/utilities/utilities.ts | 8 +- .../DiagnosticsManager.test.ts | 10 +- .../ExtensionActivation.test.ts | 16 +- test/integration-tests/SwiftSnippet.test.ts | 4 +- .../WorkspaceContext.test.ts | 6 +- test/integration-tests/commands/build.test.ts | 4 +- test/integration-tests/debugger/lldb.test.ts | 6 +- .../LanguageClientIntegration.test.ts | 11 +- .../tasks/SwiftPluginTaskProvider.test.ts | 6 +- .../tasks/SwiftTaskProvider.test.ts | 2 +- .../TestExplorerIntegration.test.ts | 38 +- .../testexplorer/XCTestOutputParser.test.ts | 2 +- .../ui/ProjectPanelProvider.test.ts | 14 +- .../utilities/lsputilities.ts | 6 +- .../utilities/testutilities.ts | 4 +- .../debugger/debugAdapterFactory.test.ts | 89 ++++- .../LanguageClientManager.test.ts | 196 +++++++--- .../tasks/SwiftPluginTaskProvider.test.ts | 15 +- .../tasks/SwiftTaskProvider.test.ts | 14 +- 51 files changed, 1061 insertions(+), 624 deletions(-) create mode 100644 src/sourcekit-lsp/LanguageClientConfiguration.ts create mode 100644 src/sourcekit-lsp/LanguageClientToolchainCoordinator.ts diff --git a/src/FolderContext.ts b/src/FolderContext.ts index 5e0a826e5..bf707c5a7 100644 --- a/src/FolderContext.ts +++ b/src/FolderContext.ts @@ -23,6 +23,7 @@ import { BackgroundCompilation } from "./BackgroundCompilation"; import { TaskQueue } from "./tasks/TaskQueue"; import { isPathInsidePath } from "./utilities/filesystem"; import { SwiftOutputChannel } from "./ui/SwiftOutputChannel"; +import { SwiftToolchain } from "./toolchain/toolchain"; export class FolderContext implements vscode.Disposable { private packageWatcher: PackageWatcher; @@ -39,13 +40,13 @@ export class FolderContext implements vscode.Disposable { */ private constructor( public folder: vscode.Uri, + public toolchain: SwiftToolchain, public linuxMain: LinuxMain, public swiftPackage: SwiftPackage, public workspaceFolder: vscode.WorkspaceFolder, public workspaceContext: WorkspaceContext ) { this.packageWatcher = new PackageWatcher(this, workspaceContext); - this.packageWatcher.install(); this.backgroundCompilation = new BackgroundCompilation(this); this.taskQueue = new TaskQueue(this); } @@ -71,16 +72,19 @@ export class FolderContext implements vscode.Disposable { ): Promise { const statusItemText = `Loading Package (${FolderContext.uriName(folder)})`; workspaceContext.statusItem.start(statusItemText); + + const toolchain = await SwiftToolchain.create(folder); const { linuxMain, swiftPackage } = await workspaceContext.statusItem.showStatusWhileRunning(statusItemText, async () => { const linuxMain = await LinuxMain.create(folder); - const swiftPackage = await SwiftPackage.create(folder, workspaceContext.toolchain); + const swiftPackage = await SwiftPackage.create(folder, toolchain); return { linuxMain, swiftPackage }; }); workspaceContext.statusItem.end(statusItemText); const folderContext = new FolderContext( folder, + toolchain, linuxMain, swiftPackage, workspaceFolder, @@ -97,6 +101,10 @@ export class FolderContext implements vscode.Disposable { folderContext.name ); } + + // Start watching for changes to Package.swift, Package.resolved and .swift-version + await folderContext.packageWatcher.install(); + return folderContext; } @@ -117,9 +125,13 @@ export class FolderContext implements vscode.Disposable { return this.workspaceFolder.uri === this.folder; } + get swiftVersion() { + return this.toolchain.swiftVersion; + } + /** reload swift package for this folder */ async reload() { - await this.swiftPackage.reload(this.workspaceContext.toolchain); + await this.swiftPackage.reload(this.toolchain); } /** reload Package.resolved for this folder */ @@ -134,7 +146,7 @@ export class FolderContext implements vscode.Disposable { /** Load Swift Plugins and store in Package */ async loadSwiftPlugins(outputChannel: SwiftOutputChannel) { - await this.swiftPackage.loadSwiftPlugins(this.workspaceContext.toolchain, outputChannel); + await this.swiftPackage.loadSwiftPlugins(this.toolchain, outputChannel); } /** diff --git a/src/PackageWatcher.ts b/src/PackageWatcher.ts index 86b4840e2..1876d9904 100644 --- a/src/PackageWatcher.ts +++ b/src/PackageWatcher.ts @@ -12,10 +12,13 @@ // //===----------------------------------------------------------------------===// +import * as path from "path"; +import * as fs from "fs/promises"; import * as vscode from "vscode"; import { FolderContext } from "./FolderContext"; import { FolderOperation, WorkspaceContext } from "./WorkspaceContext"; import { BuildFlags } from "./toolchain/BuildFlags"; +import { Version } from "./utilities/version"; /** * Watches for changes to **Package.swift** and **Package.resolved**. @@ -28,6 +31,8 @@ export class PackageWatcher { private resolvedFileWatcher?: vscode.FileSystemWatcher; private workspaceStateFileWatcher?: vscode.FileSystemWatcher; private snippetWatcher?: vscode.FileSystemWatcher; + private swiftVersionFileWatcher?: vscode.FileSystemWatcher; + private currentVersion?: Version; constructor( private folderContext: FolderContext, @@ -38,11 +43,12 @@ export class PackageWatcher { * Creates and installs {@link vscode.FileSystemWatcher file system watchers} for * **Package.swift** and **Package.resolved**. */ - install() { + async install() { this.packageFileWatcher = this.createPackageFileWatcher(); this.resolvedFileWatcher = this.createResolvedFileWatcher(); this.workspaceStateFileWatcher = this.createWorkspaceStateFileWatcher(); this.snippetWatcher = this.createSnippetFileWatcher(); + this.swiftVersionFileWatcher = await this.createSwiftVersionFileWatcher(); } /** @@ -54,6 +60,7 @@ export class PackageWatcher { this.resolvedFileWatcher?.dispose(); this.workspaceStateFileWatcher?.dispose(); this.snippetWatcher?.dispose(); + this.swiftVersionFileWatcher?.dispose(); } private createPackageFileWatcher(): vscode.FileSystemWatcher { @@ -99,6 +106,43 @@ export class PackageWatcher { return watcher; } + private async createSwiftVersionFileWatcher(): Promise { + const watcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(this.folderContext.folder, ".swift-version") + ); + watcher.onDidCreate(async () => await this.handleSwiftVersionFileChange()); + watcher.onDidChange(async () => await this.handleSwiftVersionFileChange()); + watcher.onDidDelete(async () => await this.handleSwiftVersionFileChange()); + this.currentVersion = + (await this.readSwiftVersionFile()) ?? this.folderContext.toolchain.swiftVersion; + return watcher; + } + + async handleSwiftVersionFileChange() { + try { + const version = await this.readSwiftVersionFile(); + if (version && version.toString() !== this.currentVersion?.toString()) { + this.workspaceContext.fireEvent( + this.folderContext, + FolderOperation.swiftVersionUpdated + ); + } + this.currentVersion = version ?? this.folderContext.toolchain.swiftVersion; + } catch { + // do nothing + } + } + + private async readSwiftVersionFile() { + const versionFile = path.join(this.folderContext.folder.fsPath, ".swift-version"); + try { + const contents = await fs.readFile(versionFile); + return Version.fromString(contents.toString().trim()); + } catch { + return undefined; + } + } + /** * Handles a create or change event for **Package.swift**. * diff --git a/src/SwiftSnippets.ts b/src/SwiftSnippets.ts index 22898b94a..375190e96 100644 --- a/src/SwiftSnippets.ts +++ b/src/SwiftSnippets.ts @@ -26,9 +26,9 @@ import { TaskOperation } from "./tasks/TaskQueue"; */ export function setSnippetContextKey(ctx: WorkspaceContext) { if ( - ctx.swiftVersion.isLessThan({ major: 5, minor: 7, patch: 0 }) || !ctx.currentFolder || - !ctx.currentDocument + !ctx.currentDocument || + ctx.currentFolder.swiftVersion.isLessThan({ major: 5, minor: 7, patch: 0 }) ) { contextKeys.fileIsSnippet = false; return; @@ -97,7 +97,7 @@ export async function debugSnippetWithOptions( reveal: vscode.TaskRevealKind.Always, }, }, - ctx.toolchain + folderContext.toolchain ); const snippetDebugConfig = createSnippetConfiguration(snippetName, folderContext); try { diff --git a/src/TestExplorer/TestExplorer.ts b/src/TestExplorer/TestExplorer.ts index 94d6dbf8c..2ac95fc45 100644 --- a/src/TestExplorer/TestExplorer.ts +++ b/src/TestExplorer/TestExplorer.ts @@ -67,9 +67,9 @@ export class TestExplorer { this.onDidCreateTestRunEmitter ); - this.lspTestDiscovery = new LSPTestDiscovery( - folderContext.workspaceContext.languageClientManager - ); + const workspaceContext = folderContext.workspaceContext; + const languageClientManager = workspaceContext.languageClientManager.get(folderContext); + this.lspTestDiscovery = new LSPTestDiscovery(languageClientManager); // add end of task handler to be called whenever a build task has finished. If // it is the build task for this folder then update the tests @@ -182,10 +182,10 @@ export class TestExplorer { break; case FolderOperation.focus: if (folder) { - workspace.languageClientManager.documentSymbolWatcher = ( - document, - symbols - ) => TestExplorer.onDocumentSymbols(folder, document, symbols); + const languageClientManager = + workspace.languageClientManager.get(folder); + languageClientManager.documentSymbolWatcher = (document, symbols) => + TestExplorer.onDocumentSymbols(folder, document, symbols); } } } @@ -307,7 +307,7 @@ export class TestExplorer { } }); } - const toolchain = explorer.folderContext.workspaceContext.toolchain; + const toolchain = explorer.folderContext.toolchain; // get build options before build is run so we can be sure they aren't changed // mid-build const testBuildOptions = buildOptions(toolchain); diff --git a/src/TestExplorer/TestRunner.ts b/src/TestExplorer/TestRunner.ts index ef7953f46..8c11592e0 100644 --- a/src/TestExplorer/TestRunner.ts +++ b/src/TestExplorer/TestRunner.ts @@ -396,7 +396,7 @@ export class TestRunner { this.xcTestOutputParser = testKind === TestKind.parallel ? new ParallelXCTestOutputParser( - this.folderContext.workspaceContext.toolchain.hasMultiLineParallelTestOutput + this.folderContext.toolchain.hasMultiLineParallelTestOutput ) : new XCTestOutputParser(); this.swiftTestOutputParser = new SwiftTestingOutputParser( @@ -774,7 +774,7 @@ export class TestRunner { prefix: this.folderContext.name, presentationOptions: { reveal: vscode.TaskRevealKind.Never }, }, - this.folderContext.workspaceContext.toolchain, + this.folderContext.toolchain, { ...process.env, ...testBuildConfig.env }, { readOnlyTerminal: process.platform !== "win32" } ); @@ -859,7 +859,7 @@ export class TestRunner { const buffer = await asyncfs.readFile(filename, "utf8"); const xUnitParser = new TestXUnitParser( - this.folderContext.workspaceContext.toolchain.hasMultiLineParallelTestOutput + this.folderContext.toolchain.hasMultiLineParallelTestOutput ); const results = await xUnitParser.parse( buffer, diff --git a/src/WorkspaceContext.ts b/src/WorkspaceContext.ts index 0734e8d90..e4720acdd 100644 --- a/src/WorkspaceContext.ts +++ b/src/WorkspaceContext.ts @@ -19,7 +19,7 @@ import { StatusItem } from "./ui/StatusItem"; import { SwiftOutputChannel } from "./ui/SwiftOutputChannel"; import { swiftLibraryPathKey } from "./utilities/utilities"; import { isPathInsidePath } from "./utilities/filesystem"; -import { LanguageClientManager } from "./sourcekit-lsp/LanguageClientManager"; +import { LanguageClientToolchainCoordinator } from "./sourcekit-lsp/LanguageClientToolchainCoordinator"; import { TemporaryFolder } from "./utilities/tempFolder"; import { TaskManager } from "./tasks/TaskManager"; import { makeDebugConfigurations } from "./debugger/launch"; @@ -45,7 +45,7 @@ export class WorkspaceContext implements vscode.Disposable { public currentDocument: vscode.Uri | null; public statusItem: StatusItem; public buildStatus: SwiftBuildStatus; - public languageClientManager: LanguageClientManager; + public languageClientManager: LanguageClientToolchainCoordinator; public tasks: TaskManager; public diagnostics: DiagnosticsManager; public subscriptions: vscode.Disposable[]; @@ -69,11 +69,11 @@ export class WorkspaceContext implements vscode.Disposable { extensionContext: vscode.ExtensionContext, public tempFolder: TemporaryFolder, public outputChannel: SwiftOutputChannel, - public toolchain: SwiftToolchain + public globalToolchain: SwiftToolchain ) { this.statusItem = new StatusItem(); this.buildStatus = new SwiftBuildStatus(this.statusItem); - this.languageClientManager = new LanguageClientManager(this); + this.languageClientManager = new LanguageClientToolchainCoordinator(this); this.tasks = new TaskManager(this); this.diagnostics = new DiagnosticsManager(this); this.documentation = new DocumentationManager(extensionContext, this); @@ -202,8 +202,8 @@ export class WorkspaceContext implements vscode.Disposable { this.subscriptions.length = 0; } - get swiftVersion() { - return this.toolchain.swiftVersion; + get globalToolchainSwiftVersion() { + return this.globalToolchain.swiftVersion; } /** Get swift version and create WorkspaceContext */ @@ -248,19 +248,21 @@ export class WorkspaceContext implements vscode.Disposable { contextKeys.currentTargetType = undefined; } - // Set context keys that depend on features from SourceKit-LSP - this.languageClientManager.useLanguageClient(async client => { - const experimentalCaps = client.initializeResult?.capabilities.experimental; - if (!experimentalCaps) { - contextKeys.supportsReindexing = false; - contextKeys.supportsDocumentationLivePreview = false; - return; - } - contextKeys.supportsReindexing = - experimentalCaps[ReIndexProjectRequest.method] !== undefined; - contextKeys.supportsDocumentationLivePreview = - experimentalCaps[DocCDocumentationRequest.method] !== undefined; - }); + if (this.currentFolder) { + const languageClient = this.languageClientManager.get(this.currentFolder); + languageClient.useLanguageClient(async client => { + const experimentalCaps = client.initializeResult?.capabilities.experimental; + if (!experimentalCaps) { + contextKeys.supportsReindexing = false; + contextKeys.supportsDocumentationLivePreview = false; + return; + } + contextKeys.supportsReindexing = + experimentalCaps[ReIndexProjectRequest.method] !== undefined; + contextKeys.supportsDocumentationLivePreview = + experimentalCaps[DocCDocumentationRequest.method] !== undefined; + }); + } setSnippetContextKey(this); } @@ -645,6 +647,8 @@ export enum FolderOperation { packageViewUpdated = "packageViewUpdated", // Package plugins list has been updated pluginsUpdated = "pluginsUpdated", + // The folder's swift toolchain version has been updated + swiftVersionUpdated = "swiftVersionUpdated", } /** Workspace Folder Event */ diff --git a/src/commands.ts b/src/commands.ts index 9bd986ef6..a9755fe0e 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -135,7 +135,7 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { vscode.commands.registerCommand("swift.runScript", () => runSwiftScript(ctx)), vscode.commands.registerCommand("swift.openPackage", () => { if (ctx.currentFolder) { - return openPackage(ctx.toolchain.swiftVersion, ctx.currentFolder.folder); + return openPackage(ctx.currentFolder.swiftVersion, ctx.currentFolder.folder); } }), vscode.commands.registerCommand(Commands.RUN_SNIPPET, target => @@ -146,9 +146,13 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { ), vscode.commands.registerCommand(Commands.RUN_PLUGIN_TASK, () => runPluginTask()), vscode.commands.registerCommand(Commands.RUN_TASK, name => runTask(ctx, name)), - vscode.commands.registerCommand("swift.restartLSPServer", () => - ctx.languageClientManager.restart() - ), + vscode.commands.registerCommand("swift.restartLSPServer", async () => { + if (!ctx.currentFolder) { + return; + } + const languageClientManager = ctx.languageClientManager.get(ctx.currentFolder); + await languageClientManager.restart(); + }), vscode.commands.registerCommand("swift.reindexProject", () => reindexProject(ctx)), vscode.commands.registerCommand("swift.insertFunctionComment", () => insertFunctionComment(ctx) diff --git a/src/commands/build.ts b/src/commands/build.ts index b5f230063..b74d9d011 100644 --- a/src/commands/build.ts +++ b/src/commands/build.ts @@ -60,7 +60,7 @@ export async function folderCleanBuild(folderContext: FolderContext) { presentationOptions: { reveal: vscode.TaskRevealKind.Silent }, group: vscode.TaskGroup.Clean, }, - folderContext.workspaceContext.toolchain + folderContext.toolchain ); return await executeTaskWithUI(task, "Clean Build", folderContext); diff --git a/src/commands/captureDiagnostics.ts b/src/commands/captureDiagnostics.ts index 46cbe4176..b87026a0c 100644 --- a/src/commands/captureDiagnostics.ts +++ b/src/commands/captureDiagnostics.ts @@ -22,6 +22,7 @@ import { WorkspaceContext } from "../WorkspaceContext"; import { Version } from "../utilities/version"; import { execFileStreamOutput } from "../utilities/utilities"; import configuration from "../configuration"; +import { FolderContext } from "../FolderContext"; export async function captureDiagnostics( ctx: WorkspaceContext, @@ -42,21 +43,38 @@ export async function captureDiagnostics( await fs.mkdir(diagnosticsDir); await writeLogFile(diagnosticsDir, "extension-logs.txt", extensionLogs(ctx)); - await writeLogFile(diagnosticsDir, "settings.txt", settingsLogs(ctx)); - if (captureMode === "Full") { - await writeLogFile(diagnosticsDir, "source-code-diagnostics.txt", diagnosticLogs()); + for (const folder of ctx.folders) { + const baseName = path.basename(folder.folder.fsPath); + const guid = Math.random().toString(36).substring(2, 10); + await writeLogFile( + diagnosticsDir, + `${baseName}-${guid}-settings.txt`, + settingsLogs(folder) + ); + + if (captureMode === "Full") { + await writeLogFile( + diagnosticsDir, + `${baseName}-${guid}-source-code-diagnostics.txt`, + diagnosticLogs() + ); - // The `sourcekit-lsp diagnose` command is only available in 6.0 and higher. - if (ctx.swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0))) { - await sourcekitDiagnose(ctx, diagnosticsDir); - } else { - await writeLogFile(diagnosticsDir, "sourcekit-lsp.txt", sourceKitLogs(ctx)); + // The `sourcekit-lsp diagnose` command is only available in 6.0 and higher. + if (folder.toolchain.swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0))) { + await sourcekitDiagnose(folder, diagnosticsDir); + } else { + await writeLogFile( + diagnosticsDir, + `${baseName}-${guid}-sourcekit-lsp.txt`, + sourceKitLogs(folder) + ); + } } - } - ctx.outputChannel.log(`Saved diagnostics to ${diagnosticsDir}`); - await showCapturedDiagnosticsResults(diagnosticsDir); + ctx.outputChannel.log(`Saved diagnostics to ${diagnosticsDir}`); + await showCapturedDiagnosticsResults(diagnosticsDir); + } } catch (error) { vscode.window.showErrorMessage(`Unable to capture diagnostic logs: ${error}`); } @@ -83,7 +101,7 @@ async function captureDiagnosticsMode( allowMinimalCapture: boolean ): Promise<"Minimal" | "Full" | undefined> { if ( - ctx.swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0)) || + ctx.globalToolchainSwiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0)) || vscode.workspace.getConfiguration("sourcekit-lsp").get("trace.server", "off") !== "off" ) { @@ -155,7 +173,7 @@ function extensionLogs(ctx: WorkspaceContext): string { return ctx.outputChannel.logs.join("\n"); } -function settingsLogs(ctx: WorkspaceContext): string { +function settingsLogs(ctx: FolderContext): string { const settings = JSON.stringify(vscode.workspace.getConfiguration("swift"), null, 2); return `${ctx.toolchain.diagnostics}\nSettings:\n${settings}`; } @@ -173,11 +191,13 @@ function diagnosticLogs(): string { .join("\n"); } -function sourceKitLogs(ctx: WorkspaceContext) { - return (ctx.languageClientManager.languageClientOutputChannel?.logs ?? []).join("\n"); +function sourceKitLogs(folder: FolderContext) { + const languageClient = folder.workspaceContext.languageClientManager.get(folder); + const logs = languageClient.languageClientOutputChannel?.logs ?? []; + return logs.join("\n"); } -async function sourcekitDiagnose(ctx: WorkspaceContext, dir: string) { +async function sourcekitDiagnose(ctx: FolderContext, dir: string) { const sourcekitDiagnosticDir = path.join(dir, "sourcekit-lsp"); await fs.mkdir(sourcekitDiagnosticDir); @@ -212,7 +232,7 @@ async function sourcekitDiagnose(ctx: WorkspaceContext, dir: string) { env: { ...process.env, ...configuration.swiftEnvironmentVariables }, maxBuffer: 16 * 1024 * 1024, }, - ctx.currentFolder ?? undefined + ctx ?? undefined ); } ); diff --git a/src/commands/dependencies/edit.ts b/src/commands/dependencies/edit.ts index cc3ad0bbc..26b463bec 100644 --- a/src/commands/dependencies/edit.ts +++ b/src/commands/dependencies/edit.ts @@ -36,7 +36,7 @@ export async function editDependency(identifier: string, ctx: WorkspaceContext) cwd: currentFolder.folder, prefix: currentFolder.name, }, - ctx.toolchain + currentFolder.toolchain ); const success = await executeTaskWithUI( diff --git a/src/commands/dependencies/resolve.ts b/src/commands/dependencies/resolve.ts index 542c376b9..befae55a0 100644 --- a/src/commands/dependencies/resolve.ts +++ b/src/commands/dependencies/resolve.ts @@ -47,7 +47,7 @@ export async function resolveFolderDependencies( prefix: folderContext.name, presentationOptions: { reveal: vscode.TaskRevealKind.Silent }, }, - folderContext.workspaceContext.toolchain + folderContext.toolchain ); const success = await executeTaskWithUI( diff --git a/src/commands/dependencies/unedit.ts b/src/commands/dependencies/unedit.ts index 720709edd..018bfe1b5 100644 --- a/src/commands/dependencies/unedit.ts +++ b/src/commands/dependencies/unedit.ts @@ -44,7 +44,7 @@ async function uneditFolderDependency( ) { try { const uneditOperation = new SwiftExecOperation( - ctx.toolchain.buildFlags.withAdditionalFlags([ + folder.toolchain.buildFlags.withAdditionalFlags([ "package", "unedit", ...args, diff --git a/src/commands/dependencies/update.ts b/src/commands/dependencies/update.ts index b32423c7a..c957a4dec 100644 --- a/src/commands/dependencies/update.ts +++ b/src/commands/dependencies/update.ts @@ -44,7 +44,7 @@ export async function updateFolderDependencies(folderContext: FolderContext) { prefix: folderContext.name, presentationOptions: { reveal: vscode.TaskRevealKind.Silent }, }, - folderContext.workspaceContext.toolchain + folderContext.toolchain ); const result = await executeTaskWithUI(task, "Updating Dependencies", folderContext); diff --git a/src/commands/dependencies/useLocal.ts b/src/commands/dependencies/useLocal.ts index c75a3c6c2..ba57e2c43 100644 --- a/src/commands/dependencies/useLocal.ts +++ b/src/commands/dependencies/useLocal.ts @@ -50,7 +50,7 @@ export async function useLocalDependency( folder = folders[0]; } const task = createSwiftTask( - ctx.toolchain.buildFlags.withAdditionalFlags([ + currentFolder.toolchain.buildFlags.withAdditionalFlags([ "package", "edit", "--path", @@ -63,7 +63,7 @@ export async function useLocalDependency( cwd: currentFolder.folder, prefix: currentFolder.name, }, - ctx.toolchain + currentFolder.toolchain ); const success = await executeTaskWithUI( diff --git a/src/commands/reindexProject.ts b/src/commands/reindexProject.ts index 95242c7fd..27e56e854 100644 --- a/src/commands/reindexProject.ts +++ b/src/commands/reindexProject.ts @@ -19,8 +19,17 @@ import { ReIndexProjectRequest } from "../sourcekit-lsp/extensions"; /** * Request that the SourceKit-LSP server reindexes the workspace. */ -export function reindexProject(workspaceContext: WorkspaceContext): Promise { - return workspaceContext.languageClientManager.useLanguageClient(async (client, token) => { +export async function reindexProject( + workspaceContext: WorkspaceContext +): Promise { + if (!workspaceContext.currentFolder) { + return; + } + + const languageClientManager = workspaceContext.languageClientManager.get( + workspaceContext.currentFolder + ); + return languageClientManager.useLanguageClient(async (client, token) => { try { await client.sendRequest(ReIndexProjectRequest.type, token); const result = await vscode.window.showWarningMessage( diff --git a/src/commands/resetPackage.ts b/src/commands/resetPackage.ts index 84c0c9141..988159044 100644 --- a/src/commands/resetPackage.ts +++ b/src/commands/resetPackage.ts @@ -35,10 +35,7 @@ export async function resetPackage(ctx: WorkspaceContext) { */ export async function folderResetPackage(folderContext: FolderContext) { const task = createSwiftTask( - folderContext.workspaceContext.toolchain.buildFlags.withAdditionalFlags([ - "package", - "reset", - ]), + folderContext.toolchain.buildFlags.withAdditionalFlags(["package", "reset"]), "Reset Package Dependencies", { cwd: folderContext.folder, @@ -47,7 +44,7 @@ export async function folderResetPackage(folderContext: FolderContext) { presentationOptions: { reveal: vscode.TaskRevealKind.Silent }, group: vscode.TaskGroup.Clean, }, - folderContext.workspaceContext.toolchain + folderContext.toolchain ); return await executeTaskWithUI(task, "Reset Package", folderContext).then( @@ -64,7 +61,7 @@ export async function folderResetPackage(folderContext: FolderContext) { prefix: folderContext.name, presentationOptions: { reveal: vscode.TaskRevealKind.Silent }, }, - folderContext.workspaceContext.toolchain + folderContext.toolchain ); const result = await executeTaskWithUI( diff --git a/src/commands/runSwiftScript.ts b/src/commands/runSwiftScript.ts index 713534160..e7333e5b3 100644 --- a/src/commands/runSwiftScript.ts +++ b/src/commands/runSwiftScript.ts @@ -29,11 +29,15 @@ export async function runSwiftScript(ctx: WorkspaceContext) { return; } + if (!ctx.currentFolder) { + return; + } + // Swift scripts require new swift driver to work on Windows. Swift driver is available // from v5.7 of Windows Swift if ( process.platform === "win32" && - ctx.toolchain.swiftVersion.isLessThan(new Version(5, 7, 0)) + ctx.currentFolder.swiftVersion.isLessThan(new Version(5, 7, 0)) ) { vscode.window.showErrorMessage( "Run Swift Script is unavailable with the legacy driver on Windows." @@ -84,7 +88,7 @@ export async function runSwiftScript(ctx: WorkspaceContext) { cwd: vscode.Uri.file(path.dirname(filename)), presentationOptions: { reveal: vscode.TaskRevealKind.Always, clear: true }, }, - ctx.toolchain + ctx.currentFolder.toolchain ); await ctx.tasks.executeTaskAndWait(runTask); diff --git a/src/configuration.ts b/src/configuration.ts index a3aa1596d..4006b1c9f 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -520,7 +520,7 @@ export function handleConfigurationChangeEvent( // on toolchain config change, reload window if ( event.affectsConfiguration("swift.path") && - configuration.path !== ctx.toolchain?.swiftFolderPath + configuration.path !== ctx.currentFolder?.toolchain.swiftFolderPath ) { showReloadExtensionNotification( "Changing the Swift path requires Visual Studio Code be reloaded." diff --git a/src/coverage/LcovResults.ts b/src/coverage/LcovResults.ts index 8bf1d6da0..3262648a5 100644 --- a/src/coverage/LcovResults.ts +++ b/src/coverage/LcovResults.ts @@ -96,7 +96,7 @@ export class TestCoverage { */ private async mergeProfdata(profDataFiles: string[]) { const filename = this.lcovTmpFiles.file("merged", "profdata"); - const toolchain = this.folderContext.workspaceContext.toolchain; + const toolchain = this.folderContext.toolchain; const llvmProfdata = toolchain.getToolchainExecutable("llvm-profdata"); await execFileStreamOutput( llvmProfdata, @@ -174,7 +174,7 @@ export class TestCoverage { }); await execFileStreamOutput( - this.folderContext.workspaceContext.toolchain.getToolchainExecutable("llvm-cov"), + this.folderContext.toolchain.getToolchainExecutable("llvm-cov"), [ "export", "--format", diff --git a/src/debugger/buildConfig.ts b/src/debugger/buildConfig.ts index bb37fe208..f73890452 100644 --- a/src/debugger/buildConfig.ts +++ b/src/debugger/buildConfig.ts @@ -44,7 +44,7 @@ export class BuildConfigurationFactory { ) {} private async build(): Promise { - let additionalArgs = buildOptions(this.ctx.workspaceContext.toolchain); + let additionalArgs = buildOptions(this.ctx.toolchain); if ((await this.ctx.swiftPackage.getTargets(TargetType.test)).length > 0) { additionalArgs.push(...this.testDiscoveryFlag(this.ctx)); } @@ -54,7 +54,7 @@ export class BuildConfigurationFactory { } // don't build tests for iOS etc as they don't compile - if (this.ctx.workspaceContext.toolchain.buildFlags.getDarwinTarget() === undefined) { + if (this.ctx.toolchain.buildFlags.getDarwinTarget() === undefined) { additionalArgs = ["--build-tests", ...additionalArgs]; if (this.isRelease) { additionalArgs = [...additionalArgs, "-Xswiftc", "-enable-testing"]; @@ -78,13 +78,13 @@ export class BuildConfigurationFactory { /** flag for enabling test discovery */ private testDiscoveryFlag(ctx: FolderContext): string[] { // Test discovery is only available in SwiftPM 5.1 and later. - if (ctx.workspaceContext.swiftVersion.isLessThan(new Version(5, 1, 0))) { + if (ctx.swiftVersion.isLessThan(new Version(5, 1, 0))) { return []; } // Test discovery is always enabled on Darwin. if (process.platform !== "darwin") { const hasLinuxMain = ctx.linuxMain.exists; - const testDiscoveryByDefault = ctx.workspaceContext.swiftVersion.isGreaterThanOrEqual( + const testDiscoveryByDefault = ctx.swiftVersion.isGreaterThanOrEqual( new Version(5, 4, 0) ); if (hasLinuxMain || !testDiscoveryByDefault) { @@ -275,13 +275,13 @@ export class TestingConfigurationFactory { }; // On Windows, add XCTest.dll/Testing.dll to the Path // and run the .xctest executable from the .build directory. - const runtimePath = this.ctx.workspaceContext.toolchain.runtimePath; - const xcTestPath = this.ctx.workspaceContext.toolchain.xcTestPath; + const runtimePath = this.ctx.toolchain.runtimePath; + const xcTestPath = this.ctx.toolchain.xcTestPath; if (xcTestPath && xcTestPath !== runtimePath) { testEnv.Path = `${xcTestPath};${testEnv.Path ?? process.env.Path}`; } - const swiftTestingPath = this.ctx.workspaceContext.toolchain.swiftTestingPath; + const swiftTestingPath = this.ctx.toolchain.swiftTestingPath; if (swiftTestingPath && swiftTestingPath !== runtimePath) { testEnv.Path = `${swiftTestingPath};${testEnv.Path ?? process.env.Path}`; } @@ -309,7 +309,7 @@ export class TestingConfigurationFactory { env: { ...swiftRuntimeEnv( process.env, - this.ctx.workspaceContext.toolchain.runtimePath ?? configuration.runtimePath + this.ctx.toolchain.runtimePath ?? configuration.runtimePath ), ...configuration.folder(this.ctx.workspaceFolder).testEnvironmentVariables, }, @@ -328,7 +328,7 @@ export class TestingConfigurationFactory { case TestKind.debug: // In the debug case we need to build the testing executable and then // launch it with LLDB instead of going through `swift test`. - const toolchain = this.ctx.workspaceContext.toolchain; + const toolchain = this.ctx.toolchain; const libraryPath = toolchain.swiftTestingLibraryPath(); const frameworkPath = toolchain.swiftTestingFrameworkPath(); const swiftPMTestingHelperPath = toolchain.swiftPMTestingHelperPath; @@ -409,7 +409,7 @@ export class TestingConfigurationFactory { switch (this.testKind) { case TestKind.debugRelease: case TestKind.debug: - const xcTestPath = this.ctx.workspaceContext.toolchain.xcTestPath; + const xcTestPath = this.ctx.toolchain.xcTestPath; // On macOS, find the path to xctest // and point it at the .xctest bundle from the configured build directory. if (xcTestPath === undefined) { @@ -428,7 +428,7 @@ export class TestingConfigurationFactory { }, }; default: - const swiftVersion = this.ctx.workspaceContext.toolchain.swiftVersion; + const swiftVersion = this.ctx.toolchain.swiftVersion; if ( swiftVersion.isLessThan(new Version(5, 7, 0)) && swiftVersion.isGreaterThanOrEqual(new Version(5, 6, 0)) && @@ -497,7 +497,7 @@ export class TestingConfigurationFactory { const { folder, nameSuffix } = getFolderAndNameSuffix(this.ctx, true); // On macOS, find the path to xctest // and point it at the .xctest bundle from the configured build directory. - const xctestPath = this.ctx.workspaceContext.toolchain.xcTestPath; + const xctestPath = this.ctx.toolchain.xcTestPath; if (xctestPath === undefined) { return null; } @@ -512,7 +512,7 @@ export class TestingConfigurationFactory { default: return null; } - const sanitizer = this.ctx.workspaceContext.toolchain.sanitizer(configuration.sanitizer); + const sanitizer = this.ctx.toolchain.sanitizer(configuration.sanitizer); const envCommands = Object.entries({ ...swiftRuntimeEnv(), ...configuration.folder(this.ctx.workspaceFolder).testEnvironmentVariables, @@ -540,7 +540,7 @@ export class TestingConfigurationFactory { } const swiftTestingArgs = [ - ...this.ctx.workspaceContext.toolchain.buildFlags.withAdditionalFlags(args), + ...this.ctx.toolchain.buildFlags.withAdditionalFlags(args), "--enable-swift-testing", "--event-stream-version", "0", @@ -576,10 +576,7 @@ export class TestingConfigurationFactory { } private addBuildOptionsToArgs(args: string[]): string[] { - let result = [ - ...args, - ...buildOptions(this.ctx.workspaceContext.toolchain, isDebugging(this.testKind)), - ]; + let result = [...args, ...buildOptions(this.ctx.toolchain, isDebugging(this.testKind))]; if (isRelease(this.testKind)) { result = [...result, "-c", "release", "-Xswiftc", "-enable-testing"]; } @@ -608,13 +605,11 @@ export class TestingConfigurationFactory { } private swiftVersionGreaterOrEqual(major: number, minor: number, patch: number): boolean { - return this.ctx.workspaceContext.swiftVersion.isGreaterThanOrEqual( - new Version(major, minor, patch) - ); + return this.ctx.swiftVersion.isGreaterThanOrEqual(new Version(major, minor, patch)); } private get swiftProgramPath(): string { - return this.ctx.workspaceContext.toolchain.getToolchainExecutable("swift"); + return this.ctx.toolchain.getToolchainExecutable("swift"); } private get buildDirectory(): string { @@ -624,7 +619,7 @@ export class TestingConfigurationFactory { private get artifactFolderForTestKind(): string { const mode = isRelease(this.testKind) ? "release" : "debug"; - const triple = this.ctx.workspaceContext.toolchain.unversionedTriple; + const triple = this.ctx.toolchain.unversionedTriple; return triple ? path.join(triple, mode) : mode; } @@ -678,8 +673,7 @@ export class TestingConfigurationFactory { } private get sanitizerRuntimeEnvironment() { - return this.ctx.workspaceContext.toolchain.sanitizer(configuration.sanitizer) - ?.runtimeEnvironment; + return this.ctx.toolchain.sanitizer(configuration.sanitizer)?.runtimeEnvironment; } private get testEnv() { diff --git a/src/debugger/debugAdapterFactory.ts b/src/debugger/debugAdapterFactory.ts index 6d15c58e8..106df2318 100644 --- a/src/debugger/debugAdapterFactory.ts +++ b/src/debugger/debugAdapterFactory.ts @@ -48,7 +48,7 @@ export function registerDebugger(workspaceContext: WorkspaceContext): vscode.Dis function register() { subscriptions.push(registerLoggingDebugAdapterTracker()); subscriptions.push( - registerLLDBDebugAdapter(workspaceContext.toolchain, workspaceContext.outputChannel) + registerLLDBDebugAdapter(workspaceContext, workspaceContext.outputChannel) ); } @@ -70,12 +70,12 @@ export function registerDebugger(workspaceContext: WorkspaceContext): vscode.Dis * @returns A disposable to be disposed when the extension is deactivated */ function registerLLDBDebugAdapter( - toolchain: SwiftToolchain, + workspaceContext: WorkspaceContext, outputChannel: SwiftOutputChannel ): vscode.Disposable { return vscode.debug.registerDebugConfigurationProvider( SWIFT_LAUNCH_CONFIG_TYPE, - new LLDBDebugConfigurationProvider(process.platform, toolchain, outputChannel) + new LLDBDebugConfigurationProvider(process.platform, workspaceContext, outputChannel) ); } @@ -91,7 +91,7 @@ function registerLLDBDebugAdapter( export class LLDBDebugConfigurationProvider implements vscode.DebugConfigurationProvider { constructor( private platform: NodeJS.Platform, - private toolchain: SwiftToolchain, + private workspaceContext: WorkspaceContext, private outputChannel: SwiftOutputChannel ) {} @@ -99,6 +99,11 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration _folder: vscode.WorkspaceFolder | undefined, launchConfig: vscode.DebugConfiguration ): Promise { + const folder = this.workspaceContext.folders.find( + folder => folder.workspaceFolder.uri.fsPath === _folder?.uri.fsPath + ); + const toolchain = folder?.toolchain ?? this.workspaceContext.globalToolchain; + // Fix the program path on Windows to include the ".exe" extension if ( this.platform === "win32" && @@ -133,7 +138,7 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration } // Delegate to the appropriate debug adapter extension - launchConfig.type = DebugAdapter.getLaunchConfigType(this.toolchain.swiftVersion); + launchConfig.type = DebugAdapter.getLaunchConfigType(toolchain.swiftVersion); if (launchConfig.type === LaunchConfigType.CODE_LLDB) { launchConfig.sourceLanguages = ["swift"]; if (!vscode.extensions.getExtension("vadimcn.vscode-lldb")) { @@ -141,14 +146,14 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration return undefined; } } - if (!(await this.promptForCodeLldbSettings())) { + if (!(await this.promptForCodeLldbSettings(toolchain))) { return undefined; } } else if (launchConfig.type === LaunchConfigType.LLDB_DAP) { if (launchConfig.env) { launchConfig.env = this.convertEnvironmentVariables(launchConfig.env); } - const lldbDapPath = await DebugAdapter.getLLDBDebugAdapterPath(this.toolchain); + const lldbDapPath = await DebugAdapter.getLLDBDebugAdapterPath(toolchain); // Verify that the debug adapter exists or bail otherwise if (!(await fileExists(lldbDapPath))) { vscode.window.showErrorMessage( @@ -191,8 +196,8 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration } } - private async promptForCodeLldbSettings(): Promise { - const libLldbPathResult = await getLLDBLibPath(this.toolchain); + private async promptForCodeLldbSettings(toolchain: SwiftToolchain): Promise { + const libLldbPathResult = await getLLDBLibPath(toolchain); if (!libLldbPathResult.success) { const errorMessage = `Error: ${getErrorDescription(libLldbPathResult.failure)}`; vscode.window.showWarningMessage( diff --git a/src/documentation/DocumentationPreviewEditor.ts b/src/documentation/DocumentationPreviewEditor.ts index 2b2a8fb2f..4ddd8cabc 100644 --- a/src/documentation/DocumentationPreviewEditor.ts +++ b/src/documentation/DocumentationPreviewEditor.ts @@ -203,8 +203,17 @@ export class DocumentationPreviewEditor implements vscode.Disposable { return; } + const folderContext = this.context.folders.find(folderContext => + document.uri.fsPath.startsWith(folderContext.folder.fsPath) + ); + + if (!folderContext) { + return; + } + + const languageClientManager = this.context.languageClientManager.get(folderContext); try { - const response = await this.context.languageClientManager.useLanguageClient( + const response = await languageClientManager.useLanguageClient( async (client): Promise => { return await client.sendRequest(DocCDocumentationRequest.type, { textDocument: { diff --git a/src/extension.ts b/src/extension.ts index ac7391f6b..ab74e8563 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -180,7 +180,7 @@ function handleFolderEvent( await resolveFolderDependencies(folder, true); } - if (workspace.toolchain.swiftVersion.isGreaterThanOrEqual(new Version(5, 6, 0))) { + if (folder.toolchain.swiftVersion.isGreaterThanOrEqual(new Version(5, 6, 0))) { workspace.statusItem.showStatusWhileRunning( `Loading Swift Plugins (${FolderContext.uriName(folder.workspaceFolder.uri)})`, async () => { diff --git a/src/sourcekit-lsp/LanguageClientConfiguration.ts b/src/sourcekit-lsp/LanguageClientConfiguration.ts new file mode 100644 index 000000000..476e340d6 --- /dev/null +++ b/src/sourcekit-lsp/LanguageClientConfiguration.ts @@ -0,0 +1,260 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; +import * as path from "path"; +import { + DocumentSelector, + LanguageClientOptions, + RevealOutputChannelOn, + vsdiag, +} from "vscode-languageclient"; +import configuration from "../configuration"; +import { Version } from "../utilities/version"; +import { WorkspaceContext } from "../WorkspaceContext"; +import { DiagnosticsManager } from "../DiagnosticsManager"; +import { SwiftOutputChannel } from "../ui/SwiftOutputChannel"; +import { promptForDiagnostics } from "../commands/captureDiagnostics"; +import { uriConverters } from "./uriConverters"; +import { LSPActiveDocumentManager } from "./didChangeActiveDocument"; +import { SourceKitLSPErrorHandler } from "./LanguageClientManager"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +function initializationOptions(swiftVersion: Version): any { + let options: any = { + "workspace/peekDocuments": true, // workaround for client capability to handle `PeekDocumentsRequest` + "workspace/getReferenceDocument": true, // the client can handle URIs with scheme `sourcekit-lsp:` + "textDocument/codeLens": { + supportedCommands: { + "swift.run": "swift.run", + "swift.debug": "swift.debug", + }, + }, + }; + + // Swift 6.0.0 and later supports background indexing. + // In 6.0.0 it is experimental so only "true" enables it. + // In 6.1.0 it is no longer experimental, and so "auto" or "true" enables it. + if ( + swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0)) && + (configuration.backgroundIndexing === "on" || + (configuration.backgroundIndexing === "auto" && + swiftVersion.isGreaterThanOrEqual(new Version(6, 1, 0)))) + ) { + options = { + ...options, + backgroundIndexing: true, + backgroundPreparationMode: "enabled", + }; + } + + if (swiftVersion.isGreaterThanOrEqual(new Version(6, 1, 0))) { + options = { + ...options, + "window/didChangeActiveDocument": true, // the client can send `window/didChangeActiveDocument` notifications + }; + } + + if (configuration.swiftSDK !== "") { + options = { + ...options, + swiftPM: { swiftSDK: configuration.swiftSDK }, + }; + } + + return options; +} +/* eslint-enable @typescript-eslint/no-explicit-any */ + +type SourceKitDocumentSelector = { + scheme: string; + language: string; + pattern?: string; +}[]; + +export class LanguagerClientDocumentSelectors { + static appleLangDocumentSelector: SourceKitDocumentSelector = [ + { scheme: "sourcekit-lsp", language: "swift" }, + { scheme: "file", language: "swift" }, + { scheme: "untitled", language: "swift" }, + { scheme: "file", language: "objective-c" }, + { scheme: "untitled", language: "objective-c" }, + { scheme: "file", language: "objective-cpp" }, + { scheme: "untitled", language: "objective-cpp" }, + ]; + + static cFamilyDocumentSelector: SourceKitDocumentSelector = [ + { scheme: "file", language: "c" }, + { scheme: "untitled", language: "c" }, + { scheme: "file", language: "cpp" }, + { scheme: "untitled", language: "cpp" }, + ]; + + // document selector for swift-docc documentation + static documentationDocumentSelector: SourceKitDocumentSelector = [ + { scheme: "file", language: "markdown" }, + { scheme: "untitled", language: "markdown" }, + { scheme: "file", language: "tutorial" }, + { scheme: "untitiled", language: "tutorial" }, + ]; + + static miscelaneousDocumentSelector: SourceKitDocumentSelector = [ + { scheme: "file", language: "plaintext", pattern: "**/.swift-version" }, + ]; + + static allHandledDocumentTypes(): DocumentSelector { + let documentSelector: SourceKitDocumentSelector; + switch (configuration.lsp.supportCFamily) { + case "enable": + documentSelector = [ + ...LanguagerClientDocumentSelectors.appleLangDocumentSelector, + ...LanguagerClientDocumentSelectors.cFamilyDocumentSelector, + ]; + break; + + case "disable": + documentSelector = LanguagerClientDocumentSelectors.appleLangDocumentSelector; + break; + + case "cpptools-inactive": { + const cppToolsActive = + vscode.extensions.getExtension("ms-vscode.cpptools")?.isActive; + documentSelector = + cppToolsActive === true + ? LanguagerClientDocumentSelectors.appleLangDocumentSelector + : [ + ...LanguagerClientDocumentSelectors.appleLangDocumentSelector, + ...LanguagerClientDocumentSelectors.cFamilyDocumentSelector, + ]; + } + } + documentSelector = documentSelector.filter(doc => { + return configuration.lsp.supportedLanguages.includes(doc.language); + }); + documentSelector.push(...LanguagerClientDocumentSelectors.documentationDocumentSelector); + documentSelector.push(...LanguagerClientDocumentSelectors.miscelaneousDocumentSelector); + return documentSelector; + } +} + +export function lspClientOptions( + swiftVersion: Version, + workspaceContext: WorkspaceContext, + workspaceFolder: vscode.WorkspaceFolder | undefined, + activeDocumentManager: LSPActiveDocumentManager, + errorHandler: SourceKitLSPErrorHandler, + documentSymbolWatcher?: ( + document: vscode.TextDocument, + symbols: vscode.DocumentSymbol[] + ) => void +): LanguageClientOptions { + return { + documentSelector: LanguagerClientDocumentSelectors.allHandledDocumentTypes(), + revealOutputChannelOn: RevealOutputChannelOn.Never, + workspaceFolder: workspaceFolder, + outputChannel: new SwiftOutputChannel("SourceKit Language Server"), + middleware: { + didOpen: activeDocumentManager.didOpen.bind(activeDocumentManager), + didClose: activeDocumentManager.didClose.bind(activeDocumentManager), + provideCodeLenses: async (document, token, next) => { + const result = await next(document, token); + return result?.map(codelens => { + switch (codelens.command?.command) { + case "swift.run": + codelens.command.title = `$(play) ${codelens.command.title}`; + break; + case "swift.debug": + codelens.command.title = `$(debug) ${codelens.command.title}`; + break; + } + return codelens; + }); + }, + provideDocumentSymbols: async (document, token, next) => { + const result = await next(document, token); + const documentSymbols = result as vscode.DocumentSymbol[]; + if (documentSymbolWatcher && documentSymbols) { + documentSymbolWatcher(document, documentSymbols); + } + return result; + }, + provideDefinition: async (document, position, token, next) => { + const result = await next(document, position, token); + const definitions = result as vscode.Location[]; + if ( + definitions && + path.extname(definitions[0].uri.path) === ".swiftinterface" && + definitions[0].uri.scheme === "file" + ) { + const uri = definitions[0].uri.with({ scheme: "readonly" }); + return new vscode.Location(uri, definitions[0].range); + } + return result; + }, + // temporarily remove text edit from Inlay hints while SourceKit-LSP + // returns invalid replacement text + provideInlayHints: async (document, position, token, next) => { + const result = await next(document, position, token); + // remove textEdits for swift version earlier than 5.10 as it sometimes + // generated invalid textEdits + if (swiftVersion.isLessThan(new Version(5, 10, 0))) { + result?.forEach(r => (r.textEdits = undefined)); + } + return result; + }, + provideDiagnostics: async (uri, previousResultId, token, next) => { + const result = await next(uri, previousResultId, token); + if (result?.kind === vsdiag.DocumentDiagnosticReportKind.unChanged) { + return undefined; + } + const document = uri as vscode.TextDocument; + workspaceContext.diagnostics.handleDiagnostics( + document.uri ?? uri, + DiagnosticsManager.isSourcekit, + result?.items ?? [] + ); + return undefined; + }, + handleDiagnostics: (uri, diagnostics) => { + workspaceContext.diagnostics.handleDiagnostics( + uri, + DiagnosticsManager.isSourcekit, + diagnostics + ); + }, + handleWorkDoneProgress: (() => { + let lastPrompted = new Date(0).getTime(); + return async (token, params, next) => { + const result = await next(token, params); + const now = new Date().getTime(); + const oneHour = 60 * 60 * 1000; + if ( + now - lastPrompted > oneHour && + token.toString().startsWith("sourcekitd-crashed") + ) { + // Only prompt once an hour in case sourcekit is in a crash loop + lastPrompted = now; + promptForDiagnostics(workspaceContext); + } + return result; + }; + })(), + }, + uriConverters, + errorHandler, + // Avoid attempting to reinitialize multiple times. If we fail to initialize + // we aren't doing anything different the second time and so will fail again. + initializationFailedHandler: () => false, + initializationOptions: initializationOptions(swiftVersion), + }; +} diff --git a/src/sourcekit-lsp/LanguageClientManager.ts b/src/sourcekit-lsp/LanguageClientManager.ts index cf2d69c40..7acd8373b 100644 --- a/src/sourcekit-lsp/LanguageClientManager.ts +++ b/src/sourcekit-lsp/LanguageClientManager.ts @@ -13,42 +13,34 @@ //===----------------------------------------------------------------------===// import * as vscode from "vscode"; -import * as path from "path"; import { CloseAction, CloseHandlerResult, DidChangeWorkspaceFoldersNotification, - DocumentSelector, ErrorAction, ErrorHandler, ErrorHandlerResult, LanguageClientOptions, Message, MessageType, - RevealOutputChannelOn, State, - vsdiag, } from "vscode-languageclient"; import configuration from "../configuration"; import { swiftRuntimeEnv } from "../utilities/utilities"; -import { isPathInsidePath } from "../utilities/filesystem"; import { Version } from "../utilities/version"; -import { FolderOperation, WorkspaceContext } from "../WorkspaceContext"; import { activateLegacyInlayHints } from "./inlayHints"; import { activatePeekDocuments } from "./peekDocuments"; import { FolderContext } from "../FolderContext"; import { Executable, LanguageClient, ServerOptions } from "vscode-languageclient/node"; import { ArgumentFilter, BuildFlags } from "../toolchain/BuildFlags"; -import { DiagnosticsManager } from "../DiagnosticsManager"; import { LSPLogger, LSPOutputChannel } from "./LSPOutputChannel"; import { SwiftOutputChannel } from "../ui/SwiftOutputChannel"; -import { promptForDiagnostics } from "../commands/captureDiagnostics"; import { activateGetReferenceDocument } from "./getReferenceDocument"; -import { uriConverters } from "./uriConverters"; import { LanguageClientFactory } from "./LanguageClientFactory"; import { SourceKitLogMessageNotification, SourceKitLogMessageParams } from "./extensions"; import { LSPActiveDocumentManager } from "./didChangeActiveDocument"; import { DidChangeActiveDocumentNotification } from "./extensions/DidChangeActiveDocumentRequest"; +import { lspClientOptions } from "./LanguageClientConfiguration"; /** * Manages the creation and destruction of Language clients as we move between @@ -58,63 +50,6 @@ export class LanguageClientManager implements vscode.Disposable { // known log names static indexingLogName = "SourceKit-LSP: Indexing"; - // document selector used by language client - static appleLangDocumentSelector: SourceKitDocumentSelector = [ - { scheme: "sourcekit-lsp", language: "swift" }, - { scheme: "file", language: "swift" }, - { scheme: "untitled", language: "swift" }, - { scheme: "file", language: "objective-c" }, - { scheme: "untitled", language: "objective-c" }, - { scheme: "file", language: "objective-cpp" }, - { scheme: "untitled", language: "objective-cpp" }, - ]; - // document selector used by language client - static cFamilyDocumentSelector: SourceKitDocumentSelector = [ - { scheme: "file", language: "c" }, - { scheme: "untitled", language: "c" }, - { scheme: "file", language: "cpp" }, - { scheme: "untitled", language: "cpp" }, - ]; - // document selector for swift-docc documentation - static documentationDocumentSelector: SourceKitDocumentSelector = [ - { scheme: "file", language: "markdown" }, - { scheme: "untitled", language: "markdown" }, - { scheme: "file", language: "tutorial" }, - { scheme: "untitiled", language: "tutorial" }, - ]; - static get documentSelector(): DocumentSelector { - let documentSelector: SourceKitDocumentSelector; - switch (configuration.lsp.supportCFamily) { - case "enable": - documentSelector = [ - ...LanguageClientManager.appleLangDocumentSelector, - ...LanguageClientManager.cFamilyDocumentSelector, - ]; - break; - - case "disable": - documentSelector = LanguageClientManager.appleLangDocumentSelector; - break; - - case "cpptools-inactive": { - const cppToolsActive = - vscode.extensions.getExtension("ms-vscode.cpptools")?.isActive; - documentSelector = - cppToolsActive === true - ? LanguageClientManager.appleLangDocumentSelector - : [ - ...LanguageClientManager.appleLangDocumentSelector, - ...LanguageClientManager.cFamilyDocumentSelector, - ]; - } - } - documentSelector = documentSelector.filter(doc => { - return configuration.lsp.supportedLanguages.includes(doc.language); - }); - documentSelector.push(...LanguageClientManager.documentationDocumentSelector); - return documentSelector; - } - // build argument to sourcekit-lsp filter static buildArgumentFilter: ArgumentFilter[] = [ { argument: "--build-path", include: 1 }, @@ -140,14 +75,14 @@ export class LanguageClientManager implements vscode.Disposable { private getReferenceDocument?: vscode.Disposable; private didChangeActiveDocument?: vscode.Disposable; private restartedPromise?: Promise; - private currentWorkspaceFolder?: vscode.Uri; + private currentWorkspaceFolder?: FolderContext; private waitingOnRestartCount: number; private clientReadyPromise?: Promise; public documentSymbolWatcher?: ( document: vscode.TextDocument, symbols: vscode.DocumentSymbol[] | null | undefined ) => void; - private subscriptions: { dispose(): unknown }[]; + private subscriptions: vscode.Disposable[]; private singleServerSupport: boolean; // used by single server support to keep a record of the project folders // that are not at the root of their workspace @@ -165,55 +100,18 @@ export class LanguageClientManager implements vscode.Disposable { } constructor( - public workspaceContext: WorkspaceContext, + public folderContext: FolderContext, private languageClientFactory: LanguageClientFactory = new LanguageClientFactory() ) { this.namedOutputChannels.set( LanguageClientManager.indexingLogName, new LSPOutputChannel(LanguageClientManager.indexingLogName, false, true) ); - this.swiftVersion = workspaceContext.swiftVersion; + this.swiftVersion = folderContext.swiftVersion; this.singleServerSupport = this.swiftVersion.isGreaterThanOrEqual(new Version(5, 7, 0)); this.subscriptions = []; this.subFolderWorkspaces = []; - if (this.singleServerSupport) { - this.subscriptions.push( - // add/remove folders from server - workspaceContext.onDidChangeFolders(async ({ folder, operation }) => { - if (!folder) { - return; - } - switch (operation) { - case FolderOperation.add: - await this.addFolder(folder); - break; - case FolderOperation.remove: - await this.removeFolder(folder); - break; - } - }) - ); - this.setLanguageClientFolder(undefined); - } else { - this.subscriptions.push( - // stop and start server for each folder based on which file I am looking at - workspaceContext.onDidChangeFolders(async ({ folder, operation }) => { - switch (operation) { - case FolderOperation.add: - if (folder && folder.folder) { - // if active document is inside folder then setup language client - if (this.isActiveFileInFolder(folder.folder)) { - await this.setLanguageClientFolder(folder.folder); - } - } - break; - case FolderOperation.focus: - await this.setLanguageClientFolder(folder?.folder); - break; - } - }) - ); - } + // on change config restart server const onChangeConfig = vscode.workspace.onDidChangeConfiguration(event => { if (!event.affectsConfiguration("swift.sourcekit-lsp")) { @@ -256,8 +154,10 @@ export class LanguageClientManager implements vscode.Disposable { // Swift versions prior to 5.6 don't support file changes, so need to restart // lSP server when a file is either created or deleted - if (workspaceContext.swiftVersion.isLessThan(new Version(5, 6, 0))) { - workspaceContext.outputChannel.logDiagnostic("LSP: Adding new/delete file handlers"); + if (this.swiftVersion.isLessThan(new Version(5, 6, 0))) { + folderContext.workspaceContext.outputChannel.logDiagnostic( + "LSP: Adding new/delete file handlers" + ); // restart LSP server on creation of a new file const onDidCreateFileDisposable = vscode.workspace.onDidCreateFiles(() => { this.restart(); @@ -319,14 +219,14 @@ export class LanguageClientManager implements vscode.Disposable { /** Restart language client */ async restart() { // force restart of language client - await this.setLanguageClientFolder(this.currentWorkspaceFolder, true); + await this.setLanguageClientFolder(this.folderContext, true); } get languageClientOutputChannel(): SwiftOutputChannel | undefined { return this.languageClient?.outputChannel as SwiftOutputChannel | undefined; } - private async addFolder(folderContext: FolderContext) { + async addFolder(folderContext: FolderContext) { if (!folderContext.isRootFolder) { await this.useLanguageClient(async client => { const uri = folderContext.folder; @@ -343,7 +243,7 @@ export class LanguageClientManager implements vscode.Disposable { } } - private async removeFolder(folderContext: FolderContext) { + async removeFolder(folderContext: FolderContext) { if (!folderContext.isRootFolder) { await this.useLanguageClient(async client => { const uri = folderContext.folder; @@ -372,34 +272,27 @@ export class LanguageClientManager implements vscode.Disposable { } } - /** Set folder for LSP server - * + /** + * Set folder for LSP server. * If server is already running then check if the workspace folder is the same if * it isn't then restart the server using the new workspace folder. */ - private async setLanguageClientFolder(uri?: vscode.Uri, forceRestart = false) { + async setLanguageClientFolder(folder: FolderContext, forceRestart = false) { + const uri = folder.folder; if (this.languageClient === undefined) { - this.currentWorkspaceFolder = uri; - this.restartedPromise = this.setupLanguageClient(uri); + this.currentWorkspaceFolder = folder; + this.restartedPromise = this.setupLanguageClient(folder); return; } else { // don't check for undefined uri's or if the current workspace is the same if we are // running a single server. The only way we can get here while using a single server // is when restart is called. if (!this.singleServerSupport) { - if (uri === undefined || (this.currentWorkspaceFolder === uri && !forceRestart)) { + if (this.currentWorkspaceFolder?.folder === uri && !forceRestart) { return; } } - let workspaceFolder: vscode.WorkspaceFolder | undefined; - if (uri) { - workspaceFolder = { - uri: uri, - name: FolderContext.uriName(uri), - index: 0, - }; - } - await this.restartLanguageClient(workspaceFolder); + await this.restartLanguageClient(folder); } } @@ -408,7 +301,7 @@ export class LanguageClientManager implements vscode.Disposable { * @param workspaceFolder workspace folder to send to server * @returns when done */ - private async restartLanguageClient(workspaceFolder: vscode.WorkspaceFolder | undefined) { + private async restartLanguageClient(workspaceFolder: FolderContext) { // count number of setLanguageClientFolder calls waiting on startedPromise this.waitingOnRestartCount += 1; // if in the middle of a restart then we have to wait until that @@ -429,7 +322,7 @@ export class LanguageClientManager implements vscode.Disposable { const client = this.languageClient; // language client is set to null while it is in the process of restarting this.languageClient = null; - this.currentWorkspaceFolder = workspaceFolder?.uri; + this.currentWorkspaceFolder = workspaceFolder; this.legacyInlayHints?.dispose(); this.legacyInlayHints = undefined; this.peekDocuments?.dispose(); @@ -442,7 +335,7 @@ export class LanguageClientManager implements vscode.Disposable { this.restartedPromise = client .stop() .then(async () => { - await this.setupLanguageClient(workspaceFolder?.uri); + await this.setupLanguageClient(workspaceFolder); // Now that the client has been replaced, dispose the old client's output channel. client.outputChannel.dispose(); @@ -450,26 +343,15 @@ export class LanguageClientManager implements vscode.Disposable { .catch(async reason => { // error message matches code here https://github.com/microsoft/vscode-languageserver-node/blob/2041784436fed53f4e77267a49396bca22a7aacf/client/src/common/client.ts#L1409C1-L1409C54 if (reason.message === "Stopping the server timed out") { - await this.setupLanguageClient(workspaceFolder?.uri); + await this.setupLanguageClient(workspaceFolder); } - this.workspaceContext.outputChannel.log(`${reason}`); + this.folderContext.workspaceContext.outputChannel.log(`${reason}`); }); await this.restartedPromise; } } - private isActiveFileInFolder(uri: vscode.Uri): boolean { - if (vscode.window.activeTextEditor && vscode.window.activeTextEditor.document) { - // if active document is inside folder then setup language client - const activeDocPath = vscode.window.activeTextEditor.document.uri.fsPath; - if (isPathInsidePath(activeDocPath, uri.fsPath)) { - return true; - } - } - return false; - } - - private async setupLanguageClient(folder?: vscode.Uri) { + private async setupLanguageClient(folder: FolderContext) { if (configuration.lsp.disable) { this.languageClient = undefined; return; @@ -478,16 +360,16 @@ export class LanguageClientManager implements vscode.Disposable { return this.startClient(client, errorHandler); } - private createLSPClient(folder?: vscode.Uri): { + private createLSPClient(folder: FolderContext): { client: LanguageClient; errorHandler: SourceKitLSPErrorHandler; } { - const toolchainSourceKitLSP = - this.workspaceContext.toolchain.getToolchainExecutable("sourcekit-lsp"); + const toolchain = folder.toolchain; + const toolchainSourceKitLSP = toolchain.getToolchainExecutable("sourcekit-lsp"); const lspConfig = configuration.lsp; const serverPathConfig = lspConfig.serverPath; const serverPath = serverPathConfig.length > 0 ? serverPathConfig : toolchainSourceKitLSP; - const buildFlags = this.workspaceContext.toolchain.buildFlags; + const buildFlags = toolchain.buildFlags; const sdkArguments = [ ...buildFlags.swiftDriverSDKFlags(true), ...buildFlags.swiftDriverTargetFlags(true), @@ -520,7 +402,7 @@ export class LanguageClientManager implements vscode.Disposable { sourcekit.options = { env: { ...sourcekit.options?.env, - SOURCEKIT_TOOLCHAIN_PATH: this.workspaceContext.toolchain.toolchainPath, + SOURCEKIT_TOOLCHAIN_PATH: toolchain.toolchainPath, }, }; } @@ -528,109 +410,22 @@ export class LanguageClientManager implements vscode.Disposable { const serverOptions: ServerOptions = sourcekit; let workspaceFolder = undefined; if (folder) { - workspaceFolder = { uri: folder, name: FolderContext.uriName(folder), index: 0 }; + workspaceFolder = { + uri: folder.folder, + name: FolderContext.uriName(folder.folder), + index: 0, + }; } const errorHandler = new SourceKitLSPErrorHandler(5); - const clientOptions: LanguageClientOptions = { - documentSelector: LanguageClientManager.documentSelector, - revealOutputChannelOn: RevealOutputChannelOn.Never, - workspaceFolder: workspaceFolder, - outputChannel: new SwiftOutputChannel("SourceKit Language Server"), - middleware: { - didOpen: this.activeDocumentManager.didOpen.bind(this.activeDocumentManager), - didClose: this.activeDocumentManager.didClose.bind(this.activeDocumentManager), - provideCodeLenses: async (document, token, next) => { - const result = await next(document, token); - return result?.map(codelens => { - switch (codelens.command?.command) { - case "swift.run": - codelens.command.title = `$(play) ${codelens.command.title}`; - break; - case "swift.debug": - codelens.command.title = `$(debug) ${codelens.command.title}`; - break; - } - return codelens; - }); - }, - provideDocumentSymbols: async (document, token, next) => { - const result = await next(document, token); - const documentSymbols = result as vscode.DocumentSymbol[]; - if (this.documentSymbolWatcher && documentSymbols) { - this.documentSymbolWatcher(document, documentSymbols); - } - return result; - }, - provideDefinition: async (document, position, token, next) => { - const result = await next(document, position, token); - const definitions = result as vscode.Location[]; - if ( - definitions && - path.extname(definitions[0].uri.path) === ".swiftinterface" && - definitions[0].uri.scheme === "file" - ) { - const uri = definitions[0].uri.with({ scheme: "readonly" }); - return new vscode.Location(uri, definitions[0].range); - } - return result; - }, - // temporarily remove text edit from Inlay hints while SourceKit-LSP - // returns invalid replacement text - provideInlayHints: async (document, position, token, next) => { - const result = await next(document, position, token); - // remove textEdits for swift version earlier than 5.10 as it sometimes - // generated invalid textEdits - if (this.workspaceContext.swiftVersion.isLessThan(new Version(5, 10, 0))) { - result?.forEach(r => (r.textEdits = undefined)); - } - return result; - }, - provideDiagnostics: async (uri, previousResultId, token, next) => { - const result = await next(uri, previousResultId, token); - if (result?.kind === vsdiag.DocumentDiagnosticReportKind.unChanged) { - return undefined; - } - const document = uri as vscode.TextDocument; - this.workspaceContext.diagnostics.handleDiagnostics( - document.uri ?? uri, - DiagnosticsManager.isSourcekit, - result?.items ?? [] - ); - return undefined; - }, - handleDiagnostics: (uri, diagnostics) => { - this.workspaceContext.diagnostics.handleDiagnostics( - uri, - DiagnosticsManager.isSourcekit, - diagnostics - ); - }, - handleWorkDoneProgress: (() => { - let lastPrompted = new Date(0).getTime(); - return async (token, params, next) => { - const result = await next(token, params); - const now = new Date().getTime(); - const oneHour = 60 * 60 * 1000; - if ( - now - lastPrompted > oneHour && - token.toString().startsWith("sourcekitd-crashed") - ) { - // Only prompt once an hour in case sourcekit is in a crash loop - lastPrompted = now; - promptForDiagnostics(this.workspaceContext); - } - return result; - }; - })(), - }, - uriConverters, + const clientOptions: LanguageClientOptions = lspClientOptions( + this.swiftVersion, + this.folderContext.workspaceContext, + workspaceFolder, + this.activeDocumentManager, errorHandler, - // Avoid attempting to reinitialize multiple times. If we fail to initialize - // we aren't doing anything different the second time and so will fail again. - initializationFailedHandler: () => false, - initializationOptions: this.initializationOptions(), - }; + this.documentSymbolWatcher + ); return { client: this.languageClientFactory.createLanguageClient( @@ -643,52 +438,6 @@ export class LanguageClientManager implements vscode.Disposable { }; } - /* eslint-disable @typescript-eslint/no-explicit-any */ - private initializationOptions(): any { - let options: any = { - "workspace/peekDocuments": true, // workaround for client capability to handle `PeekDocumentsRequest` - "workspace/getReferenceDocument": true, // the client can handle URIs with scheme `sourcekit-lsp:` - "textDocument/codeLens": { - supportedCommands: { - "swift.run": "swift.run", - "swift.debug": "swift.debug", - }, - }, - }; - - // Swift 6.0.0 and later supports background indexing. - // In 6.0.0 it is experimental so only "true" enables it. - // In 6.1.0 it is no longer experimental, and so "auto" or "true" enables it. - if ( - this.swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0)) && - (configuration.backgroundIndexing === "on" || - (configuration.backgroundIndexing === "auto" && - this.swiftVersion.isGreaterThanOrEqual(new Version(6, 1, 0)))) - ) { - options = { - ...options, - backgroundIndexing: true, - backgroundPreparationMode: "enabled", - }; - } - - if (this.swiftVersion.isGreaterThanOrEqual(new Version(6, 1, 0))) { - options = { - ...options, - "window/didChangeActiveDocument": true, // the client can send `window/didChangeActiveDocument` notifications - }; - } - - if (configuration.swiftSDK !== "") { - options = { - ...options, - swiftPM: { swiftSDK: configuration.swiftSDK }, - }; - } - - return options; - } - private async startClient(client: LanguageClient, errorHandler: SourceKitLSPErrorHandler) { client.onDidChangeState(e => { // if state is now running add in any sub-folder workspaces that @@ -700,13 +449,13 @@ export class LanguageClientManager implements vscode.Disposable { } }); if (client.clientOptions.workspaceFolder) { - this.workspaceContext.outputChannel.log( + this.folderContext.workspaceContext.outputChannel.log( `SourceKit-LSP setup for ${FolderContext.uriName( client.clientOptions.workspaceFolder.uri )}` ); } else { - this.workspaceContext.outputChannel.log(`SourceKit-LSP setup`); + this.folderContext.workspaceContext.outputChannel.log(`SourceKit-LSP setup`); } client.onNotification(SourceKitLogMessageNotification.type, params => { @@ -721,13 +470,13 @@ export class LanguageClientManager implements vscode.Disposable { // if sourcekit-lsp crashes during normal operation. errorHandler.enable(); - if (this.workspaceContext.swiftVersion.isLessThan(new Version(5, 7, 0))) { + if (this.swiftVersion.isLessThan(new Version(5, 7, 0))) { this.legacyInlayHints = activateLegacyInlayHints(client); } this.peekDocuments = activatePeekDocuments(client); this.getReferenceDocument = activateGetReferenceDocument(client); - this.workspaceContext.subscriptions.push(this.getReferenceDocument); + this.subscriptions.push(this.getReferenceDocument); try { if ( checkExperimentalCapability( @@ -738,14 +487,14 @@ export class LanguageClientManager implements vscode.Disposable { ) { this.didChangeActiveDocument = this.activeDocumentManager.activateDidChangeActiveDocument(client); - this.workspaceContext.subscriptions.push(this.didChangeActiveDocument); + this.subscriptions.push(this.didChangeActiveDocument); } } catch { // do nothing } }) .catch(reason => { - this.workspaceContext.outputChannel.log(`${reason}`); + this.folderContext.workspaceContext.outputChannel.log(`${reason}`); this.languageClient?.stop(); this.languageClient = undefined; throw reason; @@ -870,11 +619,6 @@ export const enum LanguageClientError { LanguageClientUnavailable = "Language Client Unavailable", } -type SourceKitDocumentSelector = { - scheme: string; - language: string; -}[]; - /** * Returns `true` if the LSP supports the supplied `method` at or * above the supplied `minVersion`. diff --git a/src/sourcekit-lsp/LanguageClientToolchainCoordinator.ts b/src/sourcekit-lsp/LanguageClientToolchainCoordinator.ts new file mode 100644 index 000000000..eb854700d --- /dev/null +++ b/src/sourcekit-lsp/LanguageClientToolchainCoordinator.ts @@ -0,0 +1,131 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import * as vscode from "vscode"; +import { Version } from "../utilities/version"; +import { FolderContext } from "../FolderContext"; +import { LanguageClientFactory } from "./LanguageClientFactory"; +import { LanguageClientManager } from "./LanguageClientManager"; +import { FolderOperation, WorkspaceContext } from "../WorkspaceContext"; + +/** + * Manages the creation of LanguageClient instances for workspace folders. + * + * A LanguageClient will be created for each unique toolchain version. If two + * folders share the same toolchain version then they will share the same LanaugeClient. + * This ensures that a folder always uses the LanaugeClient bundled with its desired toolchain. + */ +export class LanguageClientToolchainCoordinator implements vscode.Disposable { + private subscriptions: vscode.Disposable[] = []; + private clients: Map = new Map(); + + public constructor( + workspaceContext: WorkspaceContext, + languageClientFactory: LanguageClientFactory = new LanguageClientFactory() // used for testing only + ) { + this.subscriptions.push( + // stop and start server for each folder based on which file I am looking at + workspaceContext.onDidChangeFolders(async ({ folder, operation }) => { + await this.handleEvent(folder, operation, languageClientFactory); + }) + ); + + // Add any folders already in the workspace context at the time of construction. + // This is mainly for testing purposes, as this class should be created immediately + // when the extension is activated and the workspace context is first created. + for (const folder of workspaceContext.folders) { + this.handleEvent(folder, FolderOperation.add, languageClientFactory); + } + } + + private async handleEvent( + folder: FolderContext | null, + operation: FolderOperation, + languageClientFactory: LanguageClientFactory + ) { + if (!folder) { + return; + } + const singleServer = folder.swiftVersion.isGreaterThanOrEqual(new Version(5, 7, 0)); + switch (operation) { + case FolderOperation.add: { + const client = await this.create(folder, singleServer, languageClientFactory); + await (singleServer + ? client.addFolder(folder) + : client.setLanguageClientFolder(folder)); + break; + } + case FolderOperation.remove: { + const client = await this.create(folder, singleServer, languageClientFactory); + await client.removeFolder(folder); + break; + } + case FolderOperation.focus: { + if (!singleServer) { + const client = await this.create(folder, singleServer, languageClientFactory); + await client.setLanguageClientFolder(folder); + } + break; + } + } + } + + /** + * Returns the LanguageClientManager for the supplied folder. + * @param folder + * @returns + */ + public get(folder: FolderContext): LanguageClientManager { + const client = this.clients.get(folder.swiftVersion.toString()); + if (!client) { + throw new Error( + "LanguageClientManager has not yet been created. This is a bug, please file an issue at https://github.com/swiftlang/vscode-swift/issues" + ); + } + return client; + } + + /** + * Stops all LanguageClient instances. + * This should b called when the extension is deactivated. + */ + public async stop() { + for (const client of this.clients.values()) { + await client.stop(); + } + this.clients.clear(); + } + + private async create( + folder: FolderContext, + singleServerSupport: boolean, + languageClientFactory: LanguageClientFactory + ): Promise { + const versionString = folder.swiftVersion.toString(); + let client = this.clients.get(versionString); + if (!client) { + client = new LanguageClientManager(folder, languageClientFactory); + this.clients.set(versionString, client); + // Callers that must restart when switching folders will call setLanguageClientFolder themselves. + if (singleServerSupport) { + await client.setLanguageClientFolder(folder); + } + } + return client; + } + + dispose() { + this.subscriptions.forEach(item => item.dispose()); + } +} diff --git a/src/sourcekit-lsp/inlayHints.ts b/src/sourcekit-lsp/inlayHints.ts index c73eebf42..88c0cd51e 100644 --- a/src/sourcekit-lsp/inlayHints.ts +++ b/src/sourcekit-lsp/inlayHints.ts @@ -15,8 +15,8 @@ import * as vscode from "vscode"; import * as langclient from "vscode-languageclient/node"; import configuration from "../configuration"; -import { LanguageClientManager } from "./LanguageClientManager"; import { LegacyInlayHintRequest } from "./extensions"; +import { LanguagerClientDocumentSelectors } from "./LanguageClientConfiguration"; /** Provide Inlay Hints using sourcekit-lsp */ class SwiftLegacyInlayHintsProvider implements vscode.InlayHintsProvider { @@ -68,7 +68,7 @@ class SwiftLegacyInlayHintsProvider implements vscode.InlayHintsProvider { /** activate the inlay hints */ export function activateLegacyInlayHints(client: langclient.LanguageClient): vscode.Disposable { const inlayHint = vscode.languages.registerInlayHintsProvider( - LanguageClientManager.documentSelector, + LanguagerClientDocumentSelectors.allHandledDocumentTypes(), new SwiftLegacyInlayHintsProvider(client) ); diff --git a/src/tasks/SwiftPluginTaskProvider.ts b/src/tasks/SwiftPluginTaskProvider.ts index da07164cf..1cffef9c0 100644 --- a/src/tasks/SwiftPluginTaskProvider.ts +++ b/src/tasks/SwiftPluginTaskProvider.ts @@ -21,6 +21,7 @@ import { SwiftExecution } from "../tasks/SwiftExecution"; import { resolveTaskCwd } from "../utilities/tasks"; import configuration, { PluginPermissionConfiguration } from "../configuration"; import { SwiftTask } from "./SwiftTaskProvider"; +import { SwiftToolchain } from "../toolchain/toolchain"; // Interface class for defining task configuration interface TaskConfig { @@ -52,7 +53,7 @@ export class SwiftPluginTaskProvider implements vscode.TaskProvider { for (const folderContext of this.workspaceContext.folders) { for (const plugin of folderContext.swiftPackage.plugins) { tasks.push( - this.createSwiftPluginTask(plugin, { + this.createSwiftPluginTask(plugin, folderContext.toolchain, { cwd: folderContext.folder, scope: folderContext.workspaceFolder, presentationOptions: { @@ -73,16 +74,21 @@ export class SwiftPluginTaskProvider implements vscode.TaskProvider { */ // eslint-disable-next-line @typescript-eslint/no-unused-vars resolveTask(task: vscode.Task, token: vscode.CancellationToken): vscode.Task { + const currentFolder = + this.workspaceContext.currentFolder ?? this.workspaceContext.folders[0]; + if (!currentFolder) { + return task; + } // We need to create a new Task object here. // Reusing the task parameter doesn't seem to work. - const swift = this.workspaceContext.toolchain.getToolchainExecutable("swift"); + const swift = currentFolder.toolchain.getToolchainExecutable("swift"); let swiftArgs = [ "package", ...this.pluginArguments(task.definition as PluginPermissionConfiguration), task.definition.command, ...task.definition.args, ]; - swiftArgs = this.workspaceContext.toolchain.buildFlags.withAdditionalFlags(swiftArgs); + swiftArgs = currentFolder.toolchain.buildFlags.withAdditionalFlags(swiftArgs); const cwd = resolveTaskCwd(task, task.definition.cwd); const newTask = new vscode.Task( @@ -109,8 +115,12 @@ export class SwiftPluginTaskProvider implements vscode.TaskProvider { * @param config * @returns */ - createSwiftPluginTask(plugin: PackagePlugin, config: TaskConfig): SwiftTask { - const swift = this.workspaceContext.toolchain.getToolchainExecutable("swift"); + createSwiftPluginTask( + plugin: PackagePlugin, + toolchain: SwiftToolchain, + config: TaskConfig + ): SwiftTask { + const swift = toolchain.getToolchainExecutable("swift"); // Add relative path current working directory const relativeCwd = path.relative(config.scope.uri.fsPath, config.cwd.fsPath); @@ -122,7 +132,7 @@ export class SwiftPluginTaskProvider implements vscode.TaskProvider { plugin.command, ...definition.args, ]; - swiftArgs = this.workspaceContext.toolchain.buildFlags.withAdditionalFlags(swiftArgs); + swiftArgs = toolchain.buildFlags.withAdditionalFlags(swiftArgs); const presentation = config?.presentationOptions ?? {}; const task = new vscode.Task( diff --git a/src/tasks/SwiftTaskProvider.ts b/src/tasks/SwiftTaskProvider.ts index acbc15773..88fa9edc0 100644 --- a/src/tasks/SwiftTaskProvider.ts +++ b/src/tasks/SwiftTaskProvider.ts @@ -104,7 +104,7 @@ function getBuildRevealOption(): vscode.TaskRevealKind { const buildAllTaskCache = (() => { const cache = new Map(); const key = (name: string, folderContext: FolderContext, task: SwiftTask) => { - return `${name}:${folderContext.folder}:${buildOptions(folderContext.workspaceContext.toolchain).join(",")}:${task.definition.args.join(",")}`; + return `${name}:${folderContext.folder}:${buildOptions(folderContext.toolchain).join(",")}:${task.definition.args.join(",")}`; }; return { @@ -152,7 +152,7 @@ export async function createBuildAllTask( }, disableTaskQueue: true, }, - folderContext.workspaceContext.toolchain + folderContext.toolchain ); // Ensures there is one Build All task per folder context, since this can be called multiple @@ -173,7 +173,8 @@ export async function getBuildAllTask( const buildTaskName = buildAllTaskName(folderContext, release); const folderWorkingDir = folderContext.workspaceFolder.uri.fsPath; // search for build all task in task.json first, that are valid for folder - const workspaceTasks = (await vscode.tasks.fetchTasks()).filter(task => { + const tasks = await vscode.tasks.fetchTasks(); + const workspaceTasks = tasks.filter(task => { if (task.source !== "Workspace" || task.scope !== folderContext.workspaceFolder) { return false; } @@ -216,7 +217,7 @@ export async function getBuildAllTask( * Creates a {@link vscode.Task Task} to run an executable target. */ function createBuildTasks(product: Product, folderContext: FolderContext): vscode.Task[] { - const toolchain = folderContext.workspaceContext.toolchain; + const toolchain = folderContext.toolchain; let buildTaskNameSuffix = ""; if (folderContext.relativePath.length > 0) { buildTaskNameSuffix = ` (${folderContext.relativePath})`; @@ -236,7 +237,7 @@ function createBuildTasks(product: Product, folderContext: FolderContext): vscod disableTaskQueue: true, dontTriggerTestDiscovery: true, }, - folderContext.workspaceContext.toolchain + folderContext.toolchain ); const buildDebug = buildAllTaskCache.get(buildDebugName, folderContext, buildDebugTask); @@ -254,7 +255,7 @@ function createBuildTasks(product: Product, folderContext: FolderContext): vscod disableTaskQueue: true, dontTriggerTestDiscovery: true, }, - folderContext.workspaceContext.toolchain + folderContext.toolchain ); const buildRelease = buildAllTaskCache.get(buildReleaseName, folderContext, buildReleaseTask); return [buildDebug, buildRelease]; @@ -376,7 +377,7 @@ export class SwiftTaskProvider implements vscode.TaskProvider { // This is only required in Swift toolchains before v6 as SwiftPM in newer toolchains // will block multiple processes accessing the .build folder at the same time if ( - this.workspaceContext.toolchain.swiftVersion.isLessThan(new Version(6, 0, 0)) && + folderContext.toolchain.swiftVersion.isLessThan(new Version(6, 0, 0)) && activeOperation && !activeOperation.operation.isBuildOperation ) { @@ -422,9 +423,14 @@ export class SwiftTaskProvider implements vscode.TaskProvider { */ // eslint-disable-next-line @typescript-eslint/no-unused-vars resolveTask(task: vscode.Task, token: vscode.CancellationToken): vscode.Task { + const currentFolder = + this.workspaceContext.currentFolder ?? this.workspaceContext.folders[0]; + if (!currentFolder) { + return task; + } // We need to create a new Task object here. // Reusing the task parameter doesn't seem to work. - const toolchain = this.workspaceContext.toolchain; + const toolchain = currentFolder.toolchain; const swift = toolchain.getToolchainExecutable("swift"); // platform specific let platform: TaskPlatformSpecificConfig | undefined; diff --git a/src/tasks/TaskQueue.ts b/src/tasks/TaskQueue.ts index a8e530752..598fa53a3 100644 --- a/src/tasks/TaskQueue.ts +++ b/src/tasks/TaskQueue.ts @@ -118,7 +118,7 @@ export class SwiftExecOperation implements SwiftOperation { async run(): Promise { const { stdout, stderr } = await execSwift( this.args, - this.folderContext.workspaceContext.toolchain, + this.folderContext.toolchain, { cwd: this.folderContext.folder.fsPath }, this.folderContext ); diff --git a/src/toolchain/toolchain.ts b/src/toolchain/toolchain.ts index f765f692c..cdf10ff02 100644 --- a/src/toolchain/toolchain.ts +++ b/src/toolchain/toolchain.ts @@ -19,12 +19,12 @@ import * as plist from "plist"; import * as vscode from "vscode"; import configuration from "../configuration"; import { SwiftOutputChannel } from "../ui/SwiftOutputChannel"; -import { execFile, execSwift } from "../utilities/utilities"; +import { execFile, ExecFileError, execSwift } from "../utilities/utilities"; import { expandFilePathTilde, pathExists } from "../utilities/filesystem"; import { Version } from "../utilities/version"; import { BuildFlags } from "./BuildFlags"; import { Sanitizer } from "./Sanitizer"; -import { SwiftlyConfig, ToolchainVersion } from "./ToolchainVersion"; +import { SwiftlyConfig } from "./ToolchainVersion"; /** * Contents of **Info.plist** on Windows. @@ -117,11 +117,13 @@ export class SwiftToolchain { this.swiftVersionString = targetInfo.compilerVersion; } - static async create(): Promise { + static async create(folder?: vscode.Uri): Promise { const swiftFolderPath = await this.getSwiftFolderPath(); - const toolchainPath = await this.getToolchainPath(swiftFolderPath); - const targetInfo = await this.getSwiftTargetInfo(); - const swiftVersion = this.getSwiftVersion(targetInfo); + const toolchainPath = await this.getToolchainPath(swiftFolderPath, folder); + const targetInfo = await this.getSwiftTargetInfo( + this._getToolchainExecutable(toolchainPath, "swift") + ); + const swiftVersion = await this.getSwiftVersion(targetInfo); const [runtimePath, defaultSDK] = await Promise.all([ this.getRuntimePath(targetInfo), this.getDefaultSDK(), @@ -394,9 +396,13 @@ export class SwiftToolchain { * Return fullpath for toolchain executable */ public getToolchainExecutable(executable: string): string { + return SwiftToolchain._getToolchainExecutable(this.toolchainPath, executable); + } + + private static _getToolchainExecutable(toolchainPath: string, executable: string): string { // should we add `.exe` at the end of the executable name const executableSuffix = process.platform === "win32" ? ".exe" : ""; - return path.join(this.toolchainPath, "bin", executable + executableSuffix); + return path.join(toolchainPath, "bin", executable + executableSuffix); } private static getXcodeDirectory(toolchainPath: string): string | undefined { @@ -615,7 +621,7 @@ export class SwiftToolchain { /** * @returns path to Toolchain folder */ - private static async getToolchainPath(swiftPath: string): Promise { + private static async getToolchainPath(swiftPath: string, cwd?: vscode.Uri): Promise { try { switch (process.platform) { case "darwin": { @@ -623,7 +629,7 @@ export class SwiftToolchain { return path.dirname(configuration.path); } - const swiftlyToolchainLocation = await this.swiftlyToolchainLocation(); + const swiftlyToolchainLocation = await this.swiftlyToolchainLocation(cwd); if (swiftlyToolchainLocation) { return swiftlyToolchainLocation; } @@ -648,20 +654,31 @@ export class SwiftToolchain { * the path to the active toolchain. * @returns The location of the active toolchain if swiftly is being used to manage it. */ - private static async swiftlyToolchainLocation(): Promise { + private static async swiftlyToolchainLocation(cwd?: vscode.Uri): Promise { const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"]; if (swiftlyHomeDir) { const { stdout: swiftLocation } = await execFile("which", ["swift"]); if (swiftLocation.indexOf(swiftlyHomeDir) === 0) { - const swiftlyConfig = await SwiftToolchain.getSwiftlyConfig(); - if (swiftlyConfig) { - const version = ToolchainVersion.parse(swiftlyConfig.inUse); - return path.join( - os.homedir(), - "Library/Developer/Toolchains/", - `${version.identifier}.xctoolchain`, - "usr" + // Print the location of the toolchain that swiftly is using. If there + // is no cwd specified then it returns the global "inUse" toolchain otherwise + // it respects the .swift-version file in the cwd and resolves using that. + try { + const { stdout: swiftlyLocation } = await execFile( + "swiftly", + ["use", "--print-location"], + { + cwd: cwd?.fsPath, + } ); + + const trimmedLocation = swiftlyLocation.trimEnd(); + if (trimmedLocation.length > 0) { + return path.join(trimmedLocation, "usr"); + } + } catch (err: unknown) { + const error = err as ExecFileError; + // Its possible the toolchain in .swift-version is misconfigured or doesn't exist. + vscode.window.showErrorMessage(`${error.stderr}`); } } } @@ -884,10 +901,10 @@ export class SwiftToolchain { } /** @returns swift target info */ - private static async getSwiftTargetInfo(): Promise { + private static async getSwiftTargetInfo(swiftExecutable: string): Promise { try { try { - const { stdout } = await execSwift(["-print-target-info"], "default"); + const { stdout } = await execSwift(["-print-target-info"], { swiftExecutable }); const targetInfo = JSON.parse(stdout.trimEnd()) as SwiftTargetInfo; if (targetInfo.compilerVersion) { return targetInfo; @@ -896,7 +913,7 @@ export class SwiftToolchain { // hit error while running `swift -print-target-info`. We are possibly running // a version of swift 5.3 or older } - const { stdout } = await execSwift(["--version"], "default"); + const { stdout } = await execSwift(["--version"], { swiftExecutable }); return { compilerVersion: stdout.split("\n", 1)[0], paths: { runtimeLibraryPaths: [""] }, diff --git a/src/ui/LanguageStatusItems.ts b/src/ui/LanguageStatusItems.ts index 7a21ab52e..d4047257a 100644 --- a/src/ui/LanguageStatusItems.ts +++ b/src/ui/LanguageStatusItems.ts @@ -14,54 +14,64 @@ import * as vscode from "vscode"; import { Command } from "vscode-languageclient"; -import { LanguageClientManager } from "../sourcekit-lsp/LanguageClientManager"; import { WorkspaceContext, FolderOperation } from "../WorkspaceContext"; +import { LanguagerClientDocumentSelectors } from "../sourcekit-lsp/LanguageClientConfiguration"; export class LanguageStatusItems implements vscode.Disposable { - private packageSwiftItem: vscode.LanguageStatusItem; - constructor(workspaceContext: WorkspaceContext) { // Swift language version item const swiftVersionItem = vscode.languages.createLanguageStatusItem( "swiftlang-version", - LanguageClientManager.documentSelector + LanguagerClientDocumentSelectors.allHandledDocumentTypes() ); - swiftVersionItem.text = workspaceContext.toolchain.swiftVersionString; + const toolchain = + workspaceContext.currentFolder?.toolchain ?? workspaceContext.globalToolchain; + swiftVersionItem.text = toolchain.swiftVersionString; swiftVersionItem.accessibilityInformation = { - label: `Swift Version ${workspaceContext.toolchain.swiftVersion.toString()}`, + label: `Swift Version ${toolchain.swiftVersion.toString()}`, }; // Package.swift item - this.packageSwiftItem = vscode.languages.createLanguageStatusItem("swiftlang-package", [ - ...LanguageClientManager.appleLangDocumentSelector, - ...LanguageClientManager.cFamilyDocumentSelector, + const packageSwiftItem = vscode.languages.createLanguageStatusItem("swiftlang-package", [ + ...LanguagerClientDocumentSelectors.appleLangDocumentSelector, + ...LanguagerClientDocumentSelectors.cFamilyDocumentSelector, ]); - this.packageSwiftItem.text = "No Package.swift"; - this.packageSwiftItem.accessibilityInformation = { label: "There is no Package.swift" }; + packageSwiftItem.text = "No Package.swift"; + packageSwiftItem.accessibilityInformation = { label: "There is no Package.swift" }; // Update Package.swift item based on current focus const onFocus = workspaceContext.onDidChangeFolders(async ({ folder, operation }) => { switch (operation) { case FolderOperation.focus: if (folder && (await folder.swiftPackage.foundPackage)) { - this.packageSwiftItem.text = "Package.swift"; - this.packageSwiftItem.command = Command.create( + packageSwiftItem.text = "Package.swift"; + packageSwiftItem.command = Command.create( "Open Package", "swift.openPackage" ); - this.packageSwiftItem.accessibilityInformation = { + packageSwiftItem.accessibilityInformation = { label: "Open Package.swift", }; + + swiftVersionItem.text = folder.toolchain.swiftVersionString; + swiftVersionItem.accessibilityInformation = { + label: `Swift Version ${folder.toolchain.swiftVersion.toString()}`, + }; } else { - this.packageSwiftItem.text = "No Package.swift"; - this.packageSwiftItem.accessibilityInformation = { + packageSwiftItem.text = "No Package.swift"; + packageSwiftItem.accessibilityInformation = { label: "There is no Package.swift", }; - this.packageSwiftItem.command = undefined; + packageSwiftItem.command = undefined; + + swiftVersionItem.text = workspaceContext.globalToolchain.swiftVersionString; + swiftVersionItem.accessibilityInformation = { + label: `Swift Version ${workspaceContext.globalToolchain.swiftVersion.toString()}`, + }; } } }); - this.subscriptions = [onFocus, swiftVersionItem, this.packageSwiftItem]; + this.subscriptions = [onFocus, swiftVersionItem, packageSwiftItem]; } dispose() { diff --git a/src/utilities/utilities.ts b/src/utilities/utilities.ts index 5285891bb..e3787cfb8 100644 --- a/src/utilities/utilities.ts +++ b/src/utilities/utilities.ts @@ -181,17 +181,17 @@ export async function execFileStreamOutput( */ export async function execSwift( args: string[], - toolchain: SwiftToolchain | "default", + toolchain: SwiftToolchain | "default" | { swiftExecutable: string }, options: cp.ExecFileOptions = {}, folderContext?: FolderContext ): Promise<{ stdout: string; stderr: string }> { let swift: string; - if (toolchain === "default") { + if (typeof toolchain === "object" && "swiftExecutable" in toolchain) { + swift = toolchain.swiftExecutable; + } else if (toolchain === "default") { swift = getSwiftExecutable(); } else { swift = toolchain.getToolchainExecutable("swift"); - } - if (toolchain !== "default") { args = toolchain.buildFlags.withAdditionalFlags(args); } if (Object.keys(configuration.swiftEnvironmentVariables).length > 0) { diff --git a/test/integration-tests/DiagnosticsManager.test.ts b/test/integration-tests/DiagnosticsManager.test.ts index 37a47b5fc..a0c920f6e 100644 --- a/test/integration-tests/DiagnosticsManager.test.ts +++ b/test/integration-tests/DiagnosticsManager.test.ts @@ -133,7 +133,7 @@ suite("DiagnosticsManager Test Suite", function () { this.timeout(60000 * 5); workspaceContext = ctx; - toolchain = workspaceContext.toolchain; + toolchain = workspaceContext.globalToolchain; workspaceFolder = testAssetWorkspaceFolder("diagnostics"); cWorkspaceFolder = testAssetWorkspaceFolder("diagnosticsC"); cppWorkspaceFolder = testAssetWorkspaceFolder("diagnosticsCpp"); @@ -229,7 +229,7 @@ suite("DiagnosticsManager Test Suite", function () { suiteSetup(async function () { // Swift 5.10 and 6.0 on Windows have a bug where the // diagnostics are not emitted on their own line. - const swiftVersion = workspaceContext.toolchain.swiftVersion; + const swiftVersion = workspaceContext.globalToolchain.swiftVersion; if ( swiftVersion.isLessThan(new Version(5, 10, 0)) || (process.platform === "win32" && @@ -273,7 +273,9 @@ suite("DiagnosticsManager Test Suite", function () { expectedWarningDiagnostic, expectedMainErrorDiagnostic, expectedMainDictErrorDiagnostic, - ...(workspaceContext.swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0)) + ...(workspaceContext.globalToolchainSwiftVersion.isGreaterThanOrEqual( + new Version(6, 0, 0) + ) ? [expectedMacroDiagnostic] : []), ], // Should have parsed correct severity @@ -1127,7 +1129,7 @@ suite("DiagnosticsManager Test Suite", function () { // Skipped until we enable it in a nightly build suite("SourceKit-LSP diagnostics @slow", () => { suiteSetup(async function () { - if (workspaceContext.swiftVersion.isLessThan(new Version(5, 7, 0))) { + if (workspaceContext.globalToolchainSwiftVersion.isLessThan(new Version(5, 7, 0))) { this.skip(); return; } diff --git a/test/integration-tests/ExtensionActivation.test.ts b/test/integration-tests/ExtensionActivation.test.ts index d9ae25f92..59d88f981 100644 --- a/test/integration-tests/ExtensionActivation.test.ts +++ b/test/integration-tests/ExtensionActivation.test.ts @@ -111,16 +111,20 @@ suite("Extension Activation/Deactivation Tests", () => { }); test("compile_commands.json", async () => { - const lspWorkspaces = workspaceContext.languageClientManager.subFolderWorkspaces.map( - ({ fsPath }) => fsPath - ); + const folder = workspaceContext.folders[0]; + assert(folder); + + const languageClient = workspaceContext.languageClientManager.get(folder); + const lspWorkspaces = languageClient.subFolderWorkspaces.map(({ fsPath }) => fsPath); assertContains(lspWorkspaces, testAssetUri("cmake").fsPath); }); test("compile_flags.txt", async () => { - const lspWorkspaces = workspaceContext.languageClientManager.subFolderWorkspaces.map( - ({ fsPath }) => fsPath - ); + const folder = workspaceContext.folders[0]; + assert(folder); + + const languageClient = workspaceContext.languageClientManager.get(folder); + const lspWorkspaces = languageClient.subFolderWorkspaces.map(({ fsPath }) => fsPath); assertContains(lspWorkspaces, testAssetUri("cmake-compile-flags").fsPath); }); }); diff --git a/test/integration-tests/SwiftSnippet.test.ts b/test/integration-tests/SwiftSnippet.test.ts index 82b0342f4..b92c07214 100644 --- a/test/integration-tests/SwiftSnippet.test.ts +++ b/test/integration-tests/SwiftSnippet.test.ts @@ -50,7 +50,7 @@ suite("SwiftSnippet Test Suite @slow", function () { workspaceContext = ctx; const folder = await folderInRootWorkspace("defaultPackage", workspaceContext); - if (folder.workspaceContext.toolchain.swiftVersion.isLessThan(new Version(5, 9, 0))) { + if (folder.toolchain.swiftVersion.isLessThan(new Version(5, 9, 0))) { this.skip(); } await waitForNoRunningTasks(); @@ -86,7 +86,7 @@ suite("SwiftSnippet Test Suite @slow", function () { test("Run `Swift: Debug Swift Snippet` command for snippet file", async () => { const bpPromise = waitForDebugAdapterRequest( "Run hello", - workspaceContext.toolchain.swiftVersion, + workspaceContext.globalToolchain.swiftVersion, "stackTrace" ); const sessionPromise = waitUntilDebugSessionTerminates("Run hello"); diff --git a/test/integration-tests/WorkspaceContext.test.ts b/test/integration-tests/WorkspaceContext.test.ts index d1e5d709e..1dab210f1 100644 --- a/test/integration-tests/WorkspaceContext.test.ts +++ b/test/integration-tests/WorkspaceContext.test.ts @@ -211,14 +211,14 @@ suite("WorkspaceContext Test Suite", () => { test("get project templates", async () => { // This is only supported in swift versions >=5.8.0 - const swiftVersion = workspaceContext.toolchain.swiftVersion; + const swiftVersion = workspaceContext.globalToolchain.swiftVersion; if (swiftVersion.isLessThan(new Version(5, 8, 0))) { - assert.deepEqual(await workspaceContext.toolchain.getProjectTemplates(), []); + assert.deepEqual(await workspaceContext.globalToolchain.getProjectTemplates(), []); return; } // The output of `swift package init --help` will probably change at some point. // Just make sure that the most complex portions of the output are parsed correctly. - const projectTemplates = await workspaceContext.toolchain.getProjectTemplates(); + const projectTemplates = await workspaceContext.globalToolchain.getProjectTemplates(); // Contains multi-line description const toolTemplate = projectTemplates.find(template => template.id === "tool"); assert(toolTemplate); diff --git a/test/integration-tests/commands/build.test.ts b/test/integration-tests/commands/build.test.ts index e3109e0d3..338be0f5c 100644 --- a/test/integration-tests/commands/build.test.ts +++ b/test/integration-tests/commands/build.test.ts @@ -42,7 +42,7 @@ suite("Build Commands @slow", function () { // The description of this package is crashing on Windows with Swift 5.9.x and below if ( process.platform === "win32" && - ctx.toolchain.swiftVersion.isLessThanOrEqual(new Version(5, 9, 0)) + ctx.globalToolchain.swiftVersion.isLessThanOrEqual(new Version(5, 9, 0)) ) { this.skip(); } @@ -92,7 +92,7 @@ suite("Build Commands @slow", function () { // but "stackTrace" is the deterministic sync point we will use to make sure we can execute continue const bpPromise = waitForDebugAdapterRequest( "Debug PackageExe (defaultPackage)", - workspaceContext.toolchain.swiftVersion, + workspaceContext.globalToolchain.swiftVersion, "stackTrace" ); diff --git a/test/integration-tests/debugger/lldb.test.ts b/test/integration-tests/debugger/lldb.test.ts index 0f747bf91..4f505e0d8 100644 --- a/test/integration-tests/debugger/lldb.test.ts +++ b/test/integration-tests/debugger/lldb.test.ts @@ -27,8 +27,8 @@ suite("lldb contract test suite", () => { if ( process.env["CI"] && process.platform === "win32" && - ctx.swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0)) && - ctx.swiftVersion.isLessThan(new Version(6, 0, 2)) + ctx.globalToolchainSwiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0)) && + ctx.globalToolchainSwiftVersion.isLessThan(new Version(6, 0, 2)) ) { this.skip(); } @@ -38,7 +38,7 @@ suite("lldb contract test suite", () => { }); test("getLLDBLibPath Contract Test, make sure we can find lib LLDB", async () => { - const libPath = await getLLDBLibPath(workspaceContext.toolchain); + const libPath = await getLLDBLibPath(workspaceContext.globalToolchain); // Check the result for various platforms if (process.platform === "linux") { diff --git a/test/integration-tests/language/LanguageClientIntegration.test.ts b/test/integration-tests/language/LanguageClientIntegration.test.ts index 883cc49a1..25c90fb65 100644 --- a/test/integration-tests/language/LanguageClientIntegration.test.ts +++ b/test/integration-tests/language/LanguageClientIntegration.test.ts @@ -22,6 +22,7 @@ import { executeTaskAndWaitForResult, waitForNoRunningTasks } from "../../utilit import { getBuildAllTask, SwiftTask } from "../../../src/tasks/SwiftTaskProvider"; import { activateExtensionForSuite, folderInRootWorkspace } from "../utilities/testutilities"; import { waitForClientState, waitForIndex } from "../utilities/lsputilities"; +import { FolderContext } from "../../../src/FolderContext"; async function buildProject(ctx: WorkspaceContext, name: string) { await waitForNoRunningTasks(); @@ -36,22 +37,20 @@ suite("Language Client Integration Suite @slow", function () { this.timeout(3 * 60 * 1000); let clientManager: LanguageClientManager; - let workspaceContext: WorkspaceContext; + let folderContext: FolderContext; activateExtensionForSuite({ async setup(ctx) { - workspaceContext = ctx; - - await buildProject(ctx, "defaultPackage"); + folderContext = await buildProject(ctx, "defaultPackage"); // Ensure lsp client is ready - clientManager = ctx.languageClientManager; + clientManager = ctx.languageClientManager.get(folderContext); await waitForClientState(clientManager, langclient.State.Running); }, }); setup(async () => { - await waitForIndex(workspaceContext.languageClientManager); + await waitForIndex(clientManager, folderContext.swiftVersion); }); suite("Symbols", () => { diff --git a/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts b/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts index 4580f35a5..0e38ee061 100644 --- a/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts +++ b/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts @@ -184,7 +184,7 @@ suite("SwiftPluginTaskProvider Test Suite", function () { expect(swiftExecution).to.not.be.undefined; assert.deepEqual( swiftExecution.args, - workspaceContext.toolchain.buildFlags.withAdditionalFlags([ + workspaceContext.globalToolchain.buildFlags.withAdditionalFlags([ "package", ...expected, "command_plugin", @@ -205,6 +205,7 @@ suite("SwiftPluginTaskProvider Test Suite", function () { test("Exit code on success", async () => { const task = taskProvider.createSwiftPluginTask( folderContext.swiftPackage.plugins[0], + folderContext.toolchain, { cwd: folderContext.folder, scope: folderContext.workspaceFolder, @@ -222,6 +223,7 @@ suite("SwiftPluginTaskProvider Test Suite", function () { name: "not_a_command", package: "command-plugin", }, + folderContext.toolchain, { cwd: folderContext.folder, scope: folderContext.workspaceFolder, @@ -244,7 +246,7 @@ suite("SwiftPluginTaskProvider Test Suite", function () { test("provides", () => { expect(task?.execution.args).to.deep.equal( - workspaceContext.toolchain.buildFlags.withAdditionalFlags([ + folderContext.toolchain.buildFlags.withAdditionalFlags([ "package", "command_plugin", ]) diff --git a/test/integration-tests/tasks/SwiftTaskProvider.test.ts b/test/integration-tests/tasks/SwiftTaskProvider.test.ts index bbce1d457..bb1655a28 100644 --- a/test/integration-tests/tasks/SwiftTaskProvider.test.ts +++ b/test/integration-tests/tasks/SwiftTaskProvider.test.ts @@ -42,12 +42,12 @@ suite("SwiftTaskProvider Test Suite", () => { activateExtensionForSuite({ async setup(ctx) { workspaceContext = ctx; - toolchain = workspaceContext.toolchain; expect(workspaceContext.folders).to.not.have.lengthOf(0); workspaceFolder = workspaceContext.folders[0].workspaceFolder; // Make sure have another folder folderContext = await folderInRootWorkspace("diagnostics", workspaceContext); + toolchain = folderContext.toolchain; }, }); diff --git a/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts b/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts index 0770ac936..3c34eed79 100644 --- a/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts +++ b/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts @@ -49,6 +49,7 @@ import { import { Commands } from "../../../src/commands"; import { executeTaskAndWaitForResult } from "../../utilities/tasks"; import { createBuildAllTask } from "../../../src/tasks/SwiftTaskProvider"; +import { FolderContext } from "../../../src/FolderContext"; suite("Test Explorer Suite", function () { const MAX_TEST_RUN_TIME_MINUTES = 5; @@ -56,20 +57,21 @@ suite("Test Explorer Suite", function () { this.timeout(1000 * 60 * MAX_TEST_RUN_TIME_MINUTES); let workspaceContext: WorkspaceContext; + let folderContext: FolderContext; let testExplorer: TestExplorer; activateExtensionForSuite({ async setup(ctx) { workspaceContext = ctx; - const targetFolder = await folderInRootWorkspace("defaultPackage", workspaceContext); + folderContext = await folderInRootWorkspace("defaultPackage", workspaceContext); - if (!targetFolder) { + if (!folderContext) { throw new Error("Unable to find test explorer"); } - testExplorer = targetFolder.addTestExplorer(); + testExplorer = folderContext.addTestExplorer(); - await executeTaskAndWaitForResult(await createBuildAllTask(targetFolder)); + await executeTaskAndWaitForResult(await createBuildAllTask(folderContext)); // Set up the listener before bringing the text explorer in to focus, // which starts searching the workspace for tests. @@ -94,7 +96,7 @@ suite("Test Explorer Suite", function () { if ( // swift-testing was not able to produce JSON events until 6.0.2 on Windows. process.platform === "win32" && - workspaceContext.swiftVersion.isLessThan(new Version(6, 0, 2)) + workspaceContext.globalToolchainSwiftVersion.isLessThan(new Version(6, 0, 2)) ) { this.skip(); } @@ -111,7 +113,7 @@ suite("Test Explorer Suite", function () { let resetSettings: (() => Promise) | undefined; beforeEach(async function () { // lldb-dap is only present/functional in the toolchain in 6.0.2 and up. - if (workspaceContext.swiftVersion.isLessThan(new Version(6, 0, 2))) { + if (folderContext.swiftVersion.isLessThan(new Version(6, 0, 2))) { this.skip(); } @@ -155,14 +157,14 @@ suite("Test Explorer Suite", function () { test("Debugs specified XCTest test @slow", async function () { // CodeLLDB tests stall out on 5.9 and below. - if (workspaceContext.swiftVersion.isLessThan(new Version(5, 10, 0))) { + if (folderContext.swiftVersion.isLessThan(new Version(5, 10, 0))) { this.skip(); } await runXCTest(); }); test("Debugs specified swift-testing test", async function () { - if (workspaceContext.swiftVersion.isLessThan(new Version(6, 0, 0))) { + if (folderContext.swiftVersion.isLessThan(new Version(6, 0, 0))) { this.skip(); } await runSwiftTesting.call(this); @@ -172,7 +174,7 @@ suite("Test Explorer Suite", function () { suite("Standard", () => { test("Finds Tests", async function () { - if (workspaceContext.swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0))) { + if (folderContext.swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0))) { // 6.0 uses the LSP which returns tests in the order they're declared. // Includes swift-testing tests. assertTestControllerHierarchy(testExplorer.controller, [ @@ -205,7 +207,7 @@ suite("Test Explorer Suite", function () { ["testCrashing()"], ], ]); - } else if (workspaceContext.swiftVersion.isLessThan(new Version(6, 0, 0))) { + } else if (folderContext.swiftVersion.isLessThanOrEqual(new Version(6, 0, 0))) { // 5.10 uses `swift test list` which returns test alphabetically, without the round brackets. // Does not include swift-testing tests. assertTestControllerHierarchy(testExplorer.controller, [ @@ -233,10 +235,10 @@ suite("Test Explorer Suite", function () { suite("swift-testing", () => { suiteSetup(function () { if ( - workspaceContext.swiftVersion.isLessThan(new Version(6, 0, 0)) || + folderContext.swiftVersion.isLessThan(new Version(6, 0, 0)) || // swift-testing was not able to produce JSON events until 6.0.2 on Windows. (process.platform === "win32" && - workspaceContext.swiftVersion.isLessThan(new Version(6, 0, 2))) + folderContext.swiftVersion.isLessThan(new Version(6, 0, 2))) ) { this.skip(); } @@ -268,7 +270,7 @@ suite("Test Explorer Suite", function () { // Disabled until Attachments are formalized and released. test.skip("attachments", async function () { // Attachments were introduced in 6.1 - if (workspaceContext.swiftVersion.isLessThan(new Version(6, 1, 0))) { + if (folderContext.swiftVersion.isLessThan(new Version(6, 1, 0))) { this.skip(); } @@ -590,7 +592,7 @@ suite("Test Explorer Suite", function () { // as passed or failed with the message from the xunit xml. xcTestFailureMessage = runProfile === TestKind.parallel && - !workspaceContext.toolchain.hasMultiLineParallelTestOutput + !folderContext.toolchain.hasMultiLineParallelTestOutput ? "failed" : `failed - oh no`; }); @@ -599,9 +601,9 @@ suite("Test Explorer Suite", function () { suite(`swift-testing (${runProfile})`, function () { suiteSetup(function () { if ( - workspaceContext.swiftVersion.isLessThan(new Version(6, 0, 0)) || + folderContext.swiftVersion.isLessThan(new Version(6, 0, 0)) || (process.platform === "win32" && - workspaceContext.swiftVersion.isLessThan(new Version(6, 0, 2))) + folderContext.swiftVersion.isLessThan(new Version(6, 0, 2))) ) { this.skip(); } @@ -678,9 +680,7 @@ suite("Test Explorer Suite", function () { let passed: string[]; let failedId: string; - if ( - workspaceContext.swiftVersion.isGreaterThanOrEqual(new Version(6, 2, 0)) - ) { + if (folderContext.swiftVersion.isGreaterThanOrEqual(new Version(6, 2, 0))) { passed = [ `${testId}/PackageTests.swift:59:2/Parameterized test case ID: argumentIDs: [Testing.Test.Case.Argument.ID(bytes: [49])], discriminator: 0, isStable: true`, `${testId}/PackageTests.swift:59:2/Parameterized test case ID: argumentIDs: [Testing.Test.Case.Argument.ID(bytes: [51])], discriminator: 0, isStable: true`, diff --git a/test/integration-tests/testexplorer/XCTestOutputParser.test.ts b/test/integration-tests/testexplorer/XCTestOutputParser.test.ts index 2b2342f4c..5abbddc6b 100644 --- a/test/integration-tests/testexplorer/XCTestOutputParser.test.ts +++ b/test/integration-tests/testexplorer/XCTestOutputParser.test.ts @@ -73,7 +73,7 @@ ${tests.map( let hasMultiLineParallelTestOutput: boolean; activateExtensionForSuite({ async setup(ctx) { - hasMultiLineParallelTestOutput = ctx.toolchain.hasMultiLineParallelTestOutput; + hasMultiLineParallelTestOutput = ctx.globalToolchain.hasMultiLineParallelTestOutput; }, }); diff --git a/test/integration-tests/ui/ProjectPanelProvider.test.ts b/test/integration-tests/ui/ProjectPanelProvider.test.ts index 10a786178..9801f35b3 100644 --- a/test/integration-tests/ui/ProjectPanelProvider.test.ts +++ b/test/integration-tests/ui/ProjectPanelProvider.test.ts @@ -125,7 +125,7 @@ suite("ProjectPanelProvider Test Suite", function () { // In Swift 5.10 and below the build tasks are disabled while other tasks that could modify .build are running. // Typically because the extension has just started up in tests its `swift test list` that runs to gather tests // for the test explorer. If we're running 5.10 or below, poll for the build all task for up to 60 seconds. - if (workspaceContext.toolchain.swiftVersion.isLessThan(new Version(6, 0, 0))) { + if (workspaceContext.globalToolchain.swiftVersion.isLessThan(new Version(6, 0, 0))) { const startTime = Date.now(); let task: PackageNode | undefined; while (!task && Date.now() - startTime < 45 * 1000) { @@ -174,7 +174,9 @@ suite("ProjectPanelProvider Test Suite", function () { test("Executes a snippet", async function () { if ( process.platform === "win32" && - workspaceContext.toolchain.swiftVersion.isLessThanOrEqual(new Version(5, 9, 0)) + workspaceContext.globalToolchain.swiftVersion.isLessThanOrEqual( + new Version(5, 9, 0) + ) ) { this.skip(); } @@ -196,7 +198,9 @@ suite("ProjectPanelProvider Test Suite", function () { test("Includes commands", async function () { if ( process.platform === "win32" && - workspaceContext.toolchain.swiftVersion.isLessThanOrEqual(new Version(6, 0, 0)) + workspaceContext.globalToolchain.swiftVersion.isLessThanOrEqual( + new Version(6, 0, 0) + ) ) { this.skip(); } @@ -213,7 +217,9 @@ suite("ProjectPanelProvider Test Suite", function () { test("Executes a command", async function () { if ( process.platform === "win32" && - workspaceContext.toolchain.swiftVersion.isLessThanOrEqual(new Version(6, 0, 0)) + workspaceContext.globalToolchain.swiftVersion.isLessThanOrEqual( + new Version(6, 0, 0) + ) ) { this.skip(); } diff --git a/test/integration-tests/utilities/lsputilities.ts b/test/integration-tests/utilities/lsputilities.ts index 30173287d..7422d6731 100644 --- a/test/integration-tests/utilities/lsputilities.ts +++ b/test/integration-tests/utilities/lsputilities.ts @@ -49,8 +49,10 @@ export namespace WorkspaceSynchronizeRequest { langclient.MessageDirection.clientToServer; export const type = new langclient.RequestType(method); } -export async function waitForIndex(languageClientManager: LanguageClientManager): Promise { - const swiftVersion = languageClientManager.workspaceContext.swiftVersion; +export async function waitForIndex( + languageClientManager: LanguageClientManager, + swiftVersion: Version +): Promise { const requestType = swiftVersion.isGreaterThanOrEqual(new Version(6, 2, 0)) ? WorkspaceSynchronizeRequest.type : PollIndexRequest.type; diff --git a/test/integration-tests/utilities/testutilities.ts b/test/integration-tests/utilities/testutilities.ts index 510677be8..98c60dda0 100644 --- a/test/integration-tests/utilities/testutilities.ts +++ b/test/integration-tests/utilities/testutilities.ts @@ -79,7 +79,7 @@ const extensionBootstrapper = (() => { // https://github.com/swiftlang/sourcekit-lsp/commit/7e2d12a7a0d184cc820ae6af5ddbb8aa18b1501c if ( process.platform === "darwin" && - workspaceContext.toolchain.swiftVersion.isLessThan(new Version(6, 1, 0)) && + workspaceContext.globalToolchain.swiftVersion.isLessThan(new Version(6, 1, 0)) && requiresLSP ) { this.skip(); @@ -88,7 +88,7 @@ const extensionBootstrapper = (() => { this.skip(); } // CodeLLDB does not work with libllbd in Swift toolchains prior to 5.10 - if (workspaceContext.swiftVersion.isLessThan(new Version(5, 10, 0))) { + if (workspaceContext.globalToolchainSwiftVersion.isLessThan(new Version(5, 10, 0))) { restoreSettings = await updateSettings({ "swift.debugger.setupCodeLLDB": "never", }); diff --git a/test/unit-tests/debugger/debugAdapterFactory.test.ts b/test/unit-tests/debugger/debugAdapterFactory.test.ts index ebc48f7e1..7aecbb0a1 100644 --- a/test/unit-tests/debugger/debugAdapterFactory.test.ts +++ b/test/unit-tests/debugger/debugAdapterFactory.test.ts @@ -32,8 +32,11 @@ import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel"; import * as debugAdapter from "../../../src/debugger/debugAdapter"; import { Result } from "../../../src/utilities/result"; import configuration from "../../../src/configuration"; +import { WorkspaceContext } from "../../../src/WorkspaceContext"; +import { FolderContext } from "../../../src/FolderContext"; suite("LLDBDebugConfigurationProvider Tests", () => { + let mockWorkspaceContext: MockedObject; let mockToolchain: MockedObject; let mockOutputChannel: MockedObject; const mockDebugAdapter = mockGlobalObject(debugAdapter, "DebugAdapter"); @@ -44,12 +47,19 @@ suite("LLDBDebugConfigurationProvider Tests", () => { mockOutputChannel = mockObject({ log: mockFn(), }); + mockWorkspaceContext = mockObject({ + globalToolchain: instance(mockToolchain), + globalToolchainSwiftVersion: new Version(6, 0, 0), + outputChannel: instance(mockOutputChannel), + subscriptions: [], + folders: [], + }); }); test("allows specifying a 'pid' in the launch configuration", async () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockToolchain), + instance(mockWorkspaceContext), instance(mockOutputChannel) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( @@ -67,7 +77,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { test("converts 'pid' property from a string to a number", async () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockToolchain), + instance(mockWorkspaceContext), instance(mockOutputChannel) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( @@ -88,7 +98,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockToolchain), + instance(mockWorkspaceContext), instance(mockOutputChannel) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( @@ -109,7 +119,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockToolchain), + instance(mockWorkspaceContext), instance(mockOutputChannel) ); const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( @@ -150,7 +160,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { test("returns a launch configuration that uses CodeLLDB as the debug adapter", async () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockToolchain), + instance(mockWorkspaceContext), instance(mockOutputChannel) ); const launchConfig = @@ -168,7 +178,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { mockWindow.showErrorMessage.resolves("Install CodeLLDB" as any); const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockToolchain), + instance(mockWorkspaceContext), instance(mockOutputChannel) ); await expect( @@ -190,7 +200,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { mockWindow.showInformationMessage.resolves("Global" as any); const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockToolchain), + instance(mockWorkspaceContext), instance(mockOutputChannel) ); await expect( @@ -213,7 +223,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { mockLldbConfiguration.get.withArgs("library").returns(undefined); const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockToolchain), + instance(mockWorkspaceContext), instance(mockOutputChannel) ); await expect( @@ -248,7 +258,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { test("returns a launch configuration that uses lldb-dap as the debug adapter", async () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockToolchain), + instance(mockWorkspaceContext), instance(mockOutputChannel) ); const launchConfig = @@ -268,7 +278,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { mockFS({}); // Reset mockFS so that no files exist const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockToolchain), + instance(mockWorkspaceContext), instance(mockOutputChannel) ); await expect( @@ -285,7 +295,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { test("modifies program to add file extension on Windows", async () => { const configProvider = new LLDBDebugConfigurationProvider( "win32", - instance(mockToolchain), + instance(mockWorkspaceContext), instance(mockOutputChannel) ); const launchConfig = @@ -303,7 +313,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { test("does not modify program on Windows if file extension is already present", async () => { const configProvider = new LLDBDebugConfigurationProvider( "win32", - instance(mockToolchain), + instance(mockWorkspaceContext), instance(mockOutputChannel) ); const launchConfig = @@ -321,7 +331,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { test("does not modify program on macOS", async () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockToolchain), + instance(mockWorkspaceContext), instance(mockOutputChannel) ); const launchConfig = @@ -339,7 +349,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { test("does not modify program on Linux", async () => { const configProvider = new LLDBDebugConfigurationProvider( "linux", - instance(mockToolchain), + instance(mockWorkspaceContext), instance(mockOutputChannel) ); const launchConfig = @@ -357,7 +367,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { test("should convert environment variables to string[] format when using lldb-dap", async () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockToolchain), + instance(mockWorkspaceContext), instance(mockOutputChannel) ); const launchConfig = @@ -379,7 +389,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { test("should leave env undefined when environment variables are undefined and using lldb-dap", async () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockToolchain), + instance(mockWorkspaceContext), instance(mockOutputChannel) ); const launchConfig = @@ -395,7 +405,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { test("should convert empty environment variables when using lldb-dap", async () => { const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockToolchain), + instance(mockWorkspaceContext), instance(mockOutputChannel) ); const launchConfig = @@ -417,7 +427,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { } const configProvider = new LLDBDebugConfigurationProvider( "darwin", - instance(mockToolchain), + instance(mockWorkspaceContext), instance(mockOutputChannel) ); const launchConfig = @@ -433,4 +443,47 @@ suite("LLDBDebugConfigurationProvider Tests", () => { expect(launchConfig).to.have.property("env").that.deep.equals(expectedEnv); }); }); + + test("debugs with the toolchain of the supplied folder", async () => { + const debugAdapterPath = "/path/to/lldb-dap"; + mockDebugAdapter.getLaunchConfigType.returns(LaunchConfigType.LLDB_DAP); + mockDebugAdapter.getLLDBDebugAdapterPath.calledOnceWithExactly(mockToolchain); + mockDebugAdapter.getLLDBDebugAdapterPath.resolves(debugAdapterPath); + mockFS({ + [debugAdapterPath]: mockFS.file({ content: "", mode: 0o770 }), + }); + mockToolchain = mockObject({ swiftVersion: new Version(5, 10, 0) }); + const mockFolder = mockObject({ + isRootFolder: false, + folder: vscode.Uri.file("/folder"), + workspaceFolder: { + uri: vscode.Uri.file("/folder"), + name: "folder", + index: 1, + }, + toolchain: instance(mockToolchain), + }); + mockWorkspaceContext.folders.push(instance(mockFolder)); + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockWorkspaceContext), + instance(mockOutputChannel) + ); + const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( + { + uri: vscode.Uri.file("/folder"), + name: "folder", + index: 1, + }, + { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + } + ); + expect(launchConfig).to.containSubset({ + debugAdapterExecutable: debugAdapterPath, + }); + }); }); diff --git a/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts b/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts index 421fa0499..f57e58168 100644 --- a/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts +++ b/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts @@ -41,6 +41,7 @@ import { StateChangeEvent, } from "vscode-languageclient/node"; import { LanguageClientManager } from "../../../src/sourcekit-lsp/LanguageClientManager"; +import { LanguageClientToolchainCoordinator } from "../../../src/sourcekit-lsp/LanguageClientToolchainCoordinator"; import configuration from "../../../src/configuration"; import { FolderContext } from "../../../src/FolderContext"; import { LanguageClientFactory } from "../../../src/sourcekit-lsp/LanguageClientFactory"; @@ -56,6 +57,7 @@ suite("LanguageClientManager Suite", () => { let mockedConverter: MockedObject; let changeStateEmitter: AsyncEventEmitter; let mockedWorkspace: MockedObject; + let mockedFolder: MockedObject; let didChangeFoldersEmitter: AsyncEventEmitter; let mockedOutputChannel: MockedObject; let mockedToolchain: MockedObject; @@ -106,14 +108,33 @@ suite("LanguageClientManager Suite", () => { mockedOutputChannel = mockObject({ log: s => s, logDiagnostic: s => s, + appendLine: () => {}, }); didChangeFoldersEmitter = new AsyncEventEmitter(); - mockedWorkspace = mockObject({ - toolchain: instance(mockedToolchain), + mockedFolder = mockObject({ + isRootFolder: false, + folder: vscode.Uri.file("/folder1"), + workspaceFolder: { + uri: vscode.Uri.file("/folder1"), + name: "folder1", + index: 0, + }, + workspaceContext: instance( + mockObject({ + globalToolchain: instance(mockedToolchain), + globalToolchainSwiftVersion: new Version(6, 0, 0), + outputChannel: instance(mockedOutputChannel), + }) + ), swiftVersion: new Version(6, 0, 0), + toolchain: instance(mockedToolchain), + }); + mockedWorkspace = mockObject({ + globalToolchain: instance(mockedToolchain), + globalToolchainSwiftVersion: new Version(6, 0, 0), outputChannel: instance(mockedOutputChannel), subscriptions: [], - folders: [], + folders: [instance(mockedFolder)], onDidChangeFolders: mockFn(s => s.callsFake(didChangeFoldersEmitter.event)), }); mockedConverter = mockObject({ @@ -190,10 +211,18 @@ suite("LanguageClientManager Suite", () => { }); test("launches SourceKit-LSP on startup", async () => { - const sut = new LanguageClientManager(instance(mockedWorkspace), languageClientFactoryMock); + const factory = new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + languageClientFactoryMock + ); + + const sut = factory.get(instance(mockedFolder)); await waitForReturnedPromises(languageClientMock.start); - expect(sut.state).to.equal(State.Running); + expect(sut.state).to.equal( + State.Running, + "Expected LSP client to be running but it wasn't" + ); expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnceWith( /* id */ match.string, /* name */ match.string, @@ -205,11 +234,18 @@ suite("LanguageClientManager Suite", () => { test("launches SourceKit-LSP on startup with swiftSDK", async () => { mockedConfig.swiftSDK = "arm64-apple-ios"; + const factory = new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + languageClientFactoryMock + ); - const sut = new LanguageClientManager(instance(mockedWorkspace), languageClientFactoryMock); + const sut = factory.get(instance(mockedFolder)); await waitForReturnedPromises(languageClientMock.start); - expect(sut.state).to.equal(State.Running); + expect(sut.state).to.equal( + State.Running, + "Expected LSP client to be running but it wasn't" + ); expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnceWith( /* id */ match.string, /* name */ match.string, @@ -223,10 +259,13 @@ suite("LanguageClientManager Suite", () => { }); test("chooses the correct backgroundIndexing value is auto, swift version if 6.0.0", async () => { - mockedWorkspace.swiftVersion = new Version(6, 0, 0); + mockedFolder.swiftVersion = new Version(6, 0, 0); mockedConfig.backgroundIndexing = "auto"; - new LanguageClientManager(instance(mockedWorkspace), languageClientFactoryMock); + new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + languageClientFactoryMock + ); await waitForReturnedPromises(languageClientMock.start); expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnceWith( @@ -238,10 +277,13 @@ suite("LanguageClientManager Suite", () => { }); test("chooses the correct backgroundIndexing value is auto, swift version if 6.1.0", async () => { - mockedWorkspace.swiftVersion = new Version(6, 1, 0); + mockedFolder.swiftVersion = new Version(6, 1, 0); mockedConfig.backgroundIndexing = "auto"; - new LanguageClientManager(instance(mockedWorkspace), languageClientFactoryMock); + new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + languageClientFactoryMock + ); await waitForReturnedPromises(languageClientMock.start); expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnceWith( @@ -253,10 +295,13 @@ suite("LanguageClientManager Suite", () => { }); test("chooses the correct backgroundIndexing value is true, swift version if 6.0.0", async () => { - mockedWorkspace.swiftVersion = new Version(6, 0, 0); + mockedFolder.swiftVersion = new Version(6, 0, 0); mockedConfig.backgroundIndexing = "on"; - new LanguageClientManager(instance(mockedWorkspace), languageClientFactoryMock); + new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + languageClientFactoryMock + ); await waitForReturnedPromises(languageClientMock.start); expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnceWith( @@ -270,39 +315,48 @@ suite("LanguageClientManager Suite", () => { test("notifies SourceKit-LSP of WorkspaceFolder changes", async () => { const folder1 = mockObject({ isRootFolder: false, - folder: vscode.Uri.file("/folder1"), + folder: vscode.Uri.file("/folder11"), workspaceFolder: { - uri: vscode.Uri.file("/folder1"), - name: "folder1", + uri: vscode.Uri.file("/folder11"), + name: "folder11", index: 0, }, workspaceContext: instance(mockedWorkspace), + swiftVersion: new Version(6, 0, 0), }); const folder2 = mockObject({ isRootFolder: false, - folder: vscode.Uri.file("/folder2"), + folder: vscode.Uri.file("/folder22"), workspaceFolder: { - uri: vscode.Uri.file("/folder2"), - name: "folder2", + uri: vscode.Uri.file("/folder22"), + name: "folder22", index: 1, }, workspaceContext: instance(mockedWorkspace), + swiftVersion: new Version(6, 0, 0), }); - new LanguageClientManager(instance(mockedWorkspace), languageClientFactoryMock); + + new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + languageClientFactoryMock + ); await waitForReturnedPromises(languageClientMock.start); // Add the first folder mockedWorkspace.folders.push(instance(folder1)); + + languageClientMock.sendNotification.resetHistory(); await didChangeFoldersEmitter.fire({ operation: FolderOperation.add, folder: instance(folder1), workspace: instance(mockedWorkspace), }); - expect(languageClientMock.sendNotification).to.have.been.calledOnceWith( + + expect(languageClientMock.sendNotification).to.have.been.calledWithExactly( DidChangeWorkspaceFoldersNotification.type, { event: { - added: [{ name: "folder1", uri: path.normalize("/folder1") }], + added: [{ name: "folder11", uri: path.normalize("/folder11") }], removed: [], }, } as DidChangeWorkspaceFoldersParams @@ -317,11 +371,11 @@ suite("LanguageClientManager Suite", () => { folder: instance(folder2), workspace: instance(mockedWorkspace), }); - expect(languageClientMock.sendNotification).to.have.been.calledOnceWith( + expect(languageClientMock.sendNotification).to.have.been.calledWithExactly( DidChangeWorkspaceFoldersNotification.type, { event: { - added: [{ name: "folder2", uri: path.normalize("/folder2") }], + added: [{ name: "folder22", uri: path.normalize("/folder22") }], removed: [], }, } as DidChangeWorkspaceFoldersParams @@ -336,12 +390,12 @@ suite("LanguageClientManager Suite", () => { folder: instance(folder1), workspace: instance(mockedWorkspace), }); - expect(languageClientMock.sendNotification).to.have.been.calledWith( + expect(languageClientMock.sendNotification).to.have.been.calledWithExactly( DidChangeWorkspaceFoldersNotification.type, { event: { added: [], - removed: [{ name: "folder1", uri: path.normalize("/folder1") }], + removed: [{ name: "folder11", uri: path.normalize("/folder11") }], }, } as DidChangeWorkspaceFoldersParams ); @@ -349,7 +403,7 @@ suite("LanguageClientManager Suite", () => { test("doesn't launch SourceKit-LSP if disabled by the user", async () => { mockedLspConfig.disable = true; - const sut = new LanguageClientManager(instance(mockedWorkspace), languageClientFactoryMock); + const sut = new LanguageClientManager(instance(mockedFolder), languageClientFactoryMock); await waitForReturnedPromises(languageClientMock.start); expect(sut.state).to.equal(State.Stopped); @@ -359,10 +413,18 @@ suite("LanguageClientManager Suite", () => { test("user can provide a custom SourceKit-LSP executable", async () => { mockedLspConfig.serverPath = "/path/to/my/custom/sourcekit-lsp"; - const sut = new LanguageClientManager(instance(mockedWorkspace), languageClientFactoryMock); + const factory = new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + languageClientFactoryMock + ); + + const sut = factory.get(instance(mockedFolder)); await waitForReturnedPromises(languageClientMock.start); - expect(sut.state).to.equal(State.Running); + expect(sut.state).to.equal( + State.Running, + "Expected LSP client to be running but it wasn't" + ); expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnceWith( /* id */ match.string, /* name */ match.string, @@ -402,7 +464,11 @@ suite("LanguageClientManager Suite", () => { ]; }; - new LanguageClientManager(instance(mockedWorkspace), languageClientFactoryMock); + new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + languageClientFactoryMock + ); + await waitForReturnedPromises(languageClientMock.start); expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnce; @@ -442,7 +508,7 @@ suite("LanguageClientManager Suite", () => { const mockWindow = mockGlobalObject(vscode, "window"); setup(() => { - mockedWorkspace.swiftVersion = new Version(6, 1, 0); + mockedWorkspace.globalToolchainSwiftVersion = new Version(6, 1, 0); }); test("Notifies when the active document changes", async () => { @@ -458,7 +524,7 @@ suite("LanguageClientManager Suite", () => { return { dispose: () => {} }; }); - new LanguageClientManager(instance(mockedWorkspace), languageClientFactoryMock); + new LanguageClientManager(instance(mockedFolder), languageClientFactoryMock); await waitForReturnedPromises(languageClientMock.start); const activeDocumentManager = new LSPActiveDocumentManager(); @@ -490,7 +556,7 @@ suite("LanguageClientManager Suite", () => { document, }) ); - new LanguageClientManager(instance(mockedWorkspace), languageClientFactoryMock); + new LanguageClientManager(instance(mockedFolder), languageClientFactoryMock); await waitForReturnedPromises(languageClientMock.start); const activeDocumentManager = new LSPActiveDocumentManager(); @@ -515,16 +581,34 @@ suite("LanguageClientManager Suite", () => { setup(() => { mockedToolchain.swiftVersion = new Version(5, 6, 0); - mockedWorkspace.swiftVersion = new Version(5, 6, 0); + mockedWorkspace.globalToolchainSwiftVersion = new Version(5, 6, 0); + const workspaceFolder = { + uri: vscode.Uri.file("/folder1"), + name: "folder1", + index: 0, + }; + const folderContext = mockObject({ + workspaceContext: instance(mockedWorkspace), + workspaceFolder, + toolchain: instance(mockedToolchain), + }); + mockedFolder.swiftVersion = mockedToolchain.swiftVersion; + mockedWorkspace = mockObject({ + ...mockedWorkspace, + globalToolchain: instance(mockedToolchain), + currentFolder: instance(folderContext), + get globalToolchainSwiftVersion() { + return mockedToolchain.swiftVersion; + }, + folders: [instance(mockedFolder)], + }); folder1 = mockObject({ isRootFolder: false, folder: vscode.Uri.file("/folder1"), - workspaceFolder: { - uri: vscode.Uri.file("/folder1"), - name: "folder1", - index: 0, - }, + workspaceFolder, workspaceContext: instance(mockedWorkspace), + toolchain: instance(mockedToolchain), + swiftVersion: mockedToolchain.swiftVersion, }); folder2 = mockObject({ isRootFolder: false, @@ -535,12 +619,14 @@ suite("LanguageClientManager Suite", () => { index: 1, }, workspaceContext: instance(mockedWorkspace), + toolchain: instance(mockedToolchain), + swiftVersion: mockedToolchain.swiftVersion, }); }); test("doesn't launch SourceKit-LSP on startup", async () => { const sut = new LanguageClientManager( - instance(mockedWorkspace), + instance(mockedFolder), languageClientFactoryMock ); await waitForReturnedPromises(languageClientMock.start); @@ -560,10 +646,12 @@ suite("LanguageClientManager Suite", () => { ), }) ); - const sut = new LanguageClientManager( + const factory = new LanguageClientToolchainCoordinator( instance(mockedWorkspace), languageClientFactoryMock ); + + const sut = factory.get(instance(mockedFolder)); await waitForReturnedPromises(languageClientMock.start); // Add the folder to the workspace @@ -573,14 +661,17 @@ suite("LanguageClientManager Suite", () => { workspace: instance(mockedWorkspace), }); - expect(sut.state).to.equal(State.Running); - expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnceWith( + expect(sut.state).to.equal( + State.Running, + "Expected LSP client to be running but it wasn't" + ); + expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledWith( /* id */ match.string, /* name */ match.string, /* serverOptions */ match.object, /* clientOptions */ match.hasNested("workspaceFolder.uri.path", "/folder1") ); - expect(languageClientMock.start).to.have.been.calledOnce; + expect(languageClientMock.start).to.have.been.called; }); test("changes SourceKit-LSP's workspaceFolder when a new folder is focussed", async () => { @@ -592,19 +683,13 @@ suite("LanguageClientManager Suite", () => { document: instance(mockedTextDocument), }) ); - const sut = new LanguageClientManager( + const factory = new LanguageClientToolchainCoordinator( instance(mockedWorkspace), languageClientFactoryMock ); - await waitForReturnedPromises(languageClientMock.start); - // Add the first folder to the workspace - mockedTextDocument.uri = vscode.Uri.file("/folder1/file.swift"); - await didChangeFoldersEmitter.fire({ - operation: FolderOperation.add, - folder: instance(folder1), - workspace: instance(mockedWorkspace), - }); + const sut = factory.get(instance(mockedFolder)); + await waitForReturnedPromises(languageClientMock.start); // Trigger a focus event for the second folder mockedTextDocument.uri = vscode.Uri.file("/folder2/file.swift"); @@ -614,7 +699,10 @@ suite("LanguageClientManager Suite", () => { workspace: instance(mockedWorkspace), }); - expect(sut.state).to.equal(State.Running); + expect(sut.state).to.equal( + State.Running, + "Expected LSP client to be running but it wasn't" + ); expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledTwice; expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledWith( /* id */ match.string, diff --git a/test/unit-tests/tasks/SwiftPluginTaskProvider.test.ts b/test/unit-tests/tasks/SwiftPluginTaskProvider.test.ts index c7d54d27c..313023996 100644 --- a/test/unit-tests/tasks/SwiftPluginTaskProvider.test.ts +++ b/test/unit-tests/tasks/SwiftPluginTaskProvider.test.ts @@ -23,6 +23,7 @@ import { SwiftExecution } from "../../../src/tasks/SwiftExecution"; import { Version } from "../../../src/utilities/version"; import { BuildFlags } from "../../../src/toolchain/BuildFlags"; import { instance, MockedObject, mockFn, mockObject } from "../../MockUtils"; +import { FolderContext } from "../../../src/FolderContext"; suite("SwiftPluginTaskProvider Unit Test Suite", () => { let workspaceContext: MockedObject; @@ -39,14 +40,14 @@ suite("SwiftPluginTaskProvider Unit Test Suite", () => { buildFlags: instance(buildFlags), getToolchainExecutable: mockFn(s => s.withArgs("swift").returns("/path/to/bin/swift")), }); - workspaceContext = mockObject({ + const folderContext = mockObject({ + workspaceContext: instance(workspaceContext), + workspaceFolder, toolchain: instance(toolchain), - get swiftVersion() { - return toolchain.swiftVersion; - }, - set swiftVersion(version) { - toolchain.swiftVersion = version; - }, + }); + workspaceContext = mockObject({ + globalToolchain: instance(toolchain), + currentFolder: instance(folderContext), }); workspaceFolder = { uri: vscode.Uri.file("/path/to/workspace"), diff --git a/test/unit-tests/tasks/SwiftTaskProvider.test.ts b/test/unit-tests/tasks/SwiftTaskProvider.test.ts index a0f340346..324b3668e 100644 --- a/test/unit-tests/tasks/SwiftTaskProvider.test.ts +++ b/test/unit-tests/tasks/SwiftTaskProvider.test.ts @@ -57,14 +57,14 @@ suite("SwiftTaskProvider Unit Test Suite", () => { sanitizer: mockFn(), getToolchainExecutable: mockFn(s => s.withArgs("swift").returns("/path/to/bin/swift")), }); - workspaceContext = mockObject({ + const folderContext = mockObject({ + workspaceContext: instance(workspaceContext), + workspaceFolder, toolchain: instance(toolchain), - get swiftVersion() { - return toolchain.swiftVersion; - }, - set swiftVersion(value) { - toolchain.swiftVersion = value; - }, + }); + workspaceContext = mockObject({ + globalToolchain: instance(toolchain), + currentFolder: instance(folderContext), }); workspaceFolder = { uri: vscode.Uri.file("/path/to/workspace"), From 905a6bd2e8d2acbb8434e24a941670ebaf5fe4ff Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Thu, 3 Apr 2025 11:48:39 -0400 Subject: [PATCH 2/7] Dont add folder to sourcekit-lsp on startup unless singleServer isn't supported --- src/sourcekit-lsp/LanguageClientManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sourcekit-lsp/LanguageClientManager.ts b/src/sourcekit-lsp/LanguageClientManager.ts index 7acd8373b..c2537f476 100644 --- a/src/sourcekit-lsp/LanguageClientManager.ts +++ b/src/sourcekit-lsp/LanguageClientManager.ts @@ -409,7 +409,7 @@ export class LanguageClientManager implements vscode.Disposable { const serverOptions: ServerOptions = sourcekit; let workspaceFolder = undefined; - if (folder) { + if (folder && !this.singleServerSupport) { workspaceFolder = { uri: folder.folder, name: FolderContext.uriName(folder.folder), From b02b77fb471be511051ef1b035689c58cfecdf4a Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Thu, 3 Apr 2025 11:49:11 -0400 Subject: [PATCH 3/7] Don't tell sourcekit-lsp about .swift-version files --- src/sourcekit-lsp/LanguageClientConfiguration.ts | 12 +++++++++--- src/sourcekit-lsp/inlayHints.ts | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/sourcekit-lsp/LanguageClientConfiguration.ts b/src/sourcekit-lsp/LanguageClientConfiguration.ts index 476e340d6..e9aac2949 100644 --- a/src/sourcekit-lsp/LanguageClientConfiguration.ts +++ b/src/sourcekit-lsp/LanguageClientConfiguration.ts @@ -112,7 +112,7 @@ export class LanguagerClientDocumentSelectors { { scheme: "file", language: "plaintext", pattern: "**/.swift-version" }, ]; - static allHandledDocumentTypes(): DocumentSelector { + static sourcekitLSPDocumentTypes(): DocumentSelector { let documentSelector: SourceKitDocumentSelector; switch (configuration.lsp.supportCFamily) { case "enable": @@ -142,9 +142,15 @@ export class LanguagerClientDocumentSelectors { return configuration.lsp.supportedLanguages.includes(doc.language); }); documentSelector.push(...LanguagerClientDocumentSelectors.documentationDocumentSelector); - documentSelector.push(...LanguagerClientDocumentSelectors.miscelaneousDocumentSelector); return documentSelector; } + + static allHandledDocumentTypes(): DocumentSelector { + return [ + ...this.sourcekitLSPDocumentTypes(), + ...LanguagerClientDocumentSelectors.miscelaneousDocumentSelector, + ]; + } } export function lspClientOptions( @@ -159,7 +165,7 @@ export function lspClientOptions( ) => void ): LanguageClientOptions { return { - documentSelector: LanguagerClientDocumentSelectors.allHandledDocumentTypes(), + documentSelector: LanguagerClientDocumentSelectors.sourcekitLSPDocumentTypes(), revealOutputChannelOn: RevealOutputChannelOn.Never, workspaceFolder: workspaceFolder, outputChannel: new SwiftOutputChannel("SourceKit Language Server"), diff --git a/src/sourcekit-lsp/inlayHints.ts b/src/sourcekit-lsp/inlayHints.ts index 88c0cd51e..e403a5dde 100644 --- a/src/sourcekit-lsp/inlayHints.ts +++ b/src/sourcekit-lsp/inlayHints.ts @@ -68,7 +68,7 @@ class SwiftLegacyInlayHintsProvider implements vscode.InlayHintsProvider { /** activate the inlay hints */ export function activateLegacyInlayHints(client: langclient.LanguageClient): vscode.Disposable { const inlayHint = vscode.languages.registerInlayHintsProvider( - LanguagerClientDocumentSelectors.allHandledDocumentTypes(), + LanguagerClientDocumentSelectors.sourcekitLSPDocumentTypes(), new SwiftLegacyInlayHintsProvider(client) ); From 54cee280e5b2aad2beb7524a477a12847c7cbc09 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Thu, 17 Apr 2025 16:00:11 -0400 Subject: [PATCH 4/7] Refresh workspace state on folder context creation --- src/PackageWatcher.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/PackageWatcher.ts b/src/PackageWatcher.ts index 1876d9904..1f1b7255d 100644 --- a/src/PackageWatcher.ts +++ b/src/PackageWatcher.ts @@ -46,7 +46,7 @@ export class PackageWatcher { async install() { this.packageFileWatcher = this.createPackageFileWatcher(); this.resolvedFileWatcher = this.createResolvedFileWatcher(); - this.workspaceStateFileWatcher = this.createWorkspaceStateFileWatcher(); + this.workspaceStateFileWatcher = await this.createWorkspaceStateFileWatcher(); this.snippetWatcher = this.createSnippetFileWatcher(); this.swiftVersionFileWatcher = await this.createSwiftVersionFileWatcher(); } @@ -83,7 +83,7 @@ export class PackageWatcher { return watcher; } - private createWorkspaceStateFileWatcher(): vscode.FileSystemWatcher { + private async createWorkspaceStateFileWatcher(): Promise { const uri = vscode.Uri.joinPath( vscode.Uri.file( BuildFlags.buildDirectoryFromWorkspacePath(this.folderContext.folder.fsPath, true) @@ -94,6 +94,16 @@ export class PackageWatcher { watcher.onDidCreate(async () => await this.handleWorkspaceStateChange()); watcher.onDidChange(async () => await this.handleWorkspaceStateChange()); watcher.onDidDelete(async () => await this.handleWorkspaceStateChange()); + + const fileExists = await fs + .access(uri.fsPath) + .then(() => true) + .catch(() => false); + + if (fileExists) { + await this.handleWorkspaceStateChange(); + } + return watcher; } From eb44590934ed399b60079d8f9197355a350b0f6e Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Tue, 22 Apr 2025 14:00:40 -0400 Subject: [PATCH 5/7] Add swiftly documentation --- userdocs/userdocs.docc/supported-toolchains.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/userdocs/userdocs.docc/supported-toolchains.md b/userdocs/userdocs.docc/supported-toolchains.md index b2eb13cfb..819df90ad 100644 --- a/userdocs/userdocs.docc/supported-toolchains.md +++ b/userdocs/userdocs.docc/supported-toolchains.md @@ -16,3 +16,16 @@ Feature | Minimum Toolchain Required ------------------------ | ------------------------------------- lldb-dap debugging | 6.0 +## Swiftly Support + +The extension supports toolchains managed by [swiftly](https://github.com/swiftlang/swiftly), the Swift toolchain installer and manager. For instructions on installing swiftly see the [installation instructions on Swift.org](https://www.swift.org/install). + +You can choose a swiftly managed toolchain to use from the `> Swift: Select Toolchain` menu. + +If you do `swiftly use` on the command line you must restart VS Code or do `> Developer: Reload Window` in order for the VS Code Swift extension to start using the new toolchain. + +### `.swift-version` Support + +Swiftly can use a special `.swift-version` file in the root of your package so that you can share your toolchain preference with the rest of your team. The VS Code Swift extension respects this file if it exists and will use the toolchain specified within it to build and test your package. + +For more information on the `.swift-version` file see swiftly's documentation on [sharing recommended toolchain versions](https://swiftpackageindex.com/swiftlang/swiftly/main/documentation/swiftlydocs/use-toolchains#Sharing-recommended-toolchain-versions). From d4804a21143d8f9dc77279f434452bd127c73326 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Tue, 22 Apr 2025 15:00:29 -0400 Subject: [PATCH 6/7] Address some feedback --- src/PackageWatcher.ts | 31 ++++++++----------- src/debugger/debugAdapterFactory.ts | 8 ++--- .../LanguageClientToolchainCoordinator.ts | 6 ++-- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/src/PackageWatcher.ts b/src/PackageWatcher.ts index 1f1b7255d..46b189efd 100644 --- a/src/PackageWatcher.ts +++ b/src/PackageWatcher.ts @@ -19,6 +19,7 @@ import { FolderContext } from "./FolderContext"; import { FolderOperation, WorkspaceContext } from "./WorkspaceContext"; import { BuildFlags } from "./toolchain/BuildFlags"; import { Version } from "./utilities/version"; +import { fileExists } from "./utilities/filesystem"; /** * Watches for changes to **Package.swift** and **Package.resolved**. @@ -95,12 +96,7 @@ export class PackageWatcher { watcher.onDidChange(async () => await this.handleWorkspaceStateChange()); watcher.onDidDelete(async () => await this.handleWorkspaceStateChange()); - const fileExists = await fs - .access(uri.fsPath) - .then(() => true) - .catch(() => false); - - if (fileExists) { + if (await fileExists(uri.fsPath)) { await this.handleWorkspaceStateChange(); } @@ -129,18 +125,14 @@ export class PackageWatcher { } async handleSwiftVersionFileChange() { - try { - const version = await this.readSwiftVersionFile(); - if (version && version.toString() !== this.currentVersion?.toString()) { - this.workspaceContext.fireEvent( - this.folderContext, - FolderOperation.swiftVersionUpdated - ); - } - this.currentVersion = version ?? this.folderContext.toolchain.swiftVersion; - } catch { - // do nothing + const version = await this.readSwiftVersionFile(); + if (version && version.toString() !== this.currentVersion?.toString()) { + this.workspaceContext.fireEvent( + this.folderContext, + FolderOperation.swiftVersionUpdated + ); } + this.currentVersion = version ?? this.folderContext.toolchain.swiftVersion; } private async readSwiftVersionFile() { @@ -148,7 +140,10 @@ export class PackageWatcher { try { const contents = await fs.readFile(versionFile); return Version.fromString(contents.toString().trim()); - } catch { + } catch (error) { + this.workspaceContext.outputChannel.appendLine( + `Failed to read .swift-version file at ${versionFile}: ${error}` + ); return undefined; } } diff --git a/src/debugger/debugAdapterFactory.ts b/src/debugger/debugAdapterFactory.ts index 106df2318..49adb2fe0 100644 --- a/src/debugger/debugAdapterFactory.ts +++ b/src/debugger/debugAdapterFactory.ts @@ -96,13 +96,13 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration ) {} async resolveDebugConfigurationWithSubstitutedVariables( - _folder: vscode.WorkspaceFolder | undefined, + folder: vscode.WorkspaceFolder | undefined, launchConfig: vscode.DebugConfiguration ): Promise { - const folder = this.workspaceContext.folders.find( - folder => folder.workspaceFolder.uri.fsPath === _folder?.uri.fsPath + const workspaceFolder = this.workspaceContext.folders.find( + f => f.workspaceFolder.uri.fsPath === folder?.uri.fsPath ); - const toolchain = folder?.toolchain ?? this.workspaceContext.globalToolchain; + const toolchain = workspaceFolder?.toolchain ?? this.workspaceContext.globalToolchain; // Fix the program path on Windows to include the ".exe" extension if ( diff --git a/src/sourcekit-lsp/LanguageClientToolchainCoordinator.ts b/src/sourcekit-lsp/LanguageClientToolchainCoordinator.ts index eb854700d..ffc31cdcf 100644 --- a/src/sourcekit-lsp/LanguageClientToolchainCoordinator.ts +++ b/src/sourcekit-lsp/LanguageClientToolchainCoordinator.ts @@ -23,8 +23,8 @@ import { FolderOperation, WorkspaceContext } from "../WorkspaceContext"; * Manages the creation of LanguageClient instances for workspace folders. * * A LanguageClient will be created for each unique toolchain version. If two - * folders share the same toolchain version then they will share the same LanaugeClient. - * This ensures that a folder always uses the LanaugeClient bundled with its desired toolchain. + * folders share the same toolchain version then they will share the same LanguageClient. + * This ensures that a folder always uses the LanguageClient bundled with its desired toolchain. */ export class LanguageClientToolchainCoordinator implements vscode.Disposable { private subscriptions: vscode.Disposable[] = []; @@ -98,7 +98,7 @@ export class LanguageClientToolchainCoordinator implements vscode.Disposable { /** * Stops all LanguageClient instances. - * This should b called when the extension is deactivated. + * This should be called when the extension is deactivated. */ public async stop() { for (const client of this.clients.values()) { From 12381cb02504cfff3b36da5733d357cb42adf980 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Tue, 22 Apr 2025 15:32:15 -0400 Subject: [PATCH 7/7] Select folders when ctx.currentFolder is not set --- src/commands.ts | 11 ++++++-- src/commands/dependencies/resolve.ts | 6 +++-- src/utilities/folderQuickPick.ts | 38 ++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 src/utilities/folderQuickPick.ts diff --git a/src/commands.ts b/src/commands.ts index a9755fe0e..0615f42fd 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -46,6 +46,7 @@ import { runTask } from "./commands/runTask"; import { TestKind } from "./TestExplorer/TestKind"; import { pickProcess } from "./commands/pickProcess"; import { openDocumentation } from "./commands/openDocumentation"; +import showFolderSelectionQuickPick from "./utilities/folderQuickPick"; /** * References: @@ -147,10 +148,16 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { vscode.commands.registerCommand(Commands.RUN_PLUGIN_TASK, () => runPluginTask()), vscode.commands.registerCommand(Commands.RUN_TASK, name => runTask(ctx, name)), vscode.commands.registerCommand("swift.restartLSPServer", async () => { - if (!ctx.currentFolder) { + const folder = + ctx.currentFolder ?? + (await showFolderSelectionQuickPick( + ctx, + "Select a folder to restart the LSP server for" + )); + if (!folder) { return; } - const languageClientManager = ctx.languageClientManager.get(ctx.currentFolder); + const languageClientManager = ctx.languageClientManager.get(folder); await languageClientManager.restart(); }), vscode.commands.registerCommand("swift.reindexProject", () => reindexProject(ctx)), diff --git a/src/commands/dependencies/resolve.ts b/src/commands/dependencies/resolve.ts index befae55a0..75dfc41d7 100644 --- a/src/commands/dependencies/resolve.ts +++ b/src/commands/dependencies/resolve.ts @@ -17,14 +17,16 @@ import { FolderContext } from "../../FolderContext"; import { createSwiftTask, SwiftTaskProvider } from "../../tasks/SwiftTaskProvider"; import { WorkspaceContext } from "../../WorkspaceContext"; import { executeTaskWithUI, updateAfterError } from "../utilities"; +import showFolderSelectionQuickPick from "../../utilities/folderQuickPick"; /** * Executes a {@link vscode.Task task} to resolve this package's dependencies. */ export async function resolveDependencies(ctx: WorkspaceContext) { - const current = ctx.currentFolder; + const current = + ctx.currentFolder ?? + (await showFolderSelectionQuickPick(ctx, "Select a folder to resolve dependencies for")); if (!current) { - ctx.outputChannel.log("currentFolder is not set."); return false; } return await resolveFolderDependencies(current); diff --git a/src/utilities/folderQuickPick.ts b/src/utilities/folderQuickPick.ts new file mode 100644 index 000000000..93a39635e --- /dev/null +++ b/src/utilities/folderQuickPick.ts @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import * as vscode from "vscode"; +import { WorkspaceContext } from "../WorkspaceContext"; +import { FolderContext } from "../FolderContext"; + +export default async function showFolderSelectionQuickPick( + ctx: WorkspaceContext, + placeHolder: string = "Select a folder" +): Promise { + const folders: vscode.QuickPickItem[] = ctx.folders.map(folder => ({ + label: folder.name, + description: folder.folder.fsPath.toString(), + })); + const selected = await vscode.window.showQuickPick(folders, { + title: "Folder Selection", + placeHolder: placeHolder, + canPickMany: false, + }); + + if (!selected) { + return undefined; + } + + return ctx.folders.find(folder => folder.name === selected.label); +}