Skip to content

Commit

Permalink
Prompt to restart extension on xcode-select
Browse files Browse the repository at this point in the history
If the user performs an `xcode-select` and doesn't have a `swift.path`
set in their VS Code settings, prompt them to restart the extension to
pick up the new Xcode path. This only applies on macOS where
`xcode-select` is possible.
  • Loading branch information
plemarquand committed Dec 5, 2024
1 parent 3c116a4 commit 8d6c0db
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { showReloadExtensionNotification } from "./ui/ReloadExtension";
import { checkAndWarnAboutWindowsSymlinks } from "./ui/win32";
import { SwiftEnvironmentVariablesManager, SwiftTerminalProfileProvider } from "./terminal";
import { resolveFolderDependencies } from "./commands/dependencies/resolve";
import { SelectedXcodeWatcher } from "./toolchain/SelectedXcodeWatcher";

/**
* External API as exposed by the extension. Can be queried by other extensions
Expand Down Expand Up @@ -125,6 +126,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api> {
context.subscriptions.push(...commands.register(workspaceContext));
context.subscriptions.push(workspaceContext);
context.subscriptions.push(registerDebugger(workspaceContext));
context.subscriptions.push(new SelectedXcodeWatcher(outputChannel));

// listen for workspace folder changes and active text editor changes
workspaceContext.setupEventListeners();
Expand Down
93 changes: 93 additions & 0 deletions src/toolchain/SelectedXcodeWatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the VS Code Swift open source project
//
// Copyright (c) 2021-2024 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 fs from "fs/promises";
import * as vscode from "vscode";
import { SwiftOutputChannel } from "../ui/SwiftOutputChannel";
import { showReloadExtensionNotification } from "../ui/ReloadExtension";
import configuration from "../configuration";

export class SelectedXcodeWatcher implements vscode.Disposable {
private xcodePath: string | undefined;
private disposed: boolean = false;
private interval: NodeJS.Timeout | undefined;
private checkIntervalMs: number;
private xcodeSymlink: () => Promise<string | undefined>;

private static DEFAULT_CHECK_INTERVAL_MS = 2000;
private static XCODE_SYMLINK_LOCATION = "/var/select/developer_dir";

constructor(
private outputChannel: SwiftOutputChannel,
testDependencies?: {
checkIntervalMs?: number;
xcodeSymlink?: () => Promise<string | undefined>;
}
) {
this.checkIntervalMs =
testDependencies?.checkIntervalMs || SelectedXcodeWatcher.DEFAULT_CHECK_INTERVAL_MS;
this.xcodeSymlink =
testDependencies?.xcodeSymlink ||
(async () => {
try {
return await fs.readlink(SelectedXcodeWatcher.XCODE_SYMLINK_LOCATION);
} catch (e) {
return undefined;
}
});

if (!this.isValidXcodePlatform()) {
return;
}

// Deliberately not awaiting this, as we don't want to block the extension activation.
this.setup();
}

dispose() {
this.disposed = true;
clearInterval(this.interval);
}

/**
* Polls the Xcode symlink location checking if it has changed.
* If the user has `swift.path` set in their settings this check is skipped.
*/
private async setup() {
this.xcodePath = await this.xcodeSymlink();
this.interval = setInterval(async () => {
if (this.disposed) {
return clearInterval(this.interval);
}

const newXcodePath = await this.xcodeSymlink();
if (!configuration.path && newXcodePath && this.xcodePath !== newXcodePath) {
this.outputChannel.appendLine(
`Selected Xcode changed from ${this.xcodePath} to ${newXcodePath}`
);
showReloadExtensionNotification(
"The Swift Extension has detected a change in the selected Xcode. Please reload the extension to apply the changes."
);
this.xcodePath = newXcodePath;
}
}, this.checkIntervalMs);
}

/**
* Xcode selection is a macOS only concept.
*/
private isValidXcodePlatform() {
return process.platform === "darwin";
}
}
70 changes: 70 additions & 0 deletions test/unit-tests/toolchain/SelectedXcodeWatcher.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the VS Code Swift open source project
//
// Copyright (c) 2024 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 { expect } from "chai";
import { SelectedXcodeWatcher } from "../../../src/toolchain/SelectedXcodeWatcher";
import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel";
import { instance, MockedObject, mockFn, mockGlobalObject, mockObject } from "../../MockUtils";

suite("Selected Xcode Watcher", () => {
const mockedVSCodeWindow = mockGlobalObject(vscode, "window");
let mockOutputChannel: MockedObject<SwiftOutputChannel>;

setup(() => {
mockOutputChannel = mockObject<SwiftOutputChannel>({
appendLine: mockFn(),
});
});

async function run(symLinksOnCallback: (string | undefined)[]) {
return new Promise<void>(resolve => {
let ctr = 0;
const watcher = new SelectedXcodeWatcher(instance(mockOutputChannel), {
checkIntervalMs: 1,
xcodeSymlink: async () => {
if (ctr >= symLinksOnCallback.length) {
watcher.dispose();
resolve();
return;
}
const response = symLinksOnCallback[ctr];
ctr += 1;
return response;
},
});
});
}

test("Does nothing when the symlink is undefined", async () => {
await run([undefined, undefined]);

expect(mockedVSCodeWindow.showWarningMessage).to.have.not.been.called;
});

test("Does nothing when the symlink is identical", async () => {
await run(["/foo", "/foo"]);

expect(mockedVSCodeWindow.showWarningMessage).to.have.not.been.called;
});

test("Prompts to restart when the symlink changes", async () => {
await run(["/foo", "/bar"]);

expect(mockedVSCodeWindow.showWarningMessage).to.have.been.calledOnceWithExactly(
"The Swift Extension has detected a change in the selected Xcode. Please reload the extension to apply the changes.",
"Reload Extensions"
);
});
});

0 comments on commit 8d6c0db

Please sign in to comment.