@@ -108,7 +108,7 @@ public final class XcodeInspector: ObservableObject {
108108 latestActiveXcode = xcode
109109 activeDocumentURL = xcode. documentURL
110110 focusedWindow = xcode. focusedWindow
111-
111+
112112 let setFocusedElement = { [ weak self] in
113113 guard let self else { return }
114114 focusedElement = xcode. appElement. focusedElement
@@ -150,10 +150,23 @@ public class AppInstanceInspector: ObservableObject {
150150}
151151
152152public final class XcodeAppInstanceInspector : AppInstanceInspector {
153- @Published var focusedWindow : XcodeWindowInspector ?
154- @Published var documentURL : URL = . init( fileURLWithPath: " / " )
155- @Published var projectURL : URL = . init( fileURLWithPath: " / " )
156- @Published var tabs : Set < String > = [ ]
153+ public struct WorkspaceInfo {
154+ public let tabs : Set < String >
155+
156+ public func combined( with info: WorkspaceInfo ) -> WorkspaceInfo {
157+ return . init( tabs: info. tabs. union ( tabs) )
158+ }
159+ }
160+
161+ public enum WorkspaceIdentifier : Hashable {
162+ case url( URL )
163+ case unknown
164+ }
165+
166+ @Published public var focusedWindow : XcodeWindowInspector ?
167+ @Published public var documentURL : URL = . init( fileURLWithPath: " / " )
168+ @Published public var projectURL : URL = . init( fileURLWithPath: " / " )
169+ @Published public var workspaces = [ WorkspaceIdentifier: WorkspaceInfo] ( )
157170 private var longRunningTasks = Set < Task < Void , Error > > ( )
158171 private var focusedWindowObservations = Set < AnyCancellable > ( )
159172
@@ -178,27 +191,22 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector {
178191
179192 longRunningTasks. insert ( focusedWindowChanged)
180193
181- if let updatedTabs = Self . findAvailableOpenedTabs ( runningApplication) {
182- tabs = updatedTabs
183- }
194+ workspaces = Self . fetchWorkspaceInfo ( runningApplication)
184195 let updateTabsTask = Task { @MainActor in
185196 let notification = AXNotificationStream (
186197 app: runningApplication,
187- notificationNames: kAXFocusedUIElementChangedNotification
198+ notificationNames: kAXFocusedUIElementChangedNotification,
199+ kAXApplicationDeactivatedNotification
188200 )
189201 if #available( macOS 13 . 0 , * ) {
190202 for await _ in notification. debounce ( for: . seconds( 5 ) ) {
191203 try Task . checkCancellation ( )
192- if let updatedTabs = Self . findAvailableOpenedTabs ( runningApplication) {
193- tabs = updatedTabs
194- }
204+ workspaces = Self . fetchWorkspaceInfo ( runningApplication)
195205 }
196206 } else {
197207 for await _ in notification {
198208 try Task . checkCancellation ( )
199- if let updatedTabs = Self . findAvailableOpenedTabs ( runningApplication) {
200- tabs = updatedTabs
201- }
209+ workspaces = Self . fetchWorkspaceInfo ( runningApplication)
202210 }
203211 }
204212 }
@@ -239,24 +247,50 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector {
239247 }
240248 }
241249
242- static func findAvailableOpenedTabs( _ app: NSRunningApplication ) -> Set < String > ? {
250+ static func fetchWorkspaceInfo(
251+ _ app: NSRunningApplication
252+ ) -> [ WorkspaceIdentifier : WorkspaceInfo ] {
243253 let app = AXUIElementCreateApplication ( app. processIdentifier)
244- guard app. isFocused else { return nil }
245254 let windows = app. windows. filter { $0. identifier == " Xcode.WorkspaceWindow " }
246- guard !windows. isEmpty else { return [ ] }
247- var allTabs = Set < String > ( )
255+
256+ var dict = [ WorkspaceIdentifier: WorkspaceInfo] ( )
257+
248258 for window in windows {
249- guard let editArea = window. firstChild ( where: { $0. description == " editor area " } )
250- else { continue }
251- let tabBars = editArea. children { $0. description == " tab bar " }
252- for tabBar in tabBars {
253- let tabs = tabBar. children { $0. roleDescription == " tab " }
254- for tab in tabs {
255- allTabs. insert ( tab. title)
259+ let workspaceIdentifier = {
260+ for child in window. children {
261+ if child. description. starts ( with: " / " ) , child. description. count > 1 {
262+ let path = child. description
263+ let trimmedNewLine = path. trimmingCharacters ( in: . newlines)
264+ var url = URL ( fileURLWithPath: trimmedNewLine)
265+ while !FileManager. default. fileIsDirectory ( atPath: url. path) ||
266+ !url. pathExtension. isEmpty
267+ {
268+ url = url. deletingLastPathComponent ( )
269+ }
270+ return WorkspaceIdentifier . url ( url)
271+ }
256272 }
257- }
273+ return WorkspaceIdentifier . unknown
274+ } ( )
275+
276+ let tabs = {
277+ guard let editArea = window. firstChild ( where: { $0. description == " editor area " } )
278+ else { return Set < String > ( ) }
279+ var allTabs = Set < String > ( )
280+ let tabBars = editArea. children { $0. description == " tab bar " }
281+ for tabBar in tabBars {
282+ let tabs = tabBar. children { $0. roleDescription == " tab " }
283+ for tab in tabs {
284+ allTabs. insert ( tab. title)
285+ }
286+ }
287+ return allTabs
288+ } ( )
289+
290+ dict [ workspaceIdentifier] = . init( tabs: tabs)
258291 }
259- return allTabs
292+
293+ return dict
260294 }
261295}
262296
0 commit comments