diff --git a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift index 00198557..b553812b 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift @@ -3,6 +3,7 @@ import AppKit import Preferences import SuggestionInjector import SuggestionModel +import Toast import Workspace import WorkspaceSuggestionService import XcodeInspector @@ -12,6 +13,9 @@ import XPCShared /// /// For example, we can use it to generate real-time suggestions without Apple Scripts. struct PseudoCommandHandler { + static var lastTimeCommandFailedToTriggerWithAccessibilityAPI = Date(timeIntervalSince1970: 0) + private var toast: ToastController { ToastControllerDependencyKey.liveValue } + func presentPreviousSuggestion() async { let handler = WindowBaseCommandHandler() _ = try? await handler.presentPreviousSuggestion(editor: .init( @@ -43,11 +47,11 @@ struct PseudoCommandHandler { @WorkspaceActor func generateRealtimeSuggestions(sourceEditor: SourceEditor?) async { guard let filespace = await getFilespace(), - let (workspace, _) = try? await Service.shared.workspacePool - .fetchOrCreateWorkspaceAndFilespace(fileURL: filespace.fileURL) else { return } - + let (workspace, _) = try? await Service.shared.workspacePool + .fetchOrCreateWorkspaceAndFilespace(fileURL: filespace.fileURL) else { return } + if Task.isCancelled { return } - + // Can't use handler if content is not available. guard let editor = await getEditorContent(sourceEditor: sourceEditor) else { return } @@ -107,7 +111,7 @@ struct PseudoCommandHandler { if filespace.presentingSuggestion == nil { return // skip if there's no suggestion presented. } - + let content = sourceEditor.getContent() if !filespace.validateSuggestions( lines: content.lines, @@ -178,8 +182,23 @@ struct PseudoCommandHandler { if UserDefaults.shared.value(for: \.alwaysAcceptSuggestionWithAccessibilityAPI) { throw CancellationError() } - try await XcodeInspector.shared.latestActiveXcode? - .triggerCopilotCommand(name: "Accept Prompt to Code") + do { + try await XcodeInspector.shared.latestActiveXcode? + .triggerCopilotCommand(name: "Accept Prompt to Code") + } catch { + let last = Self.lastTimeCommandFailedToTriggerWithAccessibilityAPI + let now = Date() + if now.timeIntervalSince(last) > 60 * 60 { + Self.lastTimeCommandFailedToTriggerWithAccessibilityAPI = now + toast.toast(content: """ + The app is using a fallback solution to accept suggestions. \ + For better experience, please restart Xcode to re-activate the Copilot \ + menu item. + """, type: .warning) + } + + throw error + } } catch { guard let xcode = ActiveApplicationMonitor.shared.activeXcode ?? ActiveApplicationMonitor.shared.latestXcode else { return } @@ -218,8 +237,23 @@ struct PseudoCommandHandler { if UserDefaults.shared.value(for: \.alwaysAcceptSuggestionWithAccessibilityAPI) { throw CancellationError() } - try await XcodeInspector.shared.latestActiveXcode? - .triggerCopilotCommand(name: "Accept Suggestion") + do { + try await XcodeInspector.shared.latestActiveXcode? + .triggerCopilotCommand(name: "Accept Suggestion") + } catch { + let last = Self.lastTimeCommandFailedToTriggerWithAccessibilityAPI + let now = Date() + if now.timeIntervalSince(last) > 60 * 60 { + Self.lastTimeCommandFailedToTriggerWithAccessibilityAPI = now + toast.toast(content: """ + The app is using a fallback solution to accept suggestions. \ + For better experience, please restart Xcode to re-activate the Copilot \ + menu item. + """, type: .warning) + } + + throw error + } } catch { guard let xcode = ActiveApplicationMonitor.shared.activeXcode ?? ActiveApplicationMonitor.shared.latestXcode else { return } @@ -252,12 +286,12 @@ struct PseudoCommandHandler { } } } - + func dismissSuggestion() async { guard let documentURL = XcodeInspector.shared.activeDocumentURL else { return } guard let (_, filespace) = try? await Service.shared.workspacePool .fetchOrCreateWorkspaceAndFilespace(fileURL: documentURL) else { return } - + await filespace.reset() PresentInWindowSuggestionPresenter().discardSuggestion(fileURL: documentURL) } @@ -432,3 +466,4 @@ extension PseudoCommandHandler { return cursorRange } } + diff --git a/Pro b/Pro index 53c37b45..bdca40b9 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 53c37b4555353fd4b33c3905b596ca2821ab3a0f +Subproject commit bdca40b959c4dfb69a41e9dabd044757a012d77d diff --git a/Tool/Sources/XcodeInspector/XcodeInspector+TriggerCommand.swift b/Tool/Sources/XcodeInspector/XcodeInspector+TriggerCommand.swift index e8703f2b..5672a9ea 100644 --- a/Tool/Sources/XcodeInspector/XcodeInspector+TriggerCommand.swift +++ b/Tool/Sources/XcodeInspector/XcodeInspector+TriggerCommand.swift @@ -7,29 +7,41 @@ public extension XcodeAppInstanceInspector { func triggerCopilotCommand(name: String, activateXcode: Bool = true) async throws { let bundleName = Bundle.main .object(forInfoDictionaryKey: "EXTENSION_BUNDLE_NAME") as! String - try await triggerMenuItem(path: ["Editor", bundleName, name], activateXcode: activateXcode) + try await triggerMenuItem(path: ["Editor", bundleName, name], activateApp: activateXcode) } } public extension AppInstanceInspector { - @MainActor - func triggerMenuItem(path: [String], activateXcode: Bool) async throws { - guard !path.isEmpty else { return } + struct CantRunCommand: Error, LocalizedError { + let path: String + let reason: String + public var errorDescription: String? { + "Can't run command \(path): \(reason)" + } + } - struct CantRunCommand: Error, LocalizedError { - let path: [String] - var errorDescription: String? { - "Can't run command \(path.joined(separator: "/"))." - } + @MainActor + func triggerMenuItem(path: [String], activateApp: Bool) async throws { + let sourcePath = path.joined(separator: "/") + func cantRunCommand(_ reason: String) -> CantRunCommand { + return CantRunCommand(path: sourcePath, reason: reason) } - if activateXcode { + guard path.count >= 2 else { throw cantRunCommand("Path too short.") } + + if activateApp { if !runningApplication.activate() { - throw CantRunCommand(path: path) + Logger.service.error(""" + Trigger menu item \(sourcePath) failed: \ + Xcode not activated. + """) } } else { if !runningApplication.isActive { - throw CantRunCommand(path: path) + Logger.service.error(""" + Trigger menu item \(sourcePath) failed: \ + Xcode not activated. + """) } } @@ -37,7 +49,14 @@ public extension AppInstanceInspector { if UserDefaults.shared.value(for: \.triggerActionWithAccessibilityAPI) { let app = AXUIElementCreateApplication(runningApplication.processIdentifier) - guard let menuBar = app.menuBar else { throw CantRunCommand(path: path) } + + guard let menuBar = app.menuBar else { + Logger.service.error(""" + Trigger menu item \(sourcePath) failed: \ + Menu not found. + """) + throw cantRunCommand("Menu not found.") + } var path = path var currentMenu = menuBar while !path.isEmpty { @@ -47,22 +66,34 @@ public extension AppInstanceInspector { let error = AXUIElementPerformAction(button, kAXPressAction as CFString) if error != AXError.success { Logger.service.error(""" - Trigger menu item \(path.joined(separator: "/")) failed: \ + Trigger menu item \(sourcePath) failed: \ \(error.localizedDescription) """) - throw error + throw cantRunCommand(error.localizedDescription) } else { + #if DEBUG + Logger.service.info(""" + Trigger menu item \(sourcePath) succeeded. + """) + #endif return } } else if let menu = currentMenu.child(title: item) { + #if DEBUG + Logger.service.info(""" + Trigger menu item \(sourcePath): Move to \(item). + """) + #endif currentMenu = menu } else { - throw CantRunCommand(path: path) + Logger.service.error(""" + Trigger menu item \(sourcePath) failed: \ + \(item) is not found. + """) + throw cantRunCommand("\(item) is not found.") } } } else { - guard path.count >= 2 else { throw CantRunCommand(path: path) } - let clickTask = { var path = path let button = path.removeLast() @@ -103,7 +134,7 @@ public extension AppInstanceInspector { Trigger menu item \(path.joined(separator: "/")) failed: \ \(error.localizedDescription) """) - throw error + throw cantRunCommand(error.localizedDescription) } } } diff --git a/Version.xcconfig b/Version.xcconfig index 71265973..d2934015 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,3 +1,3 @@ -APP_VERSION = 0.30.1 -APP_BUILD = 313 +APP_VERSION = 0.30.2 +APP_BUILD = 314 diff --git a/appcast.xml b/appcast.xml index be5859df..ebedb1ef 100644 --- a/appcast.xml +++ b/appcast.xml @@ -3,6 +3,18 @@ Copilot for Xcode + + 0.30.2 + Thu, 01 Feb 2024 15:09:51 +0800 + 314 + 0.30.2 + 12.0 + + https://github.com/intitni/CopilotForXcode/releases/tag/0.30.2 + + + + 0.30.1 Sun, 28 Jan 2024 17:12:35 +0800