11import AppKit
2+ import AsyncAlgorithms
23import AXExtension
34import AXNotificationStream
45import Combine
@@ -14,8 +15,8 @@ public final class XcodeInspector: ObservableObject {
1415 @Published public internal( set) var activeXcode : XcodeAppInstanceInspector ?
1516 @Published public internal( set) var latestActiveXcode : XcodeAppInstanceInspector ?
1617 @Published public internal( set) var xcodes : [ XcodeAppInstanceInspector ] = [ ]
17- @Published public internal( set) var activeProjectPath = " "
18- @Published public internal( set) var activeDocumentPath = " "
18+ @Published public internal( set) var activeProjectURL = URL ( fileURLWithPath : " / " )
19+ @Published public internal( set) var activeDocumentURL = URL ( fileURLWithPath : " / " )
1920 @Published public internal( set) var focusedWindow : XcodeWindowInspector ?
2021 @Published public internal( set) var focusedEditor : SourceEditor ?
2122 @Published public internal( set) var focusedElement : AXUIElement ?
@@ -90,7 +91,8 @@ public final class XcodeInspector: ObservableObject {
9091 }
9192
9293 func observeXcode( _ xcode: XcodeAppInstanceInspector ) {
93- xcode. $document. filter { _ in xcode. isActive } . assign ( to: & $activeDocumentPath)
94+ xcode. $documentURL. filter { _ in xcode. isActive } . assign ( to: & $activeDocumentURL)
95+ xcode. $projectURL. filter { _ in xcode. isActive } . assign ( to: & $activeProjectURL)
9496 xcode. $focusedWindow. filter { _ in xcode. isActive } . assign ( to: & $focusedWindow)
9597 }
9698
@@ -100,7 +102,7 @@ public final class XcodeInspector: ObservableObject {
100102
101103 activeXcode = xcode
102104 latestActiveXcode = xcode
103- activeDocumentPath = xcode. document
105+ activeDocumentURL = xcode. documentURL
104106 focusedWindow = xcode. focusedWindow
105107
106108 let focusedElementChanged = Task { @MainActor in
@@ -137,7 +139,10 @@ public class AppInstanceInspector: ObservableObject {
137139
138140public final class XcodeAppInstanceInspector : AppInstanceInspector {
139141 @Published var focusedWindow : XcodeWindowInspector ?
140- var longRunningTasks = Set < Task < Void , Error > > ( )
142+ @Published var documentURL : URL = . init( fileURLWithPath: " / " )
143+ @Published var projectURL : URL = . init( fileURLWithPath: " / " )
144+ @Published var tabs : Set < String > = [ ]
145+ private var longRunningTasks = Set < Task < Void , Error > > ( )
141146
142147 deinit {
143148 for task in longRunningTasks { task. cancel ( ) }
@@ -146,87 +151,58 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector {
146151 override init ( runningApplication: NSRunningApplication ) {
147152 super. init ( runningApplication: runningApplication)
148153
154+ observeFocusedWindow ( )
149155 let focusedWindowChanged = Task {
150156 let notification = AXNotificationStream (
151157 app: runningApplication,
152158 notificationNames: kAXFocusedWindowChangedNotification
153159 )
154160 for await _ in notification {
155161 try Task . checkCancellation ( )
156- if let window = appElement. focusedWindow {
157- focusedWindow = XcodeWindowInspector ( uiElement: window)
158- } else {
159- focusedWindow = nil
160- }
162+ observeFocusedWindow ( )
161163 }
162164 }
163165
164166 longRunningTasks. insert ( focusedWindowChanged)
165- }
166- }
167167
168- public class XcodeWindowInspector : ObservableObject {
169- let uiElement : AXUIElement
170-
171- init ( uiElement: AXUIElement ) {
172- self . uiElement = uiElement
173- }
174- }
175-
176- public final class WorkspaceXcodeWindowInspector : XcodeWindowInspector {
177- let app : NSRunningApplication
178- @Published var documentURL : URL = . init( fileURLWithPath: " / " )
179- @Published var projectURL : URL = . init( fileURLWithPath: " / " )
180- @Published var tabs : Set < String > = [ ]
181- private var updateTabsTask : Task < Void , Error > ?
182- private var focusedElementChangedTask : Task < Void , Error > ?
183-
184- deinit {
185- updateTabsTask? . cancel ( )
186- focusedElementChangedTask? . cancel ( )
187- }
188-
189- init ( app: NSRunningApplication , uiElement: AXUIElement ) {
190- self . app = app
191- super. init ( uiElement: uiElement)
192-
193- updateTabsTask = Task { @MainActor in
194- while true {
195- try Task . checkCancellation ( )
196- if let updatedTabs = Self . findAvailableOpenedTabs ( app) {
197- tabs = updatedTabs
198- }
199- try await Task . sleep ( nanoseconds: 60 * 1_000_000_000 )
200- }
168+ if let updatedTabs = Self . findAvailableOpenedTabs ( runningApplication) {
169+ tabs = updatedTabs
201170 }
202-
203- focusedElementChangedTask = Task { @MainActor in
204- let update = {
205- let documentURL = Self . extractDocumentURL ( app, windowElement: uiElement)
206- if let documentURL {
207- self . documentURL = documentURL
171+ let updateTabsTask = Task { @MainActor in
172+ let notification = AXNotificationStream (
173+ app: runningApplication,
174+ notificationNames: kAXFocusedUIElementChangedNotification
175+ )
176+ if #available( macOS 13 . 0 , * ) {
177+ for await _ in notification. debounce ( for: . seconds( 5 ) ) {
178+ try Task . checkCancellation ( )
179+ if let updatedTabs = Self . findAvailableOpenedTabs ( runningApplication) {
180+ tabs = updatedTabs
181+ }
208182 }
209- let projectURL = Self . extractProjectURL (
210- app,
211- windowElement: uiElement,
212- fileURL: documentURL
213- )
214- if let projectURL {
215- self . projectURL = projectURL
183+ } else {
184+ for await _ in notification {
185+ try Task . checkCancellation ( )
186+ if let updatedTabs = Self . findAvailableOpenedTabs ( runningApplication) {
187+ tabs = updatedTabs
188+ }
216189 }
217190 }
218-
219- update ( )
220- let notifications = AXNotificationStream (
221- app: app,
222- element: uiElement,
223- notificationNames: kAXFocusedUIElementChangedNotification
224- )
225-
226- for await _ in notifications {
227- try Task . checkCancellation ( )
228- update ( )
191+ }
192+
193+ longRunningTasks. insert ( updateTabsTask)
194+ }
195+
196+ func observeFocusedWindow( ) {
197+ if let window = appElement. focusedWindow {
198+ let window = XcodeWindowInspector ( uiElement: window)
199+ focusedWindow = window
200+ if let workspaceWindow = window as? WorkspaceXcodeWindowInspector {
201+ workspaceWindow. $documentURL. assign ( to: & $documentURL)
202+ workspaceWindow. $projectURL. assign ( to: & $projectURL)
229203 }
204+ } else {
205+ focusedWindow = nil
230206 }
231207 }
232208
@@ -249,73 +225,5 @@ public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector {
249225 }
250226 return allTabs
251227 }
252-
253- static func extractDocumentURL(
254- _ app: NSRunningApplication ,
255- windowElement: AXUIElement
256- ) -> URL ? {
257- // fetch file path of the frontmost window of Xcode through Accessability API.
258- let application = AXUIElementCreateApplication ( app. processIdentifier)
259- var path = windowElement. document
260- if let path = path? . removingPercentEncoding {
261- let url = URL (
262- fileURLWithPath: path
263- . replacingOccurrences ( of: " file:// " , with: " " )
264- )
265- return url
266- }
267- return nil
268- }
269-
270- static func extractProjectURL(
271- _ app: NSRunningApplication ,
272- windowElement: AXUIElement ,
273- fileURL: URL ?
274- ) -> URL ? {
275- let application = AXUIElementCreateApplication ( app. processIdentifier)
276- let focusedWindow = application. focusedWindow
277- for child in focusedWindow? . children ?? [ ] {
278- if child. description. starts ( with: " / " ) , child. description. count > 1 {
279- let path = child. description
280- let trimmedNewLine = path. trimmingCharacters ( in: . newlines)
281- var url = URL ( fileURLWithPath: trimmedNewLine)
282- while !FileManager. default. fileIsDirectory ( atPath: url. path) ||
283- !url. pathExtension. isEmpty
284- {
285- url = url. deletingLastPathComponent ( )
286- }
287- return url
288- }
289- }
290-
291- guard var currentURL = fileURL else { return nil }
292- var firstDirectoryURL : URL ?
293- while currentURL. pathComponents. count > 1 {
294- defer { currentURL. deleteLastPathComponent ( ) }
295- guard FileManager . default. fileIsDirectory ( atPath: currentURL. path) else { continue }
296- if firstDirectoryURL == nil { firstDirectoryURL = currentURL }
297- let gitURL = currentURL. appendingPathComponent ( " .git " )
298- if FileManager . default. fileIsDirectory ( atPath: gitURL. path) {
299- return currentURL
300- }
301- }
302-
303- return firstDirectoryURL ?? fileURL
304- }
305- }
306-
307- public extension NSRunningApplication {
308- var isXcode : Bool { bundleIdentifier == " com.apple.dt.Xcode " }
309- var isCopilotForXcodeExtensionService : Bool {
310- bundleIdentifier == Bundle . main. bundleIdentifier
311- }
312- }
313-
314- extension FileManager {
315- func fileIsDirectory( atPath path: String ) -> Bool {
316- var isDirectory : ObjCBool = false
317- let exists = fileExists ( atPath: path, isDirectory: & isDirectory)
318- return isDirectory. boolValue && exists
319- }
320228}
321229
0 commit comments