From 1a6406097856b3d5bf8f05ef21a7dfa19e1e6889 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Tue, 23 Sep 2025 13:23:51 -0400 Subject: [PATCH 1/4] move Swiftly toolchains to the top of toolchain selection --- src/ui/ToolchainSelection.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/ToolchainSelection.ts b/src/ui/ToolchainSelection.ts index 3d2790870..7bbef0d60 100644 --- a/src/ui/ToolchainSelection.ts +++ b/src/ui/ToolchainSelection.ts @@ -332,13 +332,13 @@ async function getQuickPickItems( run: selectToolchainFolder, }); return [ + ...(swiftlyToolchains.length > 0 + ? [new SeparatorItem("swiftly"), ...swiftlyToolchains] + : []), ...(xcodes.length > 0 ? [new SeparatorItem("Xcode"), ...xcodes] : []), ...(sortedToolchains.length > 0 ? [new SeparatorItem("toolchains"), ...sortedToolchains] : []), - ...(swiftlyToolchains.length > 0 - ? [new SeparatorItem("swiftly"), ...swiftlyToolchains] - : []), new SeparatorItem("actions"), ...actionItems, ]; From 7f8f949a84538e9db76e5873dfb44cdb491bcf86 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Tue, 23 Sep 2025 14:01:07 -0400 Subject: [PATCH 2/4] clean up naming of Swiftly methods --- src/commands/installSwiftlyToolchain.ts | 4 +- src/toolchain/swiftly.ts | 58 +++++++++++++------ src/ui/ToolchainSelection.ts | 39 +++++++------ test/unit-tests/toolchain/swiftly.test.ts | 12 ++-- test/unit-tests/toolchain/toolchain.test.ts | 14 ++--- test/unit-tests/ui/ToolchainSelection.test.ts | 24 ++++---- 6 files changed, 88 insertions(+), 63 deletions(-) diff --git a/src/commands/installSwiftlyToolchain.ts b/src/commands/installSwiftlyToolchain.ts index c87dc4133..c43247e8b 100644 --- a/src/commands/installSwiftlyToolchain.ts +++ b/src/commands/installSwiftlyToolchain.ts @@ -117,7 +117,7 @@ export async function installSwiftlyToolchain(ctx: WorkspaceContext): Promise { + public static async list(logger?: SwiftLogger): Promise { if (!this.isSupported()) { return []; } @@ -235,13 +235,13 @@ export class Swiftly { } if (!(await Swiftly.supportsJsonOutput(logger))) { - return await Swiftly.getToolchainInstallLegacy(logger); + return await Swiftly.listFromSwiftlyConfig(logger); } - return await Swiftly.getListAvailableToolchains(logger); + return await Swiftly.listUsingJSONFormat(logger); } - private static async getListAvailableToolchains(logger?: SwiftLogger): Promise { + private static async listUsingJSONFormat(logger?: SwiftLogger): Promise { try { const { stdout } = await execFile("swiftly", ["list", "--format=json"]); const response = ListResult.parse(JSON.parse(stdout)); @@ -254,7 +254,7 @@ export class Swiftly { } } - private static async getToolchainInstallLegacy(logger?: SwiftLogger) { + private static async listFromSwiftlyConfig(logger?: SwiftLogger) { try { const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"]; if (!swiftlyHomeDir) { @@ -279,10 +279,19 @@ export class Swiftly { } } + /** + * Checks whether or not the current operating system supports Swiftly. + */ public static isSupported() { return process.platform === "linux" || process.platform === "darwin"; } + /** + * Retrieves the location of the toolchain that is currently in use by Swiftly. + * + * @param swiftlyPath Optional path to the Swiftly binary. + * @param cwd Optional current working directory to check within. + */ public static async inUseLocation(swiftlyPath: string = "swiftly", cwd?: vscode.Uri) { const { stdout: inUse } = await execFile(swiftlyPath, ["use", "--print-location"], { cwd: cwd?.fsPath, @@ -290,6 +299,12 @@ export class Swiftly { return inUse.trimEnd(); } + /** + * Retrieves the version name of the toolchain that is currently in use by Swiftly. + * + * @param swiftlyPath Optional path to the Swiftly binary. + * @param cwd Optional current working directory to check within. + */ public static async inUseVersion( swiftlyPath: string = "swiftly", cwd?: vscode.Uri @@ -309,6 +324,11 @@ export class Swiftly { return result.version; } + /** + * Instructs Swiftly to use a specific version of the Swift toolchain. + * + * @param version The version name to use. Obtainable via {@link Swiftly.list}. + */ public static async use(version: string): Promise { if (!this.isSupported()) { throw new Error("Swiftly is not supported on this platform"); @@ -319,6 +339,7 @@ export class Swiftly { /** * Determine if Swiftly is being used to manage the active toolchain and if so, return * the path to the active toolchain. + * * @returns The location of the active toolchain if swiftly is being used to manage it. */ public static async toolchain( @@ -383,15 +404,15 @@ export class Swiftly { } /** - * Lists all toolchains available for installation from swiftly + * Lists all toolchains available for installation from swiftly. * - * @param branch Optional branch to filter available toolchains (e.g., "main" for snapshots) - * @param logger Optional logger for error reporting - * @returns Array of available toolchains + * @param branch Optional branch to filter available toolchains (e.g., "main" for snapshots). + * @param logger Optional logger for error reporting. + * @returns Array of available toolchains. */ public static async listAvailable( - logger?: SwiftLogger, - branch?: string + branch?: string, + logger?: SwiftLogger ): Promise { if (!this.isSupported()) { return []; @@ -425,11 +446,11 @@ export class Swiftly { } /** - * Installs a toolchain via swiftly with optional progress tracking + * Installs a toolchain via swiftly with optional progress tracking. * - * @param version The toolchain version to install - * @param progressCallback Optional callback that receives progress data as JSON objects - * @param logger Optional logger for error reporting + * @param version The toolchain version to install. + * @param progressCallback Optional callback that receives progress data as JSON objects. + * @param logger Optional logger for error reporting. */ public static async installToolchain( version: string, @@ -742,6 +763,9 @@ export class Swiftly { return JSON.parse(swiftlyConfigRaw); } + /** + * Checks whether or not Swiftly is installed on the current system. + */ public static async isInstalled(): Promise { if (!this.isSupported()) { return false; diff --git a/src/ui/ToolchainSelection.ts b/src/ui/ToolchainSelection.ts index 7bbef0d60..4f5e2621f 100644 --- a/src/ui/ToolchainSelection.ts +++ b/src/ui/ToolchainSelection.ts @@ -232,24 +232,27 @@ async function getQuickPickItems( const sortedToolchains = toolchains.sort((a, b) => b.label.localeCompare(a.label)); // Find any Swift toolchains installed via Swiftly - const swiftlyToolchains = ( - await Swiftly.listAvailableToolchains(logger) - ).map(toolchainPath => ({ - type: "toolchain", - label: path.basename(toolchainPath), - category: "swiftly", - version: path.basename(toolchainPath), - onDidSelect: async () => { - try { - await Swiftly.use(toolchainPath); - void showReloadExtensionNotification( - "Changing the Swift path requires Visual Studio Code be reloaded." - ); - } catch (error) { - void vscode.window.showErrorMessage(`Failed to switch Swiftly toolchain: ${error}`); - } - }, - })); + const swiftlyToolchains = (await Swiftly.list(logger)).map( + toolchainPath => ({ + type: "toolchain", + label: path.basename(toolchainPath), + category: "swiftly", + version: path.basename(toolchainPath), + onDidSelect: async () => { + try { + await Swiftly.use(toolchainPath); + void showReloadExtensionNotification( + "Changing the Swift path requires Visual Studio Code be reloaded." + ); + } catch (error) { + logger.error(error); + void vscode.window.showErrorMessage( + `Failed to switch Swiftly toolchain: ${error}` + ); + } + }, + }) + ); if (activeToolchain) { const currentSwiftlyVersion = activeToolchain.isSwiftlyManaged diff --git a/test/unit-tests/toolchain/swiftly.test.ts b/test/unit-tests/toolchain/swiftly.test.ts index 000720ec5..78c968c1c 100644 --- a/test/unit-tests/toolchain/swiftly.test.ts +++ b/test/unit-tests/toolchain/swiftly.test.ts @@ -66,7 +66,7 @@ suite("Swiftly Unit Tests", () => { } }); - suite("getSwiftlyToolchainInstalls", () => { + suite("list()", () => { test("should return toolchain names from list-available command for version 1.1.0", async () => { // Mock version check to return 1.1.0 mockUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({ @@ -127,7 +127,7 @@ suite("Swiftly Unit Tests", () => { stderr: "", }); - const result = await Swiftly.listAvailableToolchains(); + const result = await Swiftly.list(); expect(result).to.deep.equal([ "xcode", @@ -220,7 +220,7 @@ suite("Swiftly Unit Tests", () => { stderr: "", }); - const result = await Swiftly.listAvailableToolchains(); + const result = await Swiftly.list(); expect(result).to.deep.equal([ "xcode", @@ -239,7 +239,7 @@ suite("Swiftly Unit Tests", () => { test("should return empty array when platform is not supported", async () => { mockedPlatform.setValue("win32"); - const result = await Swiftly.listAvailableToolchains(); + const result = await Swiftly.list(); expect(result).to.deep.equal([]); expect(mockUtilities.execFile).not.have.been.called; @@ -320,7 +320,7 @@ suite("Swiftly Unit Tests", () => { }); }); - suite("listAvailable", () => { + suite("listAvailable()", () => { test("should return empty array on unsupported platform", async () => { mockedPlatform.setValue("win32"); @@ -592,7 +592,7 @@ suite("Swiftly Unit Tests", () => { stderr: "", }); - const result = await Swiftly.listAvailable(undefined, "main-snapshot"); + const result = await Swiftly.listAvailable("main-snapshot"); expect(result).to.deep.equal([ { inUse: false, diff --git a/test/unit-tests/toolchain/toolchain.test.ts b/test/unit-tests/toolchain/toolchain.test.ts index 2c606b630..8af82308e 100644 --- a/test/unit-tests/toolchain/toolchain.test.ts +++ b/test/unit-tests/toolchain/toolchain.test.ts @@ -314,7 +314,7 @@ suite("SwiftToolchain Unit Test Suite", () => { }), }); - const toolchains = await Swiftly.listAvailableToolchains(); + const toolchains = await Swiftly.list(); expect(toolchains).to.deep.equal([ path.join(mockHomeDir, "toolchains", "swift-5.9.0"), path.join(mockHomeDir, "toolchains", "swift-6.0.0"), @@ -332,7 +332,7 @@ suite("SwiftToolchain Unit Test Suite", () => { }), }); - const toolchains = await Swiftly.listAvailableToolchains(); + const toolchains = await Swiftly.list(); expect(toolchains).to.deep.equal([ path.join(mockHomeDir, "toolchains", "swift-5.9.0"), path.join(mockHomeDir, "toolchains", "swift-6.0.0"), @@ -343,7 +343,7 @@ suite("SwiftToolchain Unit Test Suite", () => { mockedPlatform.setValue("linux"); mockedEnv.setValue({}); - const toolchains = await Swiftly.listAvailableToolchains(); + const toolchains = await Swiftly.list(); expect(toolchains).to.be.empty; }); @@ -354,7 +354,7 @@ suite("SwiftToolchain Unit Test Suite", () => { mockFS({}); - await expect(Swiftly.listAvailableToolchains()).to.be.rejected.then(error => { + await expect(Swiftly.list()).to.be.rejected.then(error => { expect(error.message).to.include( "Failed to retrieve Swiftly installations from disk" ); @@ -372,13 +372,13 @@ suite("SwiftToolchain Unit Test Suite", () => { }), }); - const toolchains = await Swiftly.listAvailableToolchains(); + const toolchains = await Swiftly.list(); expect(toolchains).to.be.empty; }); test("returns empty array on Windows", async () => { mockedPlatform.setValue("win32"); - const toolchains = await Swiftly.listAvailableToolchains(); + const toolchains = await Swiftly.list(); expect(toolchains).to.be.empty; }); @@ -393,7 +393,7 @@ suite("SwiftToolchain Unit Test Suite", () => { }), }); - const toolchains = await Swiftly.listAvailableToolchains(); + const toolchains = await Swiftly.list(); expect(toolchains).to.deep.equal([ path.join(mockHomeDir, "toolchains", "swift-5.9.0"), path.join(mockHomeDir, "toolchains", "swift-6.0.0"), diff --git a/test/unit-tests/ui/ToolchainSelection.test.ts b/test/unit-tests/ui/ToolchainSelection.test.ts index 19472e5e4..97f6907c4 100644 --- a/test/unit-tests/ui/ToolchainSelection.test.ts +++ b/test/unit-tests/ui/ToolchainSelection.test.ts @@ -73,7 +73,7 @@ suite("ToolchainSelection Unit Test Suite", () => { stub(SwiftToolchain, "getXcodeDeveloperDir").resolves(""); // Mock Swiftly static methods - stub(Swiftly, "listAvailableToolchains").resolves([]); + stub(Swiftly, "list").resolves([]); stub(Swiftly, "listAvailable").resolves([]); stub(Swiftly, "inUseVersion").resolves(undefined); stub(Swiftly, "use").resolves(); @@ -117,7 +117,7 @@ suite("ToolchainSelection Unit Test Suite", () => { (SwiftToolchain.findXcodeInstalls as sinon.SinonStub).resolves(xcodeInstalls); (SwiftToolchain.getToolchainInstalls as sinon.SinonStub).resolves(toolchainInstalls); - (Swiftly.listAvailableToolchains as sinon.SinonStub).resolves(swiftlyToolchains); + (Swiftly.list as sinon.SinonStub).resolves(swiftlyToolchains); (Swiftly.listAvailable as sinon.SinonStub).resolves(availableToolchains); await ToolchainSelectionModule.showToolchainSelectionQuickPick(undefined, mockLogger); @@ -125,7 +125,7 @@ suite("ToolchainSelection Unit Test Suite", () => { expect(mockedVSCodeWindow.showQuickPick).to.have.been.called; expect(SwiftToolchain.findXcodeInstalls).to.have.been.called; expect(SwiftToolchain.getToolchainInstalls).to.have.been.called; - expect(Swiftly.listAvailableToolchains).to.have.been.called; + expect(Swiftly.list).to.have.been.called; }); test("should work on Linux platform", async () => { @@ -133,14 +133,14 @@ suite("ToolchainSelection Unit Test Suite", () => { (SwiftToolchain.findXcodeInstalls as sinon.SinonStub).resolves([]); (SwiftToolchain.getToolchainInstalls as sinon.SinonStub).resolves([]); - (Swiftly.listAvailableToolchains as sinon.SinonStub).resolves([]); + (Swiftly.list as sinon.SinonStub).resolves([]); (Swiftly.listAvailable as sinon.SinonStub).resolves([]); await ToolchainSelectionModule.showToolchainSelectionQuickPick(undefined, mockLogger); expect(mockedVSCodeWindow.showQuickPick).to.have.been.called; expect(SwiftToolchain.getToolchainInstalls).to.have.been.called; - expect(Swiftly.listAvailableToolchains).to.have.been.called; + expect(Swiftly.list).to.have.been.called; }); test("should handle active toolchain correctly", async () => { @@ -158,7 +158,7 @@ suite("ToolchainSelection Unit Test Suite", () => { (SwiftToolchain.findXcodeInstalls as sinon.SinonStub).resolves([]); (SwiftToolchain.getToolchainInstalls as sinon.SinonStub).resolves(toolchainInstalls); - (Swiftly.listAvailableToolchains as sinon.SinonStub).resolves([]); + (Swiftly.list as sinon.SinonStub).resolves([]); (Swiftly.listAvailable as sinon.SinonStub).resolves([]); await ToolchainSelectionModule.showToolchainSelectionQuickPick( @@ -181,7 +181,7 @@ suite("ToolchainSelection Unit Test Suite", () => { (SwiftToolchain.findXcodeInstalls as sinon.SinonStub).resolves([]); (SwiftToolchain.getToolchainInstalls as sinon.SinonStub).resolves([]); - (Swiftly.listAvailableToolchains as sinon.SinonStub).resolves(swiftlyToolchains); + (Swiftly.list as sinon.SinonStub).resolves(swiftlyToolchains); (Swiftly.listAvailable as sinon.SinonStub).resolves([]); (Swiftly.inUseVersion as sinon.SinonStub).resolves("6.0.0"); @@ -207,7 +207,7 @@ suite("ToolchainSelection Unit Test Suite", () => { (SwiftToolchain.findXcodeInstalls as sinon.SinonStub).resolves([]); (SwiftToolchain.getToolchainInstalls as sinon.SinonStub).resolves([]); - (Swiftly.listAvailableToolchains as sinon.SinonStub).resolves([]); + (Swiftly.list as sinon.SinonStub).resolves([]); (Swiftly.listAvailable as sinon.SinonStub).resolves([ { name: "6.0.1", @@ -233,7 +233,7 @@ suite("ToolchainSelection Unit Test Suite", () => { (SwiftToolchain.findXcodeInstalls as sinon.SinonStub).resolves([]); (SwiftToolchain.getToolchainInstalls as sinon.SinonStub).resolves([]); - (Swiftly.listAvailableToolchains as sinon.SinonStub).resolves([]); + (Swiftly.list as sinon.SinonStub).resolves([]); (Swiftly.listAvailable as sinon.SinonStub).resolves([]); await ToolchainSelectionModule.showToolchainSelectionQuickPick(undefined, mockLogger); @@ -246,7 +246,7 @@ suite("ToolchainSelection Unit Test Suite", () => { (SwiftToolchain.findXcodeInstalls as sinon.SinonStub).resolves([]); (SwiftToolchain.getToolchainInstalls as sinon.SinonStub).resolves([]); - (Swiftly.listAvailableToolchains as sinon.SinonStub).resolves([]); + (Swiftly.list as sinon.SinonStub).resolves([]); (Swiftly.listAvailable as sinon.SinonStub).resolves([]); await ToolchainSelectionModule.showToolchainSelectionQuickPick(undefined, mockLogger); @@ -262,9 +262,7 @@ suite("ToolchainSelection Unit Test Suite", () => { (SwiftToolchain.getToolchainInstalls as sinon.SinonStub).rejects( new Error("Toolchain search failed") ); - (Swiftly.listAvailableToolchains as sinon.SinonStub).rejects( - new Error("Swiftly list failed") - ); + (Swiftly.list as sinon.SinonStub).rejects(new Error("Swiftly list failed")); (Swiftly.listAvailable as sinon.SinonStub).rejects( new Error("Swiftly available failed") ); From 071e8be5a196673393826a2591992c395e54e081 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Wed, 24 Sep 2025 10:22:04 -0400 Subject: [PATCH 3/4] hide Swiftly install commands when Swiftly is not available or doesn't support it --- package.json | 8 ++++++++ src/contextKeys.ts | 19 +++++++++++++++++++ src/extension.ts | 2 ++ src/toolchain/swiftly.ts | 30 ++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+) diff --git a/package.json b/package.json index 80b3ce804..51c2dc43f 100644 --- a/package.json +++ b/package.json @@ -1175,6 +1175,14 @@ "command": "swift.previewDocumentation", "when": "swift.supportsDocumentationLivePreview" }, + { + "command": "swift.installSwiftlyToolchain", + "when": "swift.supportsSwiftlyInstall" + }, + { + "command": "swift.installSwiftlySnapshotToolchain", + "when": "swift.supportsSwiftlyInstall" + }, { "command": "swift.createNewProject", "when": "!swift.isActivated || swift.createNewProjectAvailable" diff --git a/src/contextKeys.ts b/src/contextKeys.ts index c6be7ccd1..97524fce1 100644 --- a/src/contextKeys.ts +++ b/src/contextKeys.ts @@ -84,6 +84,11 @@ export interface ContextKeys { */ supportsDocumentationLivePreview: boolean; + /** + * Whether the installed version of Swiftly can be used to install toolchains from within VS Code. + */ + supportsSwiftlyInstall: boolean; + /** * Whether the swift.switchPlatform command is available. */ @@ -109,6 +114,7 @@ export function createContextKeys(): ContextKeys { let createNewProjectAvailable: boolean = false; let supportsReindexing: boolean = false; let supportsDocumentationLivePreview: boolean = false; + let supportsSwiftlyInstall: boolean = false; let switchPlatformAvailable: boolean = false; return { @@ -278,6 +284,19 @@ export function createContextKeys(): ContextKeys { }); }, + get supportsSwiftlyInstall() { + return supportsSwiftlyInstall; + }, + + set supportsSwiftlyInstall(value: boolean) { + supportsSwiftlyInstall = value; + void vscode.commands + .executeCommand("setContext", "swift.supportsSwiftlyInstall", value) + .then(() => { + /* Put in worker queue */ + }); + }, + get switchPlatformAvailable() { return switchPlatformAvailable; }, diff --git a/src/extension.ts b/src/extension.ts index 614d4b911..93676588a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -30,6 +30,7 @@ import { SwiftLogger } from "./logging/SwiftLogger"; import { SwiftLoggerFactory } from "./logging/SwiftLoggerFactory"; import { SwiftEnvironmentVariablesManager, SwiftTerminalProfileProvider } from "./terminal"; import { SelectedXcodeWatcher } from "./toolchain/SelectedXcodeWatcher"; +import { checkForSwiftlyInstallation } from "./toolchain/swiftly"; import { SwiftToolchain } from "./toolchain/toolchain"; import { LanguageStatusItems } from "./ui/LanguageStatusItems"; import { ProjectPanelProvider } from "./ui/ProjectPanelProvider"; @@ -69,6 +70,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { const contextKeys = createContextKeys(); const toolchain = await createActiveToolchain(contextKeys, logger); + checkForSwiftlyInstallation(contextKeys, logger); // If we don't have a toolchain, show an error and stop initializing the extension. // This can happen if the user has not installed Swift or if the toolchain is not diff --git a/src/toolchain/swiftly.ts b/src/toolchain/swiftly.ts index e2e1c9f68..85578620f 100644 --- a/src/toolchain/swiftly.ts +++ b/src/toolchain/swiftly.ts @@ -22,6 +22,7 @@ import { z } from "zod/v4/mini"; // Import the reusable installation function import { installSwiftlyToolchainVersion } from "../commands/installSwiftlyToolchain"; +import { ContextKeys } from "../contextKeys"; import { SwiftLogger } from "../logging/SwiftLogger"; import { showMissingToolchainDialog } from "../ui/ToolchainSelection"; import { findBinaryPath } from "../utilities/shell"; @@ -778,3 +779,32 @@ export class Swiftly { } } } + +/** + * Checks whether or not Swiftly is installed and updates context keys appropriately. + */ +export function checkForSwiftlyInstallation(contextKeys: ContextKeys, logger: SwiftLogger): void { + contextKeys.supportsSwiftlyInstall = false; + if (!Swiftly.isSupported()) { + logger.debug(`Swiftly is not available on ${process.platform}`); + return; + } + // Don't block while checking the Swiftly insallation. + void Swiftly.isInstalled().then(async isInstalled => { + if (!isInstalled) { + logger.debug("Swiftly is not installed on this system."); + return; + } + const version = await Swiftly.version(logger); + if (!version) { + logger.warn("Unable to determine Swiftly version."); + return; + } + logger.debug(`Detected Swiftly version ${version}.`); + contextKeys.supportsSwiftlyInstall = version.isGreaterThanOrEqual({ + major: 1, + minor: 1, + patch: 0, + }); + }); +} From 45fd6deb6d6015202e65e5a76989b37894ca9bcc Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Thu, 25 Sep 2025 15:24:14 -0400 Subject: [PATCH 4/4] hide Swiftly install commands from the toolchain selection quick pick when Swiftly doesn't support it --- src/ui/ToolchainSelection.ts | 92 ++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 40 deletions(-) diff --git a/src/ui/ToolchainSelection.ts b/src/ui/ToolchainSelection.ts index 4f5e2621f..2369626ef 100644 --- a/src/ui/ToolchainSelection.ts +++ b/src/ui/ToolchainSelection.ts @@ -290,60 +290,72 @@ async function getQuickPickItems( } } // Various actions that the user can perform (e.g. to install new toolchains) - const actionItems: ActionItem[] = []; - if (Swiftly.isSupported() && !(await Swiftly.isInstalled())) { - const platformName = process.platform === "linux" ? "Linux" : "macOS"; - actionItems.push({ + const actionItems: ActionItem[] = [ + ...(await getSwiftlyActions()), + { type: "action", - label: "$(swift-icon) Install Swiftly for toolchain management...", - detail: `Install https://swiftlang.github.io/swiftly to manage your toolchains on ${platformName}`, - run: installSwiftly, - }); - } + label: "$(cloud-download) Download from Swift.org...", + detail: "Open https://swift.org/install to download and install a toolchain", + run: downloadToolchain, + }, + { + type: "action", + label: "$(folder-opened) Select toolchain directory...", + detail: "Select a folder on your machine where the Swift toolchain is installed", + run: selectToolchainFolder, + }, + ]; - // Add install Swiftly toolchain actions if Swiftly is installed - if (Swiftly.isSupported() && (await Swiftly.isInstalled())) { - actionItems.push({ + return [ + ...(swiftlyToolchains.length > 0 + ? [new SeparatorItem("swiftly"), ...swiftlyToolchains] + : []), + ...(xcodes.length > 0 ? [new SeparatorItem("Xcode"), ...xcodes] : []), + ...(sortedToolchains.length > 0 + ? [new SeparatorItem("toolchains"), ...sortedToolchains] + : []), + new SeparatorItem("actions"), + ...actionItems, + ]; +} + +async function getSwiftlyActions(): Promise { + if (!Swiftly.isSupported()) { + return []; + } + if (!(await Swiftly.isInstalled())) { + const platformName = process.platform === "linux" ? "Linux" : "macOS"; + return [ + { + type: "action", + label: "$(swift-icon) Install Swiftly for toolchain management...", + detail: `Install https://swiftlang.github.io/swiftly to manage your toolchains on ${platformName}`, + run: installSwiftly, + }, + ]; + } + // We only support installing toolchains via Swiftly starting in Swiftly 1.1.0 + const swiftlyVersion = await Swiftly.version(); + if (swiftlyVersion?.isLessThan({ major: 1, minor: 1, patch: 0 })) { + return []; + } + return [ + { type: "action", label: "$(cloud-download) Install Swiftly toolchain...", detail: "Install a Swift stable release toolchain via Swiftly", run: async () => { await vscode.commands.executeCommand(Commands.INSTALL_SWIFTLY_TOOLCHAIN); }, - }); - - actionItems.push({ + }, + { type: "action", label: "$(beaker) Install Swiftly snapshot toolchain...", detail: "Install a Swift snapshot toolchain via Swiftly from development builds", run: async () => { await vscode.commands.executeCommand(Commands.INSTALL_SWIFTLY_SNAPSHOT_TOOLCHAIN); }, - }); - } - - actionItems.push({ - type: "action", - label: "$(cloud-download) Download from Swift.org...", - detail: "Open https://swift.org/install to download and install a toolchain", - run: downloadToolchain, - }); - actionItems.push({ - type: "action", - label: "$(folder-opened) Select toolchain directory...", - detail: "Select a folder on your machine where the Swift toolchain is installed", - run: selectToolchainFolder, - }); - return [ - ...(swiftlyToolchains.length > 0 - ? [new SeparatorItem("swiftly"), ...swiftlyToolchains] - : []), - ...(xcodes.length > 0 ? [new SeparatorItem("Xcode"), ...xcodes] : []), - ...(sortedToolchains.length > 0 - ? [new SeparatorItem("toolchains"), ...sortedToolchains] - : []), - new SeparatorItem("actions"), - ...actionItems, + }, ]; }