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/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/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/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/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift index d3292e2b..90a8bdf5 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) @@ -157,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" ) @@ -165,7 +166,7 @@ struct GitHubCopilotView: View { .foregroundColor(.secondary) .font(.callout) .dynamicHeightTextInFormWorkaround() - + Picker(selection: $settings.runNodeWith) { ForEach(NodeRunner.allCases, id: \.rawValue) { runner in switch runner { @@ -180,7 +181,7 @@ struct GitHubCopilotView: View { } label: { Text("Run Node with") } - + Group { switch settings.runNodeWith { case .env: @@ -257,6 +258,10 @@ struct GitHubCopilotView: View { } .opacity(isRunningAction ? 0.8 : 1) .disabled(isRunningAction) + + Button("Refresh Configuration for Enterprise and Proxy") { + refreshConfiguration() + } } SettingsDivider("Advanced") @@ -276,6 +281,17 @@ struct GitHubCopilotView: View { 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") Form { @@ -403,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/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift b/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift index d2bcea30..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 = 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 c6fa353b..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,15 +59,16 @@ 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 ) } - 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 @@ -92,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 @@ -118,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) @@ -127,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 { @@ -141,7 +145,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 )) @@ -151,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) { @@ -188,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/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/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift index 5208bf3b..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)) @@ -96,11 +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)) - } + guard let fileURL = await xcodeInspector.safe.realtimeActiveDocumentURL + else { return } await send(.sharedPanel( .promptToCodeGroup( diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift index cbb1979f..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,13 +258,12 @@ 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 } - let app = AXUIElementCreateApplication( - activeXcode.processIdentifier - ) + let app = activeXcode.appElement if let _ = app.focusedWindow { await windowsController.windows.orderFront() } @@ -295,12 +295,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) @@ -327,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 f294b545..41a40820 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) @@ -85,16 +79,10 @@ 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) + !(Date().timeIntervalSince(lastUpdateWindowOpacityTime) > 3) lastUpdateWindowOpacityTime = Date() - let activeApp = xcodeInspector.activeApplication - updateWindowOpacityTask?.cancel() let task = Task { @@ -103,11 +91,15 @@ 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 + let hasChat = !state.chatPanelState.chatTabGroup.tabInfo.isEmpty + 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 @@ -122,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" { @@ -162,7 +153,7 @@ actor WidgetWindowsController: NSObject { immediately: Bool, function: StaticString = #function, line: UInt = #line - ) async { + ) { @Sendable @MainActor func update() async { let state = store.withState { $0 } @@ -207,7 +198,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 @@ -268,11 +259,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 } @@ -288,13 +280,13 @@ 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 /// 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() @@ -302,32 +294,34 @@ 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)) + 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 { case .focusedWindowChanged, .focusedUIElementChanged: await hideWidgetForTransitions() - await updateWidgetsAndNotifyChangeOfEditor() - case .applicationActivated, .mainWindowChanged: - await updateWidgetsAndNotifyChangeOfEditor() - case .applicationDeactivated, - .moved, + await updateWidgetsAndNotifyChangeOfEditor(immediately: true) + case .applicationActivated: + await updateWidgetsAndNotifyChangeOfEditor(immediately: false) + case .mainWindowChanged: + await updateWidgetsAndNotifyChangeOfEditor(immediately: false) + case .moved, .resized, .windowMoved, .windowResized, .windowMiniaturized, .windowDeminiaturized: await updateWidgets() - case .created, .uiElementDestroyed, .xcodeCompletionPanelChanged: + case .created, .uiElementDestroyed, .xcodeCompletionPanelChanged, + .applicationDeactivated: continue } } @@ -337,9 +331,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, *) { @@ -347,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 @@ -355,12 +349,12 @@ 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) { - guard xcodeInspector.latestActiveXcode != nil else { return } + guard await xcodeInspector.safe.latestActiveXcode != nil else { return } try Task.checkCancellation() // for better looking @@ -368,12 +362,28 @@ private extension WidgetWindowsController { await hideSuggestionPanelWindow() } - await updateWindowLocation(animated: false, immediately: false) - await updateWindowOpacity(immediately: false) + updateWindowLocation(animated: false, immediately: false) + updateWindowOpacity(immediately: false) } } } } + + 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) + } + + updateWindowLocation(animated: false, immediately: false) + updateWindowOpacity(immediately: false) + } + } } extension WidgetWindowsController { diff --git a/ExtensionService/AppDelegate+Menu.swift b/ExtensionService/AppDelegate+Menu.swift index 6ccd371a..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,14 +223,28 @@ extension AppDelegate: NSMenuDelegate { } } +import XPCShared + private extension AppDelegate { @objc func restartXcodeInspector() { - XcodeInspector.shared.restart(cleanUp: true) + 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 { diff --git a/Pro b/Pro index b53b4424..908dd291 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit b53b44249d4eea82f09089753dfeca6116ad5a44 +Subproject commit 908dd2919e3da89cb05e6d57cad8228c2df08846 diff --git a/README.md b/README.md index 598f68f1..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) @@ -52,11 +53,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 +167,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". 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/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)" } 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/AppActivator/AppActivator.swift b/Tool/Sources/AppActivator/AppActivator.swift index 0afbefd5..b50f3bf4 100644 --- a/Tool/Sources/AppActivator/AppActivator.swift +++ b/Tool/Sources/AppActivator/AppActivator.swift @@ -29,17 +29,18 @@ 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() + _ = app.activate() } } 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() + _ = app.activate() } } } diff --git a/Tool/Sources/AsyncPassthroughSubject/AsyncPassthroughSubject.swift b/Tool/Sources/AsyncPassthroughSubject/AsyncPassthroughSubject.swift new file mode 100644 index 00000000..94d033d7 --- /dev/null +++ b/Tool/Sources/AsyncPassthroughSubject/AsyncPassthroughSubject.swift @@ -0,0 +1,54 @@ +import AppKit +import Foundation + +public actor AsyncPassthroughSubject { + var tasks: [AsyncStream.Continuation] = [] + + deinit { + tasks.forEach { $0.finish() } + } + + public init() {} + + public func notifications() -> AsyncStream { + AsyncStream { [weak self] continuation in + let task = Task { [weak self] in + await self?.storeContinuation(continuation) + } + + continuation.onTermination = { termination in + task.cancel() + } + } + } + + nonisolated + public func send(_ element: Element) { + Task { await _send(element) } + } + + func _send(_ element: Element) { + let tasks = tasks + for task in tasks { + task.yield(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/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/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/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) } 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() {} diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift index 0b090450..d9210485 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 {} @@ -81,22 +80,58 @@ enum GitHubCopilotRequest { } } - var request: ClientRequest { - if let networkProxy { - return .custom("setEditorInfo", .hash([ - "editorInfo": .hash([ - "name": "Xcode", - "version": "", - ]), - "editorPluginInfo": .hash([ - "name": "Copilot for Xcode", - "version": "", - ]), - "networkProxy": networkProxy, - ])) + var http: JSONValue? { + var dict: [String: JSONValue] = [:] + 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)" } - return .custom("setEditorInfo", .hash([ + 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), + ]) + } + + if dict.isEmpty { return nil } + return .hash(dict) + } + + var authProvider: JSONValue? { + var dict: [String: JSONValue] = [:] + let enterpriseURI = UserDefaults.shared.value(for: \.gitHubCopilotEnterpriseURI) + if !enterpriseURI.isEmpty { + dict["url"] = .string(enterpriseURI) + } + + if dict.isEmpty { return nil } + return .hash(dict) + } + + var request: ClientRequest { + var dict: [String: JSONValue] = [ "editorInfo": .hash([ "name": "Xcode", "version": "", @@ -105,7 +140,13 @@ enum GitHubCopilotRequest { "name": "Copilot for Xcode", "version": "", ]), - ])) + ] + + dict["editorConfiguration"] = editorConfiguration + dict["authProvider"] = authProvider + dict["networkProxy"] = networkProxy + + return .custom("setEditorInfo", .hash(dict)) } } 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()) + } } } 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/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 68137129..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") @@ -398,9 +402,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. diff --git a/Tool/Sources/SuggestionProvider/SuggestionProvider.swift b/Tool/Sources/SuggestionProvider/SuggestionProvider.swift index 7c592555..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 @@ -6,16 +6,20 @@ 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 public var usesTabsForIndentation: Bool - public var ignoreSpaceOnlySuggestions: Bool + public var ignoreSpaceOnlySuggestions: Bool 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/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) 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, 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 ac05f68b..c16b09a0 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() } } @@ -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 dd2e8621..2ccecb48 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 @@ -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 07469800..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: "") @@ -97,9 +111,13 @@ public final class XcodeInspector: ObservableObject { } init() { - restart() + AXUIElement.setGlobalMessagingTimeout(3) + Task { @XcodeInspectorActor in + restart() + } } + @XcodeInspectorActor public func restart(cleanUp: Bool = false) { if cleanUp { activeXcodeObservations.forEach { $0.cancel() } @@ -134,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 @@ -221,7 +239,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() @@ -286,7 +304,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 +319,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 { @@ -317,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) } @@ -346,7 +364,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)" ) @@ -354,7 +372,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)" ) diff --git a/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift b/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift index 92a34e97..5878a27a 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 @@ -10,6 +10,7 @@ public class XcodeWindowInspector: ObservableObject { init(uiElement: AXUIElement) { self.uiElement = uiElement + uiElement.setMessagingTimeout(2) } } @@ -19,11 +20,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 +31,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 +46,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() diff --git a/Version.xcconfig b/Version.xcconfig index 7f0f9ca8..a4dd52c7 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 = 328 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