@@ -2,7 +2,7 @@ import ActiveApplicationMonitor
22import AppKit
33import AsyncAlgorithms
44import AXExtension
5- import AXNotificationStream
5+ import Combine
66import Foundation
77import Logger
88import Preferences
@@ -11,21 +11,16 @@ import Workspace
1111import XcodeInspector
1212
1313public 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- }
42+ } . store ( in: & cancellable)
5643 }
5744
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- }
81- }
82-
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,20 @@ 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 :
12474 await cancelInFlightTasks ( )
12575 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 }
76+ await self . notifyEditingFileChange ( editor: sourceEditor. element)
77+ case . selectedTextChanged:
78+ guard let fileURL = XcodeInspector . shared. activeDocumentURL
79+ else { break }
13180 await PseudoCommandHandler ( ) . invalidateRealtimeSuggestionsIfNeeded (
13281 fileURL: fileURL,
13382 sourceEditor: sourceEditor
13483 )
13584 default :
136- continue
85+ break
13786 }
13887 }
13988 }
@@ -145,7 +94,7 @@ public actor RealtimeSuggestionController {
14594 . fetchOrCreateWorkspaceAndFilespace ( fileURL: fileURL)
14695
14796 if filespace. codeMetadata. uti == nil {
148- Logger . service. info ( " Generate cache for file. " )
97+ Logger . service. info ( " Generate cache for file. " )
14998 // avoid the command get called twice
15099 filespace. codeMetadata. uti = " "
151100 do {
@@ -161,10 +110,12 @@ public actor RealtimeSuggestionController {
161110 }
162111
163112 func triggerPrefetchDebounced( force: Bool = false ) {
164- inflightPrefetchTask = Task { @WorkspaceActor in
113+ inflightPrefetchTask = Task ( priority : . utility ) { @WorkspaceActor in
165114 try ? await Task . sleep ( nanoseconds: UInt64 ( (
166- UserDefaults . shared. value ( for: \. realtimeSuggestionDebounce)
115+ max ( UserDefaults . shared. value ( for: \. realtimeSuggestionDebounce) , 0.15 )
167116 ) * 1_000_000_000 ) )
117+
118+ if Task . isCancelled { return }
168119
169120 guard UserDefaults . shared. value ( for: \. realtimeSuggestionToggle)
170121 else { return }
@@ -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 }
0 commit comments