Skip to content

Commit efdd979

Browse files
committed
Replace AXNotificationStream creation
1 parent a464a49 commit efdd979

6 files changed

Lines changed: 69 additions & 146 deletions

File tree

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 25 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import ActiveApplicationMonitor
22
import AppKit
33
import AsyncAlgorithms
44
import AXExtension
5-
import AXNotificationStream
5+
import Combine
66
import Foundation
77
import Logger
88
import Preferences
@@ -11,21 +11,16 @@ import Workspace
1111
import XcodeInspector
1212

1313
public actor RealtimeSuggestionController {
14-
private var task: Task<Void, Error>?
14+
private var cancellable: Set<AnyCancellable> = []
1515
private var inflightPrefetchTask: Task<Void, Error>?
16-
private var windowChangeObservationTask: Task<Void, Error>?
17-
private var activeApplicationMonitorTask: Task<Void, Error>?
1816
private var editorObservationTask: Task<Void, Error>?
19-
private var focusedUIElement: AXUIElement?
2017
private var sourceEditor: SourceEditor?
2118

2219
init() {}
2320

2421
deinit {
25-
task?.cancel()
22+
cancellable.forEach { $0.cancel() }
2623
inflightPrefetchTask?.cancel()
27-
windowChangeObservationTask?.cancel()
28-
activeApplicationMonitorTask?.cancel()
2924
editorObservationTask?.cancel()
3025
}
3126

@@ -35,80 +30,35 @@ public actor RealtimeSuggestionController {
3530
}
3631

3732
private func observeXcodeChange() {
38-
task?.cancel()
39-
task = Task { [weak self] in
40-
if ActiveApplicationMonitor.shared.activeXcode != nil {
41-
await self?.handleXcodeChanged()
42-
}
43-
var previousApp = ActiveApplicationMonitor.shared.activeXcode?.info
44-
for await app in ActiveApplicationMonitor.shared.createInfoStream() {
33+
cancellable.forEach { $0.cancel() }
34+
35+
XcodeInspector.shared.$focusedEditor
36+
.sink { [weak self] editor in
4537
guard let self else { return }
46-
try Task.checkCancellation()
47-
defer { previousApp = app }
48-
49-
if let app = ActiveApplicationMonitor.shared.activeXcode,
50-
app.processIdentifier != previousApp?.processIdentifier
51-
{
52-
await self.handleXcodeChanged()
38+
Task {
39+
guard let editor else { return }
40+
await self.handleFocusElementChange(editor)
5341
}
54-
}
55-
}
56-
}
57-
58-
private func handleXcodeChanged() {
59-
guard let app = ActiveApplicationMonitor.shared.activeXcode else { return }
60-
windowChangeObservationTask?.cancel()
61-
windowChangeObservationTask = nil
62-
observeXcodeWindowChangeIfNeeded(app)
63-
}
64-
65-
private func observeXcodeWindowChangeIfNeeded(_ app: NSRunningApplication) {
66-
guard windowChangeObservationTask == nil else { return }
67-
handleFocusElementChange()
68-
69-
let notifications = AXNotificationStream(
70-
app: app,
71-
notificationNames: kAXFocusedUIElementChangedNotification,
72-
kAXMainWindowChangedNotification
73-
)
74-
windowChangeObservationTask = Task { [weak self] in
75-
for await _ in notifications {
76-
guard let self else { return }
77-
try Task.checkCancellation()
78-
await self.handleFocusElementChange()
79-
}
80-
}
42+
}.store(in: &cancellable)
8143
}
8244

83-
private func handleFocusElementChange() {
84-
guard let activeXcode = ActiveApplicationMonitor.shared.activeXcode else { return }
85-
let application = AXUIElementCreateApplication(activeXcode.processIdentifier)
86-
guard let focusElement = application.focusedElement else { return }
87-
let focusElementType = focusElement.description
88-
focusedUIElement = focusElement
89-
45+
private func handleFocusElementChange(_ sourceEditor: SourceEditor) {
9046
Task { // Notify suggestion service for open file.
9147
try await Task.sleep(nanoseconds: 500_000_000)
9248
guard let fileURL = XcodeInspector.shared.realtimeActiveDocumentURL else { return }
9349
_ = try await Service.shared.workspacePool
9450
.fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL)
9551
}
9652

97-
guard focusElementType == "Source Editor" else { return }
98-
sourceEditor = SourceEditor(runningApplication: activeXcode, element: focusElement)
53+
self.sourceEditor = sourceEditor
54+
55+
let notificationsFromEditor = sourceEditor.axNotifications
9956

10057
editorObservationTask?.cancel()
10158
editorObservationTask = nil
10259

103-
let notificationsFromEditor = AXNotificationStream(
104-
app: activeXcode,
105-
element: focusElement,
106-
notificationNames: kAXValueChangedNotification, kAXSelectedTextChangedNotification
107-
)
108-
10960
editorObservationTask = Task { [weak self] in
110-
guard let fileURL = XcodeInspector.shared.realtimeActiveDocumentURL else { return }
111-
if let sourceEditor = await self?.sourceEditor {
61+
if let fileURL = XcodeInspector.shared.realtimeActiveDocumentURL {
11262
await PseudoCommandHandler().invalidateRealtimeSuggestionsIfNeeded(
11363
fileURL: fileURL,
11464
sourceEditor: sourceEditor
@@ -119,21 +69,22 @@ public actor RealtimeSuggestionController {
11969
guard let self else { return }
12070
try Task.checkCancellation()
12171

122-
switch notification.name {
123-
case kAXValueChangedNotification:
72+
switch notification.kind {
73+
case .valueChanged:
74+
Logger.service.debug("Receive valueChanged from editor")
12475
await cancelInFlightTasks()
12576
await self.triggerPrefetchDebounced()
126-
await self.notifyEditingFileChange(editor: focusElement)
127-
case kAXSelectedTextChangedNotification:
128-
guard let sourceEditor = await sourceEditor,
129-
let fileURL = XcodeInspector.shared.activeDocumentURL
130-
else { continue }
77+
await self.notifyEditingFileChange(editor: sourceEditor.element)
78+
case .selectedTextChanged:
79+
Logger.service.debug("Receive selectedTextChanged from editor")
80+
guard let fileURL = XcodeInspector.shared.activeDocumentURL
81+
else { break }
13182
await PseudoCommandHandler().invalidateRealtimeSuggestionsIfNeeded(
13283
fileURL: fileURL,
13384
sourceEditor: sourceEditor
13485
)
13586
default:
136-
continue
87+
break
13788
}
13889
}
13990
}
@@ -179,8 +130,6 @@ public actor RealtimeSuggestionController {
179130
}
180131
if Task.isCancelled { return }
181132

182-
// Logger.service.info("Prefetch suggestions.")
183-
184133
// So the editor won't be blocked (after information are cached)!
185134
await PseudoCommandHandler().generateRealtimeSuggestions(sourceEditor: sourceEditor)
186135
}

Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift

Lines changed: 21 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import ActiveApplicationMonitor
22
import AppActivator
33
import AsyncAlgorithms
4-
import AXNotificationStream
54
import ComposableArchitecture
65
import Foundation
6+
import Logger
77
import Preferences
88
import SwiftUI
99
import Toast
@@ -345,39 +345,27 @@ public struct WidgetFeature: ReducerProtocol {
345345
}.cancellable(id: CancelID.observeUserDefaults, cancelInFlight: true)
346346

347347
case .observeWindowChange:
348-
guard let app = xcodeInspector.activeApplication else { return .none }
349-
guard app.isXcode else { return .none }
348+
guard let app = xcodeInspector.activeXcode else { return .none }
350349

351350
let documentURL = state.focusingDocumentURL
352351

353-
let notifications = AXNotificationStream(
354-
app: app.runningApplication,
355-
notificationNames:
356-
kAXApplicationActivatedNotification,
357-
kAXMovedNotification,
358-
kAXResizedNotification,
359-
kAXMainWindowChangedNotification,
360-
kAXFocusedWindowChangedNotification,
361-
kAXFocusedUIElementChangedNotification,
362-
kAXWindowMovedNotification,
363-
kAXWindowResizedNotification,
364-
kAXWindowMiniaturizedNotification,
365-
kAXWindowDeminiaturizedNotification
366-
)
352+
let notifications = app.axNotifications
367353

368354
return .run { send in
369355
await send(.observeEditorChange)
370356
await send(.panel(.switchToAnotherEditorAndUpdateContent))
371357

372358
for await notification in notifications {
359+
Logger.service.debug("Receive \(notification.kind) from Xcode")
360+
373361
try Task.checkCancellation()
374362

375363
// Hide the widgets before switching to another window/editor
376364
// so the transition looks better.
377365
if [
378-
kAXFocusedUIElementChangedNotification,
379-
kAXFocusedWindowChangedNotification,
380-
].contains(notification.name) {
366+
.focusedUIElementChanged,
367+
.focusedWindowChanged,
368+
].contains(notification.kind) {
381369
let newDocumentURL = xcodeInspector.realtimeActiveDocumentURL
382370
if documentURL != newDocumentURL {
383371
await send(.panel(.removeDisplayedContent))
@@ -388,11 +376,11 @@ public struct WidgetFeature: ReducerProtocol {
388376

389377
// update widgets.
390378
if [
391-
kAXFocusedUIElementChangedNotification,
392-
kAXApplicationActivatedNotification,
393-
kAXMainWindowChangedNotification,
394-
kAXFocusedWindowChangedNotification,
395-
].contains(notification.name) {
379+
.focusedUIElementChanged,
380+
.applicationActivated,
381+
.mainWindowChanged,
382+
.focusedWindowChanged,
383+
].contains(notification.kind) {
396384
await send(.updateWindowLocation(animated: false))
397385
await send(.updateWindowOpacity(immediately: false))
398386
await send(.observeEditorChange)
@@ -405,33 +393,20 @@ public struct WidgetFeature: ReducerProtocol {
405393
}.cancellable(id: CancelID.observeWindowChange, cancelInFlight: true)
406394

407395
case .observeEditorChange:
408-
guard let app = xcodeInspector.activeApplication else { return .none }
409-
let appElement = AXUIElementCreateApplication(
410-
app.runningApplication.processIdentifier
411-
)
412-
guard let focusedElement = appElement.focusedElement,
413-
focusedElement.description == "Source Editor",
414-
let scrollView = focusedElement.parent,
415-
let scrollBar = scrollView.verticalScrollBar
416-
else { return .none }
417-
418-
let selectionRangeChange = AXNotificationStream(
419-
app: app.runningApplication,
420-
element: focusedElement,
421-
notificationNames: kAXSelectedTextChangedNotification
422-
)
423-
let scroll = AXNotificationStream(
424-
app: app.runningApplication,
425-
element: scrollBar,
426-
notificationNames: kAXValueChangedNotification
427-
)
396+
guard let editor = xcodeInspector.focusedEditor else { return .none }
397+
398+
let selectionRangeChange = editor.axNotifications
399+
.filter { $0.kind == .selectedTextChanged }
400+
let scroll = editor.axNotifications
401+
.filter { $0.kind == .scrollPositionChanged }
428402

429403
return .run { send in
430404
if #available(macOS 13.0, *) {
431-
for await _ in merge(
405+
for await notification in merge(
432406
selectionRangeChange.debounce(for: Duration.milliseconds(500)),
433407
scroll
434408
) {
409+
Logger.service.debug("Receive \(notification.kind) from editor")
435410
guard xcodeInspector.latestActiveXcode != nil else { return }
436411
try Task.checkCancellation()
437412
await send(.updateWindowLocation(animated: false))

Core/Sources/SuggestionWidget/SuggestionWidgetController.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import ActiveApplicationMonitor
22
import AppKit
33
import AsyncAlgorithms
4-
import AXNotificationStream
54
import ChatTab
65
import Combine
76
import ComposableArchitecture

Pro

Submodule Pro updated from 861b86a to b8c7a3a

Tool/Sources/XcodeInspector/XcodeInspector.swift

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import AppKit
22
import AsyncAlgorithms
33
import AXExtension
4-
import AXNotificationStream
54
import Combine
65
import Foundation
76
import Logger
@@ -83,7 +82,7 @@ public final class XcodeInspector: ObservableObject {
8382
init() {
8483
restart()
8584
}
86-
85+
8786
public func restart(cleanUp: Bool = false) {
8887
if cleanUp {
8988
activeXcodeObservations.forEach { $0.cancel() }
@@ -111,7 +110,7 @@ public final class XcodeInspector: ObservableObject {
111110
activeApplication = activeXcode ?? runningApplications
112111
.first(where: \.isActive)
113112
.map(AppInstanceInspector.init(runningApplication:))
114-
113+
115114
appChangeObservations.forEach { $0.cancel() }
116115
appChangeObservations.removeAll()
117116

@@ -120,9 +119,9 @@ public final class XcodeInspector: ObservableObject {
120119
if let activeXcode {
121120
await setActiveXcode(activeXcode)
122121
}
123-
122+
124123
await withThrowingTaskGroup(of: Void.self) { [weak self] group in
125-
group.addTask { [weak self] in // Did activate app
124+
group.addTask { [weak self] in // Did activate app
126125
let sequence = NSWorkspace.shared.notificationCenter
127126
.notifications(named: NSWorkspace.didActivateApplicationNotification)
128127
for await notification in sequence {
@@ -154,7 +153,7 @@ public final class XcodeInspector: ObservableObject {
154153
}
155154
}
156155
}
157-
156+
158157
group.addTask { [weak self] in // Did terminate app
159158
let sequence = NSWorkspace.shared.notificationCenter
160159
.notifications(named: NSWorkspace.didTerminateApplicationNotification)
@@ -185,11 +184,10 @@ public final class XcodeInspector: ObservableObject {
185184
}
186185
}
187186
}
188-
187+
189188
appChangeObservations.insert(appChangeTask)
190189
}
191190

192-
193191
@MainActor
194192
func setActiveXcode(_ xcode: XcodeAppInstanceInspector) {
195193
previousActiveApplication = activeApplication
@@ -231,11 +229,9 @@ public final class XcodeInspector: ObservableObject {
231229

232230
setFocusedElement()
233231
let focusedElementChanged = Task { @MainActor in
234-
let notification = AXNotificationStream(
235-
app: xcode.runningApplication,
236-
notificationNames: kAXFocusedUIElementChangedNotification
237-
)
238-
for await _ in notification {
232+
for await notification in xcode.axNotifications {
233+
guard notification.kind == .focusedUIElementChanged else { continue }
234+
Logger.service.debug("Update focused element")
239235
try Task.checkCancellation()
240236
setFocusedElement()
241237
}

0 commit comments

Comments
 (0)