diff --git a/src/commands.ts b/src/commands.ts index cff624105..e55283c2b 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -98,6 +98,7 @@ export enum Commands { COVER_ALL_TESTS = "swift.coverAllTests", OPEN_MANIFEST = "swift.openManifest", RESTART_LSP = "swift.restartLSPServer", + SELECT_TOOLCHAIN = "swift.selectToolchain", } /** diff --git a/src/toolchain/SelectedXcodeWatcher.ts b/src/toolchain/SelectedXcodeWatcher.ts index d7954e810..5d06c4c05 100644 --- a/src/toolchain/SelectedXcodeWatcher.ts +++ b/src/toolchain/SelectedXcodeWatcher.ts @@ -17,6 +17,7 @@ import * as vscode from "vscode"; import { SwiftOutputChannel } from "../ui/SwiftOutputChannel"; import { showReloadExtensionNotification } from "../ui/ReloadExtension"; import configuration from "../configuration"; +import { removeToolchainPath, selectToolchain } from "../ui/ToolchainSelection"; export class SelectedXcodeWatcher implements vscode.Disposable { private xcodePath: string | undefined; @@ -66,20 +67,40 @@ export class SelectedXcodeWatcher implements vscode.Disposable { */ private async setup() { this.xcodePath = await this.xcodeSymlink(); + if ( + this.xcodePath && + configuration.path && + !configuration.path.startsWith(this.xcodePath) + ) { + this.xcodePath = undefined; // Notify user when initially launching that xcode changed since last session + } this.interval = setInterval(async () => { if (this.disposed) { return clearInterval(this.interval); } const newXcodePath = await this.xcodeSymlink(); - if (!configuration.path && newXcodePath && this.xcodePath !== newXcodePath) { + if (newXcodePath && this.xcodePath !== newXcodePath) { this.outputChannel.appendLine( `Selected Xcode changed from ${this.xcodePath} to ${newXcodePath}` ); this.xcodePath = newXcodePath; - await showReloadExtensionNotification( - "The Swift Extension has detected a change in the selected Xcode. Please reload the extension to apply the changes." - ); + if (!configuration.path) { + await showReloadExtensionNotification( + "The Swift Extension has detected a change in the selected Xcode. Please reload the extension to apply the changes." + ); + } else { + const selected = await vscode.window.showWarningMessage( + 'The Swift Extension has detected a change in the selected Xcode which does not match the value of your "swift.path" setting. Would you like to update your configured "swift.path" setting?', + "Remove From Settings", + "Select Toolchain" + ); + if (selected === "Remove From Settings") { + await removeToolchainPath(); + } else if (selected === "Select Toolchain") { + await selectToolchain(); + } + } } }, this.checkIntervalMs); } diff --git a/src/ui/ToolchainSelection.ts b/src/ui/ToolchainSelection.ts index 622f2042c..202ba8785 100644 --- a/src/ui/ToolchainSelection.ts +++ b/src/ui/ToolchainSelection.ts @@ -17,6 +17,7 @@ import * as path from "path"; import { showReloadExtensionNotification } from "./ReloadExtension"; import { SwiftToolchain } from "../toolchain/toolchain"; import configuration from "../configuration"; +import { Commands } from "../commands"; /** * Open the installation page on Swift.org @@ -28,7 +29,7 @@ export async function downloadToolchain() { "Select Toolchain" ); if (selected === "Select Toolchain") { - await vscode.commands.executeCommand("swift.selectToolchain"); + await selectToolchain(); } } } @@ -43,7 +44,7 @@ export async function installSwiftly() { "Select Toolchain" ); if (selected === "Select Toolchain") { - await vscode.commands.executeCommand("swift.selectToolchain"); + await selectToolchain(); } } } @@ -87,10 +88,14 @@ export async function showToolchainError(): Promise { if (selected === "Remove From Settings") { await removeToolchainPath(); } else if (selected === "Select Toolchain") { - await vscode.commands.executeCommand("swift.selectToolchain"); + await selectToolchain(); } } +export async function selectToolchain() { + await vscode.commands.executeCommand(Commands.SELECT_TOOLCHAIN); +} + /** A {@link vscode.QuickPickItem} that contains the path to an installed Swift toolchain */ type SwiftToolchainItem = PublicSwiftToolchainItem | XcodeToolchainItem; @@ -351,7 +356,7 @@ async function showDeveloperDirQuickPick(xcodePaths: string[]): Promise { const mockedVSCodeWindow = mockGlobalObject(vscode, "window"); let mockOutputChannel: MockedObject; const pathConfig = mockGlobalValue(configuration, "path"); + const mockWorkspace = mockGlobalObject(vscode, "workspace"); + const mockCommands = mockGlobalObject(vscode, "commands"); + let mockSwiftConfig: MockedObject; setup(function () { // Xcode only exists on macOS, so the SelectedXcodeWatcher is macOS-only. @@ -42,6 +46,12 @@ suite("Selected Xcode Watcher", () => { }); pathConfig.setValue(""); + + mockSwiftConfig = mockObject({ + inspect: mockFn(), + update: mockFn(), + }); + mockWorkspace.getConfiguration.returns(instance(mockSwiftConfig)); }); async function run(symLinksOnCallback: (string | undefined)[]) { @@ -84,11 +94,50 @@ suite("Selected Xcode Watcher", () => { ); }); - test("Ignores when path is explicitly set", async () => { + test("Warns that setting is out of date", async () => { pathConfig.setValue("/path/to/swift/bin"); - await run(["/foo", "/bar"]); + await run(["/path/to/swift/bin", "/foo", "/foo"]); - expect(mockedVSCodeWindow.showWarningMessage).to.have.not.been.called; + expect(mockedVSCodeWindow.showWarningMessage).to.have.been.calledOnceWithExactly( + 'The Swift Extension has detected a change in the selected Xcode which does not match the value of your "swift.path" setting. Would you like to update your configured "swift.path" setting?', + "Remove From Settings", + "Select Toolchain" + ); + }); + + test("Warns that setting is out of date on startup", async () => { + pathConfig.setValue("/path/to/swift/bin"); + + await run(["/foo", "/foo"]); + + expect(mockedVSCodeWindow.showWarningMessage).to.have.been.calledOnceWithExactly( + 'The Swift Extension has detected a change in the selected Xcode which does not match the value of your "swift.path" setting. Would you like to update your configured "swift.path" setting?', + "Remove From Settings", + "Select Toolchain" + ); + }); + + test("Remove setting", async () => { + pathConfig.setValue("/path/to/swift/bin"); + + mockedVSCodeWindow.showWarningMessage.resolves("Remove From Settings" as any); + + await run(["/foo", "/foo"]); + + expect(mockSwiftConfig.update.args).to.deep.equal([ + ["path", undefined, vscode.ConfigurationTarget.Global], + ["path", undefined, vscode.ConfigurationTarget.Workspace], + ]); + }); + + test("Select toolchain", async () => { + pathConfig.setValue("/path/to/swift/bin"); + + mockedVSCodeWindow.showWarningMessage.resolves("Select Toolchain" as any); + + await run(["/foo", "/foo"]); + + expect(mockCommands.executeCommand).to.have.been.calledOnceWith(Commands.SELECT_TOOLCHAIN); }); });