Skip to content

Commit e2389f7

Browse files
committed
Fix that completion panel change is not observed if Xcode is launched after the extension
1 parent 9c58fa3 commit e2389f7

3 files changed

Lines changed: 97 additions & 63 deletions

File tree

Core/Sources/AXNotificationStream/AXNotificationStream.swift

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,24 @@ public final class AXNotificationStream: AsyncSequence {
7272
.commonModes
7373
)
7474
}
75-
for name in notificationNames {
76-
AXObserverAddNotification(observer, observingElement, name as CFString, &continuation)
75+
76+
Task {
77+
for name in notificationNames {
78+
var error = AXError.cannotComplete
79+
var retryCount = 0
80+
while error == AXError.cannotComplete, retryCount < 5 {
81+
error = AXObserverAddNotification(observer, observingElement, name as CFString, &continuation)
82+
if error == .cannotComplete {
83+
try await Task.sleep(nanoseconds: 1_000_000_000)
84+
}
85+
retryCount += 1
86+
}
87+
}
88+
CFRunLoopAddSource(
89+
CFRunLoopGetMain(),
90+
AXObserverGetRunLoopSource(observer),
91+
.commonModes
92+
)
7793
}
78-
CFRunLoopAddSource(
79-
CFRunLoopGetMain(),
80-
AXObserverGetRunLoopSource(observer),
81-
.commonModes
82-
)
8394
}
8495
}

Core/Sources/XcodeInspector/XcodeInspector.swift

Lines changed: 52 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ public final class XcodeInspector: ObservableObject {
3333
.first(where: \.isActive)
3434
.map(AppInstanceInspector.init(runningApplication:))
3535

36-
if let activeXcode {
37-
setActiveXcode(activeXcode)
38-
}
39-
4036
Task { @MainActor in // Did activate app
37+
if let activeXcode {
38+
setActiveXcode(activeXcode)
39+
}
40+
4141
let sequence = NSWorkspace.shared.notificationCenter
4242
.notifications(named: NSWorkspace.didActivateApplicationNotification)
4343
for await notification in sequence {
@@ -87,7 +87,10 @@ public final class XcodeInspector: ObservableObject {
8787
}
8888
}
8989

90+
@MainActor
9091
func setActiveXcode(_ xcode: XcodeAppInstanceInspector) {
92+
xcode.refresh()
93+
9194
for task in activeXcodeObservations { task.cancel() }
9295
for cancellable in activeXcodeCancellable { cancellable.cancel() }
9396
activeXcodeObservations.removeAll()
@@ -208,6 +211,51 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector {
208211
super.init(runningApplication: runningApplication)
209212

210213
observeFocusedWindow()
214+
observe()
215+
}
216+
217+
func observeFocusedWindow() {
218+
if let window = appElement.focusedWindow {
219+
if window.identifier == "Xcode.WorkspaceWindow" {
220+
let window = WorkspaceXcodeWindowInspector(
221+
app: runningApplication,
222+
uiElement: window
223+
)
224+
focusedWindow = window
225+
focusedWindowObservations.forEach { $0.cancel() }
226+
focusedWindowObservations.removeAll()
227+
228+
documentURL = window.documentURL
229+
projectURL = window.projectURL
230+
231+
window.$documentURL
232+
.filter { $0 != .init(fileURLWithPath: "/") }
233+
.sink { [weak self] url in
234+
self?.documentURL = url
235+
}.store(in: &focusedWindowObservations)
236+
window.$projectURL
237+
.filter { $0 != .init(fileURLWithPath: "/") }
238+
.sink { [weak self] url in
239+
self?.projectURL = url
240+
}.store(in: &focusedWindowObservations)
241+
} else {
242+
let window = XcodeWindowInspector(uiElement: window)
243+
focusedWindow = window
244+
}
245+
} else {
246+
focusedWindow = nil
247+
}
248+
}
249+
250+
func refresh() {
251+
(focusedWindow as? WorkspaceXcodeWindowInspector)?.refresh()
252+
observe()
253+
}
254+
255+
func observe() {
256+
longRunningTasks.forEach { $0.cancel() }
257+
longRunningTasks = []
258+
211259
let focusedWindowChanged = Task {
212260
let notification = AXNotificationStream(
213261
app: runningApplication,
@@ -243,14 +291,9 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector {
243291

244292
longRunningTasks.insert(updateTabsTask)
245293

246-
completionPanel = appElement.firstChild { element in
247-
element.identifier == "_XC_COMPLETION_TABLE_"
248-
}?.parent
249-
250294
let completionPanelTask = Task {
251295
let stream = AXNotificationStream(
252296
app: runningApplication,
253-
element: appElement,
254297
notificationNames: kAXCreatedNotification, kAXUIElementDestroyedNotification
255298
)
256299

@@ -279,39 +322,6 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector {
279322
longRunningTasks.insert(completionPanelTask)
280323
}
281324

282-
func observeFocusedWindow() {
283-
if let window = appElement.focusedWindow {
284-
if window.identifier == "Xcode.WorkspaceWindow" {
285-
let window = WorkspaceXcodeWindowInspector(
286-
app: runningApplication,
287-
uiElement: window
288-
)
289-
focusedWindow = window
290-
focusedWindowObservations.forEach { $0.cancel() }
291-
focusedWindowObservations.removeAll()
292-
293-
documentURL = window.documentURL
294-
projectURL = window.projectURL
295-
296-
window.$documentURL
297-
.filter { $0 != .init(fileURLWithPath: "/") }
298-
.sink { [weak self] url in
299-
self?.documentURL = url
300-
}.store(in: &focusedWindowObservations)
301-
window.$projectURL
302-
.filter { $0 != .init(fileURLWithPath: "/") }
303-
.sink { [weak self] url in
304-
self?.projectURL = url
305-
}.store(in: &focusedWindowObservations)
306-
} else {
307-
let window = XcodeWindowInspector(uiElement: window)
308-
focusedWindow = window
309-
}
310-
} else {
311-
focusedWindow = nil
312-
}
313-
}
314-
315325
static func fetchWorkspaceInfo(
316326
_ app: NSRunningApplication
317327
) -> [WorkspaceIdentifier: WorkspaceInfo] {

Core/Sources/XcodeInspector/XcodeWindowInspector.swift

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,38 +23,51 @@ public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector {
2323
updateTabsTask?.cancel()
2424
focusedElementChangedTask?.cancel()
2525
}
26+
27+
public func refresh() {
28+
updateURLs()
29+
}
2630

2731
public init(app: NSRunningApplication, uiElement: AXUIElement) {
2832
self.app = app
2933
super.init(uiElement: uiElement)
3034

3135
focusedElementChangedTask = Task { @MainActor in
32-
let update = {
33-
let documentURL = Self.extractDocumentURL(windowElement: uiElement)
34-
if let documentURL {
35-
self.documentURL = documentURL
36-
}
37-
let projectURL = Self.extractProjectURL(
38-
windowElement: uiElement,
39-
fileURL: documentURL
40-
)
41-
if let projectURL {
42-
self.projectURL = projectURL
36+
updateURLs()
37+
38+
Task { @MainActor in
39+
// prevent that documentURL may not be available yet
40+
try await Task.sleep(nanoseconds: 500_000_000)
41+
if documentURL == .init(fileURLWithPath: "/") {
42+
updateURLs()
4343
}
4444
}
45-
46-
update()
45+
4746
let notifications = AXNotificationStream(
4847
app: app,
4948
notificationNames: kAXFocusedUIElementChangedNotification
5049
)
5150

5251
for await _ in notifications {
5352
try Task.checkCancellation()
54-
update()
53+
updateURLs()
5554
}
5655
}
5756
}
57+
58+
func updateURLs() {
59+
let documentURL = Self.extractDocumentURL(windowElement: uiElement)
60+
if let documentURL {
61+
self.documentURL = documentURL
62+
}
63+
let projectURL = Self.extractProjectURL(
64+
windowElement: uiElement,
65+
fileURL: documentURL
66+
)
67+
if let projectURL {
68+
self.projectURL = projectURL
69+
}
70+
}
5871

5972
static func extractDocumentURL(
6073
windowElement: AXUIElement

0 commit comments

Comments
 (0)