@@ -19,11 +19,21 @@ final class WidgetWindowsController: NSObject {
1919 var currentApplicationProcessIdentifier : pid_t ?
2020
2121 var cancellable : Set < AnyCancellable > = [ ]
22+ @MainActor
2223 var observeToAppTask : Task < Void , Error > ?
24+ @MainActor
2325 var observeToFocusedEditorTask : Task < Void , Error > ?
26+
27+ @MainActor
2428 var updateWindowOpacityTask : Task < Void , Error > ?
29+ @MainActor
2530 var lastUpdateWindowOpacityTime = Date ( timeIntervalSince1970: 0 )
2631
32+ @MainActor
33+ var updateWindowLocationTask : Task < Void , Error > ?
34+ @MainActor
35+ var lastUpdateWindowLocationTime = Date ( timeIntervalSince1970: 0 )
36+
2737 deinit {
2838 userDefaultsObservers. presentationModeChangeObserver. onChange = { }
2939 observeToAppTask? . cancel ( )
@@ -47,7 +57,7 @@ final class WidgetWindowsController: NSObject {
4757
4858 xcodeInspector. $activeApplication. sink { [ weak self] app in
4959 guard let app else { return }
50- self ? . activate ( app)
60+ Task { [ weak self] in await self ? . activate ( app) }
5161 } . store ( in: & cancellable)
5262
5363 xcodeInspector. $completionPanel. sink { [ weak self] newValue in
@@ -59,14 +69,14 @@ final class WidgetWindowsController: NSObject {
5969 // suggestion panel
6070 try await Task . sleep ( nanoseconds: 400_000_000 )
6171 }
62- await self ? . updateWindowLocation ( animated: false )
72+ await self ? . updateWindowLocation ( animated: false , immediately : false )
6373 await self ? . updateWindowOpacity ( immediately: false )
6474 }
6575 } . store ( in: & cancellable)
6676
6777 userDefaultsObservers. presentationModeChangeObserver. onChange = { [ weak self] in
6878 Task { [ weak self] in
69- await self ? . updateWindowLocation ( animated: false )
79+ await self ? . updateWindowLocation ( animated: false , immediately : false )
7080 await self ? . send ( . updateColorScheme)
7181 }
7282 }
@@ -81,18 +91,21 @@ final class WidgetWindowsController: NSObject {
8191
8292 let isChatPanelDetached = state. chatPanelState. chatPanelInASeparateWindow
8393 let hasChat = !state. chatPanelState. chatTabGroup. tabInfo. isEmpty
84- let shouldDebounce = !immediately &&
85- Date ( ) . timeIntervalSince ( lastUpdateWindowOpacityTime) < 1
86- lastUpdateWindowOpacityTime = Date ( )
94+ let shouldDebounce = await MainActor . run {
95+ defer { lastUpdateWindowOpacityTime = Date ( ) }
96+ return ( !immediately &&
97+ !( Date ( ) . timeIntervalSince ( lastUpdateWindowOpacityTime) > 5 ) )
98+ }
8799 let activeApp = xcodeInspector. activeApplication
88100
89- updateWindowOpacityTask? . cancel ( )
101+ await updateWindowOpacityTask? . cancel ( )
90102
91103 let task = Task {
92104 if shouldDebounce {
93105 try await Task . sleep ( nanoseconds: 200_000_000 )
94106 }
95107 try Task . checkCancellation ( )
108+ let xcodeInspector = self . xcodeInspector
96109 await MainActor . run {
97110 if let activeApp, activeApp. isXcode {
98111 let application = AXUIElementCreateApplication (
@@ -143,19 +156,26 @@ final class WidgetWindowsController: NSObject {
143156 }
144157 }
145158 }
146- updateWindowOpacityTask = task
147- _ = try ? await task. value
159+
160+ await MainActor . run {
161+ updateWindowOpacityTask = task
162+ }
148163 }
149164
150- func updateWindowLocation( animated: Bool ) async {
165+ func updateWindowLocation(
166+ animated: Bool ,
167+ immediately: Bool ,
168+ function: StaticString = #function,
169+ line: UInt = #line
170+ ) async {
151171 let state = store. withState { $0 }
152-
153- guard let widgetLocation = generateWidgetLocation ( ) else { return }
154- await updatePanelState ( widgetLocation)
155-
156172 let isChatPanelDetached = state. chatPanelState. chatPanelInASeparateWindow
157173
158- await MainActor . run {
174+ @Sendable @MainActor
175+ func update( ) async {
176+ guard let widgetLocation = generateWidgetLocation ( ) else { return }
177+ await updatePanelState ( widgetLocation)
178+
159179 windows. widgetWindow. setFrame (
160180 widgetLocation. widgetFrame,
161181 display: false ,
@@ -190,60 +210,101 @@ final class WidgetWindowsController: NSObject {
190210 )
191211 }
192212 }
213+
214+ let now = Date ( )
215+ let shouldThrottle = await MainActor . run {
216+ !immediately &&
217+ !( now. timeIntervalSince ( lastUpdateWindowLocationTime) > 5 )
218+ }
219+
220+ await updateWindowLocationTask? . cancel ( )
221+ let interval : TimeInterval = 0.1
222+
223+ if shouldThrottle {
224+ let delay = await max (
225+ 0 ,
226+ interval - now. timeIntervalSince ( lastUpdateWindowLocationTime)
227+ )
228+
229+ let task = Task { @MainActor in
230+ try await Task . sleep ( nanoseconds: UInt64 ( delay * 1_000_000_000 ) )
231+ try Task . checkCancellation ( )
232+ await update ( )
233+ }
234+ await MainActor . run {
235+ updateWindowLocationTask = task
236+ }
237+ } else {
238+ Task {
239+ await update ( )
240+ }
241+ }
242+ await MainActor . run {
243+ lastUpdateWindowLocationTime = Date ( )
244+ }
193245 }
194246}
195247
196248extension WidgetWindowsController : NSWindowDelegate {
249+ nonisolated
197250 func windowWillMove( _ notification: Notification ) {
198- guard ( notification. object as? NSWindow ) === windows . chatPanelWindow else { return }
251+ guard let window = notification. object as? NSWindow else { return }
199252 Task { @MainActor in
253+ guard window === windows. chatPanelWindow else { return }
200254 await Task . yield ( )
201255 store. send ( . chatPanel( . detachChatPanel) )
202256 }
203257 }
204258
259+ nonisolated
205260 func windowWillEnterFullScreen( _ notification: Notification ) {
206- guard ( notification. object as? NSWindow ) === windows . chatPanelWindow else { return }
261+ guard let window = notification. object as? NSWindow else { return }
207262 Task { @MainActor in
263+ guard window === windows. chatPanelWindow else { return }
208264 await Task . yield ( )
209265 store. send ( . chatPanel( . enterFullScreen) )
210266 }
211267 }
212268
269+ nonisolated
213270 func windowWillExitFullScreen( _ notification: Notification ) {
214- guard ( notification. object as? NSWindow ) === windows . chatPanelWindow else { return }
271+ guard let window = notification. object as? NSWindow else { return }
215272 Task { @MainActor in
273+ guard window === windows. chatPanelWindow else { return }
216274 await Task . yield ( )
217275 store. send ( . chatPanel( . exitFullScreen) )
218276 }
219277 }
220278}
221279
222280private extension WidgetWindowsController {
223- func activate( _ app: AppInstanceInspector ) {
281+ func activate( _ app: AppInstanceInspector ) async {
224282 guard currentApplicationProcessIdentifier != app. processIdentifier else { return }
225283 currentApplicationProcessIdentifier = app. processIdentifier
226- observe ( to: app)
284+ await observe ( to: app)
227285 }
228286
229- func observe( to app: AppInstanceInspector ) {
287+ func observe( to app: AppInstanceInspector ) async {
230288 guard let app = app as? XcodeAppInstanceInspector else {
231289 Task {
232- await updateWindowLocation ( animated: false )
290+ await updateWindowLocation ( animated: false , immediately : true )
233291 await updateWindowOpacity ( immediately: true )
234292 }
235293 return
236294 }
237295 let notifications = app. axNotifications
238296 if let focusedEditor = xcodeInspector. focusedEditor {
239- observe ( to: focusedEditor)
297+ await observe ( to: focusedEditor)
240298 }
241- observeToAppTask ? . cancel ( )
242- observeToAppTask = Task {
299+
300+ let task = Task {
243301 await windows. orderFront ( )
244302
245303 let documentURL = await MainActor . run { store. withState { $0. focusingDocumentURL } }
246304 for await notification in notifications {
305+ if [ . uiElementDestroyed, . created, . xcodeCompletionPanelChanged]
306+ . contains ( notification. kind) { continue }
307+
247308 try Task . checkCancellation ( )
248309
249310 // Hide the widgets before switching to another window/editor
@@ -267,23 +328,27 @@ private extension WidgetWindowsController {
267328 . mainWindowChanged,
268329 . focusedWindowChanged,
269330 ] . contains ( notification. kind) {
270- await updateWindowLocation ( animated: false )
331+ await updateWindowLocation ( animated: false , immediately : false )
271332 await updateWindowOpacity ( immediately: false )
272333 if let editor = xcodeInspector. focusedEditor {
273- observe ( to: editor)
334+ await observe ( to: editor)
274335 }
275336 await send ( . panel( . switchToAnotherEditorAndUpdateContent) )
276337 } else {
277- await updateWindowLocation ( animated: false )
338+ await updateWindowLocation ( animated: false , immediately : false )
278339 await updateWindowOpacity ( immediately: false )
279340 }
280341 }
281342 }
343+
344+ await MainActor . run {
345+ observeToAppTask? . cancel ( )
346+ observeToAppTask = task
347+ }
282348 }
283349
284- func observe( to editor: SourceEditor ) {
285- observeToFocusedEditorTask? . cancel ( )
286- observeToFocusedEditorTask = Task {
350+ func observe( to editor: SourceEditor ) async {
351+ let task = Task {
287352 let selectionRangeChange = editor. axNotifications
288353 . filter { $0. kind == . selectedTextChanged }
289354 let scroll = editor. axNotifications
@@ -292,22 +357,27 @@ private extension WidgetWindowsController {
292357 if #available( macOS 13 . 0 , * ) {
293358 for await _ in merge (
294359 selectionRangeChange. debounce ( for: Duration . milliseconds ( 500 ) ) ,
295- scroll
360+ scroll. throttle ( for : . milliseconds ( 200 ) )
296361 ) {
297362 guard xcodeInspector. latestActiveXcode != nil else { return }
298363 try Task . checkCancellation ( )
299- await updateWindowLocation ( animated: false )
364+ await updateWindowLocation ( animated: false , immediately : false )
300365 await updateWindowOpacity ( immediately: false )
301366 }
302367 } else {
303368 for await _ in merge ( selectionRangeChange, scroll) {
304369 guard xcodeInspector. latestActiveXcode != nil else { return }
305370 try Task . checkCancellation ( )
306- await updateWindowLocation ( animated: false )
371+ await updateWindowLocation ( animated: false , immediately : false )
307372 await updateWindowOpacity ( immediately: false )
308373 }
309374 }
310375 }
376+
377+ await MainActor . run {
378+ observeToFocusedEditorTask? . cancel ( )
379+ observeToFocusedEditorTask = task
380+ }
311381 }
312382}
313383
0 commit comments