diff --git a/Core/Package.swift b/Core/Package.swift index e3f59806..10d744d4 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -38,8 +38,8 @@ let isProIncluded: Bool = { return false } do { - if let content = String( - data: try Data(contentsOf: confURL), + if let content = try String( + data: Data(contentsOf: confURL), encoding: .utf8 ) { if content.hasPrefix("YES") { @@ -98,6 +98,9 @@ let package = Package( url: "https://github.com/pointfreeco/swift-composable-architecture", from: "0.55.0" ), + // quick hack to support custom UserDefaults + // https://github.com/sindresorhus/KeyboardShortcuts + .package(url: "https://github.com/intitni/KeyboardShortcuts", branch: "main"), ].pro, targets: [ // MARK: - Main @@ -134,6 +137,7 @@ let package = Package( .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "Dependencies", package: "swift-dependencies"), + .product(name: "KeyboardShortcuts", package: "KeyboardShortcuts"), ].pro([ "ProService", ]) @@ -168,6 +172,7 @@ let package = Package( .product(name: "OpenAIService", package: "Tool"), .product(name: "Preferences", package: "Tool"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + .product(name: "KeyboardShortcuts", package: "KeyboardShortcuts"), ].pro([ "ProHostApp", ]) diff --git a/Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift b/Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift index 795d14e0..d5563b51 100644 --- a/Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift +++ b/Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift @@ -19,10 +19,12 @@ public final class SystemInfoChatContextCollector: ChatContextCollector { ) -> ChatContext { return .init( systemPrompt: """ - Current Time: \( - Self.dateFormatter.string(from: Date()) - ) (You can use it to calculate time in another time zone) - """, + ## System Info + + Current Time: \( + Self.dateFormatter.string(from: Date()) + ) (You can use it to calculate time in another time zone) + """, retrievedContent: [], functions: [] ) diff --git a/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift b/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift index 3841089f..17babd6d 100644 --- a/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift +++ b/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift @@ -46,6 +46,16 @@ public class ChatGPTChatTab: ChatTab { ChatTabItemView(chat: chat) } + public func buildIcon() -> any View { + WithViewStore(chat, observe: \.isReceivingMessage) { viewStore in + if viewStore.state { + Image(systemName: "ellipsis.message") + } else { + Image(systemName: "message") + } + } + } + public func buildMenu() -> any View { ChatContextMenu(store: chat.scope(state: \.chatMenu, action: Chat.Action.chatMenu)) } @@ -125,3 +135,4 @@ public class ChatGPTChatTab: ChatTab { }.store(in: &cancellable) } } + diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index c7a9d482..2b28793c 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -133,6 +133,7 @@ struct ChatPanelMessages: View { .foregroundStyle(.secondary) .padding(4) } + .keyboardShortcut(.downArrow, modifiers: [.command]) .opacity(pinnedToBottom ? 0 : 1) .buttonStyle(.plain) .onChange(of: viewStore.state) { _ in diff --git a/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift b/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift index e2f565d7..01c592f4 100644 --- a/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift +++ b/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift @@ -1,9 +1,12 @@ +import AppKit import Foundation import SuggestionModel import XcodeInspector -struct CustomCommandTemplateProcessor { - func process(_ text: String) -> String { +public struct CustomCommandTemplateProcessor { + public init() {} + + public func process(_ text: String) -> String { let info = getEditorInformation() let editorContent = info.editorContent let updatedText = text @@ -22,6 +25,10 @@ struct CustomCommandTemplateProcessor { of: "{{active_editor_file_name}}", with: info.documentURL?.lastPathComponent ?? "" ) + .replacingOccurrences( + of: "{{clipboard}}", + with: NSPasteboard.general.string(forType: .string) ?? "" + ) return updatedText } diff --git a/Core/Sources/ChatService/DynamicContextController.swift b/Core/Sources/ChatService/DynamicContextController.swift index 17c876b8..4757fbec 100644 --- a/Core/Sources/ChatService/DynamicContextController.swift +++ b/Core/Sources/ChatService/DynamicContextController.swift @@ -1,7 +1,6 @@ import ChatContextCollector import Foundation import OpenAIService -import Parsing import Preferences import XcodeInspector @@ -65,17 +64,17 @@ final class DynamicContextController { } return contexts } - + let extraSystemPrompt = contexts .map(\.systemPrompt) .filter { !$0.isEmpty } - .joined(separator: "\n") - + .joined(separator: "\n\n") + let contextPrompts = contexts .flatMap(\.retrievedContent) .filter { !$0.content.isEmpty } .sorted { $0.priority > $1.priority } - + let contextualSystemPrompt = """ \(language.isEmpty ? "" : "You must always reply in \(language)") \(systemPrompt)\(extraSystemPrompt.isEmpty ? "" : "\n\(extraSystemPrompt)") @@ -88,30 +87,8 @@ final class DynamicContextController { extension DynamicContextController { static func parseScopes(_ prompt: inout String) -> Set { - guard !prompt.isEmpty else { return [] } - do { - let parser = Parse { - "@" - Many { - Prefix { $0.isLetter } - } separator: { - "+" - } terminator: { - " " - } - Skip { - Many { - " " - } - } - Rest() - } - let (scopes, rest) = try parser.parse(prompt) - prompt = String(rest) - return Set(scopes.map(String.init)) - } catch { - return [] - } + let parser = MessageScopeParser() + return parser(&prompt) } } diff --git a/Core/Sources/HostApp/GeneralView.swift b/Core/Sources/HostApp/GeneralView.swift index 7ce32d54..c011de4a 100644 --- a/Core/Sources/HostApp/GeneralView.swift +++ b/Core/Sources/HostApp/GeneralView.swift @@ -1,5 +1,6 @@ import Client import ComposableArchitecture +import KeyboardShortcuts import LaunchAgentManager import Preferences import SwiftUI @@ -224,6 +225,8 @@ struct GeneralSettingsView: View { var preferWidgetToStayInsideEditorWhenWidthGreaterThan @AppStorage(\.hideCircularWidget) var hideCircularWidget + @AppStorage(\.showHideWidgetShortcutGlobally) + var showHideWidgetShortcutGlobally } @StateObject var settings = Settings() @@ -286,6 +289,15 @@ struct GeneralSettingsView: View { Text("pt") } + KeyboardShortcuts.Recorder("Hotkey to Toggle Widgets", name: .showHideWidget) { _ in + // It's not used in this app! + KeyboardShortcuts.disable(.showHideWidget) + } + + Toggle(isOn: $settings.showHideWidgetShortcutGlobally) { + Text("Enable the Hotkey Globally") + } + Toggle(isOn: $settings.hideCircularWidget) { Text("Hide circular widget") } @@ -342,7 +354,7 @@ struct LargeIconPicker< } } } - + var body: some View { if #available(macOS 13.0, *) { LabeledContent { diff --git a/Core/Sources/HostApp/HostApp.swift b/Core/Sources/HostApp/HostApp.swift index 65f0ab1d..e5379319 100644 --- a/Core/Sources/HostApp/HostApp.swift +++ b/Core/Sources/HostApp/HostApp.swift @@ -1,11 +1,16 @@ import Client import ComposableArchitecture import Foundation +import KeyboardShortcuts #if canImport(LicenseManagement) import LicenseManagement #endif +extension KeyboardShortcuts.Name { + static let showHideWidget = Self("ShowHideWidget") +} + struct HostApp: ReducerProtocol { struct State: Equatable { var general = General.State() @@ -22,16 +27,20 @@ struct HostApp: ReducerProtocol { } @Dependency(\.toast) var toast + + init() { + KeyboardShortcuts.userDefaults = .shared + } var body: some ReducerProtocol { Scope(state: \.general, action: /Action.general) { General() } - + Scope(state: \.chatModelManagement, action: /Action.chatModelManagement) { ChatModelManagement() } - + Scope(state: \.embeddingModelManagement, action: /Action.embeddingModelManagement) { EmbeddingModelManagement() } @@ -40,7 +49,7 @@ struct HostApp: ReducerProtocol { switch action { case .appear: return .none - + case .informExtensionServiceAboutLicenseKeyChange: #if canImport(LicenseManagement) return .run { _ in @@ -55,13 +64,13 @@ struct HostApp: ReducerProtocol { #else return .none #endif - + case .general: return .none - + case .chatModelManagement: return .none - + case .embeddingModelManagement: return .none } @@ -70,8 +79,8 @@ struct HostApp: ReducerProtocol { } import Dependencies -import Preferences import Keychain +import Preferences struct UserDefaultsDependencyKey: DependencyKey { static var liveValue: UserDefaultsType = UserDefaults.shared @@ -80,6 +89,7 @@ struct UserDefaultsDependencyKey: DependencyKey { it.removePersistentDomain(forName: "HostAppPreview") return it }() + static var testValue: UserDefaultsType = { let it = UserDefaults(suiteName: "HostAppTest")! it.removePersistentDomain(forName: "HostAppTest") @@ -106,3 +116,4 @@ extension DependencyValues { set { self[APIKeyKeychainDependencyKey.self] = newValue } } } + diff --git a/Core/Sources/Service/GUI/ChatTabFactory.swift b/Core/Sources/Service/GUI/ChatTabFactory.swift index 6164f7c9..82114837 100644 --- a/Core/Sources/Service/GUI/ChatTabFactory.swift +++ b/Core/Sources/Service/GUI/ChatTabFactory.swift @@ -31,6 +31,7 @@ enum ChatTabFactory { ), title: BrowserChatTab.name ), + folderIfNeeded(TerminalChatTab.chatBuilders(), title: TerminalChatTab.name), ].compactMap { $0 } return collection diff --git a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift index bad5aca2..b253f89b 100644 --- a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift +++ b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift @@ -388,6 +388,12 @@ extension ChatTabPool { externalDependency: ChatTabFactory.externalDependenciesForBrowserChatTab() ) else { break } return await createTab(id: data.id, from: builder) + case TerminalChatTab.name: + guard let builder = try? await TerminalChatTab.restore( + from: data.data, + externalDependency: () + ) else { break } + return await createTab(id: data.id, from: builder) default: break } @@ -397,7 +403,7 @@ extension ChatTabPool { ) else { return nil } - return await createTab(from: builder) + return await createTab(id: data.id, from: builder) } #endif } diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift index 04a72f6c..c5903ad4 100644 --- a/Core/Sources/Service/Service.swift +++ b/Core/Sources/Service/Service.swift @@ -1,7 +1,10 @@ +import Combine import Dependencies import Foundation +import KeyboardShortcuts import Workspace import WorkspaceSuggestionService +import XcodeInspector #if canImport(ProService) import ProService @@ -12,6 +15,10 @@ import ProService public static let shared = TheActor() } +extension KeyboardShortcuts.Name { + static let showHideWidget = Self("ShowHideWidget") +} + /// The running extension service. public final class Service { public static let shared = Service() @@ -22,6 +29,7 @@ public final class Service { public let guiController = GraphicalUserInterfaceController() public let realtimeSuggestionController = RealtimeSuggestionController() public let scheduledCleaner: ScheduledCleaner + let globalShortcutManager: GlobalShortcutManager #if canImport(ProService) let proService: ProService @@ -33,6 +41,7 @@ public final class Service { scheduledCleaner = .init(workspacePool: workspacePool, guiController: guiController) workspacePool.registerPlugin { SuggestionServiceWorkspacePlugin(workspace: $0) } self.workspacePool = workspacePool + globalShortcutManager = .init(guiController: guiController) #if canImport(ProService) proService = withDependencies { dependencyValues in @@ -54,6 +63,51 @@ public final class Service { proService.start() #endif DependencyUpdater().update() + globalShortcutManager.start() + } +} + +@MainActor +final class GlobalShortcutManager { + let guiController: GraphicalUserInterfaceController + private var cancellable = Set() + + nonisolated init(guiController: GraphicalUserInterfaceController) { + self.guiController = guiController + } + + func start() { + KeyboardShortcuts.userDefaults = .shared + setupShortcutIfNeeded() + + KeyboardShortcuts.onKeyUp(for: .showHideWidget) { [guiController] in + guiController.viewStore.send(.suggestionWidget(.circularWidget(.widgetClicked))) + } + + XcodeInspector.shared.$activeApplication.sink { app in + if !UserDefaults.shared.value(for: \.showHideWidgetShortcutGlobally) { + let shouldBeEnabled = if let app, app.isXcode || app.isExtensionService { + true + } else { + false + } + if shouldBeEnabled { + self.setupShortcutIfNeeded() + } else { + self.removeShortcutIfNeeded() + } + } else { + self.setupShortcutIfNeeded() + } + }.store(in: &cancellable) + } + + func setupShortcutIfNeeded() { + KeyboardShortcuts.enable(.showHideWidget) + } + + func removeShortcutIfNeeded() { + KeyboardShortcuts.disable(.showHideWidget) } } diff --git a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift index 5161bcec..bdae5dea 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift @@ -337,7 +337,9 @@ extension PseudoCommandHandler { @WorkspaceActor func getEditorContent(sourceEditor: SourceEditor?) async -> EditorContent? { - guard let filespace = await getFilespace(), let sourceEditor else { return nil } + guard let filespace = await getFilespace(), + let sourceEditor = sourceEditor ?? XcodeInspector.shared.focusedEditor + else { return nil } let content = sourceEditor.content let uti = filespace.codeMetadata.uti ?? "" let tabSize = filespace.codeMetadata.tabSize ?? 4 diff --git a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift index aa271874..d4668628 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift @@ -409,6 +409,10 @@ extension WindowBaseCommandHandler { }() as (String, CursorRange) let viewStore = Service.shared.guiController.viewStore + + let customCommandTemplateProcessor = CustomCommandTemplateProcessor() + let newExtraSystemPrompt = extraSystemPrompt.map(customCommandTemplateProcessor.process) + let newPrompt = prompt.map(customCommandTemplateProcessor.process) _ = await Task { @MainActor in // if there is already a prompt to code presenting, we should not present another one @@ -423,8 +427,8 @@ extension WindowBaseCommandHandler { allCode: editor.content, isContinuous: isContinuous, commandName: name, - defaultPrompt: prompt ?? "", - extraSystemPrompt: extraSystemPrompt, + defaultPrompt: newPrompt ?? "", + extraSystemPrompt: newExtraSystemPrompt, generateDescriptionRequirement: generateDescription )))) }.result diff --git a/Core/Sources/SuggestionWidget/ChatWindowView.swift b/Core/Sources/SuggestionWidget/ChatWindowView.swift index f82d7a89..955e73fa 100644 --- a/Core/Sources/SuggestionWidget/ChatWindowView.swift +++ b/Core/Sources/SuggestionWidget/ChatWindowView.swift @@ -77,6 +77,7 @@ struct ChatTitleBar: View { } } } + .keyboardShortcut("m", modifiers: [.command]) WithViewStore(store, observe: { $0.chatPanelInASeparateWindow }) { viewStore in Button(action: { @@ -177,6 +178,7 @@ struct ChatTabBar: View { store: store, info: info, content: { tab.tabItem }, + icon: { tab.icon }, isSelected: info.id == viewStore.state.selectedTabId ) .contextMenu { @@ -280,7 +282,7 @@ struct ChatTabBarDropDelegate: DropDelegate { let tabs: IdentifiedArray let itemId: String @Binding var draggingTabId: String? - + func dropUpdated(info: DropInfo) -> DropProposal? { return DropProposal(operation: .move) } @@ -299,20 +301,24 @@ struct ChatTabBarDropDelegate: DropDelegate { } } -struct ChatTabBarButton: View { +struct ChatTabBarButton: View { let store: StoreOf let info: ChatTabInfo let content: () -> Content + let icon: () -> Icon let isSelected: Bool @State var isHovered: Bool = false var body: some View { HStack(spacing: 0) { - content() + HStack(spacing: 4) { + icon().foregroundColor(.secondary) + content() + } .font(.callout) .lineLimit(1) .frame(maxWidth: 120) - .padding(.horizontal, 32) + .padding(.horizontal, 28) .contentShape(Rectangle()) .onTapGesture { store.send(.tabClicked(id: info.id)) diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift index b63d74b3..47018a0e 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift @@ -218,6 +218,11 @@ public struct ChatPanelFeature: ReducerProtocol { state.chatTabGroup.tabInfo.insert(tab, at: to) return .none + case let .chatTab(id, .close): + return .run { send in + await send(.closeTabButtonClicked(id: id)) + } + case .chatTab: return .none } diff --git a/Pro b/Pro index 72fd9411..5feae55e 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 72fd9411fb566b45e8dbb2df418cb5ef7a99d5cd +Subproject commit 5feae55e1e9cb9d60166d9126419bf23c25a995d diff --git a/README.md b/README.md index 86485205..cf82fa66 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ Copilot for Xcode is an Xcode Source Editor Extension that provides GitHub Copil - [Granting Permissions to the App](#granting-permissions-to-the-app) - [Setting Up Key Bindings](#setting-up-key-bindings) - [Setting Up Suggestion Feature](#setting-up-suggestion-feature) - - [Setting Up GitHub Copilot](#setting-up-github-copilot) - - [Setting Up Codeium](#setting-up-codeium) + - [Setting Up GitHub Copilot](#setting-up-github-copilot) + - [Setting Up Codeium](#setting-up-codeium) - [Setting Up Chat Feature](#setting-up-chat-feature) - [Managing `CopilotForXcodeExtensionService.app`](#managing-copilotforxcodeextensionserviceapp) - [Update](#update) @@ -122,6 +122,12 @@ Essentially using `⌥⇧` as the "access" key combination for all bindings. Another convenient method to access commands is by using the `⇧⌘/` shortcut to search for a command in the menu bar. +#### Setting Up Global Hotkeys + +Currently, the is only one global hotkey you can set to show/hide the widgets under the General tab from the host app. + +When this hotkey is not set to enabled globally, it will only work when the service app or Xcode is active. + ### Setting Up Suggestion Feature #### Setting Up GitHub Copilot @@ -129,11 +135,11 @@ Another convenient method to access commands is by using the `⇧⌘/` shortcut 1. In the host app, navigate to "Service - GitHub Copilot" to access your GitHub Copilot account settings. 2. Click on "Install" to install the language server. 3. Optionally, set up the path to Node. The default value is simply `node`. Copilot for Xcode.app will attempt to locate Node from the following directories: `/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`. - - If your Node installation is located elsewhere, you can run `which node` from the terminal to obtain the correct path. - - If you are using a node version manager that provides a shim executable, you will need to find the path to the actual executable. Please refer to the FAQ for more information. - + + If your Node installation is located elsewhere, you can run `which node` from the terminal to obtain the correct path. + + If you are using a node version manager that provides a shim executable, you will need to find the path to the actual executable. Please refer to the FAQ for more information. + 4. Click on "Sign In", and you will be redirected to a verification website provided by GitHub. A user code will be copied to your clipboard. 5. After signing in, return to the app and click on "Confirm Sign-in" to complete the process. 6. Go to "Feature - Suggestion" and update the feature provider to "GitHub Copilot". @@ -235,7 +241,7 @@ You can detach the chat panel by simply dragging it away. Once detached, the cha | :------: | --------------------------------------------------------------------------------------------------- | | `⌘W` | Close the chat tab. | | `⌘M` | Minimize the chat, you can bring it back with any chat commands or by clicking the circular widget. | -| `⇧↩︎` | Add new line. | +| `⇧↩︎` | Add new line. | | `⇧⌘]` | Move to next tab | | `⇧⌘[` | Move to previous tab | @@ -297,7 +303,7 @@ This feature is recommended when you need to update a specific piece of code. So #### Commands - Prompt to Code: Open a prompt to code window, where you can use natural language to write or edit selected code. -- Accept Prompt to Code: Accept the result of prompt to code. +- Accept Prompt to Code: Accept the result of prompt to code. ### Custom Commands @@ -308,7 +314,7 @@ You can create custom commands that run Chat and Prompt to Code with personalize - Custom Chat: Open the chat window and immediately send a message, if provided. You can overwrite the entire system prompt through the system prompt field. - Single Round Dialog: Send a message to a temporary chat. Useful when you want to run a terminal command with `/run`. -For Send Message, Single Round Dialog and Custom Chat commands, you can use the following template arguments: +You can use the following template arguments in custom commands: | Argument | Description | | ----------------------------- | ---------------------------------------------- | @@ -316,6 +322,7 @@ For Send Message, Single Round Dialog and Custom Chat commands, you can use the | `{{active_editor_language}}` | The programming language of the active editor. | | `{{active_editor_file_url}}` | The URL of the active file in the editor. | | `{{active_editor_file_name}}` | The name of the active file in the editor. | +| `{{clipboard}}` | The content in clipboard. | ## Plus Features @@ -325,6 +332,7 @@ These features are included in another repo, and are not open sourced. The currently available Plus features include: +- Terminal tab in chat panel. - Unlimited chat/embedding models. - Tab to accept suggestions. - Persisted chat panel. diff --git a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift index 05a06da6..5b2d0cc0 100644 --- a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift @@ -1,5 +1,6 @@ import Foundation import OpenAIService +import Parsing public struct ChatContext { public struct RetrievedContent { @@ -39,3 +40,38 @@ public protocol ChatContextCollector { ) async -> ChatContext } +public struct MessageScopeParser { + public init() {} + + public func callAsFunction(_ content: inout String) -> Set { + return parseScopes(&content) + } + + func parseScopes(_ prompt: inout String) -> Set { + guard !prompt.isEmpty else { return [] } + do { + let parser = Parse { + "@" + Many { + Prefix { $0.isLetter } + } separator: { + "+" + } terminator: { + " " + } + Skip { + Many { + " " + } + } + Rest() + } + let (scopes, rest) = try parser.parse(prompt) + prompt = String(rest) + return Set(scopes.map(String.init)) + } catch { + return [] + } + } +} + diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift index bf297931..d1d90264 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift @@ -101,9 +101,8 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { : "When you don't known what I am asking, I am probably referring to the code." ) - Editing Document Context: ### + ### Editing Document Context """ - let end = "###" let relativePath = "Document Relative Path: \(context.relativePath)" let language = "Language: \(context.language.rawValue)" @@ -160,7 +159,6 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { code, codeAnnotations, fileAnnotations, - end, ] .filter { !$0.isEmpty } .joined(separator: "\n\n") @@ -180,10 +178,9 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { language, lineAnnotations, selectionRange, - end, ] .filter { !$0.isEmpty } - .joined(separator: "\n") + .joined(separator: "\n\n") } } diff --git a/Tool/Sources/ChatTab/ChatTab.swift b/Tool/Sources/ChatTab/ChatTab.swift index fc2fe82f..1c09ef5e 100644 --- a/Tool/Sources/ChatTab/ChatTab.swift +++ b/Tool/Sources/ChatTab/ChatTab.swift @@ -26,6 +26,9 @@ public protocol ChatTabType { /// Build the tabItem for this chat tab. @ViewBuilder func buildTabItem() -> any View + /// Build the icon for this chat tab. + @ViewBuilder + func buildIcon() -> any View /// Build the menu for this chat tab. @ViewBuilder func buildMenu() -> any View @@ -88,7 +91,7 @@ open class BaseChatTab { /// The tab item for this chat tab. @ViewBuilder public var tabItem: some View { - let id = "ChatTabMenu\(id)" + let id = "ChatTabTab\(id)" if let tab = self as? (any ChatTabType) { ContentView(buildView: tab.buildTabItem).id(id) .onAppear { @@ -99,6 +102,17 @@ open class BaseChatTab { } } + /// The icon for this chat tab. + @ViewBuilder + public var icon: some View { + let id = "ChatTabIcon\(id)" + if let tab = self as? (any ChatTabType) { + ContentView(buildView: tab.buildIcon).id(id) + } else { + EmptyView().id(id) + } + } + /// The tab item for this chat tab. @ViewBuilder public var menu: some View { @@ -183,6 +197,10 @@ public class EmptyChatTab: ChatTab { Text("Empty-\(id)") } + public func buildIcon() -> any View { + Image(systemName: "square") + } + public func buildMenu() -> any View { Text("Empty-\(id)") } diff --git a/Tool/Sources/ChatTab/ChatTabItem.swift b/Tool/Sources/ChatTab/ChatTabItem.swift index b74063cf..128af1e7 100644 --- a/Tool/Sources/ChatTab/ChatTabItem.swift +++ b/Tool/Sources/ChatTab/ChatTabItem.swift @@ -20,6 +20,7 @@ public struct ChatTabItem: ReducerProtocol { case updateTitle(String) case openNewTab(AnyChatTabBuilder) case tabContentUpdated + case close } public init() {} @@ -34,6 +35,8 @@ public struct ChatTabItem: ReducerProtocol { return .none case .tabContentUpdated: return .none + case .close: + return .none } } } diff --git a/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift index 52c4181b..53387616 100644 --- a/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift @@ -61,10 +61,7 @@ public struct SwiftFocusedCodeFinder: FocusedCodeFinder { } } guard let focusedNode else { - var result = - UnknownLanguageFocusedCodeFinder( - proposedSearchRange: maxFocusedCodeLineCount / 2 - ) + var result = UnknownLanguageFocusedCodeFinder(proposedSearchRange: 8) .findFocusedCode( containingRange: range, activeDocumentContext: activeDocumentContext diff --git a/Tool/Sources/OpenAIService/ChatGPTService.swift b/Tool/Sources/OpenAIService/ChatGPTService.swift index 0dc01d6b..a1ef374f 100644 --- a/Tool/Sources/OpenAIService/ChatGPTService.swift +++ b/Tool/Sources/OpenAIService/ChatGPTService.swift @@ -98,41 +98,61 @@ public class ChatGPTService: ChatGPTServiceType { await memory.appendMessage(newMessage) } - return AsyncThrowingStream { continuation in - Task(priority: .userInitiated) { - do { - var functionCall: ChatMessage.FunctionCall? - var functionCallMessageID = "" - var isInitialCall = true - loop: while functionCall != nil || isInitialCall { - isInitialCall = false - if let call = functionCall { - if !configuration.runFunctionsAutomatically { - break loop + return Debugger.$id.withValue(.init()) { + AsyncThrowingStream { continuation in + Task(priority: .userInitiated) { + do { + var functionCall: ChatMessage.FunctionCall? + var functionCallMessageID = "" + var isInitialCall = true + loop: while functionCall != nil || isInitialCall { + isInitialCall = false + if let call = functionCall { + if !configuration.runFunctionsAutomatically { + break loop + } + functionCall = nil + await runFunctionCall(call, messageId: functionCallMessageID) } - functionCall = nil - await runFunctionCall(call, messageId: functionCallMessageID) - } - let stream = try await sendMemory() - for try await content in stream { - switch content { - case let .text(text): - continuation.yield(text) - case let .functionCall(call): - if functionCall == nil { - functionCallMessageID = uuidGenerator() - functionCall = call - } else { - functionCall?.name.append(call.name) - functionCall?.arguments.append(call.arguments) + let stream = try await sendMemory() + + #if DEBUG + var reply = "" + #endif + + for try await content in stream { + switch content { + case let .text(text): + continuation.yield(text) + #if DEBUG + reply.append(text) + #endif + case let .functionCall(call): + if functionCall == nil { + functionCallMessageID = uuidGenerator() + functionCall = call + } else { + functionCall?.name.append(call.name) + functionCall?.arguments.append(call.arguments) + } + await prepareFunctionCall( + call, + messageId: functionCallMessageID + ) } - await prepareFunctionCall(call, messageId: functionCallMessageID) } + #if DEBUG + Debugger.didReceiveResponse(content: reply) + #endif } + + #if DEBUG + Debugger.didFinish() + #endif + continuation.finish() + } catch { + continuation.finish(throwing: error) } - continuation.finish() - } catch { - continuation.finish(throwing: error) } } } @@ -152,22 +172,28 @@ public class ChatGPTService: ChatGPTServiceType { ) await memory.appendMessage(newMessage) } - - let message = try await sendMemoryAndWait() - var finalResult = message?.content - var functionCall = message?.functionCall - while let call = functionCall { - if !configuration.runFunctionsAutomatically { - break + return try await Debugger.$id.withValue(.init()) { + let message = try await sendMemoryAndWait() + var finalResult = message?.content + var functionCall = message?.functionCall + while let call = functionCall { + if !configuration.runFunctionsAutomatically { + break + } + functionCall = nil + await runFunctionCall(call) + guard let nextMessage = try await sendMemoryAndWait() else { break } + finalResult = nextMessage.content + functionCall = nextMessage.functionCall } - functionCall = nil - await runFunctionCall(call) - guard let nextMessage = try await sendMemoryAndWait() else { break } - finalResult = nextMessage.content - functionCall = nextMessage.functionCall - } - return finalResult + #if DEBUG + Debugger.didReceiveResponse(content: finalResult ?? "N/A") + Debugger.didFinish() + #endif + + return finalResult + } } public func stopReceivingMessage() { @@ -239,6 +265,10 @@ extension ChatGPTService { requestBody ) + #if DEBUG + Debugger.didSendRequestBody(body: requestBody) + #endif + return AsyncThrowingStream { continuation in Task { do { @@ -348,6 +378,10 @@ extension ChatGPTService { requestBody ) + #if DEBUG + Debugger.didSendRequestBody(body: requestBody) + #endif + let response = try await api() guard let choice = response.choices.first else { return nil } @@ -388,6 +422,10 @@ extension ChatGPTService { _ call: ChatMessage.FunctionCall, messageId: String? = nil ) async -> String { + #if DEBUG + Debugger.didReceiveFunction(name: call.name, arguments: call.arguments) + #endif + let messageId = messageId ?? uuidGenerator() guard let function = functionProvider.function(named: call.name) else { @@ -413,6 +451,10 @@ extension ChatGPTService { } } + #if DEBUG + Debugger.didReceiveFunctionResult(result: result.botReadableContent) + #endif + await memory.updateMessage(id: messageId) { message in message.content = result.botReadableContent } @@ -421,6 +463,11 @@ extension ChatGPTService { } catch { // For errors, use the error message as the result. let content = "Error: \(error.localizedDescription)" + + #if DEBUG + Debugger.didReceiveFunctionResult(result: content) + #endif + await memory.updateMessage(id: messageId) { message in message.content = content } diff --git a/Tool/Sources/OpenAIService/CompletionStreamAPI.swift b/Tool/Sources/OpenAIService/CompletionStreamAPI.swift index 2524257b..8e2c3098 100644 --- a/Tool/Sources/OpenAIService/CompletionStreamAPI.swift +++ b/Tool/Sources/OpenAIService/CompletionStreamAPI.swift @@ -13,7 +13,7 @@ protocol CompletionStreamAPI { ) } -public enum FunctionCallStrategy: Encodable, Equatable { +public enum FunctionCallStrategy: Codable, Equatable { /// Forbid the bot to call any function. case none /// Let the bot choose what function to call. @@ -39,7 +39,7 @@ public enum FunctionCallStrategy: Encodable, Equatable { } /// https://platform.openai.com/docs/api-reference/chat/create -struct CompletionRequestBody: Encodable, Equatable { +struct CompletionRequestBody: Codable, Equatable { struct Message: Codable, Equatable { /// The role of the message. var role: ChatMessage.Role diff --git a/Tool/Sources/OpenAIService/Debug/Debug.swift b/Tool/Sources/OpenAIService/Debug/Debug.swift new file mode 100644 index 00000000..d90883e9 --- /dev/null +++ b/Tool/Sources/OpenAIService/Debug/Debug.swift @@ -0,0 +1,75 @@ +import AppKit +import Foundation + +enum Debugger { + @TaskLocal + static var id: UUID? + + #if DEBUG + static func didSendRequestBody(body: CompletionRequestBody) { + do { + let json = try JSONEncoder().encode(body) + let center = NSWorkspace.shared.notificationCenter + center.post( + name: .init("ServiceDebugger.ChatRequestDebug.requestSent"), + object: nil, + userInfo: [ + "id": id ?? UUID(), + "data": json, + ] + ) + } catch { + print("Failed to encode request body: \(error)") + } + } + + static func didReceiveFunction(name: String, arguments: String) { + let center = NSWorkspace.shared.notificationCenter + center.post( + name: .init("ServiceDebugger.ChatRequestDebug.receivedFunctionCall"), + object: nil, + userInfo: [ + "id": id ?? UUID(), + "name": name, + "arguments": arguments, + ] + ) + } + + static func didReceiveFunctionResult(result: String) { + let center = NSWorkspace.shared.notificationCenter + center.post( + name: .init("ServiceDebugger.ChatRequestDebug.receivedFunctionResult"), + object: nil, + userInfo: [ + "id": id ?? UUID(), + "result": result, + ] + ) + } + + static func didReceiveResponse(content: String) { + let center = NSWorkspace.shared.notificationCenter + center.post( + name: .init("ServiceDebugger.ChatRequestDebug.responseReceived"), + object: nil, + userInfo: [ + "id": id ?? UUID(), + "response": content, + ] + ) + } + + static func didFinish() { + let center = NSWorkspace.shared.notificationCenter + center.post( + name: .init("ServiceDebugger.ChatRequestDebug.finished"), + object: nil, + userInfo: [ + "id": id ?? UUID(), + ] + ) + } + #endif +} + diff --git a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift index 8462c5ac..9a117d63 100644 --- a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift +++ b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift @@ -135,9 +135,13 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { for (index, content) in retrievedContent.filter({ !$0.isEmpty }).enumerated() { if index == 0 { if !appendToSystemPrompt(""" - + + + ## Relevant Content + Below are information related to the conversation, separated by \(separator) + """) { break } } else { if !appendToSystemPrompt("\n\(separator)\n") { break } diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 276f1e3d..dd7e3f24 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -88,6 +88,11 @@ public struct UserDefaultPreferenceKeys { defaultValue: false, key: "HideCircularWidget" ) + + public let showHideWidgetShortcutGlobally = PreferenceKey( + defaultValue: false, + key: "ShowHideWidgetShortcutGlobally" + ) } // MARK: - OpenAI Account Settings diff --git a/Tool/Sources/SuggestionService/CodeiumSuggestionProvider.swift b/Tool/Sources/SuggestionService/CodeiumSuggestionProvider.swift index eb03b335..a990db46 100644 --- a/Tool/Sources/SuggestionService/CodeiumSuggestionProvider.swift +++ b/Tool/Sources/SuggestionService/CodeiumSuggestionProvider.swift @@ -28,23 +28,17 @@ actor CodeiumSuggestionProvider: SuggestionServiceProvider { } extension CodeiumSuggestionProvider { - func getSuggestions( - fileURL: URL, - content: String, - cursorPosition: SuggestionModel.CursorPosition, - tabSize: Int, - indentSize: Int, - usesTabsForIndentation: Bool, - ignoreSpaceOnlySuggestions: Bool - ) async throws -> [SuggestionModel.CodeSuggestion] { - try await (try createCodeiumServiceIfNeeded()).getCompletions( - fileURL: fileURL, - content: content, - cursorPosition: cursorPosition, - tabSize: tabSize, - indentSize: indentSize, - usesTabsForIndentation: usesTabsForIndentation, - ignoreSpaceOnlySuggestions: ignoreSpaceOnlySuggestions + func getSuggestions(_ request: SuggestionRequest) async throws + -> [SuggestionModel.CodeSuggestion] + { + try await (createCodeiumServiceIfNeeded()).getCompletions( + fileURL: request.fileURL, + content: request.content, + cursorPosition: request.cursorPosition, + tabSize: request.tabSize, + indentSize: request.indentSize, + usesTabsForIndentation: request.usesTabsForIndentation, + ignoreSpaceOnlySuggestions: request.ignoreSpaceOnlySuggestions ) } @@ -70,12 +64,12 @@ extension CodeiumSuggestionProvider { } func notifySaveTextDocument(fileURL: URL) async throws {} - + func cancelRequest() async { await (try? createCodeiumServiceIfNeeded())? .cancelRequest() } - + func terminate() async { (try? createCodeiumServiceIfNeeded())?.terminate() } diff --git a/Tool/Sources/SuggestionService/GitHubCopilotSuggestionProvider.swift b/Tool/Sources/SuggestionService/GitHubCopilotSuggestionProvider.swift index f7baeea9..3632ba77 100644 --- a/Tool/Sources/SuggestionService/GitHubCopilotSuggestionProvider.swift +++ b/Tool/Sources/SuggestionService/GitHubCopilotSuggestionProvider.swift @@ -26,23 +26,17 @@ actor GitHubCopilotSuggestionProvider: SuggestionServiceProvider { } extension GitHubCopilotSuggestionProvider { - func getSuggestions( - fileURL: URL, - content: String, - cursorPosition: SuggestionModel.CursorPosition, - tabSize: Int, - indentSize: Int, - usesTabsForIndentation: Bool, - ignoreSpaceOnlySuggestions: Bool - ) async throws -> [SuggestionModel.CodeSuggestion] { - try await (try createGitHubCopilotServiceIfNeeded()).getCompletions( - fileURL: fileURL, - content: content, - cursorPosition: cursorPosition, - tabSize: tabSize, - indentSize: indentSize, - usesTabsForIndentation: usesTabsForIndentation, - ignoreSpaceOnlySuggestions: ignoreSpaceOnlySuggestions, + func getSuggestions(_ request: SuggestionRequest) async throws + -> [SuggestionModel.CodeSuggestion] + { + try await (createGitHubCopilotServiceIfNeeded()).getCompletions( + fileURL: request.fileURL, + content: request.content, + cursorPosition: request.cursorPosition, + tabSize: request.tabSize, + indentSize: request.indentSize, + usesTabsForIndentation: request.usesTabsForIndentation, + ignoreSpaceOnlySuggestions: request.ignoreSpaceOnlySuggestions, ignoreTrailingNewLinesAndSpaces: UserDefaults.shared .value(for: \.gitHubCopilotIgnoreTrailingNewLines) ) diff --git a/Tool/Sources/SuggestionService/SuggestionService.swift b/Tool/Sources/SuggestionService/SuggestionService.swift index 4213121a..2cc194e3 100644 --- a/Tool/Sources/SuggestionService/SuggestionService.swift +++ b/Tool/Sources/SuggestionService/SuggestionService.swift @@ -4,8 +4,16 @@ import Preferences import SuggestionModel import UserDefaultsObserver -public protocol SuggestionServiceType { - func getSuggestions( +public struct SuggestionRequest { + public var fileURL: URL + public var content: String + public var cursorPosition: CursorPosition + public var tabSize: Int + public var indentSize: Int + public var usesTabsForIndentation: Bool + public var ignoreSpaceOnlySuggestions: Bool + + public init( fileURL: URL, content: String, cursorPosition: CursorPosition, @@ -13,7 +21,19 @@ public protocol SuggestionServiceType { indentSize: Int, usesTabsForIndentation: Bool, ignoreSpaceOnlySuggestions: Bool - ) async throws -> [CodeSuggestion] + ) { + self.fileURL = fileURL + self.content = content + self.cursorPosition = cursorPosition + self.tabSize = tabSize + self.indentSize = indentSize + self.usesTabsForIndentation = usesTabsForIndentation + self.ignoreSpaceOnlySuggestions = ignoreSpaceOnlySuggestions + } +} + +public protocol SuggestionServiceType { + func getSuggestions(_ request: SuggestionRequest) async throws -> [CodeSuggestion] func notifyAccepted(_ suggestion: CodeSuggestion) async func notifyRejected(_ suggestions: [CodeSuggestion]) async @@ -25,9 +45,45 @@ public protocol SuggestionServiceType { func terminate() async } +public extension SuggestionServiceType { + func getSuggestions( + fileURL: URL, + content: String, + cursorPosition: CursorPosition, + tabSize: Int, + indentSize: Int, + usesTabsForIndentation: Bool, + ignoreSpaceOnlySuggestions: Bool + ) async throws -> [CodeSuggestion] { + return try await getSuggestions(.init( + fileURL: fileURL, + content: content, + cursorPosition: cursorPosition, + tabSize: tabSize, + indentSize: indentSize, + usesTabsForIndentation: usesTabsForIndentation, + ignoreSpaceOnlySuggestions: ignoreSpaceOnlySuggestions + )) + } +} + protocol SuggestionServiceProvider: SuggestionServiceType {} public actor SuggestionService: SuggestionServiceType { + static var builtInMiddlewares: [SuggestionServiceMiddleware] = [ + DisabledLanguageSuggestionServiceMiddleware(), + ] + + static var customMiddlewares: [SuggestionServiceMiddleware] = [] + + static var middlewares: [SuggestionServiceMiddleware] { + builtInMiddlewares + customMiddlewares + } + + public static func addMiddleware(_ middleware: SuggestionServiceMiddleware) { + customMiddlewares.append(middleware) + } + let projectRootURL: URL let onServiceLaunched: (SuggestionServiceType) -> Void let providerChangeObserver = UserDefaultsObserver( @@ -75,31 +131,18 @@ public actor SuggestionService: SuggestionServiceType { } public extension SuggestionService { - func getSuggestions( - fileURL: URL, - content: String, - cursorPosition: SuggestionModel.CursorPosition, - tabSize: Int, - indentSize: Int, - usesTabsForIndentation: Bool, - ignoreSpaceOnlySuggestions: Bool + func getSuggestions( + _ request: SuggestionRequest ) async throws -> [SuggestionModel.CodeSuggestion] { - let language = languageIdentifierFromFileURL(fileURL) - if UserDefaults.shared.value(for: \.suggestionFeatureDisabledLanguageList) - .contains(where: { $0 == language.rawValue }) - { - return [] + var getSuggestion = suggestionProvider.getSuggestions + + for middleware in Self.middlewares.reversed() { + getSuggestion = { [getSuggestion] request in + try await middleware.getSuggestion(request, next: getSuggestion) + } } - return try await suggestionProvider.getSuggestions( - fileURL: fileURL, - content: content, - cursorPosition: cursorPosition, - tabSize: tabSize, - indentSize: indentSize, - usesTabsForIndentation: usesTabsForIndentation, - ignoreSpaceOnlySuggestions: ignoreSpaceOnlySuggestions - ) + return try await getSuggestion(request) } func notifyAccepted(_ suggestion: SuggestionModel.CodeSuggestion) async { diff --git a/Tool/Sources/SuggestionService/SuggestionServiceMiddleware.swift b/Tool/Sources/SuggestionService/SuggestionServiceMiddleware.swift new file mode 100644 index 00000000..29575a40 --- /dev/null +++ b/Tool/Sources/SuggestionService/SuggestionServiceMiddleware.swift @@ -0,0 +1,41 @@ +import Foundation +import SuggestionModel +import Logger + +public protocol SuggestionServiceMiddleware { + typealias Next = (SuggestionRequest) async throws -> [CodeSuggestion] + + func getSuggestion(_ request: SuggestionRequest, next: Next) async throws -> [CodeSuggestion] +} + +struct DisabledLanguageSuggestionServiceMiddleware: SuggestionServiceMiddleware { + func getSuggestion(_ request: SuggestionRequest, next: Next) async throws -> [CodeSuggestion] { + let language = languageIdentifierFromFileURL(request.fileURL) + if UserDefaults.shared.value(for: \.suggestionFeatureDisabledLanguageList) + .contains(where: { $0 == language.rawValue }) + { + #if DEBUG + Logger.service.info("Suggestion service is disabled for \(language).") + #endif + return [] + } + + return try await next(request) + } +} + +public struct DebugSuggestionServiceMiddleware: SuggestionServiceMiddleware { + public init() {} + + public func getSuggestion(_ request: SuggestionRequest, next: Next) async throws -> [CodeSuggestion] { + Logger.service.debug(""" + Get suggestion for \(request.fileURL) at \(request.cursorPosition) + """) + let suggestions = try await next(request) + Logger.service.debug(""" + Receive \(suggestions.count) suggestions for \(request.fileURL) at \(request.cursorPosition) + """) + + return suggestions + } +} diff --git a/Tool/Sources/XcodeInspector/XcodeInspector.swift b/Tool/Sources/XcodeInspector/XcodeInspector.swift index 08d74805..c218f021 100644 --- a/Tool/Sources/XcodeInspector/XcodeInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeInspector.swift @@ -24,13 +24,13 @@ public final class XcodeInspector: ObservableObject { @Published public internal(set) var focusedEditor: SourceEditor? @Published public internal(set) var focusedElement: AXUIElement? @Published public internal(set) var completionPanel: AXUIElement? - + public var focusedEditorContent: EditorInformation? { guard let documentURL = XcodeInspector.shared.realtimeActiveDocumentURL, let workspaceURL = XcodeInspector.shared.realtimeActiveWorkspaceURL, let projectURL = XcodeInspector.shared.activeProjectRootURL else { return nil } - + let editorContent = XcodeInspector.shared.focusedEditor?.content let language = languageIdentifierFromFileURL(documentURL) let relativePath = documentURL.path.replacingOccurrences(of: projectURL.path, with: "") @@ -143,8 +143,8 @@ public final class XcodeInspector: ObservableObject { @MainActor func setActiveXcode(_ xcode: XcodeAppInstanceInspector) { + activeApplication = xcode xcode.refresh() - for task in activeXcodeObservations { task.cancel() } for cancellable in activeXcodeCancellable { cancellable.cancel() } activeXcodeObservations.removeAll() @@ -214,6 +214,8 @@ public class AppInstanceInspector: ObservableObject { public let appElement: AXUIElement public let runningApplication: NSRunningApplication public var isActive: Bool { runningApplication.isActive } + public var isXcode: Bool { runningApplication.isXcode } + public var isExtensionService: Bool { runningApplication.isCopilotForXcodeExtensionService } init(runningApplication: NSRunningApplication) { self.runningApplication = runningApplication diff --git a/Version.xcconfig b/Version.xcconfig index 35911ea7..b0ca9172 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,3 +1,3 @@ -APP_VERSION = 0.25.0 -APP_BUILD = 261 +APP_VERSION = 0.26.0 +APP_BUILD = 272 diff --git a/appcast.xml b/appcast.xml index ae698149..07f4f9e8 100644 --- a/appcast.xml +++ b/appcast.xml @@ -3,6 +3,18 @@ Copilot for Xcode + + 0.26.0 + Sun, 22 Oct 2023 18:58:49 +0800 + 272 + 0.26.0 + 12.0 + + https://github.com/intitni/CopilotForXcode/releases/tag/0.26.0 + + + + 0.25.0 Wed, 11 Oct 2023 23:08:08 +0800