Skip to content

Commit 1a4d5b1

Browse files
committed
Move command trigger to XcodeInspector
1 parent 3bbaf83 commit 1a4d5b1

File tree

5 files changed

+141
-135
lines changed

5 files changed

+141
-135
lines changed

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public actor RealtimeSuggestionController {
2121
private var sourceEditor: SourceEditor?
2222

2323
init() {}
24-
24+
2525
deinit {
2626
task?.cancel()
2727
inflightPrefetchTask?.cancel()
@@ -34,7 +34,7 @@ public actor RealtimeSuggestionController {
3434
func start() {
3535
Task { await observeXcodeChange() }
3636
}
37-
37+
3838
private func observeXcodeChange() {
3939
task?.cancel()
4040
task = Task { [weak self] in
@@ -150,7 +150,8 @@ public actor RealtimeSuggestionController {
150150
// avoid the command get called twice
151151
filespace.codeMetadata.uti = ""
152152
do {
153-
try await Environment.triggerAction("Real-time Suggestions")
153+
try await XcodeInspector.shared.latestActiveXcode?
154+
.triggerCopilotCommand(name: "Real-time Suggestions")
154155
} catch {
155156
if filespace.codeMetadata.uti?.isEmpty ?? true {
156157
filespace.codeMetadata.uti = nil

Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ struct PseudoCommandHandler {
145145
}
146146
}() else {
147147
do {
148-
try await Environment.triggerAction(command.name)
148+
try await XcodeInspector.shared.latestActiveXcode?
149+
.triggerCopilotCommand(name: command.name)
149150
} catch {
150151
let presenter = PresentInWindowSuggestionPresenter()
151152
presenter.presentError(error)
@@ -167,7 +168,8 @@ struct PseudoCommandHandler {
167168
if UserDefaults.shared.value(for: \.alwaysAcceptSuggestionWithAccessibilityAPI) {
168169
throw CancellationError()
169170
}
170-
try await Environment.triggerAction("Accept Prompt to Code")
171+
try await XcodeInspector.shared.latestActiveXcode?
172+
.triggerCopilotCommand(name: "Accept Prompt to Code")
171173
} catch {
172174
guard let xcode = ActiveApplicationMonitor.shared.activeXcode
173175
?? ActiveApplicationMonitor.shared.latestXcode else { return }
@@ -206,7 +208,8 @@ struct PseudoCommandHandler {
206208
if UserDefaults.shared.value(for: \.alwaysAcceptSuggestionWithAccessibilityAPI) {
207209
throw CancellationError()
208210
}
209-
try await Environment.triggerAction("Accept Suggestion")
211+
try await XcodeInspector.shared.latestActiveXcode?
212+
.triggerCopilotCommand(name: "Accept Suggestion")
210213
} catch {
211214
guard let xcode = ActiveApplicationMonitor.shared.activeXcode
212215
?? ActiveApplicationMonitor.shared.latestXcode else { return }

Tool/Sources/Environment/Environment.swift

Lines changed: 0 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,6 @@ public struct FailedToFetchFileURLError: Error, LocalizedError {
2525
public enum Environment {
2626
public static var now = { Date() }
2727

28-
public static var isXcodeActive: () async -> Bool = {
29-
ActiveApplicationMonitor.shared.activeXcode != nil
30-
}
31-
32-
public static var frontmostXcodeWindowIsEditor: () async -> Bool = {
33-
let appleScript = """
34-
tell application "Xcode"
35-
return path of document of the first window
36-
end tell
37-
"""
38-
do {
39-
let result = try await runAppleScript(appleScript)
40-
return !result.isEmpty
41-
} catch {
42-
return false
43-
}
44-
}
45-
4628
#warning("TODO: Use XcodeInspector instead.")
4729
public static var fetchCurrentWorkspaceURLFromXcode: () async throws -> URL? = {
4830
if let xcode = ActiveApplicationMonitor.shared.activeXcode
@@ -148,114 +130,6 @@ public enum Environment {
148130
return windowElement
149131
}
150132
}
151-
152-
public static var triggerAction: (_ name: String) async throws -> Void = { name in
153-
struct CantRunCommand: Error, LocalizedError {
154-
let name: String
155-
var errorDescription: String? {
156-
"Can't run command \(name)."
157-
}
158-
}
159-
160-
guard let activeXcode = XcodeInspector.shared.latestActiveXcode?.runningApplication
161-
else { throw CantRunCommand(name: name) }
162-
163-
let bundleName = Bundle.main
164-
.object(forInfoDictionaryKey: "EXTENSION_BUNDLE_NAME") as! String
165-
166-
await Task.yield()
167-
168-
if UserDefaults.shared.value(for: \.triggerActionWithAccessibilityAPI) {
169-
if !activeXcode.isActive { activeXcode.activate() }
170-
let app = AXUIElementCreateApplication(activeXcode.processIdentifier)
171-
172-
if let editorMenu = app.menuBar?.child(title: "Editor"),
173-
let commandMenu = editorMenu.child(title: bundleName)
174-
{
175-
if let button = commandMenu.child(title: name, role: "AXMenuItem") {
176-
let error = AXUIElementPerformAction(button, kAXPressAction as CFString)
177-
if error != AXError.success {
178-
Logger.service
179-
.error("Trigger command \(name) failed: \(error.localizedDescription)")
180-
throw error
181-
} else {
182-
return
183-
}
184-
}
185-
} else if let commandMenu = app.menuBar?.child(title: bundleName),
186-
let button = commandMenu.child(title: name, role: "AXMenuItem")
187-
{
188-
let error = AXUIElementPerformAction(button, kAXPressAction as CFString)
189-
if error != AXError.success {
190-
Logger.service
191-
.error("Trigger command \(name) failed: \(error.localizedDescription)")
192-
throw error
193-
} else {
194-
return
195-
}
196-
}
197-
198-
throw CantRunCommand(name: name)
199-
} else {
200-
/// check if menu is open, if not, click the menu item.
201-
let appleScript = """
202-
tell application "System Events"
203-
set theprocs to every process whose unix id is \(activeXcode.processIdentifier)
204-
repeat with proc in theprocs
205-
set the frontmost of proc to true
206-
tell proc
207-
repeat with theMenu in menus of menu bar 1
208-
set theValue to value of attribute "AXVisibleChildren" of theMenu
209-
if theValue is not {} then
210-
return
211-
end if
212-
end repeat
213-
click menu item "\(name)" of menu 1 of menu item "\(bundleName)" of menu 1 of menu bar item "Editor" of menu bar 1
214-
end tell
215-
end repeat
216-
end tell
217-
"""
218-
219-
do {
220-
try await runAppleScript(appleScript)
221-
} catch {
222-
Logger.service
223-
.error("Trigger command \(name) failed: \(error.localizedDescription)")
224-
throw error
225-
}
226-
}
227-
}
228-
}
229-
230-
@discardableResult
231-
func runAppleScript(_ appleScript: String) async throws -> String {
232-
let task = Process()
233-
task.launchPath = "/usr/bin/osascript"
234-
task.arguments = ["-e", appleScript]
235-
let outpipe = Pipe()
236-
task.standardOutput = outpipe
237-
task.standardError = Pipe()
238-
239-
return try await withUnsafeThrowingContinuation { continuation in
240-
do {
241-
task.terminationHandler = { _ in
242-
do {
243-
if let data = try outpipe.fileHandleForReading.readToEnd(),
244-
let content = String(data: data, encoding: .utf8)
245-
{
246-
continuation.resume(returning: content)
247-
return
248-
}
249-
continuation.resume(returning: "")
250-
} catch {
251-
continuation.resume(throwing: error)
252-
}
253-
}
254-
try task.run()
255-
} catch {
256-
continuation.resume(throwing: error)
257-
}
258-
}
259133
}
260134

261135
public extension FileManager {

Tool/Sources/XcodeInspector/XcodeInspector.swift

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import AXExtension
44
import AXNotificationStream
55
import Combine
66
import Foundation
7+
import Logger
8+
import Preferences
79
import SuggestionModel
810

911
public final class XcodeInspector: ObservableObject {
@@ -88,8 +90,6 @@ public final class XcodeInspector: ObservableObject {
8890
.first(where: \.isActive)
8991
.map(AppInstanceInspector.init(runningApplication:))
9092

91-
#warning("Test Me")
92-
9393
Task { // Did activate app
9494
if let activeXcode {
9595
await setActiveXcode(activeXcode)
@@ -546,3 +546,132 @@ extension XcodeAppInstanceInspector {
546546
}
547547
}
548548

549+
// MARK: - Triggering Command
550+
551+
public extension XcodeAppInstanceInspector {
552+
func triggerCopilotCommand(name: String) async throws {
553+
let bundleName = Bundle.main
554+
.object(forInfoDictionaryKey: "EXTENSION_BUNDLE_NAME") as! String
555+
try await triggerMenuItem(path: ["Editor", bundleName, name])
556+
}
557+
}
558+
559+
public extension AppInstanceInspector {
560+
func triggerMenuItem(path: [String]) async throws {
561+
guard !path.isEmpty else { return }
562+
563+
struct CantRunCommand: Error, LocalizedError {
564+
let path: [String]
565+
var errorDescription: String? {
566+
"Can't run command \(path.joined(separator: "/"))."
567+
}
568+
}
569+
570+
if !runningApplication.isActive { runningApplication.activate() }
571+
572+
if UserDefaults.shared.value(for: \.triggerActionWithAccessibilityAPI) {
573+
let app = AXUIElementCreateApplication(runningApplication.processIdentifier)
574+
guard let menuBar = app.menuBar else { throw CantRunCommand(path: path) }
575+
var path = path
576+
var currentMenu = menuBar
577+
while !path.isEmpty {
578+
let item = path.removeFirst()
579+
580+
if path.isEmpty, let button = currentMenu.child(title: item, role: "AXMenuItem") {
581+
let error = AXUIElementPerformAction(button, kAXPressAction as CFString)
582+
if error != AXError.success {
583+
Logger.service.error("""
584+
Trigger menu item \(path.joined(separator: "/")) failed: \
585+
\(error.localizedDescription)
586+
""")
587+
throw error
588+
} else {
589+
return
590+
}
591+
} else if let menu = currentMenu.child(title: item) {
592+
currentMenu = menu
593+
} else {
594+
throw CantRunCommand(path: path)
595+
}
596+
}
597+
} else {
598+
guard path.count >= 2 else { throw CantRunCommand(path: path) }
599+
600+
let clickTask = {
601+
var path = path
602+
let button = path.removeLast()
603+
let menuBarItem = path.removeFirst()
604+
let list = path
605+
.reversed()
606+
.map { "menu 1 of menu item \"\($0)\"" }
607+
.joined(separator: " of ")
608+
return """
609+
click menu item "\(button)" of \(list) \
610+
of menu bar item "\(menuBarItem)" \
611+
of menu bar 1
612+
"""
613+
}()
614+
/// check if menu is open, if not, click the menu item.
615+
let appleScript = """
616+
tell application "System Events"
617+
set theprocs to every process whose unix id is \
618+
\(runningApplication.processIdentifier)
619+
repeat with proc in theprocs
620+
set the frontmost of proc to true
621+
tell proc
622+
repeat with theMenu in menus of menu bar 1
623+
set theValue to value of attribute "AXVisibleChildren" of theMenu
624+
if theValue is not {} then
625+
return
626+
end if
627+
end repeat
628+
\(clickTask)
629+
end tell
630+
end repeat
631+
end tell
632+
"""
633+
634+
do {
635+
try await runAppleScript(appleScript)
636+
} catch {
637+
Logger.service.error("""
638+
Trigger menu item \(path.joined(separator: "/")) failed: \
639+
\(error.localizedDescription)
640+
""")
641+
throw error
642+
}
643+
}
644+
}
645+
}
646+
647+
@discardableResult
648+
func runAppleScript(_ appleScript: String) async throws -> String {
649+
let task = Process()
650+
task.launchPath = "/usr/bin/osascript"
651+
task.arguments = ["-e", appleScript]
652+
let outpipe = Pipe()
653+
task.standardOutput = outpipe
654+
task.standardError = Pipe()
655+
656+
return try await withUnsafeThrowingContinuation { continuation in
657+
do {
658+
task.terminationHandler = { _ in
659+
do {
660+
if let data = try outpipe.fileHandleForReading.readToEnd(),
661+
let content = String(data: data, encoding: .utf8)
662+
{
663+
continuation.resume(returning: content)
664+
return
665+
}
666+
continuation.resume(returning: "")
667+
} catch {
668+
continuation.resume(throwing: error)
669+
}
670+
}
671+
try task.run()
672+
} catch {
673+
continuation.resume(throwing: error)
674+
}
675+
}
676+
}
677+

Tool/Sources/XcodeInspector/XcodeWindowInspector.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector {
3838
notificationNames: kAXFocusedUIElementChangedNotification
3939
)
4040

41-
#warning("Test Me")
4241
focusedElementChangedTask = Task { [weak self] in
4342
await self?.updateURLs()
4443

0 commit comments

Comments
 (0)