Skip to content

Commit

Permalink
Merge branch 'release/0.30.1'
Browse files Browse the repository at this point in the history
  • Loading branch information
intitni committed Jan 28, 2024
2 parents 381005f + 5a62827 commit 077560c
Show file tree
Hide file tree
Showing 38 changed files with 955 additions and 495 deletions.
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

0 comments on commit 077560c

Please sign in to comment.