diff --git a/README.md b/README.md index 7f8069c26..4a0fd60c4 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ The extension adds the following commands, available via the command palette. The following command is only available on macOS: -- **Select Target Platform**: This is an experimental command that offers code completion for iOS and tvOS projects. +- **Select Target Platform**: This is an experimental command that offers code editing support for iOS, tvOS, watchOS and visionOS projects. #### Building and Debugging diff --git a/package.json b/package.json index e61380dca..a9f5726c2 100644 --- a/package.json +++ b/package.json @@ -805,7 +805,7 @@ }, { "command": "swift.switchPlatform", - "when": "swift.isActivated && isMac" + "when": "swift.isActivated && isMac && swift.switchPlatformAvailable" }, { "command": "swift.insertFunctionComment", diff --git a/src/commands.ts b/src/commands.ts index b88e6cecb..253e463ca 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -106,8 +106,10 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { return runTestMultipleTimes(ctx.currentFolder, item, true); } }), - // Note: This is only available on macOS (gated in `package.json`) because its the only OS that has the iOS SDK available. - vscode.commands.registerCommand("swift.switchPlatform", () => switchPlatform()), + // Note: switchPlatform is only available on macOS and Swift 6.1 or later + // (gated in `package.json`) because it's the only OS and toolchain combination that + // has Darwin SDKs available and supports code editing with SourceKit-LSP + vscode.commands.registerCommand("swift.switchPlatform", () => switchPlatform(ctx)), vscode.commands.registerCommand(Commands.RESET_PACKAGE, () => resetPackage(ctx)), vscode.commands.registerCommand("swift.runScript", () => runSwiftScript(ctx)), vscode.commands.registerCommand("swift.openPackage", () => { diff --git a/src/commands/switchPlatform.ts b/src/commands/switchPlatform.ts index 821a8ccfb..e65dfda2c 100644 --- a/src/commands/switchPlatform.ts +++ b/src/commands/switchPlatform.ts @@ -13,13 +13,18 @@ //===----------------------------------------------------------------------===// import * as vscode from "vscode"; -import { DarwinCompatibleTarget, SwiftToolchain } from "../toolchain/toolchain"; +import { + DarwinCompatibleTarget, + SwiftToolchain, + getDarwinTargetTriple, +} from "../toolchain/toolchain"; import configuration from "../configuration"; +import { WorkspaceContext } from "../WorkspaceContext"; /** - * Switches the target SDK to the platform selected in a QuickPick UI. + * Switches the appropriate SDK setting to the platform selected in a QuickPick UI. */ -export async function switchPlatform() { +export async function switchPlatform(ctx: WorkspaceContext) { const picked = await vscode.window.showQuickPick( [ { value: undefined, label: "macOS" }, @@ -29,28 +34,34 @@ export async function switchPlatform() { { value: DarwinCompatibleTarget.visionOS, label: "visionOS" }, ], { - placeHolder: "Select a new target", + placeHolder: "Select a new target platform", } ); if (picked) { + // show a status item as getSDKForTarget can sometimes take a long while to run xcrun to find the SDK + const statusItemText = `Setting target platform to ${picked.label}`; + ctx.statusItem.start(statusItemText); try { - const sdkForTarget = picked.value - ? await SwiftToolchain.getSDKForTarget(picked.value) - : ""; - if (sdkForTarget !== undefined) { - if (sdkForTarget !== "") { - configuration.sdk = sdkForTarget; - vscode.window.showWarningMessage( - `Selecting the ${picked.label} SDK will provide code editing support, but compiling with this SDK will have undefined results.` - ); - } else { - configuration.sdk = undefined; - } + if (picked.value) { + // verify that the SDK for the platform actually exists + await SwiftToolchain.getSDKForTarget(picked.value); + } + const swiftSDKTriple = picked.value ? getDarwinTargetTriple(picked.value) : ""; + if (swiftSDKTriple !== "") { + // set a swiftSDK for non-macOS Darwin platforms so that SourceKit-LSP can provide syntax highlighting + configuration.swiftSDK = swiftSDKTriple; + vscode.window.showWarningMessage( + `Selecting the ${picked.label} target platform will provide code editing support, but compiling with a ${picked.label} SDK will have undefined results.` + ); } else { - vscode.window.showErrorMessage("Unable to obtain requested SDK path"); + // set swiftSDK to an empty string for macOS and other platforms + configuration.swiftSDK = ""; } } catch { - vscode.window.showErrorMessage("Unable to obtain requested SDK path"); + vscode.window.showErrorMessage( + `Unable set the Swift SDK setting to ${picked.label}, verify that the SDK exists` + ); } + ctx.statusItem.end(statusItemText); } } diff --git a/src/contextKeys.ts b/src/contextKeys.ts index e550c7707..ed735af9c 100644 --- a/src/contextKeys.ts +++ b/src/contextKeys.ts @@ -77,6 +77,11 @@ interface ContextKeys { * Whether the SourceKit-LSP server supports documentation live preview. */ supportsDocumentationLivePreview: boolean; + + /** + * Whether the swift.switchPlatform command is available. + */ + switchPlatformAvailable: boolean; } /** Creates the getters and setters for the VS Code Swift extension's context keys. */ @@ -92,6 +97,7 @@ function createContextKeys(): ContextKeys { let createNewProjectAvailable: boolean = false; let supportsReindexing: boolean = false; let supportsDocumentationLivePreview: boolean = false; + let switchPlatformAvailable: boolean = false; return { get isActivated() { @@ -200,6 +206,15 @@ function createContextKeys(): ContextKeys { value ); }, + + get switchPlatformAvailable() { + return switchPlatformAvailable; + }, + + set switchPlatformAvailable(value: boolean) { + switchPlatformAvailable = value; + vscode.commands.executeCommand("setContext", "swift.switchPlatformAvailable", value); + }, }; } diff --git a/src/extension.ts b/src/extension.ts index 612ea9e3b..748e8b522 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -76,12 +76,16 @@ export async function activate(context: vscode.ExtensionContext): Promise { contextKeys.createNewProjectAvailable = toolchain.swiftVersion.isGreaterThanOrEqual( new Version(5, 8, 0) ); + contextKeys.switchPlatformAvailable = toolchain.swiftVersion.isGreaterThanOrEqual( + new Version(6, 1, 0) + ); return toolchain; }) .catch(error => { outputChannel.log("Failed to discover Swift toolchain"); outputChannel.log(error); contextKeys.createNewProjectAvailable = false; + contextKeys.switchPlatformAvailable = false; return undefined; }); @@ -103,11 +107,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { event.affectsConfiguration("swift.SDK") || event.affectsConfiguration("swift.swiftSDK") ) { - // FIXME: There is a bug stopping us from restarting SourceKit-LSP directly. - // As long as it's fixed we won't need to reload on newer versions. - showReloadExtensionNotification( - "Changing the Swift SDK path requires the project be reloaded." - ); + vscode.commands.executeCommand("swift.restartLSPServer"); } }) ); diff --git a/test/unit-tests/commands/switchPlatform.test.ts b/test/unit-tests/commands/switchPlatform.test.ts new file mode 100644 index 000000000..f2c54f105 --- /dev/null +++ b/test/unit-tests/commands/switchPlatform.test.ts @@ -0,0 +1,85 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-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 { expect } from "chai"; +import * as vscode from "vscode"; +import { + mockObject, + mockGlobalObject, + mockGlobalModule, + MockedObject, + mockFn, + instance, +} from "../../MockUtils"; +import { + DarwinCompatibleTarget, + SwiftToolchain, + getDarwinTargetTriple, +} from "../../../src/toolchain/toolchain"; +import { WorkspaceContext } from "../../../src/WorkspaceContext"; +import { switchPlatform } from "../../../src/commands/switchPlatform"; +import { StatusItem } from "../../../src/ui/StatusItem"; +import configuration from "../../../src/configuration"; + +suite("Switch Target Platform Unit Tests", () => { + const mockedConfiguration = mockGlobalModule(configuration); + const windowMock = mockGlobalObject(vscode, "window"); + const mockSwiftToolchain = mockGlobalModule(SwiftToolchain); + let mockContext: MockedObject; + let mockedStatusItem: MockedObject; + + setup(() => { + mockedStatusItem = mockObject({ + start: mockFn(), + end: mockFn(), + }); + mockContext = mockObject({ + statusItem: instance(mockedStatusItem), + }); + }); + + test("Call Switch Platform and switch to iOS", async () => { + const selectedItem = { value: DarwinCompatibleTarget.iOS, label: "iOS" }; + windowMock.showQuickPick.resolves(selectedItem); + mockSwiftToolchain.getSDKForTarget.resolves(""); + expect(mockedConfiguration.swiftSDK).to.equal(""); + + await switchPlatform(instance(mockContext)); + + expect(windowMock.showQuickPick).to.have.been.calledOnce; + expect(windowMock.showWarningMessage).to.have.been.calledOnceWithExactly( + "Selecting the iOS target platform will provide code editing support, but compiling with a iOS SDK will have undefined results." + ); + expect(mockedStatusItem.start).to.have.been.called; + expect(mockedStatusItem.end).to.have.been.called; + expect(mockedConfiguration.swiftSDK).to.equal( + getDarwinTargetTriple(DarwinCompatibleTarget.iOS) + ); + }); + + test("Call Switch Platform and switch to macOS", async () => { + const selectedItem = { value: undefined, label: "macOS" }; + windowMock.showQuickPick.resolves(selectedItem); + mockSwiftToolchain.getSDKForTarget.resolves(""); + expect(mockedConfiguration.swiftSDK).to.equal(""); + + await switchPlatform(instance(mockContext)); + + expect(windowMock.showQuickPick).to.have.been.calledOnce; + expect(windowMock.showWarningMessage).to.not.have.been.called; + expect(mockedStatusItem.start).to.have.been.called; + expect(mockedStatusItem.end).to.have.been.called; + expect(mockedConfiguration.swiftSDK).to.equal(""); + }); +});