Skip to content

Commit 813eaff

Browse files
committed
Merge branch 'release/0.14.1'
2 parents 6943df0 + 8b4cc0b commit 813eaff

18 files changed

Lines changed: 296 additions & 149 deletions

Core/Sources/AXExtension/AXUIElement.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,16 @@ public extension AXUIElement {
2828
try? copyValue(key: kAXDocumentAttribute)
2929
}
3030

31+
/// Label in Accessibility Inspector.
3132
var description: String {
3233
(try? copyValue(key: kAXDescriptionAttribute)) ?? ""
3334
}
3435

36+
/// Type in Accessibility Inspector.
37+
var roleDescription: String {
38+
(try? copyValue(key: kAXRoleDescriptionAttribute)) ?? ""
39+
}
40+
3541
var label: String {
3642
(try? copyValue(key: kAXLabelValueAttribute)) ?? ""
3743
}
@@ -158,6 +164,29 @@ public extension AXUIElement {
158164
}
159165
return nil
160166
}
167+
168+
func children(where match: (AXUIElement) -> Bool) -> [AXUIElement] {
169+
var all = [AXUIElement]()
170+
for child in children {
171+
if match(child) { all.append(child) }
172+
}
173+
for child in children {
174+
all.append(contentsOf: child.children(where: match))
175+
}
176+
return all
177+
}
178+
179+
func firstChild(where match: (AXUIElement) -> Bool) -> AXUIElement? {
180+
for child in children {
181+
if match(child) { return child }
182+
}
183+
for child in children {
184+
if let target = child.firstChild(where: match) {
185+
return target
186+
}
187+
}
188+
return nil
189+
}
161190

162191
func visibleChild(identifier: String) -> AXUIElement? {
163192
for child in visibleChildren {

Core/Sources/Preferences/UserDefaults.swift

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@ public extension UserDefaults {
1111
shared.setupDefaultValue(for: \.suggestionPresentationMode)
1212
shared.setupDefaultValue(for: \.widgetColorScheme)
1313
shared.setupDefaultValue(for: \.customCommands)
14-
let runNodeWith: NodeRunner = shared.value(for: \.runNodeWithInteractiveLoggedInShell)
15-
? .bash
16-
: .env
17-
shared.setupDefaultValue(for: \.runNodeWith, defaultValue: runNodeWith)
14+
shared.setupDefaultValue(for: \.runNodeWith, defaultValue: .env)
1815
}
1916
}
2017

Core/Sources/Service/GUI/RealtimeSuggestionIndicatorController.swift

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -79,21 +79,12 @@ final class RealtimeSuggestionIndicatorController {
7979
}
8080
}
8181

82-
class UserDefaultsObserver: NSObject {
83-
var onChange: (() -> Void)?
84-
85-
override func observeValue(
86-
forKeyPath keyPath: String?,
87-
of object: Any?,
88-
change: [NSKeyValueChangeKey: Any]?,
89-
context: UnsafeMutableRawPointer?
90-
) {
91-
onChange?()
92-
}
93-
}
94-
9582
private let viewModel = IndicatorContentViewModel()
96-
private var userDefaultsObserver = UserDefaultsObserver()
83+
private var userDefaultsObserver = UserDefaultsObserver(
84+
object: UserDefaults.shared,
85+
forKeyPaths: [UserDefaultPreferenceKeys().realtimeSuggestionToggle.key],
86+
context: nil
87+
)
9788
private var windowChangeObservationTask: Task<Void, Error>?
9889
private var activeApplicationMonitorTask: Task<Void, Error>?
9990
private var editorObservationTask: Task<Void, Error>?
@@ -126,7 +117,7 @@ final class RealtimeSuggestionIndicatorController {
126117

127118
nonisolated init() {
128119
if ProcessInfo.processInfo.environment["IS_UNIT_TEST"] == "YES" { return }
129-
120+
130121
Task { @MainActor in
131122
observeEditorChangeIfNeeded()
132123
activeApplicationMonitorTask = Task { [weak self] in
@@ -152,17 +143,11 @@ final class RealtimeSuggestionIndicatorController {
152143

153144
Task { @MainActor in
154145
userDefaultsObserver.onChange = { [weak self] in
155-
Task { [weak self] in
146+
Task { @MainActor [weak self] in
156147
await self?.updateIndicatorVisibility()
157148
self?.updateIndicatorLocation()
158149
}
159150
}
160-
UserDefaults.shared.addObserver(
161-
userDefaultsObserver,
162-
forKeyPath: UserDefaultPreferenceKeys().realtimeSuggestionToggle.key,
163-
options: .new,
164-
context: nil
165-
)
166151
}
167152
}
168153

@@ -297,3 +282,4 @@ final class RealtimeSuggestionIndicatorController {
297282
viewModel.endPrefetch()
298283
}
299284
}
285+

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ public class RealtimeSuggestionController {
110110
guard let focusElement = application.focusedElement else { return }
111111
let focusElementType = focusElement.description
112112
focusedUIElement = focusElement
113+
114+
Task { // Notify suggestion service for open file.
115+
try await Task.sleep(nanoseconds: 500_000_000)
116+
let fileURL = try await Environment.fetchCurrentFileURL()
117+
_ = try await Workspace.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
118+
}
119+
113120
guard focusElementType == "Source Editor" else { return }
114121
sourceEditor = focusElement
115122

@@ -140,11 +147,9 @@ public class RealtimeSuggestionController {
140147

141148
Task { // Get cache ready for real-time suggestions.
142149
guard UserDefaults.shared.value(for: \.preCacheOnFileOpen) else { return }
143-
guard
144-
let fileURL = try? await Environment.fetchCurrentFileURL(),
145-
let (_, filespace) = try? await Workspace
150+
let fileURL = try await Environment.fetchCurrentFileURL()
151+
let (_, filespace) = try await Workspace
146152
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
147-
else { return }
148153

149154
if filespace.uti == nil {
150155
Logger.service.info("Generate cache for file.")
@@ -268,9 +273,10 @@ public class RealtimeSuggestionController {
268273

269274
func notifyEditingFileChange(editor: AXUIElement) async {
270275
guard let fileURL = try? await Environment.fetchCurrentFileURL(),
271-
let (workspace, filespace) = try? await Workspace
272-
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
276+
let (workspace, filespace) = try? await Workspace
277+
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
273278
else { return }
274279
workspace.notifyUpdateFile(filespace: filespace, content: editor.value)
275280
}
276281
}
282+
Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,62 @@
1+
import ActiveApplicationMonitor
2+
import AppKit
3+
import AXExtension
14
import Foundation
5+
import Logger
26

37
public final class ScheduledCleaner {
48
public init() {
59
// occasionally cleanup workspaces.
610
Task { @ServiceActor in
711
while !Task.isCancelled {
8-
try await Task.sleep(nanoseconds: 2 * 60 * 60 * 1_000_000_000)
12+
try await Task.sleep(nanoseconds: 4 * 60 * 60 * 1_000_000_000)
13+
let availableTabs = findAvailableOpenedTabs()
914
for (url, workspace) in workspaces {
1015
if workspace.isExpired {
16+
Logger.service.info("Remove idle workspace")
17+
for url in workspace.filespaces.keys {
18+
WidgetDataSource.shared.cleanup(for: url)
19+
}
20+
workspace.cleanUp(availableTabs: availableTabs)
1121
workspaces[url] = nil
1222
} else {
1323
// cleanup chats for unused files
1424
let filespaces = workspace.filespaces
15-
for (url, filespace) in filespaces {
16-
if filespace.isExpired {
25+
for (url, _) in filespaces {
26+
if workspace.isFilespaceExpired(
27+
fileURL: url,
28+
availableTabs: availableTabs
29+
) {
30+
Logger.service.info("Remove idle filespace")
1731
WidgetDataSource.shared.cleanup(for: url)
1832
}
1933
}
2034
// cleanup workspace
21-
workspace.cleanUp()
35+
workspace.cleanUp(availableTabs: availableTabs)
2236
}
2337
}
2438
}
2539
}
2640
}
41+
42+
func findAvailableOpenedTabs() -> Set<String> {
43+
guard let xcode = ActiveApplicationMonitor.latestXcode else { return [] }
44+
let app = AXUIElementCreateApplication(xcode.processIdentifier)
45+
let windows = app.windows.filter { $0.identifier == "Xcode.WorkspaceWindow" }
46+
guard !windows.isEmpty else { return [] }
47+
var allTabs = Set<String>()
48+
for window in windows {
49+
guard let editArea = window.firstChild(where: { $0.description == "editor area" })
50+
else { continue }
51+
let tabBars = editArea.children { $0.description == "tab bar" }
52+
for tabBar in tabBars {
53+
let tabs = tabBar.children { $0.roleDescription == "tab" }
54+
for tab in tabs {
55+
allTabs.insert(tab.title)
56+
}
57+
}
58+
}
59+
return allTabs
60+
}
2761
}
62+
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Foundation
2+
3+
final class UserDefaultsObserver: NSObject {
4+
var onChange: (() -> Void)?
5+
private weak var object: NSObject?
6+
private let keyPaths: [String]
7+
8+
init(object: NSObject, forKeyPaths keyPaths: [String], context: UnsafeMutableRawPointer?) {
9+
self.object = object
10+
self.keyPaths = keyPaths
11+
super.init()
12+
for keyPath in keyPaths {
13+
object.addObserver(self, forKeyPath: keyPath, options: .new, context: context)
14+
}
15+
}
16+
17+
deinit {
18+
for keyPath in keyPaths {
19+
object?.removeObserver(self, forKeyPath: keyPath)
20+
}
21+
}
22+
23+
override func observeValue(
24+
forKeyPath keyPath: String?,
25+
of object: Any?,
26+
change: [NSKeyValueChangeKey: Any]?,
27+
context: UnsafeMutableRawPointer?
28+
) {
29+
onChange?()
30+
}
31+
}

0 commit comments

Comments
 (0)