Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 104 additions & 33 deletions supacode/Clients/Workspace/WorkspaceClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,14 @@ private func performOpenWorktreeAction(
worktree: Worktree,
onError: @escaping @MainActor @Sendable (OpenActionError) -> Void
) {
let actionTitle = action.title
switch action {
case .editor:
return
case .finder:
NSWorkspace.shared.activateFileViewerSelecting([worktree.workingDirectory])
case .androidStudio, .intellij, .webstorm, .pycharm, .rubymine, .rustrover:
guard let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: action.bundleIdentifier) else {
onError(
OpenActionError(
title: "\(action.title) not found",
message: "Install \(action.title) to open this worktree."
)
)
onError(.appNotFound(action))
return
}
let configuration = NSWorkspace.OpenConfiguration()
Expand All @@ -55,42 +49,119 @@ private func performOpenWorktreeAction(
return
}
Task { @MainActor in
onError(
OpenActionError(
title: "Unable to open in \(actionTitle)",
message: error.localizedDescription
)
)
onError(.openFailed(action, error))
}
}
case .alacritty, .antigravity, .cursor, .fork, .githubDesktop, .gitkraken, .gitup, .ghostty,
.kitty, .smartgit, .sourcetree, .sublimeMerge, .terminal, .vscode, .vscodeInsiders,
.vscodium, .warp, .wezterm, .windsurf, .xcode, .zed:
AppLauncher.open(action: action, worktree: worktree, onError: onError)
}
}

/// Opens a worktree directory in an arbitrary macOS app, preferring the app's bundled
/// command-line helper when it advertises one via `OpenWorktreeAction.cliLauncher`.
///
/// A CLI launch is preferred because it lets us pass flags/arguments the plain
/// "open document" event (`NSWorkspace.open`) cannot — e.g. Zed honoring
/// `cli_default_open_behavior` to give one window per worktree. Apps without a launcher,
/// and any app whose helper is missing, fall back to `NSWorkspace.open`.
enum AppLauncher {
enum Plan: Equatable {
case cli(executable: URL, arguments: [String])
case workspaceOpen(url: URL)
}

static func plan(
launcher: OpenWorktreeAction.CLILauncher?,
appURL: URL,
cliExists: Bool,
workingDirectory: URL
) -> Plan {
guard let launcher, cliExists else {
return .workspaceOpen(url: workingDirectory)
}
return .cli(
executable: appURL.appending(path: launcher.relativeExecutablePath),
arguments: launcher.openArguments + [workingDirectory.path]
)
}

@MainActor static func open(
action: OpenWorktreeAction,
worktree: Worktree,
onError: @escaping @MainActor @Sendable (OpenActionError) -> Void
) {
guard let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: action.bundleIdentifier) else {
onError(
OpenActionError(
title: "\(action.title) not found",
message: "Install \(action.title) to open this worktree."
)
)
onError(.appNotFound(action))
return
}
NSWorkspace.shared.open(
[worktree.workingDirectory],
withApplicationAt: appURL,
configuration: .init()
) { _, error in
guard let error else {
return
let launcher = action.cliLauncher
let cliExists =
launcher.map { FileManager.default.fileExists(atPath: appURL.appending(path: $0.relativeExecutablePath).path) }
?? false
switch plan(
launcher: launcher,
appURL: appURL,
cliExists: cliExists,
workingDirectory: worktree.workingDirectory
) {
case .cli(let executable, let arguments):
let process = Process()
process.executableURL = executable
process.arguments = arguments
do {
try process.run()
} catch {
onError(.openFailed(action, error))
}
Task { @MainActor in
onError(
OpenActionError(
title: "Unable to open in \(actionTitle)",
message: error.localizedDescription
)
)
case .workspaceOpen(let url):
NSWorkspace.shared.open([url], withApplicationAt: appURL, configuration: .init()) { _, error in
guard let error else {
return
}
Task { @MainActor in
onError(.openFailed(action, error))
}
}
}
}
}

extension OpenWorktreeAction {
/// Describes how to open a worktree through an app's bundled command-line helper.
struct CLILauncher: Equatable {
/// Path to the helper executable, relative to the app bundle root.
let relativeExecutablePath: String
/// Flags placed before the worktree path; the path is always appended last.
let openArguments: [String]
}

/// The bundled CLI used to open a worktree, or `nil` to fall back to `NSWorkspace.open`.
var cliLauncher: CLILauncher? {
switch self {
case .zed:
// The bare path (no `-n`) gives one window per worktree when Zed's
// `cli_default_open_behavior` is `new_window`, focusing an already-open worktree.
CLILauncher(relativeExecutablePath: "Contents/MacOS/cli", openArguments: [])
default:
nil
}
}
}

extension OpenActionError {
static func appNotFound(_ action: OpenWorktreeAction) -> OpenActionError {
OpenActionError(
title: "\(action.title) not found",
message: "Install \(action.title) to open this worktree."
)
}

static func openFailed(_ action: OpenWorktreeAction, _ error: Error) -> OpenActionError {
OpenActionError(
title: "Unable to open in \(action.title)",
message: error.localizedDescription
)
}
}
57 changes: 57 additions & 0 deletions supacodeTests/AppLauncherTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Foundation
import SupacodeSettingsShared
import Testing

@testable import supacode

struct AppLauncherTests {
private let appURL = URL(filePath: "/Applications/Zed.app")
private let workingDirectory = URL(filePath: "/Users/me/code/worktree")

@Test func zedExposesBundledCLILauncher() {
#expect(
OpenWorktreeAction.zed.cliLauncher
== .init(relativeExecutablePath: "Contents/MacOS/cli", openArguments: [])
)
}

@Test func appsWithoutABundledCLIHaveNoLauncher() {
#expect(OpenWorktreeAction.cursor.cliLauncher == nil)
#expect(OpenWorktreeAction.finder.cliLauncher == nil)
}

@Test func planLaunchesCLIWithWorktreePathWhenAvailable() {
let plan = AppLauncher.plan(
launcher: OpenWorktreeAction.zed.cliLauncher,
appURL: appURL,
cliExists: true,
workingDirectory: workingDirectory
)
#expect(
plan == .cli(
executable: appURL.appending(path: "Contents/MacOS/cli"),
arguments: ["/Users/me/code/worktree"]
)
)
}

@Test func planFallsBackToWorkspaceOpenWhenCLIMissing() {
let plan = AppLauncher.plan(
launcher: OpenWorktreeAction.zed.cliLauncher,
appURL: appURL,
cliExists: false,
workingDirectory: workingDirectory
)
#expect(plan == .workspaceOpen(url: workingDirectory))
}

@Test func planFallsBackToWorkspaceOpenWhenNoLauncher() {
let plan = AppLauncher.plan(
launcher: nil,
appURL: appURL,
cliExists: true,
workingDirectory: workingDirectory
)
#expect(plan == .workspaceOpen(url: workingDirectory))
}
}