Skip to content

Commit 077560c

Browse files
committed
Merge branch 'release/0.30.1'
2 parents 381005f + 5a62827 commit 077560c

38 files changed

+955
-495
lines changed

Core/Sources/ChatService/CustomCommandTemplateProcessor.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public struct CustomCommandTemplateProcessor {
3939
}
4040

4141
func getEditorInformation() -> EditorInformation {
42-
let editorContent = XcodeInspector.shared.focusedEditor?.content
42+
let editorContent = XcodeInspector.shared.focusedEditor?.getContent()
4343
let documentURL = XcodeInspector.shared.activeDocumentURL
4444
let language = documentURL.map(languageIdentifierFromFileURL) ?? .plaintext
4545

Core/Sources/HostApp/DebugView.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ final class DebugSettings: ObservableObject {
1919
@AppStorage(\.disableGitIgnoreCheck) var disableGitIgnoreCheck
2020
@AppStorage(\.disableFileContentManipulationByCheatsheet)
2121
var disableFileContentManipulationByCheatsheet
22+
@AppStorage(\.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning)
23+
var restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning
24+
@AppStorage(\.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer)
25+
var restartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer
26+
@AppStorage(\.toastForTheReasonWhyXcodeInspectorNeedsToBeRestarted)
27+
var toastForTheReasonWhyXcodeInspectorNeedsToBeRestarted
2228
init() {}
2329
}
2430

@@ -75,6 +81,31 @@ struct DebugSettingsView: View {
7581
Text("Disable file content manipulation by cheatsheet")
7682
}
7783

84+
Group {
85+
Toggle(
86+
isOn: $settings
87+
.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning
88+
) {
89+
Text(
90+
"Re-activate Xcode Inspector when Accessibility API malfunctioning detected"
91+
)
92+
}
93+
94+
Toggle(
95+
isOn: $settings
96+
.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer
97+
) {
98+
Text("Trigger malfunctioning detection only with events")
99+
}
100+
101+
Toggle(
102+
isOn: $settings
103+
.toastForTheReasonWhyXcodeInspectorNeedsToBeRestarted
104+
) {
105+
Text("Toast for the reason of re-activation of Xcode Inspector")
106+
}
107+
}
108+
78109
Button("Reset migration version to 0") {
79110
UserDefaults.shared.set(nil, forKey: "OldMigrationVersion")
80111
}

Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ struct SuggestionSettingsView: View {
218218
}
219219

220220
HStack {
221-
Slider(value: $settings.realtimeSuggestionDebounce, in: 0...2, step: 0.1) {
221+
Slider(value: $settings.realtimeSuggestionDebounce, in: 0.1...2, step: 0.1) {
222222
Text("Real-time Suggestion Debounce")
223223
}
224224

Core/Sources/HostApp/TabContainer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public struct TabContainer: View {
8383
.padding(8)
8484
.background({
8585
switch message.type {
86-
case .info: return Color(nsColor: .systemIndigo)
86+
case .info: return Color.accentColor
8787
case .error: return Color(nsColor: .systemRed)
8888
case .warning: return Color(nsColor: .systemOrange)
8989
}

Core/Sources/Service/GUI/ChatTabFactory.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ enum ChatTabFactory {
4343
guard let editor = XcodeInspector.shared.focusedEditor else {
4444
return .init(selectedText: "", language: "", fileContent: "")
4545
}
46-
let content = editor.content
46+
let content = editor.getContent()
4747
return .init(
4848
selectedText: content.selectedContent,
4949
language: (

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 28 additions & 79 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-
}
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
}

Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,27 +42,32 @@ struct PseudoCommandHandler {
4242

4343
@WorkspaceActor
4444
func generateRealtimeSuggestions(sourceEditor: SourceEditor?) async {
45-
// Can't use handler if content is not available.
46-
guard
47-
let editor = await getEditorContent(sourceEditor: sourceEditor),
48-
let filespace = await getFilespace(),
45+
guard let filespace = await getFilespace(),
4946
let (workspace, _) = try? await Service.shared.workspacePool
5047
.fetchOrCreateWorkspaceAndFilespace(fileURL: filespace.fileURL) else { return }
48+
49+
if Task.isCancelled { return }
50+
51+
// Can't use handler if content is not available.
52+
guard let editor = await getEditorContent(sourceEditor: sourceEditor)
53+
else { return }
5154

5255
let fileURL = filespace.fileURL
5356
let presenter = PresentInWindowSuggestionPresenter()
5457

5558
presenter.markAsProcessing(true)
5659
defer { presenter.markAsProcessing(false) }
5760

58-
// Check if the current suggestion is still valid.
59-
if filespace.validateSuggestions(
60-
lines: editor.lines,
61-
cursorPosition: editor.cursorPosition
62-
) {
63-
return
64-
} else {
65-
presenter.discardSuggestion(fileURL: filespace.fileURL)
61+
if filespace.presentingSuggestion != nil {
62+
// Check if the current suggestion is still valid.
63+
if filespace.validateSuggestions(
64+
lines: editor.lines,
65+
cursorPosition: editor.cursorPosition
66+
) {
67+
return
68+
} else {
69+
presenter.discardSuggestion(fileURL: filespace.fileURL)
70+
}
6671
}
6772

6873
let snapshot = FilespaceSuggestionSnapshot(
@@ -78,9 +83,10 @@ struct PseudoCommandHandler {
7883
editor: editor
7984
)
8085
if let sourceEditor {
86+
let editorContent = sourceEditor.getContent()
8187
_ = filespace.validateSuggestions(
82-
lines: sourceEditor.content.lines,
83-
cursorPosition: sourceEditor.content.cursorPosition
88+
lines: editorContent.lines,
89+
cursorPosition: editorContent.cursorPosition
8490
)
8591
}
8692
if filespace.presentingSuggestion != nil {
@@ -98,9 +104,14 @@ struct PseudoCommandHandler {
98104
guard let (_, filespace) = try? await Service.shared.workspacePool
99105
.fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) else { return }
100106

107+
if filespace.presentingSuggestion == nil {
108+
return // skip if there's no suggestion presented.
109+
}
110+
111+
let content = sourceEditor.getContent()
101112
if !filespace.validateSuggestions(
102-
lines: sourceEditor.content.lines,
103-
cursorPosition: sourceEditor.content.cursorPosition
113+
lines: content.lines,
114+
cursorPosition: content.cursorPosition
104115
) {
105116
PresentInWindowSuggestionPresenter().discardSuggestion(fileURL: fileURL)
106117
}
@@ -351,7 +362,8 @@ extension PseudoCommandHandler {
351362
guard let filespace = await getFilespace(),
352363
let sourceEditor = sourceEditor ?? XcodeInspector.shared.focusedEditor
353364
else { return nil }
354-
let content = sourceEditor.content
365+
if Task.isCancelled { return nil }
366+
let content = sourceEditor.getContent()
355367
let uti = filespace.codeMetadata.uti ?? ""
356368
let tabSize = filespace.codeMetadata.tabSize ?? 4
357369
let indentSize = filespace.codeMetadata.indentSize ?? 4

0 commit comments

Comments
 (0)