From 2d2e33758bfc8377d9e2ece9fbc7d184cdf3cc1e Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 18 Feb 2024 21:32:42 +0800 Subject: [PATCH 01/35] Remove warnings --- Pro | 2 +- Tool/Sources/AppActivator/AppActivator.swift | 4 ++-- .../Swift/SwiftScopeHierarchySyntaxVisitor.swift | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Pro b/Pro index b53b4424..6b7c7906 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit b53b44249d4eea82f09089753dfeca6116ad5a44 +Subproject commit 6b7c79060eb411de4e5bc583b7e661590a381e16 diff --git a/Tool/Sources/AppActivator/AppActivator.swift b/Tool/Sources/AppActivator/AppActivator.swift index 0afbefd5..1523e01a 100644 --- a/Tool/Sources/AppActivator/AppActivator.swift +++ b/Tool/Sources/AppActivator/AppActivator.swift @@ -31,7 +31,7 @@ public extension NSWorkspace { Task { @MainActor in guard let app = XcodeInspector.shared.previousActiveApplication else { return } try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) - app.activate() + _ = app.activate() } } @@ -39,7 +39,7 @@ public extension NSWorkspace { Task { @MainActor in guard let app = XcodeInspector.shared.latestActiveXcode else { return } try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) - app.activate() + _ = app.activate() } } } diff --git a/Tool/Sources/FocusedCodeFinder/Swift/SwiftScopeHierarchySyntaxVisitor.swift b/Tool/Sources/FocusedCodeFinder/Swift/SwiftScopeHierarchySyntaxVisitor.swift index 3da83270..d353bf73 100644 --- a/Tool/Sources/FocusedCodeFinder/Swift/SwiftScopeHierarchySyntaxVisitor.swift +++ b/Tool/Sources/FocusedCodeFinder/Swift/SwiftScopeHierarchySyntaxVisitor.swift @@ -59,11 +59,11 @@ final class SwiftScopeHierarchySyntaxVisitor: SyntaxVisitor { // skip if possible - override func visit(_ node: MemberDeclBlockSyntax) -> SyntaxVisitorContinueKind { + override func visit(_ node: MemberBlockSyntax) -> SyntaxVisitorContinueKind { skipChildrenIfPossible(node) } - override func visit(_ node: MemberDeclListItemSyntax) -> SyntaxVisitorContinueKind { + override func visit(_ node: MemberBlockItemSyntax) -> SyntaxVisitorContinueKind { skipChildrenIfPossible(node) } From 769eafd31d88ab486ca8fcd2986c1042a05606a7 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 20 Feb 2024 18:27:17 +0800 Subject: [PATCH 02/35] Replace AsyncPassthroughSubject from AsyncExtension --- .../RealtimeSuggestionController.swift | 5 +- .../WidgetWindowsController.swift | 6 +- Pro | 2 +- Tool/Package.swift | 6 +- .../AsyncPassthroughSubject.swift | 69 +++++++++++++++++++ .../Apps/XcodeAppInstanceInspector.swift | 4 +- .../Sources/XcodeInspector/SourceEditor.swift | 2 +- .../XcodeInspector/XcodeInspector.swift | 4 +- .../XcodeInspector/XcodeWindowInspector.swift | 10 +-- 9 files changed, 87 insertions(+), 21 deletions(-) create mode 100644 Tool/Sources/AsyncPassthroughSubject/AsyncPassthroughSubject.swift diff --git a/Core/Sources/Service/RealtimeSuggestionController.swift b/Core/Sources/Service/RealtimeSuggestionController.swift index c6fa353b..eea674a2 100644 --- a/Core/Sources/Service/RealtimeSuggestionController.swift +++ b/Core/Sources/Service/RealtimeSuggestionController.swift @@ -65,8 +65,9 @@ public actor RealtimeSuggestionController { ) } - let valueChange = notificationsFromEditor.filter { $0.kind == .valueChanged } - let selectedTextChanged = notificationsFromEditor + let valueChange = await notificationsFromEditor.notifications() + .filter { $0.kind == .valueChanged } + let selectedTextChanged = await notificationsFromEditor.notifications() .filter { $0.kind == .selectedTextChanged } await withTaskGroup(of: Void.self) { [weak self] group in diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift index f294b545..11ffa213 100644 --- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift +++ b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift @@ -288,7 +288,7 @@ private extension WidgetWindowsController { await windows.orderFront() let documentURL = await MainActor.run { store.withState { $0.focusingDocumentURL } } - for await notification in notifications { + for await notification in await notifications.notifications() { try Task.checkCancellation() /// Hide the widgets before switching to another window/editor @@ -337,9 +337,9 @@ private extension WidgetWindowsController { func observe(to editor: SourceEditor) { observeToFocusedEditorTask?.cancel() observeToFocusedEditorTask = Task { - let selectionRangeChange = editor.axNotifications + let selectionRangeChange = await editor.axNotifications.notifications() .filter { $0.kind == .selectedTextChanged } - let scroll = editor.axNotifications + let scroll = await editor.axNotifications.notifications() .filter { $0.kind == .scrollPositionChanged } if #available(macOS 13.0, *) { diff --git a/Pro b/Pro index 6b7c7906..8c431377 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 6b7c79060eb411de4e5bc583b7e661590a381e16 +Subproject commit 8c431377e4a3fe07f56a3c6074b5ee229db98ee5 diff --git a/Tool/Package.swift b/Tool/Package.swift index dcbf7f6e..49950fa8 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -43,6 +43,7 @@ let package = Package( ), .library(name: "GitIgnoreCheck", targets: ["GitIgnoreCheck"]), .library(name: "DebounceFunction", targets: ["DebounceFunction"]), + .library(name: "AsyncPassthroughSubject", targets: ["AsyncPassthroughSubject"]), ], dependencies: [ // A fork of https://github.com/aespinilla/Tiktoken to allow loading from local files. @@ -64,7 +65,6 @@ let package = Package( .package(url: "https://github.com/GottaGetSwifty/CodableWrappers", from: "2.0.7"), .package(url: "https://github.com/krzyzanowskim/STTextView", from: "0.8.21"), .package(url: "https://github.com/google/generative-ai-swift", from: "0.4.4"), - .package(url: "https://github.com/sideeffect-io/AsyncExtensions", from: "0.5.2"), // TreeSitter .package(url: "https://github.com/intitni/SwiftTreeSitter.git", branch: "main"), @@ -173,7 +173,7 @@ let package = Package( "Logger", "Toast", "Preferences", - .product(name: "AsyncExtensions", package: "AsyncExtensions"), + "AsyncPassthroughSubject", .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), ] ), @@ -182,6 +182,8 @@ let package = Package( .target(name: "UserDefaultsObserver"), + .target(name: "AsyncPassthroughSubject"), + .target( name: "SharedUIComponents", dependencies: [ diff --git a/Tool/Sources/AsyncPassthroughSubject/AsyncPassthroughSubject.swift b/Tool/Sources/AsyncPassthroughSubject/AsyncPassthroughSubject.swift new file mode 100644 index 00000000..78ffd353 --- /dev/null +++ b/Tool/Sources/AsyncPassthroughSubject/AsyncPassthroughSubject.swift @@ -0,0 +1,69 @@ +import AppKit +import Foundation + +/// It uses notification center to mimic the behavior of a passthrough subject. +public actor AsyncPassthroughSubject { + let name: Notification.Name + var tasks: [AsyncStream.Continuation] = [] + + deinit { + tasks.forEach { $0.finish() } + } + + public init() { + name = NSNotification.Name( + "AsyncPassthroughSubject-\(UUID().uuidString)-\(String(describing: Element.self))" + ) + } + + public func notifications() -> AsyncStream { + AsyncStream { [weak self, name] continuation in + let task = Task { [weak self] in + await self?.storeContinuation(continuation) + let notifications = NSWorkspace.shared.notificationCenter.notifications(named: name) + .compactMap { + $0.object as? Element + } + for await notification in notifications { + try Task.checkCancellation() + guard self != nil else { + continuation.finish() + return + } + continuation.yield(notification) + } + } + + continuation.onTermination = { termination in + task.cancel() + } + } + } + + nonisolated + public func send(_ element: Element) { + Task { await _send(element) } + } + + func _send(_ element: Element) { + NSWorkspace.shared.notificationCenter.post(name: name, object: element) + } + + func storeContinuation(_ continuation: AsyncStream.Continuation) { + tasks.append(continuation) + } + + nonisolated + public func finish() { + Task { await _finish() } + } + + func _finish() { + let tasks = self.tasks + self.tasks = [] + for task in tasks { + task.finish() + } + } +} + diff --git a/Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift b/Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift index ac05f68b..f623be9a 100644 --- a/Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift +++ b/Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift @@ -1,5 +1,5 @@ import AppKit -import AsyncExtensions +import AsyncPassthroughSubject import AXExtension import AXNotificationStream import Combine @@ -123,7 +123,7 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector { private var focusedWindowObservations = Set() deinit { - axNotifications.send(.finished) + axNotifications.finish() for task in longRunningTasks { task.cancel() } } diff --git a/Tool/Sources/XcodeInspector/SourceEditor.swift b/Tool/Sources/XcodeInspector/SourceEditor.swift index dd2e8621..3ebadceb 100644 --- a/Tool/Sources/XcodeInspector/SourceEditor.swift +++ b/Tool/Sources/XcodeInspector/SourceEditor.swift @@ -1,5 +1,5 @@ import AppKit -import AsyncExtensions +import AsyncPassthroughSubject import AXNotificationStream import Foundation import Logger diff --git a/Tool/Sources/XcodeInspector/XcodeInspector.swift b/Tool/Sources/XcodeInspector/XcodeInspector.swift index 07469800..c421ccbf 100644 --- a/Tool/Sources/XcodeInspector/XcodeInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeInspector.swift @@ -286,7 +286,7 @@ public final class XcodeInspector: ObservableObject { setFocusedElement() let focusedElementChanged = Task { @XcodeInspectorActor in - for await notification in xcode.axNotifications { + for await notification in await xcode.axNotifications.notifications() { if notification.kind == .focusedUIElementChanged { try Task.checkCancellation() setFocusedElement() @@ -301,7 +301,7 @@ public final class XcodeInspector: ObservableObject { { let malfunctionCheck = Task { @XcodeInspectorActor [weak self] in if #available(macOS 13.0, *) { - let notifications = xcode.axNotifications.filter { + let notifications = await xcode.axNotifications.notifications().filter { $0.kind == .uiElementDestroyed }.debounce(for: .milliseconds(1000)) for await _ in notifications { diff --git a/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift b/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift index 92a34e97..29ce373a 100644 --- a/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift @@ -1,5 +1,5 @@ import AppKit -import AsyncExtensions +import AsyncPassthroughSubject import AXExtension import Combine import Foundation @@ -19,11 +19,6 @@ public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector { @Published var workspaceURL: URL = .init(fileURLWithPath: "/") @Published var projectRootURL: URL = .init(fileURLWithPath: "/") private var focusedElementChangedTask: Task? - let axNotifications: AsyncPassthroughSubject - - deinit { - focusedElementChangedTask?.cancel() - } public func refresh() { Task { @XcodeInspectorActor in updateURLs() } @@ -35,7 +30,6 @@ public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector { axNotifications: AsyncPassthroughSubject ) { self.app = app - self.axNotifications = axNotifications super.init(uiElement: uiElement) focusedElementChangedTask = Task { [weak self, axNotifications] in @@ -51,7 +45,7 @@ public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector { } group.addTask { [weak self] in - for await notification in axNotifications { + for await notification in await axNotifications.notifications() { guard notification.kind == .focusedUIElementChanged else { continue } guard let self else { return } try Task.checkCancellation() From 021abbbf8741753277c41c9d149a9c90fb7679d2 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 20 Feb 2024 21:55:43 +0800 Subject: [PATCH 03/35] Update the default chat prompt --- .../ChatService/ContextAwareAutoManagedChatGPTMemory.swift | 2 +- Core/Sources/ChatService/DynamicContextController.swift | 2 +- Tool/Sources/Preferences/Keys.swift | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Core/Sources/ChatService/ContextAwareAutoManagedChatGPTMemory.swift b/Core/Sources/ChatService/ContextAwareAutoManagedChatGPTMemory.swift index 15564dbd..9f4a53e1 100644 --- a/Core/Sources/ChatService/ContextAwareAutoManagedChatGPTMemory.swift +++ b/Core/Sources/ChatService/ContextAwareAutoManagedChatGPTMemory.swift @@ -44,7 +44,7 @@ public final class ContextAwareAutoManagedChatGPTMemory: ChatGPTMemory { systemPrompt: """ \(chatService?.systemPrompt ?? "") \(chatService?.extraSystemPrompt ?? "") - """, + """.trimmingCharacters(in: .whitespacesAndNewlines), content: content ?? "" ) return await memory.generatePrompt() diff --git a/Core/Sources/ChatService/DynamicContextController.swift b/Core/Sources/ChatService/DynamicContextController.swift index 4668649a..c6adb9a4 100644 --- a/Core/Sources/ChatService/DynamicContextController.swift +++ b/Core/Sources/ChatService/DynamicContextController.swift @@ -106,7 +106,7 @@ final class DynamicContextController { let contextualSystemPrompt = """ \(language.isEmpty ? "" : "You must always reply in \(language)") \(systemPrompt) - """ + """.trimmingCharacters(in: .whitespacesAndNewlines) await memory.mutateSystemPrompt(contextualSystemPrompt) await memory.mutateContextSystemPrompt(contextSystemPrompt) await memory.mutateRetrievedContent(retrievedContent.map(\.document)) diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 68137129..d1dd2faa 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -398,9 +398,10 @@ public extension UserDefaultPreferenceKeys { var defaultChatSystemPrompt: PreferenceKey { .init( defaultValue: """ - You are an AI programming assistant. - Your reply should be concise, clear, informative and logical. - Your reply should be formatted in Markdown. + You are a helpful senior programming assistant. + You should respond in natural language. + Your response should be correct, concise, clear, informative and logical. + Use markdown if you need to present code, table, list, etc. If you are asked to help perform a task, you MUST think step-by-step, then describe each step concisely. If you are asked to explain code, you MUST explain it step-by-step in a ordered list concisely. Make your answer short and structured. From cd4e807f955cf4d8a8efa48ee6a9bd6fe614c662 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 20 Feb 2024 21:55:56 +0800 Subject: [PATCH 04/35] Update Azure OpenAI API version --- Tool/Sources/AIModel/ChatModel.swift | 2 +- Tool/Sources/AIModel/EmbeddingModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tool/Sources/AIModel/ChatModel.swift b/Tool/Sources/AIModel/ChatModel.swift index 14ce0a58..344c996b 100644 --- a/Tool/Sources/AIModel/ChatModel.swift +++ b/Tool/Sources/AIModel/ChatModel.swift @@ -76,7 +76,7 @@ public struct ChatModel: Codable, Equatable, Identifiable { case .azureOpenAI: let baseURL = info.baseURL let deployment = info.azureOpenAIDeploymentName - let version = "2023-07-01-preview" + let version = "2024-02-15-preview" if baseURL.isEmpty { return "" } return "\(baseURL)/openai/deployments/\(deployment)/chat/completions?api-version=\(version)" case .googleAI: diff --git a/Tool/Sources/AIModel/EmbeddingModel.swift b/Tool/Sources/AIModel/EmbeddingModel.swift index a87170be..c942be9a 100644 --- a/Tool/Sources/AIModel/EmbeddingModel.swift +++ b/Tool/Sources/AIModel/EmbeddingModel.swift @@ -71,7 +71,7 @@ public struct EmbeddingModel: Codable, Equatable, Identifiable { case .azureOpenAI: let baseURL = info.baseURL let deployment = info.azureOpenAIDeploymentName - let version = "2023-07-01-preview" + let version = "2024-02-15-preview" if baseURL.isEmpty { return "" } return "\(baseURL)/openai/deployments/\(deployment)/embeddings?api-version=\(version)" } From b0a740f86569f66ec3b3ad725fe4770680863034 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 20 Feb 2024 22:01:19 +0800 Subject: [PATCH 05/35] Fix that the active application can be incorrect after debouncing --- .../SuggestionWidget/FeatureReducers/WidgetFeature.swift | 4 +--- .../Sources/SuggestionWidget/WidgetWindowsController.swift | 7 ++----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift index cbb1979f..14adee62 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift @@ -261,9 +261,7 @@ public struct WidgetFeature: ReducerProtocol { guard let windowsController, await windowsController.windows.fullscreenDetector.isOnActiveSpace else { continue } - let app = AXUIElementCreateApplication( - activeXcode.processIdentifier - ) + let app = activeXcode.appElement if let _ = app.focusedWindow { await windowsController.windows.orderFront() } diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift index 11ffa213..753aa72b 100644 --- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift +++ b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift @@ -93,8 +93,6 @@ actor WidgetWindowsController: NSObject { let shouldDebounce = !immediately && !(Date().timeIntervalSince(lastUpdateWindowOpacityTime) > 5) lastUpdateWindowOpacityTime = Date() - let activeApp = xcodeInspector.activeApplication - updateWindowOpacityTask?.cancel() let task = Task { @@ -103,11 +101,10 @@ actor WidgetWindowsController: NSObject { } try Task.checkCancellation() let xcodeInspector = self.xcodeInspector + let activeApp = xcodeInspector.activeApplication await MainActor.run { if let activeApp, activeApp.isXcode { - let application = AXUIElementCreateApplication( - activeApp.processIdentifier - ) + let application = activeApp.appElement /// We need this to hide the windows when Xcode is minimized. let noFocus = application.focusedWindow == nil windows.sharedPanelWindow.alphaValue = noFocus ? 0 : 1 From 5900121138ef0318d8573157979aa26a500f8171 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 20 Feb 2024 22:06:11 +0800 Subject: [PATCH 06/35] Add timeout for UI elements --- Tool/Sources/AXExtension/AXUIElement.swift | 16 +++++++++++++--- .../XcodeInspector/AppInstanceInspector.swift | 4 +++- .../Apps/XcodeAppInstanceInspector.swift | 1 + Tool/Sources/XcodeInspector/SourceEditor.swift | 1 + Tool/Sources/XcodeInspector/XcodeInspector.swift | 1 + .../XcodeInspector/XcodeWindowInspector.swift | 1 + 6 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Tool/Sources/AXExtension/AXUIElement.swift b/Tool/Sources/AXExtension/AXUIElement.swift index e63af6a4..58b12d00 100644 --- a/Tool/Sources/AXExtension/AXUIElement.swift +++ b/Tool/Sources/AXExtension/AXUIElement.swift @@ -4,6 +4,16 @@ import Foundation // MARK: - State public extension AXUIElement { + /// Set global timeout in seconds. + static func setGlobalMessagingTimeout(_ timeout: Float) { + AXUIElementSetMessagingTimeout(AXUIElementCreateSystemWide(), timeout) + } + + /// Set timeout in seconds for this element. + func setMessagingTimeout(_ timeout: Float) { + AXUIElementSetMessagingTimeout(self, timeout) + } + var identifier: String { (try? copyValue(key: kAXIdentifierAttribute)) ?? "" } @@ -63,7 +73,7 @@ public extension AXUIElement { var isEnabled: Bool { (try? copyValue(key: kAXEnabledAttribute)) ?? false } - + var isHidden: Bool { (try? copyValue(key: kAXHiddenAttribute)) ?? false } @@ -183,9 +193,9 @@ public extension AXUIElement { } return all } - + func firstParent(where match: (AXUIElement) -> Bool) -> AXUIElement? { - guard let parent = self.parent else { return nil } + guard let parent = parent else { return nil } if match(parent) { return parent } return parent.firstParent(where: match) } diff --git a/Tool/Sources/XcodeInspector/AppInstanceInspector.swift b/Tool/Sources/XcodeInspector/AppInstanceInspector.swift index 12192abe..1245d98f 100644 --- a/Tool/Sources/XcodeInspector/AppInstanceInspector.swift +++ b/Tool/Sources/XcodeInspector/AppInstanceInspector.swift @@ -8,7 +8,9 @@ public class AppInstanceInspector: ObservableObject { public let bundleIdentifier: String? public var appElement: AXUIElement { - return AXUIElementCreateApplication(runningApplication.processIdentifier) + let app = AXUIElementCreateApplication(runningApplication.processIdentifier) + app.setMessagingTimeout(2) + return app } public var isTerminated: Bool { diff --git a/Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift b/Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift index f623be9a..c16b09a0 100644 --- a/Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift +++ b/Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift @@ -267,6 +267,7 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector { if isCompletionPanel() { await MainActor.run { self.completionPanel = notification.element + self.completionPanel?.setMessagingTimeout(1) self.axNotifications.send(.init( kind: .xcodeCompletionPanelChanged, element: notification.element diff --git a/Tool/Sources/XcodeInspector/SourceEditor.swift b/Tool/Sources/XcodeInspector/SourceEditor.swift index 3ebadceb..2ccecb48 100644 --- a/Tool/Sources/XcodeInspector/SourceEditor.swift +++ b/Tool/Sources/XcodeInspector/SourceEditor.swift @@ -56,6 +56,7 @@ public class SourceEditor { public init(runningApplication: NSRunningApplication, element: AXUIElement) { self.runningApplication = runningApplication self.element = element + element.setMessagingTimeout(2) observeAXNotifications() } diff --git a/Tool/Sources/XcodeInspector/XcodeInspector.swift b/Tool/Sources/XcodeInspector/XcodeInspector.swift index c421ccbf..dbd76869 100644 --- a/Tool/Sources/XcodeInspector/XcodeInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeInspector.swift @@ -97,6 +97,7 @@ public final class XcodeInspector: ObservableObject { } init() { + AXUIElement.setGlobalMessagingTimeout(3) restart() } diff --git a/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift b/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift index 29ce373a..5878a27a 100644 --- a/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift @@ -10,6 +10,7 @@ public class XcodeWindowInspector: ObservableObject { init(uiElement: AXUIElement) { self.uiElement = uiElement + uiElement.setMessagingTimeout(2) } } From 3c3bacb27a22bcdcbf35876caa5b781aca452f94 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 20 Feb 2024 22:29:30 +0800 Subject: [PATCH 07/35] Bump version to 0.30.5 --- Version.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Version.xcconfig b/Version.xcconfig index 7f0f9ca8..c6735829 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,3 +1,3 @@ -APP_VERSION = 0.30.4 -APP_BUILD = 320 +APP_VERSION = 0.30.5 +APP_BUILD = 323 From dbbb30c12b8ef9c65a7762f91f9aad09397fb11c Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 21 Feb 2024 21:01:33 +0800 Subject: [PATCH 08/35] Update CopilotForXcodeKit to 0.3.0 --- Pro | 2 +- Tool/Sources/SuggestionProvider/SuggestionProvider.swift | 6 ++++++ .../Workspace+SuggestionService.swift | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Pro b/Pro index 8c431377..9c113247 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 8c431377e4a3fe07f56a3c6074b5ee229db98ee5 +Subproject commit 9c1132472f9fec91e7529c00dfcb463e2b07ff93 diff --git a/Tool/Sources/SuggestionProvider/SuggestionProvider.swift b/Tool/Sources/SuggestionProvider/SuggestionProvider.swift index 7c592555..44d51801 100644 --- a/Tool/Sources/SuggestionProvider/SuggestionProvider.swift +++ b/Tool/Sources/SuggestionProvider/SuggestionProvider.swift @@ -6,7 +6,9 @@ import UserDefaultsObserver public struct SuggestionRequest { public var fileURL: URL + public var relativePath: String public var content: String + public var lines: [String] public var cursorPosition: CursorPosition public var tabSize: Int public var indentSize: Int @@ -15,7 +17,9 @@ public struct SuggestionRequest { public init( fileURL: URL, + relativePath: String, content: String, + lines: [String], cursorPosition: CursorPosition, tabSize: Int, indentSize: Int, @@ -23,7 +27,9 @@ public struct SuggestionRequest { ignoreSpaceOnlySuggestions: Bool ) { self.fileURL = fileURL + self.relativePath = relativePath self.content = content + self.lines = lines self.cursorPosition = cursorPosition self.tabSize = tabSize self.indentSize = indentSize diff --git a/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift b/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift index 62688298..3dd91dda 100644 --- a/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift +++ b/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift @@ -57,7 +57,9 @@ public extension Workspace { let completions = try await suggestionService.getSuggestions( .init( fileURL: fileURL, + relativePath: fileURL.path.replacingOccurrences(of: projectRootURL.path, with: ""), content: editor.lines.joined(separator: ""), + lines: editor.lines, cursorPosition: editor.cursorPosition, tabSize: editor.tabSize, indentSize: editor.indentSize, From 8325c3e852834bb08ca5b1e5ad7b1f0af7359ed5 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 20 Feb 2024 19:17:57 +0800 Subject: [PATCH 09/35] Support GitHub Copilot enterprise --- .../AccountSettings/GitHubCopilotView.swift | 12 +++++++++++ .../GitHubCopilotRequest.swift | 21 +++++++++++++++++-- Tool/Sources/Preferences/Keys.swift | 4 ++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift index d3292e2b..d822331d 100644 --- a/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift +++ b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift @@ -19,6 +19,7 @@ struct GitHubCopilotView: View { @AppStorage(\.gitHubCopilotProxyUsername) var gitHubCopilotProxyUsername @AppStorage(\.gitHubCopilotProxyPassword) var gitHubCopilotProxyPassword @AppStorage(\.gitHubCopilotUseStrictSSL) var gitHubCopilotUseStrictSSL + @AppStorage(\.gitHubCopilotEnterpriseURI) var gitHubCopilotEnterpriseURI @AppStorage(\.gitHubCopilotIgnoreTrailingNewLines) var gitHubCopilotIgnoreTrailingNewLines @AppStorage(\.disableGitHubCopilotSettingsAutoRefreshOnAppear) @@ -275,6 +276,17 @@ struct GitHubCopilotView: View { .dynamicHeightTextInFormWorkaround() Toggle("Verbose Log", isOn: $settings.gitHubCopilotVerboseLog) } + + SettingsDivider("Enterprise") + + Form { + TextField( + text: $settings.gitHubCopilotEnterpriseURI, + prompt: Text("Leave it blank if non is available.") + ) { + Text("Auth Provider URL") + } + } SettingsDivider("Proxy") diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift index 0b090450..744077ba 100644 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift +++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift @@ -80,9 +80,26 @@ enum GitHubCopilotRequest { ]) } } + + var editorConfiguration: JSONValue? { + var dict: [String: JSONValue] = [:] + if let networkProxy { + dict["http"] = networkProxy + } + + let enterpriseURI = UserDefaults.shared.value(for: \.gitHubCopilotEnterpriseURI) + if !enterpriseURI.isEmpty { + dict["github-enterprise"] = .hash([ + "uri": .string(enterpriseURI) + ]) + } + + if dict.isEmpty { return nil } + return .hash(dict) + } var request: ClientRequest { - if let networkProxy { + if let editorConfiguration { return .custom("setEditorInfo", .hash([ "editorInfo": .hash([ "name": "Xcode", @@ -92,7 +109,7 @@ enum GitHubCopilotRequest { "name": "Copilot for Xcode", "version": "", ]), - "networkProxy": networkProxy, + "editorConfiguration": editorConfiguration, ])) } diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index d1dd2faa..d1257dbf 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -163,6 +163,10 @@ public extension UserDefaultPreferenceKeys { var gitHubCopilotProxyPort: PreferenceKey { .init(defaultValue: "", key: "GitHubCopilotProxyPort") } + + var gitHubCopilotEnterpriseURI: PreferenceKey { + .init(defaultValue: "", key: "GitHubCopilotEnterpriseURI") + } var gitHubCopilotUseStrictSSL: PreferenceKey { .init(defaultValue: true, key: "GitHubCopilotUseStrictSSL") From af2f668edb701b5df72366095e8671ef43b3a73c Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 20 Feb 2024 22:48:17 +0800 Subject: [PATCH 10/35] Support Copilot.vim 1.17.0 to 1.19.x --- .../GitHubCopilotRequest.swift | 72 +++++++++++++------ 1 file changed, 49 insertions(+), 23 deletions(-) diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift index 744077ba..5fcba1c7 100644 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift +++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift @@ -49,7 +49,6 @@ public struct GitHubCopilotCodeSuggestion: Codable, Equatable { public var displayText: String } - enum GitHubCopilotRequest { struct SetEditorInfo: GitHubCopilotRequestType { struct Response: Codable {} @@ -80,40 +79,61 @@ enum GitHubCopilotRequest { ]) } } - - var editorConfiguration: JSONValue? { + + var http: JSONValue? { var dict: [String: JSONValue] = [:] - if let networkProxy { - dict["http"] = networkProxy + let host = UserDefaults.shared.value(for: \.gitHubCopilotProxyHost) + if host.isEmpty { return nil } + var port = UserDefaults.shared.value(for: \.gitHubCopilotProxyPort) + if port.isEmpty { port = "80" } + let username = UserDefaults.shared.value(for: \.gitHubCopilotProxyUsername) + let password = UserDefaults.shared.value(for: \.gitHubCopilotProxyPassword) + let strictSSL = UserDefaults.shared.value(for: \.gitHubCopilotUseStrictSSL) + + let url = if !username.isEmpty { + "http://\(username):\(password)@\(host):\(port)" + } else { + "http://\(host):\(port)" } - + + dict["proxy"] = .string(url) + dict["proxyStrictSSL"] = .bool(strictSSL) + + if dict.isEmpty { return nil } + + return .hash(dict) + } + + var editorConfiguration: JSONValue? { + var dict: [String: JSONValue] = [:] + dict["http"] = http + let enterpriseURI = UserDefaults.shared.value(for: \.gitHubCopilotEnterpriseURI) if !enterpriseURI.isEmpty { dict["github-enterprise"] = .hash([ - "uri": .string(enterpriseURI) + "uri": .string(enterpriseURI), ]) } - + if dict.isEmpty { return nil } return .hash(dict) } - var request: ClientRequest { - if let editorConfiguration { - return .custom("setEditorInfo", .hash([ - "editorInfo": .hash([ - "name": "Xcode", - "version": "", - ]), - "editorPluginInfo": .hash([ - "name": "Copilot for Xcode", - "version": "", - ]), - "editorConfiguration": editorConfiguration, - ])) + var authProvider: JSONValue? { + var dict: [String: JSONValue] = [:] + let enterpriseURI = UserDefaults.shared.value(for: \.gitHubCopilotEnterpriseURI) + if !enterpriseURI.isEmpty { + dict["github-enterprise"] = .hash([ + "url": .string(enterpriseURI), + ]) } - return .custom("setEditorInfo", .hash([ + if dict.isEmpty { return nil } + return .hash(dict) + } + + var request: ClientRequest { + var dict: [String: JSONValue] = [ "editorInfo": .hash([ "name": "Xcode", "version": "", @@ -122,7 +142,13 @@ enum GitHubCopilotRequest { "name": "Copilot for Xcode", "version": "", ]), - ])) + ] + + dict["editorConfiguration"] = editorConfiguration + dict["authProvider"] = authProvider + dict["networkProxy"] = networkProxy + + return .custom("setEditorInfo", .hash(dict)) } } From 8fea60a1b209fea93ababd6fdc709e996ea1116d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 21 Feb 2024 21:22:12 +0800 Subject: [PATCH 11/35] Fix auth provider url position --- Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift index 5fcba1c7..d9210485 100644 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift +++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift @@ -123,9 +123,7 @@ enum GitHubCopilotRequest { var dict: [String: JSONValue] = [:] let enterpriseURI = UserDefaults.shared.value(for: \.gitHubCopilotEnterpriseURI) if !enterpriseURI.isEmpty { - dict["github-enterprise"] = .hash([ - "url": .string(enterpriseURI), - ]) + dict["url"] = .string(enterpriseURI) } if dict.isEmpty { return nil } From 3ad7e5591e4c374b9886c0588cf2b8c58462d78a Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 21 Feb 2024 21:22:28 +0800 Subject: [PATCH 12/35] Bump Copilot.vim to 1.19.2 --- .../GitHubCopilotService/GitHubCopilotInstallationManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotInstallationManager.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotInstallationManager.swift index 723c0ede..92877938 100644 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotInstallationManager.swift +++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotInstallationManager.swift @@ -10,7 +10,7 @@ public struct GitHubCopilotInstallationManager { return URL(string: link)! } - static let latestSupportedVersion = "1.17.0" + static let latestSupportedVersion = "1.19.2" public init() {} From 65043e034b6a10fb74bff3f2b91d36975c340d99 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 21 Feb 2024 21:40:03 +0800 Subject: [PATCH 13/35] Replace miss-used NSWorkspace notification center --- Core/Sources/Service/XPCService.swift | 2 +- Pro | 2 +- .../AsyncPassthroughSubject.swift | 4 ++-- Tool/Sources/OpenAIService/Debug/Debug.swift | 10 +++++----- Tool/Sources/XcodeInspector/XcodeInspector.swift | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Core/Sources/Service/XPCService.swift b/Core/Sources/Service/XPCService.swift index ee078b23..3c2cf47b 100644 --- a/Core/Sources/Service/XPCService.swift +++ b/Core/Sources/Service/XPCService.swift @@ -187,7 +187,7 @@ public class XPCService: NSObject, XPCServiceProtocol { public func postNotification(name: String, withReply reply: @escaping () -> Void) { reply() - NSWorkspace.shared.notificationCenter.post(name: .init(name), object: nil) + NotificationCenter.default.post(name: .init(name), object: nil) } // MARK: - Requests diff --git a/Pro b/Pro index 9c113247..67da617c 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 9c1132472f9fec91e7529c00dfcb463e2b07ff93 +Subproject commit 67da617cfc212e787b2b5f1eec352ac6c590ffd3 diff --git a/Tool/Sources/AsyncPassthroughSubject/AsyncPassthroughSubject.swift b/Tool/Sources/AsyncPassthroughSubject/AsyncPassthroughSubject.swift index 78ffd353..42da6b26 100644 --- a/Tool/Sources/AsyncPassthroughSubject/AsyncPassthroughSubject.swift +++ b/Tool/Sources/AsyncPassthroughSubject/AsyncPassthroughSubject.swift @@ -20,7 +20,7 @@ public actor AsyncPassthroughSubject { AsyncStream { [weak self, name] continuation in let task = Task { [weak self] in await self?.storeContinuation(continuation) - let notifications = NSWorkspace.shared.notificationCenter.notifications(named: name) + let notifications = NotificationCenter.default.notifications(named: name) .compactMap { $0.object as? Element } @@ -46,7 +46,7 @@ public actor AsyncPassthroughSubject { } func _send(_ element: Element) { - NSWorkspace.shared.notificationCenter.post(name: name, object: element) + NotificationCenter.default.post(name: name, object: element) } func storeContinuation(_ continuation: AsyncStream.Continuation) { diff --git a/Tool/Sources/OpenAIService/Debug/Debug.swift b/Tool/Sources/OpenAIService/Debug/Debug.swift index d90883e9..b27358e0 100644 --- a/Tool/Sources/OpenAIService/Debug/Debug.swift +++ b/Tool/Sources/OpenAIService/Debug/Debug.swift @@ -9,7 +9,7 @@ enum Debugger { static func didSendRequestBody(body: CompletionRequestBody) { do { let json = try JSONEncoder().encode(body) - let center = NSWorkspace.shared.notificationCenter + let center = NotificationCenter.default center.post( name: .init("ServiceDebugger.ChatRequestDebug.requestSent"), object: nil, @@ -24,7 +24,7 @@ enum Debugger { } static func didReceiveFunction(name: String, arguments: String) { - let center = NSWorkspace.shared.notificationCenter + let center = NotificationCenter.default center.post( name: .init("ServiceDebugger.ChatRequestDebug.receivedFunctionCall"), object: nil, @@ -37,7 +37,7 @@ enum Debugger { } static func didReceiveFunctionResult(result: String) { - let center = NSWorkspace.shared.notificationCenter + let center = NotificationCenter.default center.post( name: .init("ServiceDebugger.ChatRequestDebug.receivedFunctionResult"), object: nil, @@ -49,7 +49,7 @@ enum Debugger { } static func didReceiveResponse(content: String) { - let center = NSWorkspace.shared.notificationCenter + let center = NotificationCenter.default center.post( name: .init("ServiceDebugger.ChatRequestDebug.responseReceived"), object: nil, @@ -61,7 +61,7 @@ enum Debugger { } static func didFinish() { - let center = NSWorkspace.shared.notificationCenter + let center = NotificationCenter.default center.post( name: .init("ServiceDebugger.ChatRequestDebug.finished"), object: nil, diff --git a/Tool/Sources/XcodeInspector/XcodeInspector.swift b/Tool/Sources/XcodeInspector/XcodeInspector.swift index dbd76869..d3aacc45 100644 --- a/Tool/Sources/XcodeInspector/XcodeInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeInspector.swift @@ -222,7 +222,7 @@ public final class XcodeInspector: ObservableObject { } group.addTask { [weak self] in // malfunctioning - let sequence = NSWorkspace.shared.notificationCenter + let sequence = NotificationCenter.default .notifications(named: .accessibilityAPIMalfunctioning) for await notification in sequence { try Task.checkCancellation() @@ -347,7 +347,7 @@ public final class XcodeInspector: ObservableObject { else { return } if let editor = focusedEditor, !editor.element.isSourceEditor { - NSWorkspace.shared.notificationCenter.post( + NotificationCenter.default.post( name: .accessibilityAPIMalfunctioning, object: "Source Editor Element Corrupted: \(source)" ) @@ -355,7 +355,7 @@ public final class XcodeInspector: ObservableObject { if element.description != focusedElement?.description || element.role != focusedElement?.role { - NSWorkspace.shared.notificationCenter.post( + NotificationCenter.default.post( name: .accessibilityAPIMalfunctioning, object: "Element Inconsistency: \(source)" ) From 079ad69b7c38c0d1174af2c776fcba6374bd1d9f Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 21 Feb 2024 21:51:31 +0800 Subject: [PATCH 14/35] Support refreshing configuration --- .../AccountSettings/GitHubCopilotView.swift | 33 ++++++++++++++++--- Pro | 2 +- .../GitHubCopilotService.swift | 18 ++++++++-- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift index d822331d..90a8bdf5 100644 --- a/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift +++ b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift @@ -158,7 +158,7 @@ struct GitHubCopilotView: View { ) { Text("Path to Node (v18+)") } - + Text( "Provide the path to the executable if it can't be found by the app, shim executable is not supported" ) @@ -166,7 +166,7 @@ struct GitHubCopilotView: View { .foregroundColor(.secondary) .font(.callout) .dynamicHeightTextInFormWorkaround() - + Picker(selection: $settings.runNodeWith) { ForEach(NodeRunner.allCases, id: \.rawValue) { runner in switch runner { @@ -181,7 +181,7 @@ struct GitHubCopilotView: View { } label: { Text("Run Node with") } - + Group { switch settings.runNodeWith { case .env: @@ -258,6 +258,10 @@ struct GitHubCopilotView: View { } .opacity(isRunningAction ? 0.8 : 1) .disabled(isRunningAction) + + Button("Refresh Configuration for Enterprise and Proxy") { + refreshConfiguration() + } } SettingsDivider("Advanced") @@ -276,9 +280,9 @@ struct GitHubCopilotView: View { .dynamicHeightTextInFormWorkaround() Toggle("Verbose Log", isOn: $settings.gitHubCopilotVerboseLog) } - + SettingsDivider("Enterprise") - + Form { TextField( text: $settings.gitHubCopilotEnterpriseURI, @@ -415,6 +419,25 @@ struct GitHubCopilotView: View { } } } + + func refreshConfiguration() { + NotificationCenter.default.post( + name: .gitHubCopilotShouldRefreshEditorInformation, + object: nil + ) + + Task { + let service = try getService() + do { + try await service.postNotification( + name: Notification.Name + .gitHubCopilotShouldRefreshEditorInformation.rawValue + ) + } catch { + toast(error.localizedDescription, .error) + } + } + } } struct ActivityIndicatorView: NSViewRepresentable { diff --git a/Pro b/Pro index 67da617c..528831a7 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 67da617cfc212e787b2b5f1eec352ac6c590ffd3 +Subproject commit 528831a7e8f106f23b1aac427aed32220165dfd0 diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift index 77cff0fe..01225c22 100644 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift @@ -1,3 +1,4 @@ +import AppKit import Foundation import LanguageClient import LanguageServerProtocol @@ -51,6 +52,11 @@ enum GitHubCopilotError: Error, LocalizedError { } } +public extension Notification.Name { + static let gitHubCopilotShouldRefreshEditorInformation = Notification + .Name("com.intii.CopilotForXcode.GitHubCopilotShouldRefreshEditorInformation") +} + public class GitHubCopilotBaseService { let projectRootURL: URL var server: GitHubCopilotLSP @@ -160,8 +166,16 @@ public class GitHubCopilotBaseService { self.server = server localProcessServer = localServer - Task { - try await server.sendRequest(GitHubCopilotRequest.SetEditorInfo()) + Task { [weak self] in + _ = try? await server.sendRequest(GitHubCopilotRequest.SetEditorInfo()) + + for await notification in NotificationCenter.default + .notifications(named: .gitHubCopilotShouldRefreshEditorInformation) + { + print("Yes!") + guard let self else { return } + _ = try? await server.sendRequest(GitHubCopilotRequest.SetEditorInfo()) + } } } From d609ddb41cf78760a87a2ca6b8e7bd43d2311549 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 21 Feb 2024 21:53:12 +0800 Subject: [PATCH 15/35] Update realtime suggestion debounce to minimum 0.25 --- Core/Sources/Service/RealtimeSuggestionController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/Sources/Service/RealtimeSuggestionController.swift b/Core/Sources/Service/RealtimeSuggestionController.swift index eea674a2..41cb0d57 100644 --- a/Core/Sources/Service/RealtimeSuggestionController.swift +++ b/Core/Sources/Service/RealtimeSuggestionController.swift @@ -142,7 +142,7 @@ public actor RealtimeSuggestionController { func triggerPrefetchDebounced(force: Bool = false) { inflightPrefetchTask = Task(priority: .utility) { @WorkspaceActor in try? await Task.sleep(nanoseconds: UInt64( - max(UserDefaults.shared.value(for: \.realtimeSuggestionDebounce), 0.15) + max(UserDefaults.shared.value(for: \.realtimeSuggestionDebounce), 0.25) * 1_000_000_000 )) From d7bcfcd4ea76369492da112570956964d1346e34 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 21 Feb 2024 22:05:31 +0800 Subject: [PATCH 16/35] Discard suggestion when the focused editor element is changed --- .../SuggestionWidget/FeatureReducers/PanelFeature.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift index 5208bf3b..77750517 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift @@ -96,12 +96,10 @@ public struct PanelFeature: ReducerProtocol { case .switchToAnotherEditorAndUpdateContent: state.content.error = nil + state.content.suggestion = nil return .run { send in guard let fileURL = xcodeInspector.realtimeActiveDocumentURL else { return } - if let suggestion = await fetchSuggestionProvider(fileURL: fileURL) { - await send(.presentSuggestionProvider(suggestion, displayContent: false)) - } - + await send(.sharedPanel( .promptToCodeGroup( .updateActivePromptToCode(documentURL: fileURL) From ce0b20bca46cb96a56922477ab49b2eba2a66abd Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 21 Feb 2024 22:08:18 +0800 Subject: [PATCH 17/35] Update locations immediately in some situations --- .../SuggestionWidget/WidgetWindowsController.swift | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift index 753aa72b..00324890 100644 --- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift +++ b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift @@ -299,10 +299,10 @@ private extension WidgetWindowsController { await send(.updateFocusingDocumentURL) } - func updateWidgetsAndNotifyChangeOfEditor() async { - await updateWindowLocation(animated: false, immediately: false) - await updateWindowOpacity(immediately: false) + func updateWidgetsAndNotifyChangeOfEditor(immediately: Bool) async { await send(.panel(.switchToAnotherEditorAndUpdateContent)) + await updateWindowLocation(animated: false, immediately: immediately) + await updateWindowOpacity(immediately: immediately) } func updateWidgets() async { @@ -313,9 +313,11 @@ private extension WidgetWindowsController { switch notification.kind { case .focusedWindowChanged, .focusedUIElementChanged: await hideWidgetForTransitions() - await updateWidgetsAndNotifyChangeOfEditor() - case .applicationActivated, .mainWindowChanged: - await updateWidgetsAndNotifyChangeOfEditor() + await updateWidgetsAndNotifyChangeOfEditor(immediately: true) + case .applicationActivated: + await updateWidgetsAndNotifyChangeOfEditor(immediately: false) + case .mainWindowChanged: + await updateWidgetsAndNotifyChangeOfEditor(immediately: false) case .applicationDeactivated, .moved, .resized, From c517f6679edeea87721a620dfc5277ab06184b1a Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 21 Feb 2024 22:12:31 +0800 Subject: [PATCH 18/35] Remove the notification center thing --- .../AsyncPassthroughSubject.swift | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/Tool/Sources/AsyncPassthroughSubject/AsyncPassthroughSubject.swift b/Tool/Sources/AsyncPassthroughSubject/AsyncPassthroughSubject.swift index 42da6b26..94d033d7 100644 --- a/Tool/Sources/AsyncPassthroughSubject/AsyncPassthroughSubject.swift +++ b/Tool/Sources/AsyncPassthroughSubject/AsyncPassthroughSubject.swift @@ -1,37 +1,19 @@ import AppKit import Foundation -/// It uses notification center to mimic the behavior of a passthrough subject. public actor AsyncPassthroughSubject { - let name: Notification.Name var tasks: [AsyncStream.Continuation] = [] deinit { tasks.forEach { $0.finish() } } - public init() { - name = NSNotification.Name( - "AsyncPassthroughSubject-\(UUID().uuidString)-\(String(describing: Element.self))" - ) - } + public init() {} public func notifications() -> AsyncStream { - AsyncStream { [weak self, name] continuation in + AsyncStream { [weak self] continuation in let task = Task { [weak self] in await self?.storeContinuation(continuation) - let notifications = NotificationCenter.default.notifications(named: name) - .compactMap { - $0.object as? Element - } - for await notification in notifications { - try Task.checkCancellation() - guard self != nil else { - continuation.finish() - return - } - continuation.yield(notification) - } } continuation.onTermination = { termination in @@ -46,7 +28,10 @@ public actor AsyncPassthroughSubject { } func _send(_ element: Element) { - NotificationCenter.default.post(name: name, object: element) + let tasks = tasks + for task in tasks { + task.yield(element) + } } func storeContinuation(_ continuation: AsyncStream.Continuation) { From 6b4e6f837f6b135f664fe6038c7d6e98038f8c99 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 21 Feb 2024 22:17:52 +0800 Subject: [PATCH 19/35] Fix that the suggestion panel counld dance around when the completion panel updates --- .../WidgetWindowsController.swift | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift index 00324890..fb9d4dbd 100644 --- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift +++ b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift @@ -28,6 +28,8 @@ actor WidgetWindowsController: NSObject { var updateWindowLocationTask: Task? var lastUpdateWindowLocationTime = Date(timeIntervalSince1970: 0) + var beatingCompletionPanelTask: Task? + deinit { userDefaultsObservers.presentationModeChangeObserver.onChange = {} observeToAppTask?.cancel() @@ -61,15 +63,7 @@ actor WidgetWindowsController: NSObject { xcodeInspector.$completionPanel.sink { [weak self] newValue in Task { [weak self] in - if newValue == nil { - // so that the buttons on the suggestion panel could be - // clicked - // before the completion panel updates the location of the - // suggestion panel - try await Task.sleep(nanoseconds: 400_000_000) - } - await self?.updateWindowLocation(animated: false, immediately: false) - await self?.updateWindowOpacity(immediately: false) + await self?.handleCompletionPanelChange(isDisplaying: newValue != nil) } }.store(in: &cancellable) @@ -373,6 +367,22 @@ private extension WidgetWindowsController { } } } + + func handleCompletionPanelChange(isDisplaying: Bool) { + beatingCompletionPanelTask?.cancel() + beatingCompletionPanelTask = Task { + if !isDisplaying { + // so that the buttons on the suggestion panel could be + // clicked + // before the completion panel updates the location of the + // suggestion panel + try await Task.sleep(nanoseconds: 400_000_000) + } + + await updateWindowLocation(animated: false, immediately: false) + await updateWindowOpacity(immediately: false) + } + } } extension WidgetWindowsController { From 5944902dee3591b90373d308c6f0832cf8b12f63 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 21 Feb 2024 22:23:24 +0800 Subject: [PATCH 20/35] Bump build number --- Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Version.xcconfig b/Version.xcconfig index c6735829..18d73005 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,3 +1,3 @@ APP_VERSION = 0.30.5 -APP_BUILD = 323 +APP_BUILD = 325 From 321236a735c0539f2fd6e40288798ad3757632bd Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 21 Feb 2024 23:09:50 +0800 Subject: [PATCH 21/35] Ignore .applicationDeactivated because it's handled somewhere else --- Core/Sources/SuggestionWidget/WidgetWindowsController.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift index fb9d4dbd..558aa5f7 100644 --- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift +++ b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift @@ -312,15 +312,15 @@ private extension WidgetWindowsController { await updateWidgetsAndNotifyChangeOfEditor(immediately: false) case .mainWindowChanged: await updateWidgetsAndNotifyChangeOfEditor(immediately: false) - case .applicationDeactivated, - .moved, + case .moved, .resized, .windowMoved, .windowResized, .windowMiniaturized, .windowDeminiaturized: await updateWidgets() - case .created, .uiElementDestroyed, .xcodeCompletionPanelChanged: + case .created, .uiElementDestroyed, .xcodeCompletionPanelChanged, + .applicationDeactivated: continue } } From efe5e2051f1dcfbc6e663313fdc71f23fc41f2ab Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 21 Feb 2024 23:10:23 +0800 Subject: [PATCH 22/35] Remove the useless await --- .../WidgetWindowsController.swift | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift index 558aa5f7..2afddee7 100644 --- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift +++ b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift @@ -79,11 +79,7 @@ actor WidgetWindowsController: NSObject { await send(.updatePanelStateToMatch(location)) } - func updateWindowOpacity(immediately: Bool) async { - let state = store.withState { $0 } - - let isChatPanelDetached = state.chatPanelState.chatPanelInASeparateWindow - let hasChat = !state.chatPanelState.chatTabGroup.tabInfo.isEmpty + func updateWindowOpacity(immediately: Bool) { let shouldDebounce = !immediately && !(Date().timeIntervalSince(lastUpdateWindowOpacityTime) > 5) lastUpdateWindowOpacityTime = Date() @@ -153,7 +149,7 @@ actor WidgetWindowsController: NSObject { immediately: Bool, function: StaticString = #function, line: UInt = #line - ) async { + ) { @Sendable @MainActor func update() async { let state = store.withState { $0 } @@ -259,11 +255,12 @@ private extension WidgetWindowsController { func activate(_ app: AppInstanceInspector) { Task { if app.isXcode { - await updateWindowLocation(animated: false, immediately: true) - await updateWindowOpacity(immediately: false) + updateWindowLocation(animated: false, immediately: true) + updateWindowOpacity(immediately: false) } else { - await updateWindowOpacity(immediately: true) - await updateWindowLocation(animated: false, immediately: false) + updateWindowOpacity(immediately: true) + updateWindowLocation(animated: false, immediately: false) + await hideSuggestionPanelWindow() } } guard currentApplicationProcessIdentifier != app.processIdentifier else { return } @@ -295,13 +292,13 @@ private extension WidgetWindowsController { func updateWidgetsAndNotifyChangeOfEditor(immediately: Bool) async { await send(.panel(.switchToAnotherEditorAndUpdateContent)) - await updateWindowLocation(animated: false, immediately: immediately) - await updateWindowOpacity(immediately: immediately) + updateWindowLocation(animated: false, immediately: immediately) + updateWindowOpacity(immediately: immediately) } func updateWidgets() async { - await updateWindowLocation(animated: false, immediately: false) - await updateWindowOpacity(immediately: false) + updateWindowLocation(animated: false, immediately: false) + updateWindowOpacity(immediately: false) } switch notification.kind { @@ -348,8 +345,8 @@ private extension WidgetWindowsController { await hideSuggestionPanelWindow() } - await updateWindowLocation(animated: false, immediately: false) - await updateWindowOpacity(immediately: false) + updateWindowLocation(animated: false, immediately: false) + updateWindowOpacity(immediately: false) } } else { for await notification in merge(selectionRangeChange, scroll) { @@ -361,8 +358,8 @@ private extension WidgetWindowsController { await hideSuggestionPanelWindow() } - await updateWindowLocation(animated: false, immediately: false) - await updateWindowOpacity(immediately: false) + updateWindowLocation(animated: false, immediately: false) + updateWindowOpacity(immediately: false) } } } @@ -379,8 +376,8 @@ private extension WidgetWindowsController { try await Task.sleep(nanoseconds: 400_000_000) } - await updateWindowLocation(animated: false, immediately: false) - await updateWindowOpacity(immediately: false) + updateWindowLocation(animated: false, immediately: false) + updateWindowOpacity(immediately: false) } } } From f9c30c696b82c0c075e2b5be6d036f7a79a146d1 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 21 Feb 2024 23:10:37 +0800 Subject: [PATCH 23/35] Adjust interval --- Core/Sources/SuggestionWidget/WidgetWindowsController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift index 2afddee7..fa540b3c 100644 --- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift +++ b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift @@ -81,7 +81,7 @@ actor WidgetWindowsController: NSObject { func updateWindowOpacity(immediately: Bool) { let shouldDebounce = !immediately && - !(Date().timeIntervalSince(lastUpdateWindowOpacityTime) > 5) + !(Date().timeIntervalSince(lastUpdateWindowOpacityTime) > 3) lastUpdateWindowOpacityTime = Date() updateWindowOpacityTask?.cancel() @@ -194,7 +194,7 @@ actor WidgetWindowsController: NSObject { let now = Date() let shouldThrottle = !immediately && - !(now.timeIntervalSince(lastUpdateWindowLocationTime) > 5) + !(now.timeIntervalSince(lastUpdateWindowLocationTime) > 3) updateWindowLocationTask?.cancel() let interval: TimeInterval = 0.1 From 5e86b93595dc01dfb6820229007373e0f77347fd Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 21 Feb 2024 23:10:52 +0800 Subject: [PATCH 24/35] Move the state retrieval to after the debouncing --- Core/Sources/SuggestionWidget/WidgetWindowsController.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift index fa540b3c..cd49a183 100644 --- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift +++ b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift @@ -93,6 +93,10 @@ actor WidgetWindowsController: NSObject { let xcodeInspector = self.xcodeInspector let activeApp = xcodeInspector.activeApplication await MainActor.run { + let state = store.withState { $0 } + let isChatPanelDetached = state.chatPanelState.chatPanelInASeparateWindow + let hasChat = !state.chatPanelState.chatTabGroup.tabInfo.isEmpty + if let activeApp, activeApp.isXcode { let application = activeApp.appElement /// We need this to hide the windows when Xcode is minimized. From c1f506b07fe09d6f384e9cdd962585b17e2e9303 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 21 Feb 2024 23:21:49 +0800 Subject: [PATCH 25/35] Bump version --- Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Version.xcconfig b/Version.xcconfig index 18d73005..6138d7da 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,3 +1,3 @@ APP_VERSION = 0.30.5 -APP_BUILD = 325 +APP_BUILD = 326 From 0266337954f74f69f28142de7d79cdb0fbaa1835 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 22 Feb 2024 01:18:13 +0800 Subject: [PATCH 26/35] Make XcodeInspector a bit more thread safe --- .../OpenAIPromptToCodeService.swift | 2 +- ExtensionService/AppDelegate+Menu.swift | 4 +- Pro | 2 +- .../ActiveDocumentChatContextCollector.swift | 3 +- .../GetEditorInfo.swift | 8 --- ...cyActiveDocumentChatContextCollector.swift | 45 ++++++++-------- .../XcodeInspector/XcodeInspector.swift | 51 ++++++++++++------- 7 files changed, 64 insertions(+), 51 deletions(-) delete mode 100644 Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/GetEditorInfo.swift diff --git a/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift b/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift index d2bcea30..237d5bc4 100644 --- a/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift +++ b/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift @@ -31,7 +31,7 @@ public final class OpenAIPromptToCodeService: PromptToCodeServiceType { return userPreferredLanguage.isEmpty ? "" : " in \(userPreferredLanguage)" }() - let editor: EditorInformation = XcodeInspector.shared.getFocusedEditorContent() ?? .init( + let editor: EditorInformation = await XcodeInspector.shared.getFocusedEditorContent() ?? .init( editorContent: .init( content: source.content, lines: source.lines, diff --git a/ExtensionService/AppDelegate+Menu.swift b/ExtensionService/AppDelegate+Menu.swift index 6ccd371a..b77609f5 100644 --- a/ExtensionService/AppDelegate+Menu.swift +++ b/ExtensionService/AppDelegate+Menu.swift @@ -218,7 +218,9 @@ extension AppDelegate: NSMenuDelegate { private extension AppDelegate { @objc func restartXcodeInspector() { - XcodeInspector.shared.restart(cleanUp: true) + Task { + await XcodeInspector.shared.restart(cleanUp: true) + } } @objc func reactivateObservationsToXcode() { diff --git a/Pro b/Pro index 528831a7..6bc4b4c2 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 528831a7e8f106f23b1aac427aed32220165dfd0 +Subproject commit 6bc4b4c253b9bc52ae95615ac18a1dae7e0caa43 diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift index 39b38126..a93e1347 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift @@ -22,7 +22,8 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { content: String, configuration: ChatGPTConfiguration ) async -> ChatContext { - guard let info = getEditorInformation() else { return .empty } + guard let info = await XcodeInspector.shared.getFocusedEditorContent() + else { return .empty } let context = getActiveDocumentContext(info) activeDocumentContext = context diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/GetEditorInfo.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/GetEditorInfo.swift deleted file mode 100644 index 4861294b..00000000 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/GetEditorInfo.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation -import SuggestionModel -import XcodeInspector - -func getEditorInformation() -> EditorInformation? { - return XcodeInspector.shared.getFocusedEditorContent() -} - diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift index c7c4d20f..c0590b00 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift @@ -13,8 +13,9 @@ public struct LegacyActiveDocumentChatContextCollector: ChatContextCollector { scopes: Set, content: String, configuration: ChatGPTConfiguration - ) -> ChatContext { - guard let content = getEditorInformation() else { return .empty } + ) async -> ChatContext { + guard let content = await XcodeInspector.shared.getFocusedEditorContent() + else { return .empty } let relativePath = content.relativePath let selectionRange = content.editorContent?.selections.first ?? .outOfScope let editorContent = { @@ -79,26 +80,26 @@ public struct LegacyActiveDocumentChatContextCollector: ChatContextCollector { return .init( systemPrompt: """ - Active Document Context:### - Document Relative Path: \(relativePath) - Selection Range Start: \ - Line \(selectionRange.start.line) \ - Character \(selectionRange.start.character) - Selection Range End: \ - Line \(selectionRange.end.line) \ - Character \(selectionRange.end.character) - Cursor Position: \ - Line \(selectionRange.end.line) \ - Character \(selectionRange.end.character) - \(editorContent) - Line Annotations: - \( - content.editorContent?.lineAnnotations - .map { " - \($0)" } - .joined(separator: "\n") ?? "N/A" - ) - ### - """, + Active Document Context:### + Document Relative Path: \(relativePath) + Selection Range Start: \ + Line \(selectionRange.start.line) \ + Character \(selectionRange.start.character) + Selection Range End: \ + Line \(selectionRange.end.line) \ + Character \(selectionRange.end.character) + Cursor Position: \ + Line \(selectionRange.end.line) \ + Character \(selectionRange.end.character) + \(editorContent) + Line Annotations: + \( + content.editorContent?.lineAnnotations + .map { " - \($0)" } + .joined(separator: "\n") ?? "N/A" + ) + ### + """, retrievedContent: [], functions: [] ) diff --git a/Tool/Sources/XcodeInspector/XcodeInspector.swift b/Tool/Sources/XcodeInspector/XcodeInspector.swift index d3aacc45..d7eaf53a 100644 --- a/Tool/Sources/XcodeInspector/XcodeInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeInspector.swift @@ -20,6 +20,16 @@ public enum XcodeInspectorActor: GlobalActor { public final class XcodeInspector: ObservableObject { public static let shared = XcodeInspector() + + @XcodeInspectorActor + @dynamicMemberLookup + public class Safe { + var inspector: XcodeInspector { .shared } + nonisolated init() {} + public subscript(dynamicMember member: KeyPath) -> T { + inspector[keyPath: member] + } + } private var toast: ToastController { ToastControllerDependencyKey.liveValue } @@ -27,6 +37,9 @@ public final class XcodeInspector: ObservableObject { private var activeXcodeObservations = Set>() private var appChangeObservations = Set>() private var activeXcodeCancellable = Set() + + #warning("TODO: Find a good way to make XcodeInspector thread safe!") + public var safe = Safe() @Published public fileprivate(set) var activeApplication: AppInstanceInspector? @Published public fileprivate(set) var previousActiveApplication: AppInstanceInspector? @@ -45,13 +58,14 @@ public final class XcodeInspector: ObservableObject { /// /// - note: This method is expensive. It needs to convert index based ranges to line based /// ranges. - public func getFocusedEditorContent() -> EditorInformation? { - guard let documentURL = XcodeInspector.shared.realtimeActiveDocumentURL, - let workspaceURL = XcodeInspector.shared.realtimeActiveWorkspaceURL, - let projectURL = XcodeInspector.shared.activeProjectRootURL + @XcodeInspectorActor + public func getFocusedEditorContent() async -> EditorInformation? { + guard let documentURL = realtimeActiveDocumentURL, + let workspaceURL = realtimeActiveWorkspaceURL, + let projectURL = activeProjectRootURL else { return nil } - let editorContent = XcodeInspector.shared.focusedEditor?.getContent() + let editorContent = focusedEditor?.getContent() let language = languageIdentifierFromFileURL(documentURL) let relativePath = documentURL.path.replacingOccurrences(of: projectURL.path, with: "") @@ -98,9 +112,12 @@ public final class XcodeInspector: ObservableObject { init() { AXUIElement.setGlobalMessagingTimeout(3) - restart() + Task { @XcodeInspectorActor in + restart() + } } + @XcodeInspectorActor public func restart(cleanUp: Bool = false) { if cleanUp { activeXcodeObservations.forEach { $0.cancel() } @@ -135,7 +152,7 @@ public final class XcodeInspector: ObservableObject { let appChangeTask = Task(priority: .utility) { [weak self] in guard let self else { return } if let activeXcode { - await setActiveXcode(activeXcode) + setActiveXcode(activeXcode) } await withThrowingTaskGroup(of: Void.self) { [weak self] group in @@ -318,24 +335,24 @@ public final class XcodeInspector: ObservableObject { checkForAccessibilityMalfunction("Reactivate Xcode") } - xcode.$completionPanel.receive(on: DispatchQueue.main).sink { [weak self] element in - self?.completionPanel = element + xcode.$completionPanel.sink { [weak self] element in + Task { @XcodeInspectorActor in self?.completionPanel = element } }.store(in: &activeXcodeCancellable) - xcode.$documentURL.receive(on: DispatchQueue.main).sink { [weak self] url in - self?.activeDocumentURL = url + xcode.$documentURL.sink { [weak self] url in + Task { @XcodeInspectorActor in self?.activeDocumentURL = url } }.store(in: &activeXcodeCancellable) - xcode.$workspaceURL.receive(on: DispatchQueue.main).sink { [weak self] url in - self?.activeWorkspaceURL = url + xcode.$workspaceURL.sink { [weak self] url in + Task { @XcodeInspectorActor in self?.activeWorkspaceURL = url } }.store(in: &activeXcodeCancellable) - xcode.$projectRootURL.receive(on: DispatchQueue.main).sink { [weak self] url in - self?.activeProjectRootURL = url + xcode.$projectRootURL.sink { [weak self] url in + Task { @XcodeInspectorActor in self?.activeProjectRootURL = url } }.store(in: &activeXcodeCancellable) - xcode.$focusedWindow.receive(on: DispatchQueue.main).sink { [weak self] window in - self?.focusedWindow = window + xcode.$focusedWindow.sink { [weak self] window in + Task { @XcodeInspectorActor in self?.focusedWindow = window } }.store(in: &activeXcodeCancellable) } From 9bee878adfa3d033d655c02286acf94cd20de3fd Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 22 Feb 2024 02:00:34 +0800 Subject: [PATCH 27/35] Access XcodeInspector thru actor --- .../ChatPlugin/TerminalChatPlugin.swift | 4 +- Core/Sources/ChatService/ChatService.swift | 18 ++++++--- .../CustomCommandTemplateProcessor.swift | 10 ++--- .../OpenAIPromptToCodeService.swift | 33 ++++++++-------- .../RealtimeSuggestionController.swift | 17 +++++---- .../PseudoCommandHandler.swift | 19 ++++++---- .../WindowBaseCommandHandler.swift | 38 ++++++++++++++----- .../FeatureReducers/PanelFeature.swift | 7 ++-- .../FeatureReducers/WidgetFeature.swift | 5 +-- .../WidgetWindowsController.swift | 4 +- Pro | 2 +- Tool/Sources/AppActivator/AppActivator.swift | 5 ++- .../CodeiumService/CodeiumService.swift | 7 ++-- Tool/Sources/Workspace/WorkspacePool.swift | 2 +- 14 files changed, 102 insertions(+), 69 deletions(-) diff --git a/Core/Sources/ChatPlugin/TerminalChatPlugin.swift b/Core/Sources/ChatPlugin/TerminalChatPlugin.swift index 6e2d9d1e..285b2947 100644 --- a/Core/Sources/ChatPlugin/TerminalChatPlugin.swift +++ b/Core/Sources/ChatPlugin/TerminalChatPlugin.swift @@ -34,8 +34,8 @@ public actor TerminalChatPlugin: ChatPlugin { } do { - let fileURL = XcodeInspector.shared.realtimeActiveDocumentURL - let projectURL = XcodeInspector.shared.realtimeActiveProjectURL + let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL + let projectURL = await XcodeInspector.shared.safe.realtimeActiveProjectURL var environment = [String: String]() if let fileURL { diff --git a/Core/Sources/ChatService/ChatService.swift b/Core/Sources/ChatService/ChatService.swift index 60fe7dfa..e8b7dae0 100644 --- a/Core/Sources/ChatService/ChatService.swift +++ b/Core/Sources/ChatService/ChatService.swift @@ -217,8 +217,14 @@ public final class ChatService: ObservableObject { guard let info else { return } let templateProcessor = CustomCommandTemplateProcessor() - mutateSystemPrompt(info.specifiedSystemPrompt.map(templateProcessor.process)) - mutateExtraSystemPrompt(info.extraSystemPrompt.map(templateProcessor.process) ?? "") + if let specifiedSystemPrompt = info.specifiedSystemPrompt { + await mutateSystemPrompt(templateProcessor.process(specifiedSystemPrompt)) + } + if let extraSystemPrompt = info.extraSystemPrompt { + await mutateExtraSystemPrompt(templateProcessor.process(extraSystemPrompt)) + } else { + mutateExtraSystemPrompt("") + } let customCommandPrefix = { if let name = info.name { return "[\(name)] " } @@ -250,9 +256,9 @@ public final class ChatService: ObservableObject { let templateProcessor = CustomCommandTemplateProcessor() if let systemPrompt { if overwriteSystemPrompt { - mutateSystemPrompt(templateProcessor.process(systemPrompt)) + await mutateSystemPrompt(templateProcessor.process(systemPrompt)) } else { - mutateExtraSystemPrompt(templateProcessor.process(systemPrompt)) + await mutateExtraSystemPrompt(templateProcessor.process(systemPrompt)) } } return try await sendAndWait(content: templateProcessor.process(prompt)) @@ -265,10 +271,10 @@ public final class ChatService: ObservableObject { ) async throws -> String { let templateProcessor = CustomCommandTemplateProcessor() if let systemPrompt { - mutateSystemPrompt(templateProcessor.process(systemPrompt)) + await mutateSystemPrompt(templateProcessor.process(systemPrompt)) } if let extraSystemPrompt { - mutateExtraSystemPrompt(templateProcessor.process(extraSystemPrompt)) + await mutateExtraSystemPrompt(templateProcessor.process(extraSystemPrompt)) } return try await sendAndWait(content: templateProcessor.process(prompt)) } diff --git a/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift b/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift index 0fa3abfe..d9dae12e 100644 --- a/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift +++ b/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift @@ -6,8 +6,8 @@ import XcodeInspector public struct CustomCommandTemplateProcessor { public init() {} - public func process(_ text: String) -> String { - let info = getEditorInformation() + public func process(_ text: String) async -> String { + let info = await getEditorInformation() let editorContent = info.editorContent let updatedText = text .replacingOccurrences(of: "{{selected_code}}", with: """ @@ -38,9 +38,9 @@ public struct CustomCommandTemplateProcessor { let documentURL: URL? } - func getEditorInformation() -> EditorInformation { - let editorContent = XcodeInspector.shared.focusedEditor?.getContent() - let documentURL = XcodeInspector.shared.activeDocumentURL + func getEditorInformation() async -> EditorInformation { + let editorContent = await XcodeInspector.shared.safe.focusedEditor?.getContent() + let documentURL = await XcodeInspector.shared.safe.activeDocumentURL let language = documentURL.map(languageIdentifierFromFileURL) ?? .plaintext return .init( diff --git a/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift b/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift index 237d5bc4..6804ecf8 100644 --- a/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift +++ b/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift @@ -31,22 +31,23 @@ public final class OpenAIPromptToCodeService: PromptToCodeServiceType { return userPreferredLanguage.isEmpty ? "" : " in \(userPreferredLanguage)" }() - let editor: EditorInformation = await XcodeInspector.shared.getFocusedEditorContent() ?? .init( - editorContent: .init( - content: source.content, - lines: source.lines, - selections: [source.range], - cursorPosition: .outOfScope, - lineAnnotations: [] - ), - selectedContent: code, - selectedLines: [], - documentURL: source.documentURL, - workspaceURL: source.projectRootURL, - projectRootURL: source.projectRootURL, - relativePath: "", - language: source.language - ) + let editor: EditorInformation = await XcodeInspector.shared.getFocusedEditorContent() + ?? .init( + editorContent: .init( + content: source.content, + lines: source.lines, + selections: [source.range], + cursorPosition: .outOfScope, + lineAnnotations: [] + ), + selectedContent: code, + selectedLines: [], + documentURL: source.documentURL, + workspaceURL: source.projectRootURL, + projectRootURL: source.projectRootURL, + relativePath: "", + language: source.language + ) let rule: String = { func generateDescription(index: Int) -> String { diff --git a/Core/Sources/Service/RealtimeSuggestionController.swift b/Core/Sources/Service/RealtimeSuggestionController.swift index 41cb0d57..4d07e62d 100644 --- a/Core/Sources/Service/RealtimeSuggestionController.swift +++ b/Core/Sources/Service/RealtimeSuggestionController.swift @@ -45,7 +45,8 @@ public actor RealtimeSuggestionController { 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 } + guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL + else { return } _ = try await Service.shared.workspacePool .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) } @@ -58,7 +59,7 @@ public actor RealtimeSuggestionController { editorObservationTask = nil editorObservationTask = Task { [weak self] in - if let fileURL = XcodeInspector.shared.realtimeActiveDocumentURL { + if let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL { await PseudoCommandHandler().invalidateRealtimeSuggestionsIfNeeded( fileURL: fileURL, sourceEditor: sourceEditor @@ -93,7 +94,8 @@ public actor RealtimeSuggestionController { } group.addTask { let handler = { - guard let fileURL = XcodeInspector.shared.activeDocumentURL else { return } + guard let fileURL = await XcodeInspector.shared.safe.activeDocumentURL + else { return } await PseudoCommandHandler().invalidateRealtimeSuggestionsIfNeeded( fileURL: fileURL, sourceEditor: sourceEditor @@ -119,7 +121,8 @@ public actor RealtimeSuggestionController { Task { @WorkspaceActor in // Get cache ready for real-time suggestions. guard UserDefaults.shared.value(for: \.preCacheOnFileOpen) else { return } - guard let fileURL = XcodeInspector.shared.realtimeActiveDocumentURL else { return } + guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL + else { return } let (_, filespace) = try await Service.shared.workspacePool .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) @@ -128,7 +131,7 @@ public actor RealtimeSuggestionController { // avoid the command get called twice filespace.codeMetadata.uti = "" do { - try await XcodeInspector.shared.latestActiveXcode? + try await XcodeInspector.shared.safe.latestActiveXcode? .triggerCopilotCommand(name: "Real-time Suggestions") } catch { if filespace.codeMetadata.uti?.isEmpty ?? true { @@ -152,7 +155,7 @@ public actor RealtimeSuggestionController { else { return } if UserDefaults.shared.value(for: \.disableSuggestionFeatureGlobally), - let fileURL = XcodeInspector.shared.activeDocumentURL, + let fileURL = await XcodeInspector.shared.safe.activeDocumentURL, let (workspace, _) = try? await Service.shared.workspacePool .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) { @@ -189,7 +192,7 @@ public actor RealtimeSuggestionController { } func notifyEditingFileChange(editor: AXUIElement) async { - guard let fileURL = XcodeInspector.shared.activeDocumentURL, + guard let fileURL = await XcodeInspector.shared.safe.activeDocumentURL, let (workspace, _) = try? await Service.shared.workspacePool .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) else { return } diff --git a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift index b553812b..d6de7239 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift @@ -159,7 +159,7 @@ struct PseudoCommandHandler { } }() else { do { - try await XcodeInspector.shared.latestActiveXcode? + try await XcodeInspector.shared.safe.latestActiveXcode? .triggerCopilotCommand(name: command.name) } catch { let presenter = PresentInWindowSuggestionPresenter() @@ -183,7 +183,7 @@ struct PseudoCommandHandler { throw CancellationError() } do { - try await XcodeInspector.shared.latestActiveXcode? + try await XcodeInspector.shared.safe.latestActiveXcode? .triggerCopilotCommand(name: "Accept Prompt to Code") } catch { let last = Self.lastTimeCommandFailedToTriggerWithAccessibilityAPI @@ -238,7 +238,7 @@ struct PseudoCommandHandler { throw CancellationError() } do { - try await XcodeInspector.shared.latestActiveXcode? + try await XcodeInspector.shared.safe.latestActiveXcode? .triggerCopilotCommand(name: "Accept Suggestion") } catch { let last = Self.lastTimeCommandFailedToTriggerWithAccessibilityAPI @@ -288,7 +288,7 @@ struct PseudoCommandHandler { } func dismissSuggestion() async { - guard let documentURL = XcodeInspector.shared.activeDocumentURL else { return } + guard let documentURL = await XcodeInspector.shared.safe.activeDocumentURL else { return } guard let (_, filespace) = try? await Service.shared.workspacePool .fetchOrCreateWorkspaceAndFilespace(fileURL: documentURL) else { return } @@ -377,14 +377,14 @@ extension PseudoCommandHandler { return (content, split, [range], range.start) } - func getFileURL() -> URL? { - XcodeInspector.shared.realtimeActiveDocumentURL + func getFileURL() async -> URL? { + await XcodeInspector.shared.safe.realtimeActiveDocumentURL } @WorkspaceActor func getFilespace() async -> Filespace? { guard - let fileURL = getFileURL(), + let fileURL = await getFileURL(), let (_, filespace) = try? await Service.shared.workspacePool .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) else { return nil } @@ -394,7 +394,10 @@ extension PseudoCommandHandler { @WorkspaceActor func getEditorContent(sourceEditor: SourceEditor?) async -> EditorContent? { guard let filespace = await getFilespace(), - let sourceEditor = sourceEditor ?? XcodeInspector.shared.focusedEditor + let sourceEditor = await { + if let sourceEditor { sourceEditor } + else { await XcodeInspector.shared.safe.focusedEditor } + }() else { return nil } if Task.isCancelled { return nil } let content = sourceEditor.getContent() diff --git a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift index 6e7abe7f..2bf83cfe 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift @@ -39,7 +39,8 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler { defer { presenter.markAsProcessing(false) } - guard let fileURL = XcodeInspector.shared.realtimeActiveDocumentURL else { return } + guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL + else { return } let (workspace, filespace) = try await Service.shared.workspacePool .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) @@ -80,7 +81,8 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler { @WorkspaceActor private func _presentNextSuggestion(editor: EditorContent) async throws { - guard let fileURL = XcodeInspector.shared.realtimeActiveDocumentURL else { return } + guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL + else { return } let (workspace, filespace) = try await Service.shared.workspacePool .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) workspace.selectNextSuggestion(forFileAt: fileURL) @@ -105,7 +107,8 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler { @WorkspaceActor private func _presentPreviousSuggestion(editor: EditorContent) async throws { - guard let fileURL = XcodeInspector.shared.realtimeActiveDocumentURL else { return } + guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL + else { return } let (workspace, filespace) = try await Service.shared.workspacePool .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) workspace.selectPreviousSuggestion(forFileAt: fileURL) @@ -130,7 +133,8 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler { @WorkspaceActor private func _rejectSuggestion(editor: EditorContent) async throws { - guard let fileURL = XcodeInspector.shared.realtimeActiveDocumentURL else { return } + guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL + else { return } let (workspace, _) = try await Service.shared.workspacePool .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) @@ -140,7 +144,8 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler { @WorkspaceActor func acceptSuggestion(editor: EditorContent) async throws -> UpdatedContent? { - guard let fileURL = XcodeInspector.shared.realtimeActiveDocumentURL else { return nil } + guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL + else { return nil } let (workspace, _) = try await Service.shared.workspacePool .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) @@ -173,7 +178,8 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler { } func acceptPromptToCode(editor: EditorContent) async throws -> UpdatedContent? { - guard let fileURL = XcodeInspector.shared.realtimeActiveDocumentURL else { return nil } + guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL + else { return nil } let injector = SuggestionInjector() var lines = editor.lines @@ -248,7 +254,8 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler { @WorkspaceActor func prepareCache(editor: EditorContent) async throws -> UpdatedContent? { - guard let fileURL = XcodeInspector.shared.realtimeActiveDocumentURL else { return nil } + guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL + else { return nil } let (_, filespace) = try await Service.shared.workspacePool .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) filespace.codeMetadata.uti = editor.uti @@ -351,7 +358,8 @@ extension WindowBaseCommandHandler { generateDescription: Bool?, name: String? ) async throws { - guard let fileURL = XcodeInspector.shared.realtimeActiveDocumentURL else { return } + guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL + else { return } let (workspace, filespace) = try await Service.shared.workspacePool .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) guard workspace.suggestionPlugin?.isSuggestionFeatureEnabled ?? false else { @@ -398,8 +406,18 @@ extension WindowBaseCommandHandler { let viewStore = Service.shared.guiController.viewStore let customCommandTemplateProcessor = CustomCommandTemplateProcessor() - let newExtraSystemPrompt = extraSystemPrompt.map(customCommandTemplateProcessor.process) - let newPrompt = prompt.map(customCommandTemplateProcessor.process) + + let newExtraSystemPrompt: String? = if let extraSystemPrompt { + await customCommandTemplateProcessor.process(extraSystemPrompt) + } else { + nil + } + + let newPrompt: String? = if let prompt { + await customCommandTemplateProcessor.process(prompt) + } else { + nil + } _ = await Task { @MainActor in // if there is already a prompt to code presenting, we should not present another one diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift index 77750517..48d92447 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift @@ -53,7 +53,7 @@ public struct PanelFeature: ReducerProtocol { switch action { case .presentSuggestion: return .run { send in - guard let fileURL = xcodeInspector.activeDocumentURL, + guard let fileURL = await xcodeInspector.safe.activeDocumentURL, let provider = await fetchSuggestionProvider(fileURL: fileURL) else { return } await send(.presentSuggestionProvider(provider, displayContent: true)) @@ -98,8 +98,9 @@ public struct PanelFeature: ReducerProtocol { state.content.error = nil state.content.suggestion = nil return .run { send in - guard let fileURL = xcodeInspector.realtimeActiveDocumentURL else { return } - + guard let fileURL = await xcodeInspector.safe.realtimeActiveDocumentURL + else { return } + await send(.sharedPanel( .promptToCodeGroup( .updateActivePromptToCode(documentURL: fileURL) diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift index 14adee62..2cff1d83 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift @@ -293,12 +293,11 @@ public struct WidgetFeature: ReducerProtocol { }.cancellable(id: CancelID.observeUserDefaults, cancelInFlight: true) case .updateActiveApplication: - if let app = xcodeInspector.activeApplication, app.isXcode { - return .run { send in + return .run { send in + if let app = await xcodeInspector.safe.activeApplication, app.isXcode { await send(.panel(.switchToAnotherEditorAndUpdateContent)) } } - return .none case .updateColorScheme: let widgetColorScheme = UserDefaults.shared.value(for: \.widgetColorScheme) diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift index cd49a183..afb38964 100644 --- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift +++ b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift @@ -91,7 +91,7 @@ actor WidgetWindowsController: NSObject { } try Task.checkCancellation() let xcodeInspector = self.xcodeInspector - let activeApp = xcodeInspector.activeApplication + let activeApp = await xcodeInspector.safe.activeApplication await MainActor.run { let state = store.withState { $0 } let isChatPanelDetached = state.chatPanelState.chatPanelInASeparateWindow @@ -286,7 +286,7 @@ private extension WidgetWindowsController { /// Hide the widgets before switching to another window/editor /// so the transition looks better. func hideWidgetForTransitions() async { - let newDocumentURL = xcodeInspector.realtimeActiveDocumentURL + let newDocumentURL = await xcodeInspector.safe.realtimeActiveDocumentURL if documentURL != newDocumentURL { await send(.panel(.removeDisplayedContent)) await hidePanelWindows() diff --git a/Pro b/Pro index 6bc4b4c2..0746e5db 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 6bc4b4c253b9bc52ae95615ac18a1dae7e0caa43 +Subproject commit 0746e5db88d47366246eb7ebf5529562166dbe8e diff --git a/Tool/Sources/AppActivator/AppActivator.swift b/Tool/Sources/AppActivator/AppActivator.swift index 1523e01a..b50f3bf4 100644 --- a/Tool/Sources/AppActivator/AppActivator.swift +++ b/Tool/Sources/AppActivator/AppActivator.swift @@ -29,7 +29,8 @@ public extension NSWorkspace { static func activatePreviousActiveApp(delay: TimeInterval = 0.2) { Task { @MainActor in - guard let app = XcodeInspector.shared.previousActiveApplication else { return } + guard let app = await XcodeInspector.shared.safe.previousActiveApplication + else { return } try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) _ = app.activate() } @@ -37,7 +38,7 @@ public extension NSWorkspace { static func activatePreviousActiveXcode(delay: TimeInterval = 0.2) { Task { @MainActor in - guard let app = XcodeInspector.shared.latestActiveXcode else { return } + guard let app = await XcodeInspector.shared.safe.latestActiveXcode else { return } try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) _ = app.activate() } diff --git a/Tool/Sources/CodeiumService/CodeiumService.swift b/Tool/Sources/CodeiumService/CodeiumService.swift index 0bc1116b..7448e102 100644 --- a/Tool/Sources/CodeiumService/CodeiumService.swift +++ b/Tool/Sources/CodeiumService/CodeiumService.swift @@ -95,7 +95,7 @@ public class CodeiumSuggestionService { throw CodeiumError.languageServerOutdated } - let metadata = try getMetadata() + let metadata = try await getMetadata() let tempFolderURL = FileManager.default.temporaryDirectory let managerDirectoryURL = tempFolderURL .appendingPathComponent("com.intii.CopilotForXcode") @@ -179,14 +179,15 @@ public class CodeiumSuggestionService { } extension CodeiumSuggestionService { - func getMetadata() throws -> Metadata { + func getMetadata() async throws -> Metadata { guard let key = authService.key else { struct E: Error, LocalizedError { var errorDescription: String? { "Codeium not signed in." } } throw E() } - var ideVersion = XcodeInspector.shared.latestActiveXcode?.version ?? fallbackXcodeVersion + var ideVersion = await XcodeInspector.shared.safe.latestActiveXcode?.version + ?? fallbackXcodeVersion let versionNumberSegmentCount = ideVersion.split(separator: ".").count if versionNumberSegmentCount == 2 { ideVersion += ".0" diff --git a/Tool/Sources/Workspace/WorkspacePool.swift b/Tool/Sources/Workspace/WorkspacePool.swift index a5891e91..7798ad6e 100644 --- a/Tool/Sources/Workspace/WorkspacePool.swift +++ b/Tool/Sources/Workspace/WorkspacePool.swift @@ -94,7 +94,7 @@ public class WorkspacePool { } // If we can get the workspace URL directly. - if let currentWorkspaceURL = XcodeInspector.shared.realtimeActiveWorkspaceURL { + if let currentWorkspaceURL = await XcodeInspector.shared.safe.realtimeActiveWorkspaceURL { if let existed = workspaces[currentWorkspaceURL] { // Reuse the existed workspace. let filespace = existed.createFilespaceIfNeeded(fileURL: fileURL) From b1dddcc207247309a68f783934bbb737e5c8d697 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 22 Feb 2024 12:15:29 +0800 Subject: [PATCH 28/35] Update --- Pro | 2 +- Tool/Sources/SuggestionProvider/SuggestionProvider.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Pro b/Pro index 0746e5db..e1d60802 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 0746e5db88d47366246eb7ebf5529562166dbe8e +Subproject commit e1d608020cdf80304efad71577b8f2e5916c1206 diff --git a/Tool/Sources/SuggestionProvider/SuggestionProvider.swift b/Tool/Sources/SuggestionProvider/SuggestionProvider.swift index 44d51801..f1af1acd 100644 --- a/Tool/Sources/SuggestionProvider/SuggestionProvider.swift +++ b/Tool/Sources/SuggestionProvider/SuggestionProvider.swift @@ -1,4 +1,4 @@ -import AppKit +import AppKit import Foundation import Preferences import SuggestionModel @@ -13,7 +13,7 @@ public struct SuggestionRequest { public var tabSize: Int public var indentSize: Int public var usesTabsForIndentation: Bool - public var ignoreSpaceOnlySuggestions: Bool + public var ignoreSpaceOnlySuggestions: Bool public init( fileURL: URL, From a03221f96c294d2844f695bdc15435eafde9ae06 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 22 Feb 2024 12:15:43 +0800 Subject: [PATCH 29/35] Support opening ExtensionManager from menu item --- ExtensionService/AppDelegate+Menu.swift | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/ExtensionService/AppDelegate+Menu.swift b/ExtensionService/AppDelegate+Menu.swift index b77609f5..e655522d 100644 --- a/ExtensionService/AppDelegate+Menu.swift +++ b/ExtensionService/AppDelegate+Menu.swift @@ -46,6 +46,12 @@ extension AppDelegate { keyEquivalent: "" ) + let openExtensionManager = NSMenuItem( + title: "Open Extension Manager", + action: #selector(openExtensionManager), + keyEquivalent: "" + ) + let openCopilotForXcode = NSMenuItem( title: "Open \(hostAppName)", action: #selector(openCopilotForXcode), @@ -82,18 +88,19 @@ extension AppDelegate { keyEquivalent: "" ) quitItem.target = self - + let reactivateObservationsItem = NSMenuItem( title: "Reactivate Observations to Xcode", action: #selector(reactivateObservationsToXcode), keyEquivalent: "" ) - + reactivateObservationsItem.target = self statusBarMenu.addItem(copilotName) statusBarMenu.addItem(openCopilotForXcode) statusBarMenu.addItem(checkForUpdate) + statusBarMenu.addItem(openExtensionManager) statusBarMenu.addItem(.separator()) statusBarMenu.addItem(openGlobalChat) statusBarMenu.addItem(.separator()) @@ -216,16 +223,28 @@ extension AppDelegate: NSMenuDelegate { } } +import XPCShared + private extension AppDelegate { @objc func restartXcodeInspector() { Task { await XcodeInspector.shared.restart(cleanUp: true) } } - + @objc func reactivateObservationsToXcode() { XcodeInspector.shared.reactivateObservationsToXcode() } + + @objc func openExtensionManager() { + guard let data = try? JSONEncoder().encode(ExtensionServiceRequests.OpenExtensionManager()) + else { return } + service.handleXPCServiceRequests( + endpoint: ExtensionServiceRequests.OpenExtensionManager.endpoint, + requestBody: data, + reply: { _, _ in } + ) + } } private extension NSMenuItem { From a82942db808bc45c948dd6b2a8e7c4b08fbb12a4 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 22 Feb 2024 12:17:23 +0800 Subject: [PATCH 30/35] Update build number --- Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Version.xcconfig b/Version.xcconfig index 6138d7da..a4dd52c7 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,3 +1,3 @@ APP_VERSION = 0.30.5 -APP_BUILD = 326 +APP_BUILD = 328 From 9c7e86a1deef2631f9296fee92d57ff425e2498d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 22 Feb 2024 15:43:29 +0800 Subject: [PATCH 31/35] Access XcodeInspector state through actor --- .../FeatureReducers/WidgetFeature.swift | 14 ++++++++++++-- .../SuggestionWidget/WidgetWindowsController.swift | 8 ++++---- Pro | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift index 2cff1d83..ba4a81bc 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift @@ -100,6 +100,7 @@ public struct WidgetFeature: ReducerProtocol { case updatePanelStateToMatch(WidgetLocation) case updateFocusingDocumentURL + case setFocusingDocumentURL(to: URL?) case updateKeyWindow(WindowCanBecomeKey) case toastPanel(ToastPanel.Action) @@ -257,7 +258,8 @@ public struct WidgetFeature: ReducerProtocol { .notifications(named: NSWorkspace.activeSpaceDidChangeNotification) for await _ in sequence { try Task.checkCancellation() - guard let activeXcode = xcodeInspector.activeXcode else { continue } + guard let activeXcode = await xcodeInspector.safe.activeXcode + else { continue } guard let windowsController, await windowsController.windows.fullscreenDetector.isOnActiveSpace else { continue } @@ -324,7 +326,15 @@ public struct WidgetFeature: ReducerProtocol { return .none case .updateFocusingDocumentURL: - state.focusingDocumentURL = xcodeInspector.realtimeActiveDocumentURL + return .run { send in + await send(.setFocusingDocumentURL( + to: await xcodeInspector.safe + .realtimeActiveDocumentURL + )) + } + + case let .setFocusingDocumentURL(url): + state.focusingDocumentURL = url return .none case let .updatePanelStateToMatch(widgetLocation): diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift index afb38964..41a40820 100644 --- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift +++ b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift @@ -92,6 +92,7 @@ actor WidgetWindowsController: NSObject { try Task.checkCancellation() let xcodeInspector = self.xcodeInspector let activeApp = await xcodeInspector.safe.activeApplication + let latestActiveXcode = await xcodeInspector.safe.latestActiveXcode await MainActor.run { let state = store.withState { $0 } let isChatPanelDetached = state.chatPanelState.chatPanelInASeparateWindow @@ -113,8 +114,7 @@ actor WidgetWindowsController: NSObject { } } else if let activeApp, activeApp.isExtensionService { let noFocus = { - guard let xcode = xcodeInspector.latestActiveXcode - else { return true } + guard let xcode = latestActiveXcode else { return true } if let window = xcode.appElement.focusedWindow, window.role == "AXWindow" { @@ -341,7 +341,7 @@ private extension WidgetWindowsController { selectionRangeChange.debounce(for: Duration.milliseconds(500)), scroll ) { - guard xcodeInspector.latestActiveXcode != nil else { return } + guard await xcodeInspector.safe.latestActiveXcode != nil else { return } try Task.checkCancellation() // for better looking @@ -354,7 +354,7 @@ private extension WidgetWindowsController { } } else { for await notification in merge(selectionRangeChange, scroll) { - guard xcodeInspector.latestActiveXcode != nil else { return } + guard await xcodeInspector.safe.latestActiveXcode != nil else { return } try Task.checkCancellation() // for better looking diff --git a/Pro b/Pro index e1d60802..9a3fd882 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit e1d608020cdf80304efad71577b8f2e5916c1206 +Subproject commit 9a3fd882dd70ac6842fb6b552c6ed4a42cc7d9c0 From e864847026b8c8900d9525f3cd923546b126479e Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 22 Feb 2024 16:51:53 +0800 Subject: [PATCH 32/35] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 9a3fd882..908dd291 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 9a3fd882dd70ac6842fb6b552c6ed4a42cc7d9c0 +Subproject commit 908dd2919e3da89cb05e6d57cad8228c2df08846 From d586dad1931a4361e309e63f5330111c4f02f044 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 22 Feb 2024 17:07:06 +0800 Subject: [PATCH 33/35] Update appcast.xml --- appcast.xml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/appcast.xml b/appcast.xml index e5c8cdce..f3b44987 100644 --- a/appcast.xml +++ b/appcast.xml @@ -2,7 +2,19 @@ Copilot for Xcode - + + + 0.30.5 + Thu, 22 Feb 2024 17:05:45 +0800 + 328 + 0.30.5 + 12.0 + + https://github.com/intitni/CopilotForXcode/releases/tag/0.30.5 + + + + 0.30.5 Wed, 21 Feb 2024 23:18:59 +0800 From f8394dc2a0347e7a427e9e5311be995da005d3e2 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 22 Feb 2024 22:22:46 +0800 Subject: [PATCH 34/35] Update README.md --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 598f68f1..7ea866ea 100644 --- a/README.md +++ b/README.md @@ -52,11 +52,12 @@ For suggestion features: - Active GitHub Copilot subscription. - For Codeium users: - Active Codeium account. +- Access to other LLMs. For chat and prompt to code features: - A valid OpenAI API key. -- Access to other LLMs, such as Azure OpenAI and LocalAI. +- Access to other LLMs. ## Permissions Required @@ -165,6 +166,15 @@ The installed language server is located at `~/Library/Application Support/com.i The installed language server is located at `~/Library/Application Support/com.intii.CopilotForXcode/Codeium/executable/`. +#### Using Locally run LLMs + +You can also use locally run LLMs or as a suggestion provider using the [Custom Suggestion Service](https://github.com/intitni/CustomSuggestionServiceForCopilotForXcode) extension. It supports: + +- LLM with OpenAI compatible completions API +- LLM with OpenAI compatible chat completions API +- [Tabby](https://tabby.tabbyml.com) +- etc. + ### Setting Up Chat Feature 1. In the host app, navigate to "Service - Chat Model". From 38faffe822694c0d5394eff520a829bb13641350 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 22 Feb 2024 22:23:56 +0800 Subject: [PATCH 35/35] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ea866ea..cbff5a77 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Copilot for Xcode is an Xcode Source Editor Extension that provides GitHub Copil - [Setting Up Suggestion Feature](#setting-up-suggestion-feature) - [Setting Up GitHub Copilot](#setting-up-github-copilot) - [Setting Up Codeium](#setting-up-codeium) + - [Using Locally Run LLMs](#using-locally-run-llms) - [Setting Up Chat Feature](#setting-up-chat-feature) - [Managing `CopilotForXcodeExtensionService.app`](#managing-copilotforxcodeextensionserviceapp) - [Update](#update) @@ -166,7 +167,7 @@ The installed language server is located at `~/Library/Application Support/com.i The installed language server is located at `~/Library/Application Support/com.intii.CopilotForXcode/Codeium/executable/`. -#### Using Locally run LLMs +#### Using Locally Run LLMs You can also use locally run LLMs or as a suggestion provider using the [Custom Suggestion Service](https://github.com/intitni/CustomSuggestionServiceForCopilotForXcode) extension. It supports: