Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
7485ff2
Merge tag '0.30.0' into develop
intitni Jan 22, 2024
4818634
Prevent the chat window size and position being reset when it's hidden
intitni Jan 25, 2024
23b7f3c
Use isWindowHidden to control the opacity of chat window
intitni Jan 25, 2024
ae6c551
Merge branch 'feature/fix-detached-window-resized-repositioned-once-d…
intitni Jan 25, 2024
6e6bc96
Add a dismiss button to non-compact mode suggestion
intitni Jan 26, 2024
97eb465
Truncate file name in the middle when it's too long
intitni Jan 26, 2024
e5293a1
Use accent color instead of indigo
intitni Jan 26, 2024
94bee9d
Bump GitHub Copilot version to 1.17.0
intitni Jan 26, 2024
2ed2634
Merge branch 'feature/bump-github-copilot-1-17-0' into develop
intitni Jan 26, 2024
b516513
Bump Codeium language server to 1.6.9
intitni Jan 26, 2024
4ad9263
Merge branch 'feature/bump-codeium-to-1.6.9' into develop
intitni Jan 26, 2024
9f1d0cf
Add AsyncExtensions to use a concurrency version of passthrough subject
intitni Jan 25, 2024
53d49ef
Adjust AXNotificationStream
intitni Jan 25, 2024
930780f
Handle AXNotification on app in XcodeAppInstanceInspector
intitni Jan 25, 2024
a464a49
Handle source editor AXNotifications in SourceEditor
intitni Jan 25, 2024
efdd979
Replace AXNotificationStream creation
intitni Jan 25, 2024
e564d0f
Update Logger
intitni Jan 25, 2024
0c8c20f
Minor adjustment
intitni Jan 26, 2024
a8537c0
Update
intitni Jan 26, 2024
5bdc538
Update
intitni Jan 26, 2024
8419b80
Merge branch 'feature/handle-ax-notifications-universally' into develop
intitni Jan 26, 2024
535cbba
WIP
intitni Jan 24, 2024
eeff068
Add accessibility api malfunction check
intitni Jan 24, 2024
143db5a
Skip checks within 5 seconds since last recovery
intitni Jan 27, 2024
85ddfe4
Extend debounce interval
intitni Jan 27, 2024
44e4634
Add check when activate Xcode
intitni Jan 27, 2024
a491f45
Update
intitni Jan 27, 2024
5421b6e
Adjust logs
intitni Jan 27, 2024
1c2ee31
Update
intitni Jan 27, 2024
d1fc4d6
Merge branch 'feature/accessibility-api-malfunction-recovery' into de…
intitni Jan 27, 2024
3e8ff22
Prevent getting multiple content from SourceEditor in a single run
intitni Jan 27, 2024
cd124aa
Implement cache for SourceEditor
intitni Jan 27, 2024
8b3ad69
Skip invalidating suggestion early if no suggestion presented
intitni Jan 27, 2024
786e19a
Prevent using split by \.newLine for faster speed
intitni Jan 27, 2024
6654aac
Prevent events to be sent to a store rapidly
intitni Jan 27, 2024
cf9cbd8
Make minimum suggestion debounce to 0.15
intitni Jan 27, 2024
e1231f1
Get content after we check cancellation
intitni Jan 27, 2024
569fb71
Remove logs
intitni Jan 27, 2024
95f2963
Update dependency
intitni Jan 27, 2024
26b8ebe
Adjust logs
intitni Jan 27, 2024
b482d82
Remove optional
intitni Jan 27, 2024
bdc9c0b
Update
intitni Jan 27, 2024
e29dca3
Merge branch 'feature/performance-fix-for-realtime-suggestion' into d…
intitni Jan 27, 2024
eda2e8a
Remove optional
intitni Jan 27, 2024
8d9b257
Bump version to 0.30.1
intitni Jan 28, 2024
20b560f
Add tests for SourceEditor.Cache
intitni Jan 28, 2024
9b75b43
Add todos
intitni Jan 28, 2024
3c72675
Update prompt
intitni Jan 28, 2024
d580cd4
Add missing task cancellation check
intitni Jan 28, 2024
da24d8b
Add todo
intitni Jan 28, 2024
cf9aa83
Update appcasts.xml
intitni Jan 28, 2024
5a62827
Update README.md
intitni Jan 28, 2024
077560c
Merge branch 'release/0.30.1'
intitni Jan 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public struct CustomCommandTemplateProcessor {
}

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

Expand Down
31 changes: 31 additions & 0 deletions Core/Sources/HostApp/DebugView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ final class DebugSettings: ObservableObject {
@AppStorage(\.disableGitIgnoreCheck) var disableGitIgnoreCheck
@AppStorage(\.disableFileContentManipulationByCheatsheet)
var disableFileContentManipulationByCheatsheet
@AppStorage(\.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning)
var restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning
@AppStorage(\.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer)
var restartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer
@AppStorage(\.toastForTheReasonWhyXcodeInspectorNeedsToBeRestarted)
var toastForTheReasonWhyXcodeInspectorNeedsToBeRestarted
init() {}
}

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

Group {
Toggle(
isOn: $settings
.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning
) {
Text(
"Re-activate Xcode Inspector when Accessibility API malfunctioning detected"
)
}

Toggle(
isOn: $settings
.restartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer
) {
Text("Trigger malfunctioning detection only with events")
}

Toggle(
isOn: $settings
.toastForTheReasonWhyXcodeInspectorNeedsToBeRestarted
) {
Text("Toast for the reason of re-activation of Xcode Inspector")
}
}

Button("Reset migration version to 0") {
UserDefaults.shared.set(nil, forKey: "OldMigrationVersion")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ struct SuggestionSettingsView: View {
}

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

Expand Down
2 changes: 1 addition & 1 deletion Core/Sources/HostApp/TabContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public struct TabContainer: View {
.padding(8)
.background({
switch message.type {
case .info: return Color(nsColor: .systemIndigo)
case .info: return Color.accentColor
case .error: return Color(nsColor: .systemRed)
case .warning: return Color(nsColor: .systemOrange)
}
Expand Down
2 changes: 1 addition & 1 deletion Core/Sources/Service/GUI/ChatTabFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ enum ChatTabFactory {
guard let editor = XcodeInspector.shared.focusedEditor else {
return .init(selectedText: "", language: "", fileContent: "")
}
let content = editor.content
let content = editor.getContent()
return .init(
selectedText: content.selectedContent,
language: (
Expand Down
107 changes: 28 additions & 79 deletions Core/Sources/Service/RealtimeSuggestionController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import ActiveApplicationMonitor
import AppKit
import AsyncAlgorithms
import AXExtension
import AXNotificationStream
import Combine
import Foundation
import Logger
import Preferences
Expand All @@ -11,21 +11,16 @@ import Workspace
import XcodeInspector

public actor RealtimeSuggestionController {
private var task: Task<Void, Error>?
private var cancellable: Set<AnyCancellable> = []
private var inflightPrefetchTask: Task<Void, Error>?
private var windowChangeObservationTask: Task<Void, Error>?
private var activeApplicationMonitorTask: Task<Void, Error>?
private var editorObservationTask: Task<Void, Error>?
private var focusedUIElement: AXUIElement?
private var sourceEditor: SourceEditor?

init() {}

deinit {
task?.cancel()
cancellable.forEach { $0.cancel() }
inflightPrefetchTask?.cancel()
windowChangeObservationTask?.cancel()
activeApplicationMonitorTask?.cancel()
editorObservationTask?.cancel()
}

Expand All @@ -35,80 +30,35 @@ public actor RealtimeSuggestionController {
}

private func observeXcodeChange() {
task?.cancel()
task = Task { [weak self] in
if ActiveApplicationMonitor.shared.activeXcode != nil {
await self?.handleXcodeChanged()
}
var previousApp = ActiveApplicationMonitor.shared.activeXcode?.info
for await app in ActiveApplicationMonitor.shared.createInfoStream() {
cancellable.forEach { $0.cancel() }

XcodeInspector.shared.$focusedEditor
.sink { [weak self] editor in
guard let self else { return }
try Task.checkCancellation()
defer { previousApp = app }

if let app = ActiveApplicationMonitor.shared.activeXcode,
app.processIdentifier != previousApp?.processIdentifier
{
await self.handleXcodeChanged()
Task {
guard let editor else { return }
await self.handleFocusElementChange(editor)
}
}
}
}.store(in: &cancellable)
}

private func handleXcodeChanged() {
guard let app = ActiveApplicationMonitor.shared.activeXcode else { return }
windowChangeObservationTask?.cancel()
windowChangeObservationTask = nil
observeXcodeWindowChangeIfNeeded(app)
}

private func observeXcodeWindowChangeIfNeeded(_ app: NSRunningApplication) {
guard windowChangeObservationTask == nil else { return }
handleFocusElementChange()

let notifications = AXNotificationStream(
app: app,
notificationNames: kAXFocusedUIElementChangedNotification,
kAXMainWindowChangedNotification
)
windowChangeObservationTask = Task { [weak self] in
for await _ in notifications {
guard let self else { return }
try Task.checkCancellation()
await self.handleFocusElementChange()
}
}
}

private func handleFocusElementChange() {
guard let activeXcode = ActiveApplicationMonitor.shared.activeXcode else { return }
let application = AXUIElementCreateApplication(activeXcode.processIdentifier)
guard let focusElement = application.focusedElement else { return }
let focusElementType = focusElement.description
focusedUIElement = focusElement

private func handleFocusElementChange(_ sourceEditor: SourceEditor) {
Task { // Notify suggestion service for open file.
try await Task.sleep(nanoseconds: 500_000_000)
guard let fileURL = XcodeInspector.shared.realtimeActiveDocumentURL else { return }
_ = try await Service.shared.workspacePool
.fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL)
}

guard focusElementType == "Source Editor" else { return }
sourceEditor = SourceEditor(runningApplication: activeXcode, element: focusElement)
self.sourceEditor = sourceEditor

let notificationsFromEditor = sourceEditor.axNotifications

editorObservationTask?.cancel()
editorObservationTask = nil

let notificationsFromEditor = AXNotificationStream(
app: activeXcode,
element: focusElement,
notificationNames: kAXValueChangedNotification, kAXSelectedTextChangedNotification
)

editorObservationTask = Task { [weak self] in
guard let fileURL = XcodeInspector.shared.realtimeActiveDocumentURL else { return }
if let sourceEditor = await self?.sourceEditor {
if let fileURL = XcodeInspector.shared.realtimeActiveDocumentURL {
await PseudoCommandHandler().invalidateRealtimeSuggestionsIfNeeded(
fileURL: fileURL,
sourceEditor: sourceEditor
Expand All @@ -119,21 +69,20 @@ public actor RealtimeSuggestionController {
guard let self else { return }
try Task.checkCancellation()

switch notification.name {
case kAXValueChangedNotification:
switch notification.kind {
case .valueChanged:
await cancelInFlightTasks()
await self.triggerPrefetchDebounced()
await self.notifyEditingFileChange(editor: focusElement)
case kAXSelectedTextChangedNotification:
guard let sourceEditor = await sourceEditor,
let fileURL = XcodeInspector.shared.activeDocumentURL
else { continue }
await self.notifyEditingFileChange(editor: sourceEditor.element)
case .selectedTextChanged:
guard let fileURL = XcodeInspector.shared.activeDocumentURL
else { break }
await PseudoCommandHandler().invalidateRealtimeSuggestionsIfNeeded(
fileURL: fileURL,
sourceEditor: sourceEditor
)
default:
continue
break
}
}
}
Expand All @@ -145,7 +94,7 @@ public actor RealtimeSuggestionController {
.fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL)

if filespace.codeMetadata.uti == nil {
Logger.service.info("Generate cache for file.")
Logger.service.info("Generate cache for file.")
// avoid the command get called twice
filespace.codeMetadata.uti = ""
do {
Expand All @@ -161,10 +110,12 @@ public actor RealtimeSuggestionController {
}

func triggerPrefetchDebounced(force: Bool = false) {
inflightPrefetchTask = Task { @WorkspaceActor in
inflightPrefetchTask = Task(priority: .utility) { @WorkspaceActor in
try? await Task.sleep(nanoseconds: UInt64((
UserDefaults.shared.value(for: \.realtimeSuggestionDebounce)
max(UserDefaults.shared.value(for: \.realtimeSuggestionDebounce), 0.15)
) * 1_000_000_000))

if Task.isCancelled { return }

guard UserDefaults.shared.value(for: \.realtimeSuggestionToggle)
else { return }
Expand All @@ -179,8 +130,6 @@ public actor RealtimeSuggestionController {
}
if Task.isCancelled { return }

// Logger.service.info("Prefetch suggestions.")

// So the editor won't be blocked (after information are cached)!
await PseudoCommandHandler().generateRealtimeSuggestions(sourceEditor: sourceEditor)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,27 +42,32 @@ struct PseudoCommandHandler {

@WorkspaceActor
func generateRealtimeSuggestions(sourceEditor: SourceEditor?) async {
// Can't use handler if content is not available.
guard
let editor = await getEditorContent(sourceEditor: sourceEditor),
let filespace = await getFilespace(),
guard let filespace = await getFilespace(),
let (workspace, _) = try? await Service.shared.workspacePool
.fetchOrCreateWorkspaceAndFilespace(fileURL: filespace.fileURL) else { return }

if Task.isCancelled { return }

// Can't use handler if content is not available.
guard let editor = await getEditorContent(sourceEditor: sourceEditor)
else { return }

let fileURL = filespace.fileURL
let presenter = PresentInWindowSuggestionPresenter()

presenter.markAsProcessing(true)
defer { presenter.markAsProcessing(false) }

// Check if the current suggestion is still valid.
if filespace.validateSuggestions(
lines: editor.lines,
cursorPosition: editor.cursorPosition
) {
return
} else {
presenter.discardSuggestion(fileURL: filespace.fileURL)
if filespace.presentingSuggestion != nil {
// Check if the current suggestion is still valid.
if filespace.validateSuggestions(
lines: editor.lines,
cursorPosition: editor.cursorPosition
) {
return
} else {
presenter.discardSuggestion(fileURL: filespace.fileURL)
}
}

let snapshot = FilespaceSuggestionSnapshot(
Expand All @@ -78,9 +83,10 @@ struct PseudoCommandHandler {
editor: editor
)
if let sourceEditor {
let editorContent = sourceEditor.getContent()
_ = filespace.validateSuggestions(
lines: sourceEditor.content.lines,
cursorPosition: sourceEditor.content.cursorPosition
lines: editorContent.lines,
cursorPosition: editorContent.cursorPosition
)
}
if filespace.presentingSuggestion != nil {
Expand All @@ -98,9 +104,14 @@ struct PseudoCommandHandler {
guard let (_, filespace) = try? await Service.shared.workspacePool
.fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) else { return }

if filespace.presentingSuggestion == nil {
return // skip if there's no suggestion presented.
}

let content = sourceEditor.getContent()
if !filespace.validateSuggestions(
lines: sourceEditor.content.lines,
cursorPosition: sourceEditor.content.cursorPosition
lines: content.lines,
cursorPosition: content.cursorPosition
) {
PresentInWindowSuggestionPresenter().discardSuggestion(fileURL: fileURL)
}
Expand Down Expand Up @@ -351,7 +362,8 @@ extension PseudoCommandHandler {
guard let filespace = await getFilespace(),
let sourceEditor = sourceEditor ?? XcodeInspector.shared.focusedEditor
else { return nil }
let content = sourceEditor.content
if Task.isCancelled { return nil }
let content = sourceEditor.getContent()
let uti = filespace.codeMetadata.uti ?? ""
let tabSize = filespace.codeMetadata.tabSize ?? 4
let indentSize = filespace.codeMetadata.indentSize ?? 4
Expand Down
Loading