From 5f0550b7f586539aad73728e904f660a41db5a17 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Thu, 2 Oct 2025 15:26:30 -0400 Subject: [PATCH] ask where to use newly installed Swiftly toolchain --- src/commands.ts | 9 +- src/commands/installSwiftlyToolchain.ts | 138 +++++++--------------- src/toolchain/swiftly.ts | 9 +- src/ui/ToolchainSelection.ts | 2 +- test/unit-tests/toolchain/swiftly.test.ts | 47 ++------ 5 files changed, 61 insertions(+), 144 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index a94225b4d..4a8834bca 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -30,10 +30,7 @@ import { useLocalDependency } from "./commands/dependencies/useLocal"; import { generateLaunchConfigurations } from "./commands/generateLaunchConfigurations"; import { generateSourcekitConfiguration } from "./commands/generateSourcekitConfiguration"; import { insertFunctionComment } from "./commands/insertFunctionComment"; -import { - installSwiftlySnapshotToolchain, - installSwiftlyToolchain, -} from "./commands/installSwiftlyToolchain"; +import { promptToInstallSwiftlyToolchain } from "./commands/installSwiftlyToolchain"; import { newSwiftFile } from "./commands/newFile"; import { openDocumentation } from "./commands/openDocumentation"; import { openEducationalNote } from "./commands/openEducationalNote"; @@ -356,11 +353,11 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { ), vscode.commands.registerCommand( Commands.INSTALL_SWIFTLY_TOOLCHAIN, - async () => await installSwiftlyToolchain(ctx) + async () => await promptToInstallSwiftlyToolchain(ctx, "stable") ), vscode.commands.registerCommand( Commands.INSTALL_SWIFTLY_SNAPSHOT_TOOLCHAIN, - async () => await installSwiftlySnapshotToolchain(ctx) + async () => await promptToInstallSwiftlyToolchain(ctx, "snapshot") ), ]; } diff --git a/src/commands/installSwiftlyToolchain.ts b/src/commands/installSwiftlyToolchain.ts index 5feadcb99..4bfe58eff 100644 --- a/src/commands/installSwiftlyToolchain.ts +++ b/src/commands/installSwiftlyToolchain.ts @@ -12,33 +12,22 @@ // //===----------------------------------------------------------------------===// import * as vscode from "vscode"; -import { QuickPickItem } from "vscode"; import { WorkspaceContext } from "../WorkspaceContext"; import { SwiftLogger } from "../logging/SwiftLogger"; -import { AvailableToolchain, Swiftly, SwiftlyProgressData } from "../toolchain/swiftly"; -import { showReloadExtensionNotification } from "../ui/ReloadExtension"; - -interface SwiftlyToolchainItem extends QuickPickItem { - toolchain: AvailableToolchain; -} - -async function downloadAndInstallToolchain(selected: SwiftlyToolchainItem, ctx: WorkspaceContext) { - return await installSwiftlyToolchainVersion(selected.toolchain.version.name, ctx.logger, true); -} +import { Swiftly, SwiftlyProgressData } from "../toolchain/swiftly"; +import { askWhereToSetToolchain } from "../ui/ToolchainSelection"; /** - * Installs a Swiftly toolchain by version string + * Installs a Swiftly toolchain and shows a progress notification to the user. + * * @param version The toolchain version to install * @param logger Optional logger for error reporting - * @param showReloadNotification Whether to show reload notification after installation * @returns Promise true if installation succeeded, false otherwise */ -export async function installSwiftlyToolchainVersion( +export async function installSwiftlyToolchainWithProgress( version: string, - logger?: SwiftLogger, - showReloadNotification: boolean = true, - token?: vscode.CancellationToken + logger?: SwiftLogger ): Promise { try { await vscode.window.withProgress( @@ -47,8 +36,7 @@ export async function installSwiftlyToolchainVersion( title: `Installing Swift ${version}`, cancellable: true, }, - async (progress, progressToken) => { - const effectiveToken = token || progressToken; + async (progress, token) => { progress.report({ message: "Starting installation..." }); let lastProgress = 0; @@ -71,7 +59,7 @@ export async function installSwiftlyToolchainVersion( } }, logger, - effectiveToken + token ); progress.report({ @@ -81,11 +69,6 @@ export async function installSwiftlyToolchainVersion( } ); - if (showReloadNotification) { - void showReloadExtensionNotification( - `Swift ${version} has been installed and activated. Visual Studio Code needs to be reloaded.` - ); - } return true; } catch (error) { const errorMessage = (error as Error).message; @@ -104,7 +87,10 @@ export async function installSwiftlyToolchainVersion( /** * Shows a quick pick dialog to install available Swiftly toolchains */ -export async function installSwiftlyToolchain(ctx: WorkspaceContext): Promise { +export async function promptToInstallSwiftlyToolchain( + ctx: WorkspaceContext, + type: "stable" | "snapshot" +): Promise { if (!Swiftly.isSupported()) { ctx.logger?.warn("Swiftly is not supported on this platform."); void vscode.window.showErrorMessage( @@ -121,7 +107,21 @@ export async function installSwiftlyToolchain(ctx: WorkspaceContext): Promise !toolchain.installed); + const uninstalledToolchains = availableToolchains + .filter(toolchain => !toolchain.installed) + .filter(toolchain => toolchain.version.type === type); if (uninstalledToolchains.length === 0) { ctx.logger?.debug("All available toolchains are already installed."); @@ -154,85 +156,27 @@ export async function installSwiftlyToolchain(ctx: WorkspaceContext): Promise { - if (!Swiftly.isSupported()) { - void vscode.window.showErrorMessage( - "Swiftly is not supported on this platform. Only macOS and Linux are supported." - ); + const target = await askWhereToSetToolchain(); + if (!target) { return; } - if (!(await Swiftly.isInstalled())) { - void vscode.window.showErrorMessage( - "Swiftly is not installed. Please install Swiftly first from https://www.swift.org/install/" - ); + // Install the toolchain via Swiftly + if (!(await installSwiftlyToolchainWithProgress(selected.toolchain.version.name, ctx.logger))) { return; } - - // Prompt user to enter the branch for snapshot toolchains - const branch = await vscode.window.showInputBox({ - title: "Enter Swift Snapshot Branch", - prompt: "Enter the branch name to list snapshot toolchains (e.g., 'main-snapshot', '6.1-snapshot')", - placeHolder: "main-snapshot", - value: "main-snapshot", - }); - - if (!branch) { - return; // User cancelled input - } - - const availableToolchains = await Swiftly.listAvailable(branch, ctx.logger); - - if (availableToolchains.length === 0) { - ctx.logger?.debug("No toolchains available for installation via Swiftly."); - void vscode.window.showInformationMessage( - "No toolchains are available for installation via Swiftly." - ); - return; - } - - // Filter for only uninstalled snapshot toolchains - const uninstalledSnapshotToolchains = availableToolchains.filter( - toolchain => !toolchain.installed && toolchain.version.type === "snapshot" - ); - - if (uninstalledSnapshotToolchains.length === 0) { - ctx.logger?.debug("All available snapshot toolchains are already installed."); - void vscode.window.showInformationMessage( - "All available snapshot toolchains are already installed." + // Tell Swiftly to use the newly installed toolchain + if (target === vscode.ConfigurationTarget.Workspace) { + await Promise.all( + vscode.workspace.workspaceFolders?.map(folder => + Swiftly.use(selected.toolchain.version.name, folder.uri.fsPath) + ) ?? [] ); return; } - - const quickPickItems = uninstalledSnapshotToolchains.map(toolchain => ({ - label: `$(cloud-download) ${toolchain.version.name}`, - description: "snapshot", - detail: `Date: ${ - toolchain.version.type === "snapshot" ? toolchain.version.date || "Unknown" : "Unknown" - } • Branch: ${toolchain.version.type === "snapshot" ? toolchain.version.branch || "Unknown" : "Unknown"}`, - toolchain: toolchain, - })); - - const selected = await vscode.window.showQuickPick(quickPickItems, { - title: "Install Swift Snapshot Toolchain via Swiftly", - placeHolder: "Pick a Swift snapshot toolchain to install", - canPickMany: false, - }); - - if (!selected) { - return; - } - - await downloadAndInstallToolchain(selected, ctx); + await Swiftly.use(selected.toolchain.version.name); } diff --git a/src/toolchain/swiftly.ts b/src/toolchain/swiftly.ts index 7674b7dc7..f7f87ccfe 100644 --- a/src/toolchain/swiftly.ts +++ b/src/toolchain/swiftly.ts @@ -21,8 +21,7 @@ import * as Stream from "stream"; import * as vscode from "vscode"; import { z } from "zod/v4/mini"; -// Import the reusable installation function -import { installSwiftlyToolchainVersion } from "../commands/installSwiftlyToolchain"; +import { installSwiftlyToolchainWithProgress } from "../commands/installSwiftlyToolchain"; import { ContextKeys } from "../contextKeys"; import { SwiftLogger } from "../logging/SwiftLogger"; import { showMissingToolchainDialog } from "../ui/ToolchainSelection"; @@ -153,8 +152,7 @@ export function parseSwiftlyMissingToolchainError( export async function handleMissingSwiftlyToolchain( version: string, logger?: SwiftLogger, - folder?: vscode.Uri, - token?: vscode.CancellationToken + folder?: vscode.Uri ): Promise { logger?.info(`Attempting to handle missing toolchain: ${version}`); @@ -167,7 +165,7 @@ export async function handleMissingSwiftlyToolchain( // Use the existing installation function without showing reload notification // (since we want to continue the current operation) - return await installSwiftlyToolchainVersion(version, logger, false, token); + return await installSwiftlyToolchainWithProgress(version, logger); } export class Swiftly { @@ -532,7 +530,6 @@ export class Swiftly { const installArgs = [ "install", version, - "--use", "--assume-yes", "--post-install-file", postInstallFilePath, diff --git a/src/ui/ToolchainSelection.ts b/src/ui/ToolchainSelection.ts index 74846b78a..6966c0f1b 100644 --- a/src/ui/ToolchainSelection.ts +++ b/src/ui/ToolchainSelection.ts @@ -505,7 +505,7 @@ export async function removeToolchainPath() { await swiftSettings.update("path", undefined, vscode.ConfigurationTarget.Workspace); } -async function askWhereToSetToolchain(): Promise { +export async function askWhereToSetToolchain(): Promise { if (!vscode.workspace.workspaceFolders) { return vscode.ConfigurationTarget.Global; } diff --git a/test/unit-tests/toolchain/swiftly.test.ts b/test/unit-tests/toolchain/swiftly.test.ts index cefca73d8..1a258bf42 100644 --- a/test/unit-tests/toolchain/swiftly.test.ts +++ b/test/unit-tests/toolchain/swiftly.test.ts @@ -18,7 +18,7 @@ import * as os from "os"; import { match } from "sinon"; import * as vscode from "vscode"; -import { installSwiftlyToolchainVersion } from "@src/commands/installSwiftlyToolchain"; +import { installSwiftlyToolchainWithProgress } from "@src/commands/installSwiftlyToolchain"; import * as SwiftOutputChannelModule from "@src/logging/SwiftOutputChannel"; import { Swiftly, @@ -476,7 +476,7 @@ suite("Swiftly Unit Tests", () => { expect(mockUtilities.execFileStreamOutput).to.have.been.calledWith( "swiftly", - ["install", "6.0.0", "--use", "--assume-yes", "--post-install-file", match.string], + ["install", "6.0.0", "--assume-yes", "--post-install-file", match.string], match.any, match.any, match.any, @@ -862,7 +862,7 @@ suite("Swiftly Unit Tests", () => { // Verify swiftly install was called with post-install file argument expect(mockUtilities.execFileStreamOutput).to.have.been.calledWith( "swiftly", - ["install", "6.0.0", "--use", "--assume-yes", "--post-install-file", match.string], + ["install", "6.0.0", "--assume-yes", "--post-install-file", match.string], match.any, match.any, match.any, @@ -964,13 +964,12 @@ apt-get -y install libncurses5-dev`; .withArgs("swiftly", [ "install", "6.0.0", - "--use", "--assume-yes", "--post-install-file", match.string, ]) .callsFake(async (_command, args) => { - const postInstallPath = args[5]; + const postInstallPath = args[4]; await fs.writeFile(postInstallPath, validScript); return; }); @@ -1021,13 +1020,12 @@ apt-get -y install build-essential`; .withArgs("swiftly", [ "install", "6.0.0", - "--use", "--assume-yes", "--post-install-file", match.string, ]) .callsFake(async (_command, args) => { - const postInstallPath = args[5]; + const postInstallPath = args[4]; await fs.writeFile(postInstallPath, validScript); return; }); @@ -1066,13 +1064,12 @@ apt-get -y install build-essential`; .withArgs("swiftly", [ "install", "6.0.0", - "--use", "--assume-yes", "--post-install-file", match.string, ]) .callsFake(async (_command, args) => { - const postInstallPath = args[5]; + const postInstallPath = args[4]; await fs.writeFile(postInstallPath, invalidScript); return; }); @@ -1107,13 +1104,12 @@ apt-get -y install build-essential`; .withArgs("swiftly", [ "install", "6.0.0", - "--use", "--assume-yes", "--post-install-file", match.string, ]) .callsFake(async (_command, args) => { - const postInstallPath = args[5]; + const postInstallPath = args[4]; await fs.writeFile(postInstallPath, validScript); return; }); @@ -1176,13 +1172,12 @@ yum install ncurses-devel`; .withArgs("swiftly", [ "install", "6.0.0", - "--use", "--assume-yes", "--post-install-file", match.string, ]) .callsFake(async (_command, args) => { - const postInstallPath = args[5]; + const postInstallPath = args[4]; await fs.writeFile(postInstallPath, yumScript); return; }); @@ -1222,13 +1217,12 @@ yum remove important-system-package`; .withArgs("swiftly", [ "install", "6.0.0", - "--use", "--assume-yes", "--post-install-file", match.string, ]) .callsFake(async (_command, args) => { - const postInstallPath = args[5]; + const postInstallPath = args[4]; await fs.writeFile(postInstallPath, malformedScript); return; }); @@ -1263,13 +1257,12 @@ apt-get -y install libncurses5-dev .withArgs("swiftly", [ "install", "6.0.0", - "--use", "--assume-yes", "--post-install-file", match.string, ]) .callsFake(async (_command, args) => { - const postInstallPath = args[5]; + const postInstallPath = args[4]; await fs.writeFile(postInstallPath, scriptWithComments); return; }); @@ -1424,18 +1417,11 @@ apt-get -y install libncurses5-dev .rejects(new Error(Swiftly.cancellationMessage)); await expect( - Swiftly.installToolchain("6.0.0", undefined, undefined, mockToken as any) + Swiftly.installToolchain("6.0.0", undefined, undefined, mockToken) ).to.eventually.be.rejectedWith(Swiftly.cancellationMessage); }); test("installSwiftlyToolchainVersion should handle cancellation gracefully", async () => { - const mockLogger = { - info: () => {}, - error: () => {}, - warn: () => {}, - debug: () => {}, - }; - // Mock window.withProgress to simulate cancellation mockWindow.withProgress.callsFake(async (_options, task) => { const mockProgress = { report: () => {} }; @@ -1459,20 +1445,13 @@ apt-get -y install libncurses5-dev .withArgs("swiftly") .rejects(new Error(Swiftly.cancellationMessage)); - const result = await installSwiftlyToolchainVersion("6.0.0", mockLogger as any, false); + const result = await installSwiftlyToolchainWithProgress("6.0.0"); expect(result).to.be.false; expect(mockWindow.showErrorMessage).to.not.have.been.called; }); test("installSwiftlyToolchainVersion should show error for non-cancellation errors", async () => { - const mockLogger = { - info: () => {}, - error: () => {}, - warn: () => {}, - debug: () => {}, - }; - // Mock window.withProgress to simulate a regular error mockWindow.withProgress.callsFake(async (_options, task) => { const mockProgress = { report: () => {} }; @@ -1489,7 +1468,7 @@ apt-get -y install libncurses5-dev .withArgs("swiftly") .rejects(new Error("Network error")); - const result = await installSwiftlyToolchainVersion("6.0.0", mockLogger as any, false); + const result = await installSwiftlyToolchainWithProgress("6.0.0"); expect(result).to.be.false; expect(mockWindow.showErrorMessage).to.have.been.calledWith(