diff --git a/Resources/Info.plist b/Resources/Info.plist index f1beb4f9..708488ce 100644 --- a/Resources/Info.plist +++ b/Resources/Info.plist @@ -12,6 +12,21 @@ $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 + CFBundleDocumentTypes + + + CFBundleTypeName + Folder + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + LSItemContentTypes + + public.folder + + + CFBundleName $(PRODUCT_NAME) CFBundlePackageType diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index fbf3fad3..674b9d5c 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -368,13 +368,24 @@ enum FinderServicePathResolver { return canonical } + private static func resolvedDirectoryURL(from url: URL) -> URL { + let standardized = url.standardizedFileURL + if standardized.hasDirectoryPath { + return standardized + } + if let resourceValues = try? standardized.resourceValues(forKeys: [.isDirectoryKey]), + resourceValues.isDirectory == true { + return standardized + } + return standardized.deletingLastPathComponent() + } + static func orderedUniqueDirectories(from pathURLs: [URL]) -> [String] { var seen: Set = [] var directories: [String] = [] for url in pathURLs { - let standardized = url.standardizedFileURL - let directoryURL = standardized.hasDirectoryPath ? standardized : standardized.deletingLastPathComponent() + let directoryURL = resolvedDirectoryURL(from: url) let path = canonicalDirectoryPath(directoryURL.path(percentEncoded: false)) guard !path.isEmpty else { continue } if seen.insert(path).inserted { @@ -2154,6 +2165,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent Self.shared = self } + func application(_ application: NSApplication, open urls: [URL]) { + let directories = externalOpenDirectories(from: urls) + guard !directories.isEmpty else { return } + + prepareForExplicitOpenIntentAtStartup() + for directory in directories { + openWorkspaceForExternalDirectory( + workingDirectory: directory, + debugSource: "application.openURLs" + ) + } + } + func applicationDidFinishLaunching(_ notification: Notification) { let env = ProcessInfo.processInfo.environment let isRunningUnderXCTest = isRunningUnderXCTest(env) @@ -5077,11 +5101,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent target: ServiceOpenTarget, error: AutoreleasingUnsafeMutablePointer ) { - didHandleExplicitOpenIntentAtStartup = true - if !didAttemptStartupSessionRestore { - startupSessionSnapshot = nil - didAttemptStartupSessionRestore = true - } + prepareForExplicitOpenIntentAtStartup() let pathURLs = servicePathURLs(from: pasteboard) guard !pathURLs.isEmpty else { @@ -5089,7 +5109,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return } - let directories = FinderServicePathResolver.orderedUniqueDirectories(from: pathURLs) + let directories = externalOpenDirectories(from: pathURLs) guard !directories.isEmpty else { error.pointee = Self.serviceErrorNoPath return @@ -5134,10 +5154,32 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } private func openWorkspaceFromService(workingDirectory: String) { + openWorkspaceForExternalDirectory( + workingDirectory: workingDirectory, + debugSource: "service.openTab" + ) + } + + private func prepareForExplicitOpenIntentAtStartup() { + didHandleExplicitOpenIntentAtStartup = true + if !didAttemptStartupSessionRestore { + startupSessionSnapshot = nil + didAttemptStartupSessionRestore = true + } + } + + private func externalOpenDirectories(from urls: [URL]) -> [String] { + FinderServicePathResolver.orderedUniqueDirectories(from: urls.filter { $0.isFileURL }) + } + + private func openWorkspaceForExternalDirectory( + workingDirectory: String, + debugSource: String + ) { if addWorkspaceInPreferredMainWindow( workingDirectory: workingDirectory, shouldBringToFront: true, - debugSource: "service.openTab" + debugSource: debugSource ) != nil { return } diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index 5c0ad1af..c787a69a 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -1197,6 +1197,55 @@ final class AppDelegateWindowContextRoutingTests: XCTestCase { XCTAssertEqual(managerB.tabs.count, originalTabCountB + 1) XCTAssertTrue(managerB.tabs.contains(where: { $0.id == createdWorkspaceId })) } + + func testApplicationOpenURLsAddsWorkspaceForDroppedFolderURL() throws { + _ = NSApplication.shared + let app = AppDelegate() + + let windowId = UUID() + let window = makeMainWindow(id: windowId) + defer { window.orderOut(nil) } + + let manager = TabManager() + app.registerMainWindow( + window, + windowId: windowId, + tabManager: manager, + sidebarState: SidebarState(), + sidebarSelectionState: SidebarSelectionState() + ) + + window.makeKeyAndOrderFront(nil) + _ = app.synchronizeActiveMainWindowContext(preferredWindow: window) + + let defaults = UserDefaults.standard + let previousWelcomeShown = defaults.object(forKey: WelcomeSettings.shownKey) + defaults.set(true, forKey: WelcomeSettings.shownKey) + defer { + if let previousWelcomeShown { + defaults.set(previousWelcomeShown, forKey: WelcomeSettings.shownKey) + } else { + defaults.removeObject(forKey: WelcomeSettings.shownKey) + } + } + + let rootDirectory = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) + .appendingPathComponent(UUID().uuidString, isDirectory: true) + let droppedDirectory = rootDirectory.appendingPathComponent("project", isDirectory: true) + try FileManager.default.createDirectory(at: droppedDirectory, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: rootDirectory) } + + let existingWorkspaceIds = Set(manager.tabs.map(\.id)) + + app.application( + NSApplication.shared, + open: [URL(fileURLWithPath: droppedDirectory.path)] + ) + + let createdWorkspace = manager.tabs.first { !existingWorkspaceIds.contains($0.id) } + XCTAssertNotNil(createdWorkspace) + XCTAssertEqual(createdWorkspace?.currentDirectory, droppedDirectory.path) + } } @MainActor