Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import AppKit
import Preferences
import SuggestionInjector
import SuggestionModel
import Toast
import Workspace
import WorkspaceSuggestionService
import XcodeInspector
Expand All @@ -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(
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -432,3 +466,4 @@ extension PseudoCommandHandler {
return cursorRange
}
}

2 changes: 1 addition & 1 deletion Pro
Submodule Pro updated from 53c37b to bdca40
69 changes: 50 additions & 19 deletions Tool/Sources/XcodeInspector/XcodeInspector+TriggerCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,56 @@ 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.
""")
}
}

await Task.yield()

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 {
Expand All @@ -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()
Expand Down Expand Up @@ -103,7 +134,7 @@ public extension AppInstanceInspector {
Trigger menu item \(path.joined(separator: "/")) failed: \
\(error.localizedDescription)
""")
throw error
throw cantRunCommand(error.localizedDescription)
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Version.xcconfig
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
APP_VERSION = 0.30.1
APP_BUILD = 313
APP_VERSION = 0.30.2
APP_BUILD = 314

12 changes: 12 additions & 0 deletions appcast.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
<channel>
<title>Copilot for Xcode</title>

<item>
<title>0.30.2</title>
<pubDate>Thu, 01 Feb 2024 15:09:51 +0800</pubDate>
<sparkle:version>314</sparkle:version>
<sparkle:shortVersionString>0.30.2</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>12.0</sparkle:minimumSystemVersion>
<sparkle:releaseNotesLink>
https://github.com/intitni/CopilotForXcode/releases/tag/0.30.2
</sparkle:releaseNotesLink>
<enclosure url="https://github.com/intitni/CopilotForXcode/releases/download/0.30.2/Copilot.for.Xcode.app.zip" length="41214394" type="application/octet-stream" sparkle:edSignature="6a/v6PRi2JuGnlaRTNueUlO5v7T196/FcHavwSRyT8lkj8WYGm3PhFJqGJz87oyIgVZfhGsohYigBVGNDn8DCA=="/>
</item>

<item>
<title>0.30.1</title>
<pubDate>Sun, 28 Jan 2024 17:12:35 +0800</pubDate>
Expand Down