From f90f0dc571b5b7ce3072a83bc7f2d61db30b0c90 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 19 Jun 2024 21:45:08 +0800 Subject: [PATCH 01/44] Prevent re-parsing markdown on scroll --- Core/Sources/ChatGPTChatTab/Chat.swift | 14 +++++--- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 23 ++++++++----- .../ChatGPTChatTab/Views/BotMessage.swift | 13 ++++--- .../Views/ThemedMarkdownText.swift | 10 ++++-- .../ChatGPTChatTab/Views/UserMessage.swift | 34 +++++++++++-------- 5 files changed, 58 insertions(+), 36 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/Chat.swift b/Core/Sources/ChatGPTChatTab/Chat.swift index 35d808d2..f3170256 100644 --- a/Core/Sources/ChatGPTChatTab/Chat.swift +++ b/Core/Sources/ChatGPTChatTab/Chat.swift @@ -1,6 +1,7 @@ import ChatService import ComposableArchitecture import Foundation +import MarkdownUI import OpenAIService import Preferences import Terminal @@ -40,12 +41,14 @@ public struct DisplayedChatMessage: Equatable { public var id: String public var role: Role public var text: String + public var markdownContent: MarkdownContent public var references: [Reference] = [] public init(id: String, role: Role, text: String, references: [Reference]) { self.id = id self.role = role self.text = text + self.markdownContent = .init(text) self.references = references } } @@ -151,7 +154,7 @@ struct Chat { case let .setIsEnabled(isEnabled): state.isEnabled = isEnabled return .none - + case .sendButtonTapped: guard !state.typedMessage.isEmpty else { return .none } let message = state.typedMessage @@ -213,11 +216,11 @@ struct Chat { await openURL(url) } } - + case .manuallyScrolledUp: state.isPinnedToBottom = false return .none - + case .scrollToBottomButtonTapped: state.isPinnedToBottom = true return .none @@ -248,7 +251,7 @@ struct Chat { let debouncedHistoryChange = TimedDebounceFunction(duration: 0.2) { await send(.historyChanged) } - + for await _ in stream { await debouncedHistoryChange() } @@ -502,9 +505,10 @@ private actor TimedDebounceFunction { } } } - + func fire() async { lastFireTime = Date() await block() } } + diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index 6fc3b60f..139ed7ab 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -283,20 +283,27 @@ struct ChatHistoryItem: View { var body: some View { WithPerceptionTracking { let text = message.text + let markdownContent = message.markdownContent switch message.role { case .user: - UserMessage(id: message.id, text: text, chat: chat) - .listRowInsets(EdgeInsets( - top: 0, - leading: -8, - bottom: 0, - trailing: -8 - )) - .padding(.vertical, 4) + UserMessage( + id: message.id, + text: text, + markdownContent: markdownContent, + chat: chat + ) + .listRowInsets(EdgeInsets( + top: 0, + leading: -8, + bottom: 0, + trailing: -8 + )) + .padding(.vertical, 4) case .assistant: BotMessage( id: message.id, text: text, + markdownContent: markdownContent, references: message.references, chat: chat ) diff --git a/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift b/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift index 1683fd88..2605d6d5 100644 --- a/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift +++ b/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift @@ -8,6 +8,7 @@ struct BotMessage: View { var r: Double { messageBubbleCornerRadius } let id: String let text: String + let markdownContent: MarkdownContent let references: [DisplayedChatMessage.Reference] let chat: StoreOf @Environment(\.colorScheme) var colorScheme @@ -43,7 +44,7 @@ struct BotMessage: View { } } - ThemedMarkdownText(text) + ThemedMarkdownText(markdownContent) } .frame(alignment: .trailing) .padding() @@ -209,14 +210,16 @@ struct ReferenceIcon: View { } #Preview("Bot Message") { - BotMessage( - id: "1", - text: """ + let text = """ **Hey**! What can I do for you?**Hey**! What can I do for you?**Hey**! What can I do for you?**Hey**! What can I do for you? ```swift func foo() {} ``` - """, + """ + return BotMessage( + id: "1", + text: text, + markdownContent: .init(text), references: .init(repeating: .init( title: "ReferenceList", subtitle: "/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift:100", diff --git a/Core/Sources/ChatGPTChatTab/Views/ThemedMarkdownText.swift b/Core/Sources/ChatGPTChatTab/Views/ThemedMarkdownText.swift index eca57a29..15da1780 100644 --- a/Core/Sources/ChatGPTChatTab/Views/ThemedMarkdownText.swift +++ b/Core/Sources/ChatGPTChatTab/Views/ThemedMarkdownText.swift @@ -12,14 +12,18 @@ struct ThemedMarkdownText: View { @AppStorage(\.chatCodeFont) var chatCodeFont @Environment(\.colorScheme) var colorScheme - let text: String + let content: MarkdownContent init(_ text: String) { - self.text = text + self.content = .init(text) + } + + init(_ content: MarkdownContent) { + self.content = content } var body: some View { - Markdown(text) + Markdown(content) .textSelection(.enabled) .markdownTheme(.custom( fontSize: chatFontSize, diff --git a/Core/Sources/ChatGPTChatTab/Views/UserMessage.swift b/Core/Sources/ChatGPTChatTab/Views/UserMessage.swift index d8c2af86..edac231a 100644 --- a/Core/Sources/ChatGPTChatTab/Views/UserMessage.swift +++ b/Core/Sources/ChatGPTChatTab/Views/UserMessage.swift @@ -7,11 +7,12 @@ struct UserMessage: View { var r: Double { messageBubbleCornerRadius } let id: String let text: String + let markdownContent: MarkdownContent let chat: StoreOf @Environment(\.colorScheme) var colorScheme var body: some View { - ThemedMarkdownText(text) + ThemedMarkdownText(markdownContent) .frame(alignment: .leading) .padding() .background { @@ -50,21 +51,24 @@ struct UserMessage: View { } #Preview { - UserMessage( + let text = #""" + Please buy me a coffee! + | Coffee | Milk | + |--------|------| + | Espresso | No | + | Latte | Yes | + ```swift + func foo() {} + ``` + ```objectivec + - (void)bar {} + ``` + """# + + return UserMessage( id: "A", - text: #""" - Please buy me a coffee! - | Coffee | Milk | - |--------|------| - | Espresso | No | - | Latte | Yes | - ```swift - func foo() {} - ``` - ```objectivec - - (void)bar {} - ``` - """#, + text: text, + markdownContent: .init(text), chat: .init( initialState: .init(history: [] as [DisplayedChatMessage], isReceivingMessage: false), reducer: { Chat(service: .init()) } From 6b58a40b1e2e9b63beb9c80d01067f99bc63c1b3 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 22 Jun 2024 23:42:10 +0800 Subject: [PATCH 02/44] Bump Copilot.vim to 1.37.0 --- Tool/Package.swift | 2 +- .../GitHubCopilotInstallationManager.swift | 4 ++-- .../LanguageServer/GitHubCopilotService.swift | 22 ++++++++++++++----- ...ert.js => load-self-signed-cert-1.34.0.js} | 2 +- 4 files changed, 21 insertions(+), 9 deletions(-) rename Tool/Sources/GitHubCopilotService/Resources/{load-self-signed-cert.js => load-self-signed-cert-1.34.0.js} (95%) diff --git a/Tool/Package.swift b/Tool/Package.swift index 86672ba4..6265ffb5 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -323,7 +323,7 @@ let package = Package( .product(name: "LanguageServerProtocol", package: "LanguageServerProtocol"), .product(name: "CopilotForXcodeKit", package: "CopilotForXcodeKit"), ], - resources: [.copy("Resources/load-self-signed-cert.js")] + resources: [.copy("Resources/load-self-signed-cert-1.34.0.js")] ), .testTarget( name: "GitHubCopilotServiceTests", diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift index 182fbe04..7778b0e5 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift @@ -6,12 +6,12 @@ public struct GitHubCopilotInstallationManager { public private(set) static var isInstalling = false static var downloadURL: URL { - let commitHash = "c79d711cbf7c6672c6c57d6df7c5ab7b6cac2b7a" + let commitHash = "0668308e68b0ac28b332b204b469fbe04601536a" let link = "https://github.com/github/copilot.vim/archive/\(commitHash).zip" return URL(string: link)! } - static let latestSupportedVersion = "1.33.0" + static let latestSupportedVersion = "1.37.0" static let minimumSupportedVersion = "1.32.0" public init() {} diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift index 6d7f17cf..75cebdee 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift @@ -111,18 +111,28 @@ public class GitHubCopilotBaseService { let urls = try GitHubCopilotBaseService.createFoldersIfNeeded() let executionParams: Process.ExecutionParameters let runner = UserDefaults.shared.value(for: \.runNodeWith) - - let agentJSURL = urls.executableURL.appendingPathComponent("copilot/dist/agent.js") - guard FileManager.default.fileExists(atPath: agentJSURL.path) else { + + guard let agentJSURL = { + let languageServerDotJS = urls.executableURL + .appendingPathComponent("copilot/dist/language-server.js") + if FileManager.default.fileExists(atPath: languageServerDotJS.path) { + return languageServerDotJS + } + let agentsDotJS = urls.executableURL.appendingPathComponent("copilot/dist/agent.js") + if FileManager.default.fileExists(atPath: agentsDotJS.path) { + return agentsDotJS + } + return nil + }() else { throw GitHubCopilotError.languageServerNotInstalled } let indexJSURL: URL = try { if UserDefaults.shared.value(for: \.gitHubCopilotLoadKeyChainCertificates) { - let url = urls.executableURL.appendingPathComponent("load-self-signed-cert.js") + let url = urls.executableURL.appendingPathComponent("load-self-signed-cert-1.34.0.js") if !FileManager.default.fileExists(atPath: url.path) { let file = Bundle.module.url( - forResource: "load-self-signed-cert", + forResource: "load-self-signed-cert-1.34.0", withExtension: "js" )! do { @@ -399,6 +409,8 @@ public final class GitHubCopilotService: GitHubCopilotBaseService, return completions } catch let error as ServerError { switch error { + case .serverError(1000, _, _): + throw GitHubCopilotError.languageServerError(error) case .serverError: // sometimes the content inside language server is not new enough, which can // lead to an version mismatch error. We can try a few times until the content diff --git a/Tool/Sources/GitHubCopilotService/Resources/load-self-signed-cert.js b/Tool/Sources/GitHubCopilotService/Resources/load-self-signed-cert-1.34.0.js similarity index 95% rename from Tool/Sources/GitHubCopilotService/Resources/load-self-signed-cert.js rename to Tool/Sources/GitHubCopilotService/Resources/load-self-signed-cert-1.34.0.js index 112c99ac..44bcec49 100644 --- a/Tool/Sources/GitHubCopilotService/Resources/load-self-signed-cert.js +++ b/Tool/Sources/GitHubCopilotService/Resources/load-self-signed-cert-1.34.0.js @@ -37,4 +37,4 @@ function duplicated(cert, index, arr) { initialize(); -require("./copilot/dist/agent.js"); +require("./copilot/dist/language-server.js"); From d456d583b3749c46e8706bdd49bf90078964a7f8 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 22 Jun 2024 23:58:47 +0800 Subject: [PATCH 03/44] Update CopilotForXcodeKit --- Pro | 2 +- Tool/Package.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Pro b/Pro index 5ec5a365..43632988 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 5ec5a3653a8861f6f6520e53af3f881bdd966e0c +Subproject commit 43632988abab32cf7994acc97f3653be1bc48add diff --git a/Tool/Package.swift b/Tool/Package.swift index 6265ffb5..b1ddc67b 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -71,7 +71,7 @@ let package = Package( url: "https://github.com/intitni/generative-ai-swift", branch: "support-setting-base-url" ), - .package(url: "https://github.com/intitni/CopilotForXcodeKit", branch: "develop"), + .package(url: "https://github.com/intitni/CopilotForXcodeKit", from: "0.7.1"), // TreeSitter .package(url: "https://github.com/intitni/SwiftTreeSitter.git", branch: "main"), From e0b2687a9794280ce0af6053ae8cb62b8ab115a2 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 23 Jun 2024 00:07:27 +0800 Subject: [PATCH 04/44] Support presenting suggestion error when needed --- .../PseudoCommandHandler.swift | 11 ++++++ .../SuggestionService/SuggestionService.swift | 35 +++++++++++-------- .../LanguageServer/GitHubCopilotService.swift | 15 ++++---- 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift index 833c13a8..c99e5efb 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift @@ -1,7 +1,9 @@ import ActiveApplicationMonitor import AppKit import CodeiumService +import enum CopilotForXcodeKit.SuggestionServiceError import Dependencies +import Logger import PlusFeatureFlag import Preferences import SuggestionInjector @@ -107,7 +109,16 @@ struct PseudoCommandHandler { } else { presenter.discardSuggestion(fileURL: fileURL) } + } catch let error as SuggestionServiceError { + switch error { + case let .notice(error): + presenter.presentErrorMessage(error.localizedDescription) + case .silent: + Logger.service.error(error.localizedDescription) + return + } } catch { + Logger.service.error(error.localizedDescription) return } } diff --git a/Core/Sources/SuggestionService/SuggestionService.swift b/Core/Sources/SuggestionService/SuggestionService.swift index 5fdd6ccc..e34b37ce 100644 --- a/Core/Sources/SuggestionService/SuggestionService.swift +++ b/Core/Sources/SuggestionService/SuggestionService.swift @@ -1,6 +1,7 @@ import BuiltinExtension import CodeiumService import struct CopilotForXcodeKit.WorkspaceInfo +import enum CopilotForXcodeKit.SuggestionServiceError import Foundation import GitHubCopilotService import Preferences @@ -63,22 +64,28 @@ public extension SuggestionService { _ request: SuggestionRequest, workspaceInfo: CopilotForXcodeKit.WorkspaceInfo ) async throws -> [SuggestionModel.CodeSuggestion] { - var getSuggestion = suggestionProvider.getSuggestions(_:workspaceInfo:) - let configuration = await configuration - - for middleware in middlewares.reversed() { - getSuggestion = { [getSuggestion] request, workspaceInfo in - try await middleware.getSuggestion( - request, - configuration: configuration, - next: { [getSuggestion] request in - try await getSuggestion(request, workspaceInfo) - } - ) + do { + var getSuggestion = suggestionProvider.getSuggestions(_:workspaceInfo:) + let configuration = await configuration + + for middleware in middlewares.reversed() { + getSuggestion = { [getSuggestion] request, workspaceInfo in + try await middleware.getSuggestion( + request, + configuration: configuration, + next: { [getSuggestion] request in + try await getSuggestion(request, workspaceInfo) + } + ) + } } + + return try await getSuggestion(request, workspaceInfo) + } catch let error as SuggestionServiceError { + throw error + } catch { + throw SuggestionServiceError.silent(error) } - - return try await getSuggestion(request, workspaceInfo) } func notifyAccepted( diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift index 75cebdee..0a59ddff 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift @@ -1,4 +1,5 @@ import AppKit +import enum CopilotForXcodeKit.SuggestionServiceError import Foundation import LanguageClient import LanguageServerProtocol @@ -95,7 +96,7 @@ public class GitHubCopilotBaseService { let projectRootURL: URL var server: GitHubCopilotLSP var localProcessServer: CopilotLocalProcessServer? - + deinit { localProcessServer?.terminate() } @@ -111,7 +112,7 @@ public class GitHubCopilotBaseService { let urls = try GitHubCopilotBaseService.createFoldersIfNeeded() let executionParams: Process.ExecutionParameters let runner = UserDefaults.shared.value(for: \.runNodeWith) - + guard let agentJSURL = { let languageServerDotJS = urls.executableURL .appendingPathComponent("copilot/dist/language-server.js") @@ -129,7 +130,8 @@ public class GitHubCopilotBaseService { let indexJSURL: URL = try { if UserDefaults.shared.value(for: \.gitHubCopilotLoadKeyChainCertificates) { - let url = urls.executableURL.appendingPathComponent("load-self-signed-cert-1.34.0.js") + let url = urls.executableURL + .appendingPathComponent("load-self-signed-cert-1.34.0.js") if !FileManager.default.fileExists(atPath: url.path) { let file = Bundle.module.url( forResource: "load-self-signed-cert-1.34.0", @@ -409,8 +411,9 @@ public final class GitHubCopilotService: GitHubCopilotBaseService, return completions } catch let error as ServerError { switch error { - case .serverError(1000, _, _): - throw GitHubCopilotError.languageServerError(error) + case .serverError(1000, _, _): // not logged-in error + throw SuggestionServiceError + .notice(GitHubCopilotError.languageServerError(error)) case .serverError: // sometimes the content inside language server is not new enough, which can // lead to an version mismatch error. We can try a few times until the content @@ -429,7 +432,7 @@ public final class GitHubCopilotService: GitHubCopilotBaseService, throw error } } - + func recoverContent() async { try? await notifyChangeTextDocument( fileURL: fileURL, From 404c64b23663c4a971abdbe6ab4c303b3ccb41ca Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 28 Jun 2024 14:10:09 +0800 Subject: [PATCH 05/44] Expose the error --- .../LanguageServer/GitHubCopilotService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift index 0a59ddff..bb7a0fe1 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift @@ -447,7 +447,7 @@ public final class GitHubCopilotService: GitHubCopilotBaseService, // And sometimes the language server's content was not up to date and may generate // weird result when the cursor position exceeds the line. let task = Task { @GitHubCopilotSuggestionActor in - try? await notifyChangeTextDocument( + try await notifyChangeTextDocument( fileURL: fileURL, content: content, version: 1 From 9a52e0d9fc5ab6551e9dd10fee920549138d7315 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 28 Jun 2024 14:35:10 +0800 Subject: [PATCH 06/44] Bump version to 0.33.5 --- Version.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Version.xcconfig b/Version.xcconfig index 8d45ddbc..74114724 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,3 +1,3 @@ -APP_VERSION = 0.33.4 -APP_BUILD = 390 +APP_VERSION = 0.33.5 +APP_BUILD = 391 From ecd711febc88c68d2225575c08e6b29ecc357f99 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 28 Jun 2024 15:01:54 +0800 Subject: [PATCH 07/44] Rename TMPDIR to RELEASEDIR to avoid conflict --- Makefile | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index b16f8417..7d460e78 100644 --- a/Makefile +++ b/Makefile @@ -6,20 +6,20 @@ setup: # Usage: make appcast app=path/to/bundle.app tag=1.0.0 [channel=beta] [release=1] appcast: - $(eval TMPDIR := ~/Library/Caches/CopilotForXcodeRelease/$(shell uuidgen)) + $(eval RELEASEDIR := ~/Library/Caches/CopilotForXcodeRelease/$(shell uuidgen)) $(eval BUNDLENAME := $(shell basename "$(app)")) $(eval WORKDIR := $(shell dirname "$(app)")) $(eval ZIPNAME := $(ZIPNAME_BASE)$(if $(channel),.$(channel).$(if $(release),$(release),1))) $(eval RELEASENOTELINK := $(GITHUB_URL)releases/tag/$(tag)) - mkdir -p $(TMPDIR) - cp appcast.xml $(TMPDIR)/appcast.xml + mkdir -p $(RELEASEDIR) + cp appcast.xml $(RELEASEDIR)/appcast.xml cd $(WORKDIR) && ditto -c -k --sequesterRsrc --keepParent "$(BUNDLENAME)" "$(ZIPNAME).zip" - cd $(WORKDIR) && cp "$(ZIPNAME).zip" $(TMPDIR)/ - touch $(TMPDIR)/$(ZIPNAME).html - echo "" > $(TMPDIR)/$(ZIPNAME).html - -Core/.build/artifacts/sparkle/bin/generate_appcast $(TMPDIR) --download-url-prefix "$(GITHUB_URL)releases/download/$(tag)/" --release-notes-url-prefix "$(RELEASENOTELINK)" $(if $(channel),--channel "$(channel)") - mv -f $(TMPDIR)/appcast.xml . - rm -rf $(TMPDIR) + cd $(WORKDIR) && cp "$(ZIPNAME).zip" $(RELEASEDIR)/ + touch $(RELEASEDIR)/$(ZIPNAME).html + echo "" > $(RELEASEDIR)/$(ZIPNAME).html + -Core/.build/artifacts/sparkle/bin/generate_appcast $(RELEASEDIR) --download-url-prefix "$(GITHUB_URL)releases/download/$(tag)/" --release-notes-url-prefix "$(RELEASENOTELINK)" $(if $(channel),--channel "$(channel)") + mv -f $(RELEASEDIR)/appcast.xml . + rm -rf $(RELEASEDIR) sed -i '' 's/$(ZIPNAME).html/$(tag)/g' appcast.xml -.PHONY: setup appcast \ No newline at end of file +.PHONY: setup appcast From 1926da391c54040fc0a3aac729578d4b715742fc Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 28 Jun 2024 15:27:35 +0800 Subject: [PATCH 08/44] Update appcast file location --- Config.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config.xcconfig b/Config.xcconfig index 1c65b4c7..81d6e2ba 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -3,7 +3,7 @@ SLASH = / HOST_APP_NAME = Copilot for Xcode BUNDLE_IDENTIFIER_BASE = com.intii.CopilotForXcode -SPARKLE_FEED_URL = https:$(SLASH)$(SLASH)raw.githubusercontent.com/intitni/CopilotForXcode/main/appcast.xml +SPARKLE_FEED_URL = https:$(SLASH)$(SLASH)copilotforxcode.intii.com/appcast.xml SPARKLE_PUBLIC_KEY = WDzm5GHnc6c8kjeJEgX5GuGiPpW6Lc/ovGjLnrrZvPY= APPLICATION_SUPPORT_FOLDER = com.intii.CopilotForXcode EXTENSION_BUNDLE_NAME = Copilot From 38de4bc82609081e57fe26b4255d3e9f1757c0e7 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 28 Jun 2024 18:19:03 +0800 Subject: [PATCH 09/44] Remove unused content --- Pro | 2 +- Tool/Package.swift | 2 - .../SharedUIComponents/AsyncCodeBlock.swift | 2 +- .../Experiment/NewCodeBlock.swift | 302 ------------------ 4 files changed, 2 insertions(+), 306 deletions(-) delete mode 100644 Tool/Sources/SharedUIComponents/Experiment/NewCodeBlock.swift diff --git a/Pro b/Pro index 43632988..5e667a80 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 43632988abab32cf7994acc97f3653be1bc48add +Subproject commit 5e667a80660b8d1b05f81a2ea32d1102438eefb9 diff --git a/Tool/Package.swift b/Tool/Package.swift index b1ddc67b..42124850 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -65,7 +65,6 @@ let package = Package( ), .package(url: "https://github.com/apple/swift-syntax.git", exact: "509.0.2"), .package(url: "https://github.com/GottaGetSwifty/CodableWrappers", from: "2.0.7"), - .package(url: "https://github.com/krzyzanowskim/STTextView", from: "0.8.21"), // A fork of https://github.com/google/generative-ai-swift to support setting base url. .package( url: "https://github.com/intitni/generative-ai-swift", @@ -219,7 +218,6 @@ let package = Package( "Preferences", "SuggestionModel", "DebounceFunction", - .product(name: "STTextView", package: "STTextView"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), ] ), diff --git a/Tool/Sources/SharedUIComponents/AsyncCodeBlock.swift b/Tool/Sources/SharedUIComponents/AsyncCodeBlock.swift index f05bdd34..c38d8373 100644 --- a/Tool/Sources/SharedUIComponents/AsyncCodeBlock.swift +++ b/Tool/Sources/SharedUIComponents/AsyncCodeBlock.swift @@ -42,7 +42,7 @@ public struct AsyncCodeBlock: View { ) } else { let intersection = NSIntersectionRange(targetRange, range) - guard !intersection.isEmpty else { return } + guard !(intersection.length == 0) else { return } let rangeA = intersection mutable.addAttribute( .foregroundColor, diff --git a/Tool/Sources/SharedUIComponents/Experiment/NewCodeBlock.swift b/Tool/Sources/SharedUIComponents/Experiment/NewCodeBlock.swift deleted file mode 100644 index c9c45c00..00000000 --- a/Tool/Sources/SharedUIComponents/Experiment/NewCodeBlock.swift +++ /dev/null @@ -1,302 +0,0 @@ -import STTextView -import SwiftUI - -private let insetBottom = 12 as Double -private let insetTop = 12 as Double - -/// This SwiftUI view can be used to view and edit rich text. -struct _CodeBlock: View { - @Binding private var selection: NSRange? - @State private var contentHeight: Double = 500 - let font: NSFont - let commonPrecedingSpaceCount: Int - let highlightedCode: AttributedString - let colorScheme: ColorScheme - let droppingLeadingSpaces: Bool - let scenario: String - - /// Create a text edit view with a certain text that uses a certain options. - /// - Parameters: - /// - text: The attributed string content - /// - options: Editor options - /// - plugins: Editor plugins - public init( - code: String, - language: String, - firstLinePrecedingSpaceCount: Int, - scenario: String, - colorScheme: ColorScheme, - font: NSFont, - droppingLeadingSpaces: Bool, - selection: Binding = .constant(nil) - ) { - _selection = selection - self.font = font - self.colorScheme = colorScheme - self.droppingLeadingSpaces = droppingLeadingSpaces - self.scenario = scenario - - let padding = firstLinePrecedingSpaceCount > 0 - ? String(repeating: " ", count: firstLinePrecedingSpaceCount) - : "" - let result = Self.highlight( - code: padding + code, - language: language, - scenario: scenario, - colorScheme: colorScheme, - font: font, - droppingLeadingSpaces: droppingLeadingSpaces - ) - commonPrecedingSpaceCount = result.commonLeadingSpaceCount - highlightedCode = result.code - } - - public var body: some View { - _CodeBlockRepresentable( - text: highlightedCode, - selection: $selection, - font: font, - onHeightChange: { height in - print("Q", height) - contentHeight = height - } - ) - .frame(height: contentHeight, alignment: .topLeading) - .background(.background) - .colorScheme(colorScheme) - .onAppear { - print("") - } - } - - static func highlight( - code: String, - language: String, - scenario: String, - colorScheme: ColorScheme, - font: NSFont, - droppingLeadingSpaces: Bool - ) -> (code: AttributedString, commonLeadingSpaceCount: Int) { - let (lines, commonLeadingSpaceCount) = CodeHighlighting.highlighted( - code: code, - language: language, - scenario: scenario, - brightMode: colorScheme != .dark, - droppingLeadingSpaces: droppingLeadingSpaces, - font: font, - replaceSpacesWithMiddleDots: false - ) - - let string = NSMutableAttributedString() - for (index, line) in lines.enumerated() { - string.append(line) - if index < lines.count - 1 { - string.append(NSAttributedString(string: "\n")) - } - } - - return (code: .init(string), commonLeadingSpaceCount: commonLeadingSpaceCount) - } -} - -private struct _CodeBlockRepresentable: NSViewRepresentable { - @Environment(\.isEnabled) private var isEnabled - @Environment(\.lineSpacing) private var lineSpacing - - @Binding private var selection: NSRange? - let text: AttributedString - let font: NSFont - let onHeightChange: (Double) -> Void - - init( - text: AttributedString, - selection: Binding, - font: NSFont, - onHeightChange: @escaping (Double) -> Void - ) { - self.text = text - _selection = selection - self.font = font - self.onHeightChange = onHeightChange - } - - func makeNSView(context: Context) -> NSScrollView { - let scrollView = STTextViewFrameObservable.scrollableTextView() - scrollView.contentInsets = .init(top: 0, left: 0, bottom: insetBottom, right: 0) - scrollView.automaticallyAdjustsContentInsets = false - let textView = scrollView.documentView as! STTextView - textView.delegate = context.coordinator - textView.highlightSelectedLine = false - textView.widthTracksTextView = true - textView.heightTracksTextView = true - textView.isEditable = true - - textView.setSelectedRange(NSRange()) - let lineNumberRuler = STLineNumberRulerView(textView: textView) - lineNumberRuler.backgroundColor = .clear - lineNumberRuler.separatorColor = .clear - lineNumberRuler.rulerInsets = .init(leading: 10, trailing: 10) - scrollView.verticalRulerView = lineNumberRuler - let columnNumberRuler = ColumnRuler(textView: textView) - scrollView.horizontalRulerView = columnNumberRuler - scrollView.rulersVisible = true - - context.coordinator.isUpdating = true - textView.setAttributedString(NSAttributedString(text)) - context.coordinator.isUpdating = false - - return scrollView - } - - func updateNSView(_ scrollView: NSScrollView, context: Context) { - context.coordinator.parent = self - - let textView = scrollView.documentView as! STTextViewFrameObservable - - textView.onHeightChange = onHeightChange - textView.showsInvisibleCharacters = true - textView.textContainer.lineBreakMode = .byCharWrapping - - if let columnNumberRuler = scrollView.horizontalRulerView as? ColumnRuler { - columnNumberRuler.columnNumber = 5 - } - - do { - context.coordinator.isUpdating = true - if context.coordinator.isDidChangeText == false { - textView.setAttributedString(.init(text)) - } - context.coordinator.isUpdating = false - context.coordinator.isDidChangeText = false - } - - if textView.selectedRange() != selection, let selection { - textView.setSelectedRange(selection) - } - - if textView.isSelectable != isEnabled { - textView.isSelectable = isEnabled - } - - textView.isEditable = false - - if !textView.widthTracksTextView { - textView.widthTracksTextView = false - } - - if !textView.heightTracksTextView { - textView.heightTracksTextView = true - } - - if textView.font != font { - textView.font = font - } - } - - func makeCoordinator() -> TextCoordinator { - TextCoordinator(parent: self) - } - - private func styledAttributedString(_ typingAttributes: [NSAttributedString.Key: Any]) - -> AttributedString - { - let paragraph = (typingAttributes[.paragraphStyle] as! NSParagraphStyle) - .mutableCopy() as! NSMutableParagraphStyle - if paragraph.lineSpacing != lineSpacing { - paragraph.lineSpacing = lineSpacing - var typingAttributes = typingAttributes - typingAttributes[.paragraphStyle] = paragraph - - let attributeContainer = AttributeContainer(typingAttributes) - var styledText = text - styledText.mergeAttributes(attributeContainer, mergePolicy: .keepNew) - return styledText - } - - return text - } - - class TextCoordinator: STTextViewDelegate { - var parent: _CodeBlockRepresentable - var isUpdating: Bool = false - var isDidChangeText: Bool = false - var enqueuedValue: AttributedString? - - init(parent: _CodeBlockRepresentable) { - self.parent = parent - } - - func textViewDidChangeText(_ notification: Notification) { - guard let textView = notification.object as? STTextView else { - return - } - - (textView as! STTextViewFrameObservable).recalculateSize() - } - - func textViewDidChangeSelection(_ notification: Notification) { - guard let textView = notification.object as? STTextView else { - return - } - - Task { @MainActor in - self.parent.selection = textView.selectedRange() - } - } - } -} - -private class STTextViewFrameObservable: STTextView { - var onHeightChange: ((Double) -> Void)? - func recalculateSize() { - var maxY = 0 as Double - textLayoutManager.enumerateTextLayoutFragments( - in: textLayoutManager.documentRange, - options: [.ensuresLayout] - ) { fragment in - print(fragment.layoutFragmentFrame) - maxY = max(maxY, fragment.layoutFragmentFrame.maxY) - return true - } - onHeightChange?(maxY) - } -} - -private final class ColumnRuler: NSRulerView { - var columnNumber: Int = 0 - - private var textView: STTextView? { - clientView as? STTextView - } - - public required init(textView: STTextView, scrollView: NSScrollView? = nil) { - super.init( - scrollView: scrollView ?? textView.enclosingScrollView, - orientation: .verticalRuler - ) - clientView = textView - ruleThickness = insetBottom - } - - @available(*, unavailable) - required init(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func draw(_: NSRect) { - guard let context: CGContext = NSGraphicsContext.current?.cgContext else { return } - NSColor.windowBackgroundColor.withAlphaComponent(0.6).setFill() - context.fill(bounds) - - let insetLeft = scrollView?.verticalRulerView?.bounds.width ?? 0 - var drawingBounds = bounds - drawingBounds.origin.x += insetLeft + 4 - let fontSize = 10 as Double - drawingBounds.origin.y = (insetTop - fontSize) / 2 - NSString(string: "\(columnNumber)").draw(in: drawingBounds, withAttributes: [ - .font: NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular), - .foregroundColor: NSColor.tertiaryLabelColor, - ]) - } -} - From 9fd0312370f094942253fd8fe5859876648baecb Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 30 May 2024 17:52:11 +0800 Subject: [PATCH 10/44] Add extension identifier to builtin extensions --- Tool/Sources/BuiltinExtension/BuiltinExtension.swift | 2 ++ Tool/Sources/CodeiumService/CodeiumExtension.swift | 2 ++ Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift | 2 ++ 3 files changed, 6 insertions(+) diff --git a/Tool/Sources/BuiltinExtension/BuiltinExtension.swift b/Tool/Sources/BuiltinExtension/BuiltinExtension.swift index 39ccddb7..e9ceb0d4 100644 --- a/Tool/Sources/BuiltinExtension/BuiltinExtension.swift +++ b/Tool/Sources/BuiltinExtension/BuiltinExtension.swift @@ -6,6 +6,8 @@ import Preferences public protocol BuiltinExtension: CopilotForXcodeExtensionCapability { /// An id that let the extension manager determine whether the extension is in use. var suggestionServiceId: BuiltInSuggestionFeatureProvider { get } + /// An identifier for the extension. + var extensionIdentifier: String { get } /// All chat builders provided by this extension. var chatTabTypes: [any ChatTab.Type] { get } diff --git a/Tool/Sources/CodeiumService/CodeiumExtension.swift b/Tool/Sources/CodeiumService/CodeiumExtension.swift index 25d4e2ef..93ab66da 100644 --- a/Tool/Sources/CodeiumService/CodeiumExtension.swift +++ b/Tool/Sources/CodeiumService/CodeiumExtension.swift @@ -12,6 +12,8 @@ import Workspace } public final class CodeiumExtension: BuiltinExtension { + public var extensionIdentifier: String { "com.codeium" } + public var suggestionServiceId: Preferences.BuiltInSuggestionFeatureProvider { .codeium } public let suggestionService: CodeiumSuggestionService? diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift index c0c51793..04bcce65 100644 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift +++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift @@ -7,6 +7,8 @@ import Preferences import Workspace public final class GitHubCopilotExtension: BuiltinExtension { + public var extensionIdentifier: String { "com.github.copilot" } + public var suggestionServiceId: Preferences.BuiltInSuggestionFeatureProvider { .gitHubCopilot } public let suggestionService: GitHubCopilotSuggestionService? From 8f94fd6c87996d5b6d2bca5fe2bc165f8d6df079 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 27 Jun 2024 15:46:31 +0800 Subject: [PATCH 11/44] Add naive implementation of GitHubCopilotChatService --- .../BuiltinExtension/BuiltinExtension.swift | 27 ++++ .../GitHubCopilotExtension.swift | 2 + .../LanguageServer/GitHubCopilotRequest.swift | 126 +++++++++++++++++ .../LanguageServer/GitHubCopilotService.swift | 36 ++++- .../Services/GitHubCopilotChatService.swift | 128 ++++++++++++++++++ 5 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift diff --git a/Tool/Sources/BuiltinExtension/BuiltinExtension.swift b/Tool/Sources/BuiltinExtension/BuiltinExtension.swift index e9ceb0d4..7effe675 100644 --- a/Tool/Sources/BuiltinExtension/BuiltinExtension.swift +++ b/Tool/Sources/BuiltinExtension/BuiltinExtension.swift @@ -1,3 +1,4 @@ +import ChatContextCollector import ChatTab import CopilotForXcodeKit import Foundation @@ -23,3 +24,29 @@ public extension BuiltinExtension { var chatTabTypes: [any ChatTab.Type] { [] } } +// MAKR: - ChatService + +/// A temporary protocol for ChatServiceType. Migrate it to CopilotForXcodeKit when finished. +public protocol BuiltinExtensionChatServiceType: ChatServiceType { + typealias Message = ChatServiceMessage + typealias RetrievedContent = ChatContext.RetrievedContent +} + +public struct ChatServiceMessage: Codable { + public enum Role: Codable, Equatable { + case system + case user + case assistant + case tool + case other(String) + } + + public var role: Role + public var text: String + + public init(role: Role, text: String) { + self.role = role + self.text = text + } +} + diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift index 04bcce65..6331a168 100644 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift +++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift @@ -12,6 +12,7 @@ public final class GitHubCopilotExtension: BuiltinExtension { public var suggestionServiceId: Preferences.BuiltInSuggestionFeatureProvider { .gitHubCopilot } public let suggestionService: GitHubCopilotSuggestionService? + public let chatService: GitHubCopilotChatService? private var extensionUsage = ExtensionUsage( isSuggestionServiceInUse: false, @@ -29,6 +30,7 @@ public final class GitHubCopilotExtension: BuiltinExtension { self.workspacePool = workspacePool serviceLocator = ServiceLocator(workspacePool: workspacePool) suggestionService = .init(serviceLocator: serviceLocator) + chatService = .init(serviceLocator: serviceLocator) } public func workspaceDidOpen(_: WorkspaceInfo) {} diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift index 2b1c107d..22c8e789 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift @@ -49,6 +49,11 @@ public struct GitHubCopilotCodeSuggestion: Codable, Equatable { public var displayText: String } +enum GitHubCopilotChatSource: String, Codable { + case panel + case inline +} + enum GitHubCopilotRequest { struct SetEditorInfo: GitHubCopilotRequestType { struct Response: Codable {} @@ -330,5 +335,126 @@ enum GitHubCopilotRequest { ])) } } + + struct ConversationCreate: GitHubCopilotRequestType { + struct Response: Codable { + var conversationId: String + var turnId: String + } + + struct RequestBody: Codable { + var workDoneToken: String + var turns: [Turn]; struct Turn: Codable { + var request: String + var response: String? + } + + var capabilities: [Capabilities]; struct Capabilities: Codable { + var allSkills: Bool? + var skills: [String] + } + + var options: [String: String]? + var doc: GitHubCopilotDoc? + var computeSuggestions: Bool? + var references: [Reference]?; struct Reference: Codable { + var uri: String + var position: Position? + var visibleRange: CursorRange? + var selectionRange: CursorRange? + var openedAt: Date? + var activatedAt: Date? + } + + var source: GitHubCopilotChatSource? // inline or panel + var workspaceFolder: String? + } + + let requestBody: RequestBody + + var request: ClientRequest { + let data = (try? JSONEncoder().encode(requestBody)) ?? Data() + let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:]) + return .custom("conversation/create", .hash([ + "doc": dict, + ])) + } + } + + struct ConversationTurn: GitHubCopilotRequestType { + struct Response: Codable {} + + struct RequestBody: Codable { + var workDoneToken: String + var conversationId: String + var message: String + var followUp: FollowUp?; struct FollowUp: Codable { + var id: String + var type: String + } + + var options: [String: String]? + var doc: GitHubCopilotDoc? + var computeSuggestions: Bool? + var references: [Reference]?; struct Reference: Codable { + var uri: String + var position: Position? + var visibleRange: CursorRange? + var selectionRange: CursorRange? + var openedAt: Date? + var activatedAt: Date? + } + + var workspaceFolder: String? + } + + let requestBody: RequestBody + + var request: ClientRequest { + let data = (try? JSONEncoder().encode(requestBody)) ?? Data() + let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:]) + return .custom("conversation/turn", .hash([ + "doc": dict, + ])) + } + } + + struct ConversationTurnDelete: GitHubCopilotRequestType { + struct Response: Codable {} + + struct RequestBody: Codable { + var conversationId: String + var turnId: String + var options: [String: String]? + var source: GitHubCopilotChatSource? + } + + let requestBody: RequestBody + + var request: ClientRequest { + let data = (try? JSONEncoder().encode(requestBody)) ?? Data() + let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:]) + return .custom("conversation/turnDelete", .hash([ + "doc": dict, + ])) + } + } + + struct ConversationDestroy: GitHubCopilotRequestType { + struct Response: Codable {} + + struct RequestBody: Codable { + var conversationId: String + var options: [String: String]? + } + + let requestBody: RequestBody + + var request: ClientRequest { + let data = (try? JSONEncoder().encode(requestBody)) ?? Data() + let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:]) + return .custom("conversation/destroy", dict) + } + } } diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift index bb7a0fe1..a7115a4f 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift @@ -96,6 +96,9 @@ public class GitHubCopilotBaseService { let projectRootURL: URL var server: GitHubCopilotLSP var localProcessServer: CopilotLocalProcessServer? + @GitHubCopilotSuggestionActor + private var serverNotificationHandlers = + [AnyHashable: (ServerNotification) async throws -> Bool]() deinit { localProcessServer?.terminate() @@ -199,8 +202,22 @@ public class GitHubCopilotBaseService { let localServer = CopilotLocalProcessServer(executionParameters: executionParams) localServer.logMessages = UserDefaults.shared.value(for: \.gitHubCopilotVerboseLog) - localServer.notificationHandler = { notification, respond in - respond(.handlerUnavailable(notification.method.rawValue)) + localServer.notificationHandler = { [weak self] notification, respond in + Task { + for handler in self?.serverNotificationHandlers.values ?? [] { + do { + let handled = try await handler(notification) + if handled { + respond(nil) + return + } + } catch { + respond(.failure(error)) + return + } + } + respond(.handlerUnavailable(notification.method.rawValue)) + } } let server = InitializingServer(server: localServer) @@ -286,6 +303,21 @@ public class GitHubCopilotBaseService { return (supportURL, gitHubCopilotFolderURL, executableFolderURL, supportFolderURL) } + + func registerNotificationHandler( + id: AnyHashable, + _ block: @escaping (ServerNotification) async throws -> Bool + ) { + Task { @GitHubCopilotSuggestionActor in + self.serverNotificationHandlers[id] = block + } + } + + func unregisterNotificationHandler(id: AnyHashable) { + Task { @GitHubCopilotSuggestionActor in + self.serverNotificationHandlers[id] = nil + } + } } public final class GitHubCopilotAuthService: GitHubCopilotBaseService, diff --git a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift b/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift new file mode 100644 index 00000000..5ff08e9a --- /dev/null +++ b/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift @@ -0,0 +1,128 @@ +import BuiltinExtension +import CopilotForXcodeKit +import Foundation +import XcodeInspector + +public final class GitHubCopilotChatService: BuiltinExtensionChatServiceType { + let serviceLocator: any ServiceLocatorType + + init(serviceLocator: any ServiceLocatorType) { + self.serviceLocator = serviceLocator + } + + /// - note: Let's do it in a naive way for proof of concept. We will create a new chat for each + /// message in this version. + public func sendMessage( + _ message: String, + history: [Message], + references: [RetrievedContent], + workspace: WorkspaceInfo + ) async -> AsyncThrowingStream { + guard let service = await serviceLocator.getService(from: workspace) + else { return .finished(throwing: CancellationError()) } + let id = UUID().uuidString + let editorContent = await XcodeInspector.shared.getFocusedEditorContent() + do { + let createResponse = try await service.server + .sendRequest(GitHubCopilotRequest.ConversationCreate(requestBody: .init( + workDoneToken: "", + turns: convertHistory(history: history), + capabilities: [ + .init(allSkills: true, skills: []), + ], + doc: .init( + source: editorContent?.editorContent?.content ?? "", + tabSize: 1, + indentSize: 4, + insertSpaces: true, + path: editorContent?.documentURL.path ?? "", + uri: editorContent?.documentURL.path ?? "", + relativePath: editorContent?.relativePath ?? "", + languageId: editorContent?.language.rawValue ?? "plaintext", + position: .zero + ), + source: .panel, + workspaceFolder: workspace.projectURL.path + ))) + + let stream = AsyncThrowingStream.init { continuation in + service.registerNotificationHandler(id: id) { notification in + if notification.method.rawValue == "" { + return true + } + + return false + } + + continuation.onTermination = { _ in + Task { + try await service.server.sendRequest( + GitHubCopilotRequest.ConversationDestroy(requestBody: .init( + conversationId: createResponse.conversationId + )) + ) + } + } + } + + _ = try await service.server + .sendRequest(GitHubCopilotRequest.ConversationTurn(requestBody: .init( + workDoneToken: "", + conversationId: createResponse.conversationId, + message: message + ))) + + return stream + } catch { + return .finished(throwing: error) + } + } +} + +extension GitHubCopilotChatService { + typealias Turn = GitHubCopilotRequest.ConversationCreate.RequestBody.Turn + func convertHistory(history: [Message]) -> [Turn] { + guard let firstIndexOfUserMessage = history.firstIndex(where: { $0.role == .user }) + else { return [] } + + var currentTurn = Turn(request: "", response: nil) + var turns: [Turn] = [] + for i in firstIndexOfUserMessage.. String { + return message + } +} + From f7c0cc0b6c3ccba63bea6582198fd7845ca9df36 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 28 Jun 2024 14:08:53 +0800 Subject: [PATCH 12/44] Move chat related models to ChatBasic --- .../QueryWebsiteFunction.swift | 1 + .../SearchFunction.swift | 1 + .../WebChatContextCollector.swift | 1 + .../ChatService/ChatFunctionProvider.swift | 1 + Pro | 2 +- Tool/Package.swift | 16 ++ .../ChatGPTFunction.swift | 10 +- Tool/Sources/ChatBasic/ChatMessage.swift | 163 ++++++++++++++++++ .../JSONSchema.swift | 0 .../ChatContextCollector.swift | 3 +- .../ActiveDocumentChatContextCollector.swift | 1 + .../GetCodeCodeAroundLineFunction.swift | 1 + .../LanguageServer/GitHubCopilotRequest.swift | 1 - Tool/Sources/LangChain/AgentTool.swift | 3 +- .../Chains/RefineDocumentChain.swift | 1 + .../RelevantInformationExtractionChain.swift | 1 + .../StructuredOutputChatModelChain.swift | 1 + .../APIs/ChatCompletionsAPIDefinition.swift | 1 + .../APIs/OpenAIChatCompletionsService.swift | 1 + .../OpenAIService/ChatGPTService.swift | 1 + .../Configuration/ChatGPTConfiguration.swift | 88 ++++++++++ .../EmbeddingConfiguration.swift | 49 ++++++ .../UserPreferenceChatGPTConfiguration.swift | 90 +--------- ...UserPreferenceEmbeddingConfiguration.swift | 50 +----- .../OpenAIService/EmbeddingService.swift | 1 + .../FucntionCall/ChatGPTFuntionProvider.swift | 2 + .../Memory/AutoManagedChatGPTMemory.swift | 1 + ...ManagedChatGPTMemoryGoogleAIStrategy.swift | 2 +- ...toManagedChatGPTMemoryOpenAIStrategy.swift | 1 + Tool/Sources/OpenAIService/Models.swift | 161 +---------------- 30 files changed, 349 insertions(+), 306 deletions(-) rename Tool/Sources/{OpenAIService/FucntionCall => ChatBasic}/ChatGPTFunction.swift (93%) create mode 100644 Tool/Sources/ChatBasic/ChatMessage.swift rename Tool/Sources/{OpenAIService/FucntionCall => ChatBasic}/JSONSchema.swift (100%) diff --git a/Core/Sources/ChatContextCollectors/WebChatContextCollector/QueryWebsiteFunction.swift b/Core/Sources/ChatContextCollectors/WebChatContextCollector/QueryWebsiteFunction.swift index e4a22903..6d5de208 100644 --- a/Core/Sources/ChatContextCollectors/WebChatContextCollector/QueryWebsiteFunction.swift +++ b/Core/Sources/ChatContextCollectors/WebChatContextCollector/QueryWebsiteFunction.swift @@ -1,3 +1,4 @@ +import ChatBasic import Foundation import LangChain import OpenAIService diff --git a/Core/Sources/ChatContextCollectors/WebChatContextCollector/SearchFunction.swift b/Core/Sources/ChatContextCollectors/WebChatContextCollector/SearchFunction.swift index 99c88312..56719f5c 100644 --- a/Core/Sources/ChatContextCollectors/WebChatContextCollector/SearchFunction.swift +++ b/Core/Sources/ChatContextCollectors/WebChatContextCollector/SearchFunction.swift @@ -1,4 +1,5 @@ import BingSearchService +import ChatBasic import Foundation import OpenAIService import Preferences diff --git a/Core/Sources/ChatContextCollectors/WebChatContextCollector/WebChatContextCollector.swift b/Core/Sources/ChatContextCollectors/WebChatContextCollector/WebChatContextCollector.swift index 851fdcf7..848ca0fa 100644 --- a/Core/Sources/ChatContextCollectors/WebChatContextCollector/WebChatContextCollector.swift +++ b/Core/Sources/ChatContextCollectors/WebChatContextCollector/WebChatContextCollector.swift @@ -1,3 +1,4 @@ +import ChatBasic import ChatContextCollector import Foundation import OpenAIService diff --git a/Core/Sources/ChatService/ChatFunctionProvider.swift b/Core/Sources/ChatService/ChatFunctionProvider.swift index dffab8f2..de8ca5bf 100644 --- a/Core/Sources/ChatService/ChatFunctionProvider.swift +++ b/Core/Sources/ChatService/ChatFunctionProvider.swift @@ -1,3 +1,4 @@ +import ChatBasic import Foundation import OpenAIService diff --git a/Pro b/Pro index 5e667a80..a3a3b9cc 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 5e667a80660b8d1b05f81a2ea32d1102438eefb9 +Subproject commit a3a3b9cc93dd128911b4e718511dc4b1da5303ce diff --git a/Tool/Package.swift b/Tool/Package.swift index 42124850..67d3cc77 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -170,6 +170,16 @@ let package = Package( name: "SuggestionModelTests", dependencies: ["SuggestionModel"] ), + + .target( + name: "ChatBasic", + dependencies: [ + "AIModel", + "Preferences", + "Keychain", + .product(name: "CodableWrappers", package: "CodableWrappers"), + ] + ), .target(name: "AXExtension"), @@ -205,8 +215,10 @@ let package = Package( name: "BuiltinExtension", dependencies: [ "SuggestionModel", + "ChatBasic", "Workspace", "ChatTab", + "AIModel", .product(name: "CopilotForXcodeKit", package: "CopilotForXcodeKit"), ] ), @@ -288,6 +300,7 @@ let package = Package( "OpenAIService", "ObjectiveCExceptionHandling", "USearchIndex", + "ChatBasic", .product(name: "Parsing", package: "swift-parsing"), .product(name: "SwiftSoup", package: "SwiftSoup"), ] @@ -312,6 +325,7 @@ let package = Package( dependencies: [ "LanguageClient", "SuggestionModel", + "ChatBasic", "Logger", "Preferences", "Terminal", @@ -354,6 +368,7 @@ let package = Package( "Preferences", "TokenEncoder", "Keychain", + "BuiltinExtension", .product(name: "JSONRPC", package: "JSONRPC"), .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), .product(name: "GoogleGenerativeAI", package: "generative-ai-swift"), @@ -390,6 +405,7 @@ let package = Package( name: "ChatContextCollector", dependencies: [ "SuggestionModel", + "ChatBasic", "OpenAIService", ] ), diff --git a/Tool/Sources/OpenAIService/FucntionCall/ChatGPTFunction.swift b/Tool/Sources/ChatBasic/ChatGPTFunction.swift similarity index 93% rename from Tool/Sources/OpenAIService/FucntionCall/ChatGPTFunction.swift rename to Tool/Sources/ChatBasic/ChatGPTFunction.swift index d2d8aaad..1131c247 100644 --- a/Tool/Sources/OpenAIService/FucntionCall/ChatGPTFunction.swift +++ b/Tool/Sources/ChatBasic/ChatGPTFunction.swift @@ -100,12 +100,12 @@ public extension ChatGPTArgumentsCollectingFunction { } } -struct ChatGPTFunctionSchema: Codable, Equatable { - var name: String - var description: String - var parameters: JSONSchemaValue +public struct ChatGPTFunctionSchema: Codable, Equatable { + public var name: String + public var description: String + public var parameters: JSONSchemaValue - init(name: String, description: String, parameters: JSONSchemaValue) { + public init(name: String, description: String, parameters: JSONSchemaValue) { self.name = name self.description = description self.parameters = parameters diff --git a/Tool/Sources/ChatBasic/ChatMessage.swift b/Tool/Sources/ChatBasic/ChatMessage.swift new file mode 100644 index 00000000..689fc0e5 --- /dev/null +++ b/Tool/Sources/ChatBasic/ChatMessage.swift @@ -0,0 +1,163 @@ +import CodableWrappers +import Foundation + +public struct ChatMessage: Equatable, Codable { + public typealias ID = String + + public enum Role: String, Codable, Equatable { + case system + case user + case assistant + } + + public struct FunctionCall: Codable, Equatable { + public var name: String + public var arguments: String + public init(name: String, arguments: String) { + self.name = name + self.arguments = arguments + } + } + + public struct ToolCall: Codable, Equatable, Identifiable { + public var id: String + public var type: String + public var function: FunctionCall + public var response: ToolCallResponse + public init( + id: String, + type: String, + function: FunctionCall, + response: ToolCallResponse? = nil + ) { + self.id = id + self.type = type + self.function = function + self.response = response ?? .init(content: "", summary: nil) + } + } + + public struct ToolCallResponse: Codable, Equatable { + public var content: String + public var summary: String? + public init(content: String, summary: String?) { + self.content = content + self.summary = summary + } + } + + public struct Reference: Codable, Equatable { + public enum Kind: String, Codable { + case `class` + case `struct` + case `enum` + case `actor` + case `protocol` + case `extension` + case `case` + case property + case `typealias` + case function + case method + case text + case webpage + case other + } + + public var title: String + public var subTitle: String + public var uri: String + public var content: String + public var startLine: Int? + public var endLine: Int? + @FallbackDecoding + public var kind: Kind + + public init( + title: String, + subTitle: String, + content: String, + uri: String, + startLine: Int?, + endLine: Int?, + kind: Kind + ) { + self.title = title + self.subTitle = subTitle + self.content = content + self.uri = uri + self.startLine = startLine + self.endLine = endLine + self.kind = kind + } + } + + /// The role of a message. + @FallbackDecoding + public var role: Role + + /// The content of the message, either the chat message, or a result of a function call. + public var content: String? { + didSet { tokensCount = nil } + } + + /// A function call from the bot. + public var toolCalls: [ToolCall]? { + didSet { tokensCount = nil } + } + + /// The function name of a reply to a function call. + public var name: String? { + didSet { tokensCount = nil } + } + + /// The summary of a message that is used for display. + public var summary: String? + + /// The id of the message. + public var id: ID + + /// The number of tokens of this message. + public var tokensCount: Int? + + /// The references of this message. + @FallbackDecoding> + public var references: [Reference] + + /// Is the message considered empty. + public var isEmpty: Bool { + if let content, !content.isEmpty { return false } + if let toolCalls, !toolCalls.isEmpty { return false } + if let name, !name.isEmpty { return false } + return true + } + + public init( + id: String = UUID().uuidString, + role: Role, + content: String?, + name: String? = nil, + toolCalls: [ToolCall]? = nil, + summary: String? = nil, + tokenCount: Int? = nil, + references: [Reference] = [] + ) { + self.role = role + self.content = content + self.name = name + self.toolCalls = toolCalls + self.summary = summary + self.id = id + tokensCount = tokenCount + self.references = references + } +} + +public struct ReferenceKindFallback: FallbackValueProvider { + public static var defaultValue: ChatMessage.Reference.Kind { .other } +} + +public struct ChatMessageRoleFallback: FallbackValueProvider { + public static var defaultValue: ChatMessage.Role { .user } +} + diff --git a/Tool/Sources/OpenAIService/FucntionCall/JSONSchema.swift b/Tool/Sources/ChatBasic/JSONSchema.swift similarity index 100% rename from Tool/Sources/OpenAIService/FucntionCall/JSONSchema.swift rename to Tool/Sources/ChatBasic/JSONSchema.swift diff --git a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift index 711620b5..3e45ed63 100644 --- a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift @@ -1,3 +1,4 @@ +import ChatBasic import Foundation import OpenAIService import Parsing @@ -14,7 +15,7 @@ public struct ChatContext { public struct RetrievedContent { public var document: ChatMessage.Reference public var priority: Int - + public init(document: ChatMessage.Reference, priority: Int) { self.document = document self.priority = priority diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift index d88a9da9..297b8521 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift @@ -1,4 +1,5 @@ import ASTParser +import ChatBasic import ChatContextCollector import Dependencies import FocusedCodeFinder diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/GetCodeCodeAroundLineFunction.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/GetCodeCodeAroundLineFunction.swift index fd72e225..31d14ba1 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/GetCodeCodeAroundLineFunction.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/GetCodeCodeAroundLineFunction.swift @@ -1,4 +1,5 @@ import ASTParser +import ChatBasic import Foundation import OpenAIService import SuggestionModel diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift index 22c8e789..c8975a6c 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift @@ -13,7 +13,6 @@ struct GitHubCopilotDoc: Codable { var relativePath: String var languageId: CodeLanguage var position: Position - /// Buffer version. Not sure what this is for, not sure how to get it var version: Int = 0 } diff --git a/Tool/Sources/LangChain/AgentTool.swift b/Tool/Sources/LangChain/AgentTool.swift index d221adad..652bbfac 100644 --- a/Tool/Sources/LangChain/AgentTool.swift +++ b/Tool/Sources/LangChain/AgentTool.swift @@ -1,3 +1,4 @@ +import ChatBasic import Foundation import OpenAIService @@ -36,7 +37,7 @@ public class FunctionCallingAgentTool: AgentTool, ChatGPTFun try await function.call(arguments: arguments, reportProgress: reportProgress) } - public var argumentSchema: OpenAIService.JSONSchemaValue { function.argumentSchema } + public var argumentSchema: ChatBasic.JSONSchemaValue { function.argumentSchema } public typealias Arguments = F.Arguments public typealias Result = F.Result diff --git a/Tool/Sources/LangChain/Chains/RefineDocumentChain.swift b/Tool/Sources/LangChain/Chains/RefineDocumentChain.swift index 3b24e6ad..b46974cd 100644 --- a/Tool/Sources/LangChain/Chains/RefineDocumentChain.swift +++ b/Tool/Sources/LangChain/Chains/RefineDocumentChain.swift @@ -1,5 +1,6 @@ import Foundation import OpenAIService +import ChatBasic public final class RefineDocumentChain: Chain { public struct Input { diff --git a/Tool/Sources/LangChain/Chains/RelevantInformationExtractionChain.swift b/Tool/Sources/LangChain/Chains/RelevantInformationExtractionChain.swift index 4c9f696a..4d7d358f 100644 --- a/Tool/Sources/LangChain/Chains/RelevantInformationExtractionChain.swift +++ b/Tool/Sources/LangChain/Chains/RelevantInformationExtractionChain.swift @@ -1,3 +1,4 @@ +import ChatBasic import Foundation import OpenAIService diff --git a/Tool/Sources/LangChain/Chains/StructuredOutputChatModelChain.swift b/Tool/Sources/LangChain/Chains/StructuredOutputChatModelChain.swift index 6ea1dbb5..b362bbce 100644 --- a/Tool/Sources/LangChain/Chains/StructuredOutputChatModelChain.swift +++ b/Tool/Sources/LangChain/Chains/StructuredOutputChatModelChain.swift @@ -1,3 +1,4 @@ +import ChatBasic import Foundation import Logger import OpenAIService diff --git a/Tool/Sources/OpenAIService/APIs/ChatCompletionsAPIDefinition.swift b/Tool/Sources/OpenAIService/APIs/ChatCompletionsAPIDefinition.swift index a86aba7b..39235219 100644 --- a/Tool/Sources/OpenAIService/APIs/ChatCompletionsAPIDefinition.swift +++ b/Tool/Sources/OpenAIService/APIs/ChatCompletionsAPIDefinition.swift @@ -2,6 +2,7 @@ import AIModel import CodableWrappers import Foundation import Preferences +import ChatBasic struct ChatCompletionsRequestBody: Codable, Equatable { struct Message: Codable, Equatable { diff --git a/Tool/Sources/OpenAIService/APIs/OpenAIChatCompletionsService.swift b/Tool/Sources/OpenAIService/APIs/OpenAIChatCompletionsService.swift index 07b27ce2..7a6672cb 100644 --- a/Tool/Sources/OpenAIService/APIs/OpenAIChatCompletionsService.swift +++ b/Tool/Sources/OpenAIService/APIs/OpenAIChatCompletionsService.swift @@ -1,5 +1,6 @@ import AIModel import AsyncAlgorithms +import ChatBasic import Foundation import Logger import Preferences diff --git a/Tool/Sources/OpenAIService/ChatGPTService.swift b/Tool/Sources/OpenAIService/ChatGPTService.swift index 76b39322..c2f718f6 100644 --- a/Tool/Sources/OpenAIService/ChatGPTService.swift +++ b/Tool/Sources/OpenAIService/ChatGPTService.swift @@ -1,5 +1,6 @@ import AIModel import AsyncAlgorithms +import ChatBasic import Dependencies import Foundation import IdentifiedCollections diff --git a/Tool/Sources/OpenAIService/Configuration/ChatGPTConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/ChatGPTConfiguration.swift index 9f464233..3b0bd896 100644 --- a/Tool/Sources/OpenAIService/Configuration/ChatGPTConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/ChatGPTConfiguration.swift @@ -39,3 +39,91 @@ public extension ChatGPTConfiguration { } } +public class OverridingChatGPTConfiguration: ChatGPTConfiguration { + public struct Overriding: Codable { + public var temperature: Double? + public var modelId: String? + public var model: ChatModel? + public var stop: [String]? + public var maxTokens: Int? + public var minimumReplyTokens: Int? + public var runFunctionsAutomatically: Bool? + public var apiKey: String? + + public init( + temperature: Double? = nil, + modelId: String? = nil, + model: ChatModel? = nil, + stop: [String]? = nil, + maxTokens: Int? = nil, + minimumReplyTokens: Int? = nil, + runFunctionsAutomatically: Bool? = nil, + apiKey: String? = nil + ) { + self.temperature = temperature + self.modelId = modelId + self.model = model + self.stop = stop + self.maxTokens = maxTokens + self.minimumReplyTokens = minimumReplyTokens + self.runFunctionsAutomatically = runFunctionsAutomatically + self.apiKey = apiKey + } + } + + private let configuration: ChatGPTConfiguration + public var overriding = Overriding() + public var textWindowTerminator: ((String) -> Bool)? + + public init( + overriding configuration: any ChatGPTConfiguration, + with overrides: Overriding = .init() + ) { + overriding = overrides + self.configuration = configuration + } + + public var temperature: Double { + overriding.temperature ?? configuration.temperature + } + + public var model: ChatModel? { + if let model = overriding.model { return model } + let models = UserDefaults.shared.value(for: \.chatModels) + guard let id = overriding.modelId else { return configuration.model } + if id == "com.github.copilot" { + return .init(id: id, name: "GitHub Copilot", format: .openAI, info: .init()) + } + guard let model = models.first(where: { $0.id == id }) else { return configuration.model } + return model + } + + public var stop: [String] { + overriding.stop ?? configuration.stop + } + + public var maxTokens: Int { + if let maxTokens = overriding.maxTokens { return maxTokens } + if let model { return model.info.maxTokens } + return configuration.maxTokens + } + + public var minimumReplyTokens: Int { + if let minimumReplyTokens = overriding.minimumReplyTokens { return minimumReplyTokens } + return maxTokens / 5 + } + + public var runFunctionsAutomatically: Bool { + overriding.runFunctionsAutomatically ?? configuration.runFunctionsAutomatically + } + + public var apiKey: String { + if let apiKey = overriding.apiKey { return apiKey } + guard let name = model?.info.apiKeyName else { return configuration.apiKey } + return (try? Keychain.apiKey.get(name)) ?? configuration.apiKey + } + + public var shouldEndTextWindow: (String) -> Bool { + textWindowTerminator ?? configuration.shouldEndTextWindow + } +} diff --git a/Tool/Sources/OpenAIService/Configuration/EmbeddingConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/EmbeddingConfiguration.swift index fdaba303..267f1945 100644 --- a/Tool/Sources/OpenAIService/Configuration/EmbeddingConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/EmbeddingConfiguration.swift @@ -35,3 +35,52 @@ public extension EmbeddingConfiguration { } } +public class OverridingEmbeddingConfiguration: EmbeddingConfiguration { + public struct Overriding { + public var modelId: String? + public var model: EmbeddingModel? + public var maxTokens: Int? + public var dimensions: Int? + + public init( + modelId: String? = nil, + model: EmbeddingModel? = nil, + maxTokens: Int? = nil, + dimensions: Int? = nil + ) { + self.modelId = modelId + self.model = model + self.maxTokens = maxTokens + self.dimensions = dimensions + } + } + + private let configuration: EmbeddingConfiguration + public var overriding = Overriding() + + public init( + overriding configuration: any EmbeddingConfiguration, + with overrides: Overriding = .init() + ) { + overriding = overrides + self.configuration = configuration + } + + public var model: EmbeddingModel? { + if let model = overriding.model { return model } + let models = UserDefaults.shared.value(for: \.embeddingModels) + guard let id = overriding.modelId, + let model = models.first(where: { $0.id == id }) + else { return configuration.model } + return model + } + + public var maxToken: Int { + overriding.maxTokens ?? configuration.maxToken + } + + public var dimensions: Int { + overriding.dimensions ?? configuration.dimensions + } +} + diff --git a/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift index 2272d60e..9c1b7fd7 100644 --- a/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift @@ -1,4 +1,5 @@ import AIModel +import ChatBasic import Foundation import Keychain import Preferences @@ -40,7 +41,7 @@ public struct UserPreferenceChatGPTConfiguration: ChatGPTConfiguration { public var runFunctionsAutomatically: Bool { true } - + public var shouldEndTextWindow: (String) -> Bool { { _ in true } } @@ -50,90 +51,3 @@ public struct UserPreferenceChatGPTConfiguration: ChatGPTConfiguration { } } -public class OverridingChatGPTConfiguration: ChatGPTConfiguration { - public struct Overriding: Codable { - public var temperature: Double? - public var modelId: String? - public var model: ChatModel? - public var stop: [String]? - public var maxTokens: Int? - public var minimumReplyTokens: Int? - public var runFunctionsAutomatically: Bool? - public var apiKey: String? - - public init( - temperature: Double? = nil, - modelId: String? = nil, - model: ChatModel? = nil, - stop: [String]? = nil, - maxTokens: Int? = nil, - minimumReplyTokens: Int? = nil, - runFunctionsAutomatically: Bool? = nil, - apiKey: String? = nil - ) { - self.temperature = temperature - self.modelId = modelId - self.model = model - self.stop = stop - self.maxTokens = maxTokens - self.minimumReplyTokens = minimumReplyTokens - self.runFunctionsAutomatically = runFunctionsAutomatically - self.apiKey = apiKey - } - } - - private let configuration: ChatGPTConfiguration - public var overriding = Overriding() - public var textWindowTerminator: ((String) -> Bool)? - - public init( - overriding configuration: any ChatGPTConfiguration, - with overrides: Overriding = .init() - ) { - overriding = overrides - self.configuration = configuration - } - - public var temperature: Double { - overriding.temperature ?? configuration.temperature - } - - public var model: ChatModel? { - if let model = overriding.model { return model } - let models = UserDefaults.shared.value(for: \.chatModels) - guard let id = overriding.modelId, - let model = models.first(where: { $0.id == id }) - else { return configuration.model } - return model - } - - public var stop: [String] { - overriding.stop ?? configuration.stop - } - - public var maxTokens: Int { - if let maxTokens = overriding.maxTokens { return maxTokens } - if let model { return model.info.maxTokens } - return configuration.maxTokens - } - - public var minimumReplyTokens: Int { - if let minimumReplyTokens = overriding.minimumReplyTokens { return minimumReplyTokens } - return maxTokens / 5 - } - - public var runFunctionsAutomatically: Bool { - overriding.runFunctionsAutomatically ?? configuration.runFunctionsAutomatically - } - - public var apiKey: String { - if let apiKey = overriding.apiKey { return apiKey } - guard let name = model?.info.apiKeyName else { return configuration.apiKey } - return (try? Keychain.apiKey.get(name)) ?? configuration.apiKey - } - - public var shouldEndTextWindow: (String) -> Bool { - textWindowTerminator ?? configuration.shouldEndTextWindow - } -} - diff --git a/Tool/Sources/OpenAIService/Configuration/UserPreferenceEmbeddingConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/UserPreferenceEmbeddingConfiguration.swift index de400ba5..d7c711de 100644 --- a/Tool/Sources/OpenAIService/Configuration/UserPreferenceEmbeddingConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/UserPreferenceEmbeddingConfiguration.swift @@ -1,4 +1,5 @@ import AIModel +import ChatBasic import Foundation import Preferences @@ -39,52 +40,3 @@ public struct UserPreferenceEmbeddingConfiguration: EmbeddingConfiguration { } } -public class OverridingEmbeddingConfiguration: EmbeddingConfiguration { - public struct Overriding { - public var modelId: String? - public var model: EmbeddingModel? - public var maxTokens: Int? - public var dimensions: Int? - - public init( - modelId: String? = nil, - model: EmbeddingModel? = nil, - maxTokens: Int? = nil, - dimensions: Int? = nil - ) { - self.modelId = modelId - self.model = model - self.maxTokens = maxTokens - self.dimensions = dimensions - } - } - - private let configuration: EmbeddingConfiguration - public var overriding = Overriding() - - public init( - overriding configuration: any EmbeddingConfiguration, - with overrides: Overriding = .init() - ) { - overriding = overrides - self.configuration = configuration - } - - public var model: EmbeddingModel? { - if let model = overriding.model { return model } - let models = UserDefaults.shared.value(for: \.embeddingModels) - guard let id = overriding.modelId, - let model = models.first(where: { $0.id == id }) - else { return configuration.model } - return model - } - - public var maxToken: Int { - overriding.maxTokens ?? configuration.maxToken - } - - public var dimensions: Int { - overriding.dimensions ?? configuration.dimensions - } -} - diff --git a/Tool/Sources/OpenAIService/EmbeddingService.swift b/Tool/Sources/OpenAIService/EmbeddingService.swift index d5bf2f41..0e54d3ac 100644 --- a/Tool/Sources/OpenAIService/EmbeddingService.swift +++ b/Tool/Sources/OpenAIService/EmbeddingService.swift @@ -1,3 +1,4 @@ +import ChatBasic import Foundation import Logger diff --git a/Tool/Sources/OpenAIService/FucntionCall/ChatGPTFuntionProvider.swift b/Tool/Sources/OpenAIService/FucntionCall/ChatGPTFuntionProvider.swift index c3a60341..fd0bd460 100644 --- a/Tool/Sources/OpenAIService/FucntionCall/ChatGPTFuntionProvider.swift +++ b/Tool/Sources/OpenAIService/FucntionCall/ChatGPTFuntionProvider.swift @@ -1,3 +1,4 @@ +import ChatBasic import Foundation public protocol ChatGPTFunctionProvider { @@ -16,3 +17,4 @@ public struct NoChatGPTFunctionProvider: ChatGPTFunctionProvider { public var functions: [any ChatGPTFunction] { [] } public init() {} } + diff --git a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift index 7675c9ff..237ce417 100644 --- a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift +++ b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift @@ -1,3 +1,4 @@ +import ChatBasic import Foundation import Logger import Preferences diff --git a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemoryStrategy/AutoManagedChatGPTMemoryGoogleAIStrategy.swift b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemoryStrategy/AutoManagedChatGPTMemoryGoogleAIStrategy.swift index 1451bf67..6d527e3d 100644 --- a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemoryStrategy/AutoManagedChatGPTMemoryGoogleAIStrategy.swift +++ b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemoryStrategy/AutoManagedChatGPTMemoryGoogleAIStrategy.swift @@ -1,3 +1,4 @@ +import ChatBasic import Foundation import GoogleGenerativeAI import Logger @@ -29,4 +30,3 @@ extension AutoManagedChatGPTMemory { } } - diff --git a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemoryStrategy/AutoManagedChatGPTMemoryOpenAIStrategy.swift b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemoryStrategy/AutoManagedChatGPTMemoryOpenAIStrategy.swift index 3619a7e9..07a6bda5 100644 --- a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemoryStrategy/AutoManagedChatGPTMemoryOpenAIStrategy.swift +++ b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemoryStrategy/AutoManagedChatGPTMemoryOpenAIStrategy.swift @@ -1,3 +1,4 @@ +import ChatBasic import Foundation import Logger import TokenEncoder diff --git a/Tool/Sources/OpenAIService/Models.swift b/Tool/Sources/OpenAIService/Models.swift index af95a6e5..98355339 100644 --- a/Tool/Sources/OpenAIService/Models.swift +++ b/Tool/Sources/OpenAIService/Models.swift @@ -1,3 +1,4 @@ +import ChatBasic import CodableWrappers import Foundation @@ -8,163 +9,5 @@ struct Cancellable { } } -public struct ChatMessage: Equatable, Codable { - public typealias ID = String - - public enum Role: String, Codable, Equatable { - case system - case user - case assistant - } - - public struct FunctionCall: Codable, Equatable { - public var name: String - public var arguments: String - public init(name: String, arguments: String) { - self.name = name - self.arguments = arguments - } - } - - public struct ToolCall: Codable, Equatable, Identifiable { - public var id: String - public var type: String - public var function: FunctionCall - public var response: ToolCallResponse - public init( - id: String, - type: String, - function: FunctionCall, - response: ToolCallResponse? = nil - ) { - self.id = id - self.type = type - self.function = function - self.response = response ?? .init(content: "", summary: nil) - } - } - - public struct ToolCallResponse: Codable, Equatable { - public var content: String - public var summary: String? - public init(content: String, summary: String?) { - self.content = content - self.summary = summary - } - } - - public struct Reference: Codable, Equatable { - public enum Kind: String, Codable { - case `class` - case `struct` - case `enum` - case `actor` - case `protocol` - case `extension` - case `case` - case property - case `typealias` - case function - case method - case text - case webpage - case other - } - - public var title: String - public var subTitle: String - public var uri: String - public var content: String - public var startLine: Int? - public var endLine: Int? - @FallbackDecoding - public var kind: Kind - - public init( - title: String, - subTitle: String, - content: String, - uri: String, - startLine: Int?, - endLine: Int?, - kind: Kind - ) { - self.title = title - self.subTitle = subTitle - self.content = content - self.uri = uri - self.startLine = startLine - self.endLine = endLine - self.kind = kind - } - } - - /// The role of a message. - @FallbackDecoding - public var role: Role - - /// The content of the message, either the chat message, or a result of a function call. - public var content: String? { - didSet { tokensCount = nil } - } - - /// A function call from the bot. - public var toolCalls: [ToolCall]? { - didSet { tokensCount = nil } - } - - /// The function name of a reply to a function call. - public var name: String? { - didSet { tokensCount = nil } - } - - /// The summary of a message that is used for display. - public var summary: String? - - /// The id of the message. - public var id: ID - - /// The number of tokens of this message. - var tokensCount: Int? - - /// The references of this message. - @FallbackDecoding> - public var references: [Reference] - - /// Is the message considered empty. - var isEmpty: Bool { - if let content, !content.isEmpty { return false } - if let toolCalls, !toolCalls.isEmpty { return false } - if let name, !name.isEmpty { return false } - return true - } - - public init( - id: String = UUID().uuidString, - role: Role, - content: String?, - name: String? = nil, - toolCalls: [ToolCall]? = nil, - summary: String? = nil, - tokenCount: Int? = nil, - references: [Reference] = [] - ) { - self.role = role - self.content = content - self.name = name - self.toolCalls = toolCalls - self.summary = summary - self.id = id - tokensCount = tokenCount - self.references = references - } -} - -public struct ReferenceKindFallback: FallbackValueProvider { - public static var defaultValue: ChatMessage.Reference.Kind { .other } -} - -public struct ChatMessageRoleFallback: FallbackValueProvider { - public static var defaultValue: ChatMessage.Role { .user } -} +public typealias ChatMessage = ChatBasic.ChatMessage From 6101a4444bc3549e7fad89d68f5951aaa8b7d808 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 30 Jun 2024 21:54:10 +0800 Subject: [PATCH 13/44] Bump CopilotForXcodeKit to 0.7.2 --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index a3a3b9cc..f1d396ca 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit a3a3b9cc93dd128911b4e718511dc4b1da5303ce +Subproject commit f1d396caac4704e9ffcad0224cdd341ab3fe5cfb From d8c57970f4f51723c55fec522282650e796145da Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 30 Jun 2024 21:55:38 +0800 Subject: [PATCH 14/44] Add ServerNotificationHandler to handle notifications --- .../CopilotLocalProcessServer.swift | 176 +++++++++++------- 1 file changed, 112 insertions(+), 64 deletions(-) diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift index 0a8dba2c..af325c6d 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift @@ -18,7 +18,8 @@ class CopilotLocalProcessServer { public convenience init( path: String, arguments: [String], - environment: [String: String]? = nil + environment: [String: String]? = nil, + serverNotificationHandler: ServerNotificationHandler ) { let params = Process.ExecutionParameters( path: path, @@ -26,10 +27,13 @@ class CopilotLocalProcessServer { environment: environment ) - self.init(executionParameters: params) + self.init(executionParameters: params, serverNotificationHandler: serverNotificationHandler) } - init(executionParameters parameters: Process.ExecutionParameters) { + init( + executionParameters parameters: Process.ExecutionParameters, + serverNotificationHandler: ServerNotificationHandler + ) { transport = StdioDataTransport() let framing = SeperatedHTTPHeaderMessageFraming() let messageTransport = MessageTransport( @@ -37,7 +41,10 @@ class CopilotLocalProcessServer { messageProtocol: framing ) customTransport = CustomDataTransport(nextTransport: messageTransport) - wrappedServer = CustomJSONRPCLanguageServer(dataTransport: customTransport) + wrappedServer = CustomJSONRPCLanguageServer( + dataTransport: customTransport, + serverNotificationHandler: serverNotificationHandler + ) process = Process() @@ -45,8 +52,9 @@ class CopilotLocalProcessServer { // we need to get the request IDs from a custom transport before the data // is written to the language server. customTransport.onWriteRequest = { [weak self] request in - if request.method == "getCompletionsCycling" - || request.method == "textDocument/inlineCompletion" { + if request.method == "getCompletionsCycling" + || request.method == "textDocument/inlineCompletion" + { Task { @MainActor [weak self] in self?.ongoingCompletionRequestIDs.append(request.id) } @@ -85,7 +93,7 @@ class CopilotLocalProcessServer { get { return wrappedServer?.logMessages ?? false } set { wrappedServer?.logMessages = newValue } } - + func terminate() { process.terminate() } @@ -155,36 +163,47 @@ final class CustomJSONRPCLanguageServer: Server { private let protocolTransport: ProtocolTransport - public var requestHandler: RequestHandler? - public var notificationHandler: NotificationHandler? + var requestHandler: RequestHandler? + var serverNotificationHandler: ServerNotificationHandler + + @available(*, deprecated, message: "Use `serverNotificationHandler` instead.") + var notificationHandler: NotificationHandler? { + get { nil } + set {} + } private var outOfBandError: Error? - init(protocolTransport: ProtocolTransport) { + init( + protocolTransport: ProtocolTransport, + serverNotificationHandler: ServerNotificationHandler + ) { + self.serverNotificationHandler = serverNotificationHandler self.protocolTransport = protocolTransport internalServer = JSONRPCLanguageServer(protocolTransport: protocolTransport) - let previouseRequestHandler = protocolTransport.requestHandler - let previouseNotificationHandler = protocolTransport.notificationHandler + let previousRequestHandler = protocolTransport.requestHandler - protocolTransport - .requestHandler = { [weak self] in - guard let self else { return } - if !self.handleRequest($0, data: $1, callback: $2) { - previouseRequestHandler?($0, $1, $2) - } - } - protocolTransport - .notificationHandler = { [weak self] in - guard let self else { return } - if !self.handleNotification($0, data: $1, block: $2) { - previouseNotificationHandler?($0, $1, $2) - } + protocolTransport.requestHandler = { [weak self] in + guard let self else { return } + if !self.handleRequest($0, data: $1, callback: $2) { + previousRequestHandler?($0, $1, $2) } + } + protocolTransport.notificationHandler = { [weak self] in + guard let self else { return } + self.handleNotification($0, data: $1, block: $2) + } } - convenience init(dataTransport: DataTransport) { - self.init(protocolTransport: ProtocolTransport(dataTransport: dataTransport)) + convenience init( + dataTransport: DataTransport, + serverNotificationHandler: ServerNotificationHandler + ) { + self.init( + protocolTransport: ProtocolTransport(dataTransport: dataTransport), + serverNotificationHandler: serverNotificationHandler + ) } deinit { @@ -203,7 +222,72 @@ extension CustomJSONRPCLanguageServer { _ anyNotification: AnyJSONRPCNotification, data: Data, block: @escaping (Error?) -> Void + ) { + Task { + do { + try await serverNotificationHandler.handleNotification( + anyNotification, + data: data + ) + block(nil) + } catch { + block(error) + } + } + } + + func sendNotification( + _ notif: ClientNotification, + completionHandler: @escaping (ServerError?) -> Void + ) { + internalServer.sendNotification(notif, completionHandler: completionHandler) + } +} + +extension CustomJSONRPCLanguageServer { + private func handleRequest( + _ request: AnyJSONRPCRequest, + data: Data, + callback: @escaping (AnyJSONRPCResponse) -> Void ) -> Bool { + return false + } +} + +extension CustomJSONRPCLanguageServer { + public func sendRequest( + _ request: ClientRequest, + completionHandler: @escaping (ServerResult) -> Void + ) { + internalServer.sendRequest(request, completionHandler: completionHandler) + } +} + +@GitHubCopilotSuggestionActor +final class ServerNotificationHandler { + typealias Handler = ( + _ anyNotification: AnyJSONRPCNotification, + _ data: Data + ) async throws -> Bool + + var handlers = [AnyHashable: Handler]() + nonisolated init() {} + + func handleNotification( + _ anyNotification: AnyJSONRPCNotification, + data: Data + ) async throws { + for handler in handlers.values { + do { + let handled = try await handler(anyNotification, data) + if handled { + return + } + } catch { + throw ServerError.notificationDispatchFailed(error) + } + } + let methodName = anyNotification.method let debugDescription = { if let params = anyNotification.params { @@ -223,65 +307,29 @@ extension CustomJSONRPCLanguageServer { Logger.gitHubCopilot .info("\(anyNotification.method): \(debugDescription)") } - block(nil) - return true case "LogMessage": if UserDefaults.shared.value(for: \.gitHubCopilotVerboseLog) { Logger.gitHubCopilot .info("\(anyNotification.method): \(debugDescription)") } - block(nil)// - return true case "statusNotification": if UserDefaults.shared.value(for: \.gitHubCopilotVerboseLog) { Logger.gitHubCopilot .info("\(anyNotification.method): \(debugDescription)") } - block(nil) - return true case "featureFlagsNotification": if UserDefaults.shared.value(for: \.gitHubCopilotVerboseLog) { Logger.gitHubCopilot .info("\(anyNotification.method): \(debugDescription)") } - block(nil) - return true case "conversation/preconditionsNotification": if UserDefaults.shared.value(for: \.gitHubCopilotVerboseLog) { Logger.gitHubCopilot .info("\(anyNotification.method): \(debugDescription)") } - block(nil) - return true default: - return false + throw ServerError.handlerUnavailable(methodName) } } - - public func sendNotification( - _ notif: ClientNotification, - completionHandler: @escaping (ServerError?) -> Void - ) { - internalServer.sendNotification(notif, completionHandler: completionHandler) - } -} - -extension CustomJSONRPCLanguageServer { - private func handleRequest( - _ request: AnyJSONRPCRequest, - data: Data, - callback: @escaping (AnyJSONRPCResponse) -> Void - ) -> Bool { - return false - } -} - -extension CustomJSONRPCLanguageServer { - public func sendRequest( - _ request: ClientRequest, - completionHandler: @escaping (ServerResult) -> Void - ) { - internalServer.sendRequest(request, completionHandler: completionHandler) - } } From 54759810f52b241bcfbfebfb806a98dc734e4c70 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 30 Jun 2024 21:56:12 +0800 Subject: [PATCH 15/44] Support injecting notification handler from everywhere --- .../LanguageServer/GitHubCopilotService.swift | 39 +++++++------------ 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift index a7115a4f..76982a4e 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift @@ -96,9 +96,7 @@ public class GitHubCopilotBaseService { let projectRootURL: URL var server: GitHubCopilotLSP var localProcessServer: CopilotLocalProcessServer? - @GitHubCopilotSuggestionActor - private var serverNotificationHandlers = - [AnyHashable: (ServerNotification) async throws -> Bool]() + let notificationHandler: ServerNotificationHandler deinit { localProcessServer?.terminate() @@ -107,16 +105,19 @@ public class GitHubCopilotBaseService { init(designatedServer: GitHubCopilotLSP) { projectRootURL = URL(fileURLWithPath: "/") server = designatedServer + notificationHandler = .init() } init(projectRootURL: URL) throws { self.projectRootURL = projectRootURL - let (server, localServer) = try { + let notificationHandler = ServerNotificationHandler() + self.notificationHandler = notificationHandler + let (server, localServer) = try { [notificationHandler] in let urls = try GitHubCopilotBaseService.createFoldersIfNeeded() let executionParams: Process.ExecutionParameters let runner = UserDefaults.shared.value(for: \.runNodeWith) - guard let agentJSURL = { + guard let agentJSURL = { () -> URL? in let languageServerDotJS = urls.executableURL .appendingPathComponent("copilot/dist/language-server.js") if FileManager.default.fileExists(atPath: languageServerDotJS.path) { @@ -199,26 +200,12 @@ public class GitHubCopilotBaseService { ) }() } - let localServer = CopilotLocalProcessServer(executionParameters: executionParams) + let localServer = CopilotLocalProcessServer( + executionParameters: executionParams, + serverNotificationHandler: notificationHandler + ) localServer.logMessages = UserDefaults.shared.value(for: \.gitHubCopilotVerboseLog) - localServer.notificationHandler = { [weak self] notification, respond in - Task { - for handler in self?.serverNotificationHandlers.values ?? [] { - do { - let handled = try await handler(notification) - if handled { - respond(nil) - return - } - } catch { - respond(.failure(error)) - return - } - } - respond(.handlerUnavailable(notification.method.rawValue)) - } - } let server = InitializingServer(server: localServer) server.initializeParamsProvider = { @@ -306,16 +293,16 @@ public class GitHubCopilotBaseService { func registerNotificationHandler( id: AnyHashable, - _ block: @escaping (ServerNotification) async throws -> Bool + _ block: @escaping ServerNotificationHandler.Handler ) { Task { @GitHubCopilotSuggestionActor in - self.serverNotificationHandlers[id] = block + self.notificationHandler.handlers[id] = block } } func unregisterNotificationHandler(id: AnyHashable) { Task { @GitHubCopilotSuggestionActor in - self.serverNotificationHandlers[id] = nil + self.notificationHandler.handlers[id] = nil } } } From ae0f411bd9853c6dca4d724547e4a0229e8242e6 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 30 Jun 2024 22:00:36 +0800 Subject: [PATCH 16/44] Add new request types --- .../LanguageServer/GitHubCopilotRequest.swift | 27 +++++++++---------- Tool/Sources/Preferences/Keys.swift | 4 +++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift index c8975a6c..074ffc44 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift @@ -135,9 +135,14 @@ enum GitHubCopilotRequest { } var request: ClientRequest { + let pretendToBeVSCode = UserDefaults.shared + .value(for: \.gitHubCopilotPretendIDEToBeVSCode) var dict: [String: JSONValue] = [ - "editorInfo": .hash([ - "name": "Xcode", + "editorInfo": pretendToBeVSCode ? .hash([ + "name": "vscode", + "version": "1.89.1", + ]) : .hash([ + "name": "xcode", "version": "", ]), "editorPluginInfo": .hash([ @@ -348,7 +353,7 @@ enum GitHubCopilotRequest { var response: String? } - var capabilities: [Capabilities]; struct Capabilities: Codable { + var capabilities: Capabilities; struct Capabilities: Codable { var allSkills: Bool? var skills: [String] } @@ -374,9 +379,7 @@ enum GitHubCopilotRequest { var request: ClientRequest { let data = (try? JSONEncoder().encode(requestBody)) ?? Data() let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:]) - return .custom("conversation/create", .hash([ - "doc": dict, - ])) + return .custom("conversation/create", dict) } } @@ -412,12 +415,10 @@ enum GitHubCopilotRequest { var request: ClientRequest { let data = (try? JSONEncoder().encode(requestBody)) ?? Data() let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:]) - return .custom("conversation/turn", .hash([ - "doc": dict, - ])) + return .custom("conversation/turn", dict) } } - + struct ConversationTurnDelete: GitHubCopilotRequestType { struct Response: Codable {} @@ -433,12 +434,10 @@ enum GitHubCopilotRequest { var request: ClientRequest { let data = (try? JSONEncoder().encode(requestBody)) ?? Data() let dict = (try? JSONDecoder().decode(JSONValue.self, from: data)) ?? .hash([:]) - return .custom("conversation/turnDelete", .hash([ - "doc": dict, - ])) + return .custom("conversation/turnDelete", dict) } } - + struct ConversationDestroy: GitHubCopilotRequestType { struct Response: Codable {} diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index f081a00c..9fdbc92f 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -191,6 +191,10 @@ public extension UserDefaultPreferenceKeys { var gitHubCopilotLoadKeyChainCertificates: PreferenceKey { .init(defaultValue: false, key: "GitHubCopilotLoadKeyChainCertificates") } + + var gitHubCopilotPretendIDEToBeVSCode: PreferenceKey { + .init(defaultValue: false, key: "GitHubCopilotPretendIDEToBeVSCode") + } } // MARK: - Codeium Settings From 250c54bdcaebc209159d636ccec8996a77b9e7cc Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 30 Jun 2024 22:01:17 +0800 Subject: [PATCH 17/44] Add GitHubCopilotChatService --- .../BuiltinExtension/BuiltinExtension.swift | 44 ++-- .../GitHubCopilotExtension.swift | 5 +- .../CopilotLocalProcessServer.swift | 1 + .../Services/GitHubCopilotChatService.swift | 198 +++++++++++++----- 4 files changed, 180 insertions(+), 68 deletions(-) diff --git a/Tool/Sources/BuiltinExtension/BuiltinExtension.swift b/Tool/Sources/BuiltinExtension/BuiltinExtension.swift index 7effe675..88a830e4 100644 --- a/Tool/Sources/BuiltinExtension/BuiltinExtension.swift +++ b/Tool/Sources/BuiltinExtension/BuiltinExtension.swift @@ -1,4 +1,4 @@ -import ChatContextCollector +import ChatBasic import ChatTab import CopilotForXcodeKit import Foundation @@ -28,25 +28,37 @@ public extension BuiltinExtension { /// A temporary protocol for ChatServiceType. Migrate it to CopilotForXcodeKit when finished. public protocol BuiltinExtensionChatServiceType: ChatServiceType { - typealias Message = ChatServiceMessage - typealias RetrievedContent = ChatContext.RetrievedContent + typealias Message = ChatMessage + + func sendMessage( + _ message: String, + history: [Message], + references: [RetrievedContent], + workspace: WorkspaceInfo + ) async -> AsyncThrowingStream } -public struct ChatServiceMessage: Codable { - public enum Role: Codable, Equatable { - case system - case user - case assistant - case tool - case other(String) +public struct RetrievedContent { + public var document: ChatMessage.Reference + public var priority: Int + + public init(document: ChatMessage.Reference, priority: Int) { + self.document = document + self.priority = priority } +} - public var role: Role - public var text: String +public enum ChatServiceMemoryMutation: Codable { + public typealias Message = ChatMessage - public init(role: Role, text: String) { - self.role = role - self.text = text - } + /// Add a new message to the end of memory. + /// If an id is not provided, a new id will be generated. + /// If an id is provided, and a message with the same id exists the message with the same + /// id will be updated. + case appendMessage(id: String?, role: Message.Role, text: String) + /// Update the message with the given id. + case updateMessage(id: String, role: Message.Role, text: String) + /// Stream the content into a message with the given id. + case streamIntoMessage(id: String, role: Message.Role?, text: String?) } diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift index 6331a168..3222f7c0 100644 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift +++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift @@ -11,8 +11,8 @@ public final class GitHubCopilotExtension: BuiltinExtension { public var suggestionServiceId: Preferences.BuiltInSuggestionFeatureProvider { .gitHubCopilot } - public let suggestionService: GitHubCopilotSuggestionService? - public let chatService: GitHubCopilotChatService? + public let suggestionService: GitHubCopilotSuggestionService + public let chatService: GitHubCopilotChatService private var extensionUsage = ExtensionUsage( isSuggestionServiceInUse: false, @@ -141,6 +141,7 @@ protocol ServiceLocatorType { class ServiceLocator: ServiceLocatorType { let workspacePool: WorkspacePool + init(workspacePool: WorkspacePool) { self.workspacePool = workspacePool } diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift index af325c6d..64b35868 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift @@ -105,6 +105,7 @@ extension CopilotLocalProcessServer: LanguageServerProtocol.Server { set { wrappedServer?.requestHandler = newValue } } + @available(*, deprecated, message: "Use `ServerNotificationHandler` instead") public var notificationHandler: NotificationHandler? { get { wrappedServer?.notificationHandler } set { wrappedServer?.notificationHandler = newValue } diff --git a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift b/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift index 5ff08e9a..f52b28d3 100644 --- a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift +++ b/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift @@ -1,6 +1,8 @@ import BuiltinExtension +import ChatBasic import CopilotForXcodeKit import Foundation +import LanguageServerProtocol import XcodeInspector public final class GitHubCopilotChatService: BuiltinExtensionChatServiceType { @@ -22,90 +24,131 @@ public final class GitHubCopilotChatService: BuiltinExtensionChatServiceType { else { return .finished(throwing: CancellationError()) } let id = UUID().uuidString let editorContent = await XcodeInspector.shared.getFocusedEditorContent() - do { - let createResponse = try await service.server - .sendRequest(GitHubCopilotRequest.ConversationCreate(requestBody: .init( - workDoneToken: "", - turns: convertHistory(history: history), - capabilities: [ - .init(allSkills: true, skills: []), - ], - doc: .init( - source: editorContent?.editorContent?.content ?? "", - tabSize: 1, - indentSize: 4, - insertSpaces: true, - path: editorContent?.documentURL.path ?? "", - uri: editorContent?.documentURL.path ?? "", - relativePath: editorContent?.relativePath ?? "", - languageId: editorContent?.language.rawValue ?? "plaintext", - position: .zero - ), - source: .panel, - workspaceFolder: workspace.projectURL.path - ))) - - let stream = AsyncThrowingStream.init { continuation in - service.registerNotificationHandler(id: id) { notification in - if notification.method.rawValue == "" { - return true - } + let workDoneToken = UUID().uuidString + let turns = convertHistory(history: history, message: message) + let request = GitHubCopilotRequest.ConversationCreate(requestBody: .init( + workDoneToken: workDoneToken, + turns: turns, + capabilities: .init(allSkills: true, skills: []), + doc: .init( + source: editorContent?.editorContent?.content ?? "", + tabSize: 1, + indentSize: 4, + insertSpaces: true, + path: editorContent?.documentURL.path ?? "", + uri: editorContent?.documentURL.path ?? "", + relativePath: editorContent?.relativePath ?? "", + languageId: editorContent?.language ?? .plaintext, + position: editorContent?.editorContent?.cursorPosition ?? .zero + ), + source: .panel, + workspaceFolder: workspace.projectURL.path + )) + + var cont: AsyncThrowingStream.Continuation? = nil + let stream = AsyncThrowingStream { continuation in + cont = continuation + let startTimestamp = Date() + service.registerNotificationHandler(id: id) { notification, data in + // just incase the conversation is stuck, we will cancel it after timeout + if Date().timeIntervalSince(startTimestamp) > 60 * 30 { + continuation.finish(throwing: CancellationError()) return false } - continuation.onTermination = { _ in - Task { - try await service.server.sendRequest( - GitHubCopilotRequest.ConversationDestroy(requestBody: .init( - conversationId: createResponse.conversationId - )) + switch notification.method { + case "$/progress": + do { + let progress = try JSONDecoder().decode( + JSONRPC.self, + from: data + ).params + guard progress.token == workDoneToken else { return false } + if let reply = progress.value.reply, progress.value.kind == "report" { + continuation.yield(reply) + } else if progress.value.kind == "end" { + if let error = progress.value.error, + progress.value.cancellationReason == nil + { + continuation.finish(throwing: ServerError.serverError( + code: 0, + message: error, + data: nil + )) + } else { + continuation.finish() + } + } + return true + } catch { + return false + } + case "conversation/context": + do { + _ = try JSONDecoder().decode( + JSONRPC.self, + from: data ) + throw ServerError.clientDataUnavailable(CancellationError()) + } catch { + return false } + + default: + return false } } + } - _ = try await service.server - .sendRequest(GitHubCopilotRequest.ConversationTurn(requestBody: .init( - workDoneToken: "", - conversationId: createResponse.conversationId, - message: message - ))) - - return stream + do { + let createResponse = try await service.server.sendRequest(request) + cont?.onTermination = { _ in + Task { + service.unregisterNotificationHandler(id: id) + _ = try await service.server.sendRequest( + GitHubCopilotRequest.ConversationDestroy(requestBody: .init( + conversationId: createResponse.conversationId + )) + ) + } + } } catch { - return .finished(throwing: error) + cont?.finish(throwing: error) } + + return stream } } extension GitHubCopilotChatService { typealias Turn = GitHubCopilotRequest.ConversationCreate.RequestBody.Turn - func convertHistory(history: [Message]) -> [Turn] { + func convertHistory(history: [Message], message: String) -> [Turn] { guard let firstIndexOfUserMessage = history.firstIndex(where: { $0.role == .user }) - else { return [] } + else { return [.init(request: message, response: nil)] } var currentTurn = Turn(request: "", response: nil) var turns: [Turn] = [] for i in firstIndexOfUserMessage.. String { return message } + + struct JSONRPC: Decodable { + var jsonrpc: String + var method: String + var params: Params + } + + struct StreamProgressParams: Decodable { + struct Value: Decodable { + struct Step: Decodable { + var id: String + var title: String + var status: String + } + + struct FollowUp: Decodable { + var id: String + var type: String + var message: String + } + + var kind: String + var title: String? + var conversationId: String + var turnId: String + var steps: [Step]? + var followUp: FollowUp? + var suggestedTitle: String? + var reply: String? + var annotations: [String]? + var hideText: Bool? + var cancellationReason: String? + var error: String? + } + + var token: String + var value: Value + } + + struct ConversationContextParams: Decodable { + enum SkillID: String, Decodable { + case currentEditor = "current-editor" + case projectLabels = "project-labels" + case recentFiles = "recent-files" + case references + case problemsInActiveDocument = "problems-in-active-document" + } + + var conversationId: String + var turnId: String + var skillId: String + } + + struct ConversationContextResponseBody: Encodable {} } From 4fdb4e51ceb03590e3de4cd22c711f0459bc3216 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 30 Jun 2024 22:02:07 +0800 Subject: [PATCH 18/44] Hack ChatGPTService to support GitHub copilot chat --- ...iltinExtensionChatCompletionsService.swift | 106 ++++++++++++++++++ .../OpenAIService/ChatGPTService.swift | 18 ++- .../UserPreferenceChatGPTConfiguration.swift | 6 + 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 Tool/Sources/OpenAIService/APIs/BuiltinExtensionChatCompletionsService.swift diff --git a/Tool/Sources/OpenAIService/APIs/BuiltinExtensionChatCompletionsService.swift b/Tool/Sources/OpenAIService/APIs/BuiltinExtensionChatCompletionsService.swift new file mode 100644 index 00000000..9c5f4dbb --- /dev/null +++ b/Tool/Sources/OpenAIService/APIs/BuiltinExtensionChatCompletionsService.swift @@ -0,0 +1,106 @@ +import AsyncAlgorithms +import BuiltinExtension +import ChatBasic +import Foundation +import XcodeInspector + +#warning("This is a temporary implementation for proof of concept.") + +actor BuiltinExtensionChatCompletionsService { + typealias RequestBody = ChatCompletionsRequestBody + + enum CustomError: Swift.Error, LocalizedError { + case chatServiceNotFound + + var errorDescription: String? { + switch self { + case .chatServiceNotFound: + return "Chat service not found." + } + } + } + + var extensionManager: BuiltinExtensionManager { .shared } + + let extensionIdentifier: String + let requestBody: RequestBody + + init(extensionIdentifier: String, requestBody: RequestBody) { + self.extensionIdentifier = extensionIdentifier + self.requestBody = requestBody + } +} + +extension BuiltinExtensionChatCompletionsService: ChatCompletionsAPI { + func callAsFunction() async throws -> ChatCompletionResponseBody { + fatalError() + } +} + +extension BuiltinExtensionChatCompletionsService: ChatCompletionsStreamAPI { + func callAsFunction( + ) async throws -> AsyncThrowingStream { + let service = try getChatService() + let (message, history) = extractMessageAndHistory(from: requestBody) + guard let workspaceURL = XcodeInspector.shared.realtimeActiveWorkspaceURL, + let projectURL = XcodeInspector.shared.realtimeActiveProjectURL + else { throw CancellationError() } + let stream = await service.sendMessage( + message, + history: history, + references: [], + workspace: .init( + workspaceURL: workspaceURL, + projectURL: projectURL + ) + ) + + return stream.map { text in + ChatCompletionsStreamDataChunk( + id: nil, + object: nil, + model: nil, + message: .init( + role: .assistant, + content: text, + toolCalls: nil + ), + finishReason: nil + ) + }.toStream() + } +} + +extension BuiltinExtensionChatCompletionsService { + func getChatService() throws -> any BuiltinExtensionChatServiceType { + guard let ext = extensionManager.extensions + .first(where: { $0.extensionIdentifier == extensionIdentifier }), + let service = ext.chatService as? BuiltinExtensionChatServiceType + else { + throw CustomError.chatServiceNotFound + } + return service + } +} + +extension BuiltinExtensionChatCompletionsService { + func extractMessageAndHistory( + from request: RequestBody + ) -> (message: String, history: [ChatMessage]) { + let messages = request.messages + + if let lastIndexNotUserMessage = messages.lastIndex(where: { $0.role != .user }) { + let message = messages[(lastIndexNotUserMessage + 1)...] + .map { $0.content } + .joined(separator: "\n\n") + let history = Array(messages[0...lastIndexNotUserMessage]) + return (message, history.map { + .init(id: UUID().uuidString, role: $0.role.asChatMessageRole, content: $0.content) + }) + } else { // everything is user message + let message = messages.map { $0.content }.joined(separator: "\n\n") + return (message, []) + } + } +} + diff --git a/Tool/Sources/OpenAIService/ChatGPTService.swift b/Tool/Sources/OpenAIService/ChatGPTService.swift index c2f718f6..669fae10 100644 --- a/Tool/Sources/OpenAIService/ChatGPTService.swift +++ b/Tool/Sources/OpenAIService/ChatGPTService.swift @@ -90,13 +90,21 @@ public class ChatGPTService: ChatGPTServiceType { var runningTask: Task? var buildCompletionStreamAPI: ChatCompletionsStreamAPIBuilder = { apiKey, model, endpoint, requestBody, prompt in + + if model.id == "com.github.copilot" { + return BuiltinExtensionChatCompletionsService( + extensionIdentifier: model.id, + requestBody: requestBody + ) + } + switch model.format { case .googleAI: return GoogleAIChatCompletionsService( apiKey: apiKey, model: model, requestBody: requestBody, - prompt: prompt, + prompt: prompt, baseURL: endpoint.absoluteString ) case .openAI, .openAICompatible, .azureOpenAI: @@ -125,6 +133,14 @@ public class ChatGPTService: ChatGPTServiceType { var buildCompletionAPI: ChatCompletionsAPIBuilder = { apiKey, model, endpoint, requestBody, prompt in + + if model.id == "com.github.copilot" { + return BuiltinExtensionChatCompletionsService( + extensionIdentifier: model.id, + requestBody: requestBody + ) + } + switch model.format { case .googleAI: return GoogleAIChatCompletionsService( diff --git a/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift index 9c1b7fd7..119445ef 100644 --- a/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift @@ -16,12 +16,18 @@ public struct UserPreferenceChatGPTConfiguration: ChatGPTConfiguration { if let chatModelKey { let id = UserDefaults.shared.value(for: chatModelKey) + if id == "com.github.copilot" { + return .init(id: id, name: "GitHub Copilot", format: .openAI, info: .init()) + } if let model = models.first(where: { $0.id == id }) { return model } } let id = UserDefaults.shared.value(for: \.defaultChatFeatureChatModelId) + if id == "com.github.copilot" { + return .init(id: id, name: "GitHub Copilot", format: .openAI, info: .init()) + } return models.first { $0.id == id } ?? models.first } From b2bcfc770e35344398079d6930153634f57055f9 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 30 Jun 2024 22:16:12 +0800 Subject: [PATCH 19/44] Add settings for GitHub Copilot chat --- .../ChatGPTChatTab/ChatContextMenu.swift | 43 +++++++++++-------- .../AccountSettings/GitHubCopilotView.swift | 18 ++++---- .../Chat/ChatSettingsGeneralSectionView.swift | 16 ++++--- 3 files changed, 45 insertions(+), 32 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatContextMenu.swift b/Core/Sources/ChatGPTChatTab/ChatContextMenu.swift index 768c064b..a791db29 100644 --- a/Core/Sources/ChatGPTChatTab/ChatContextMenu.swift +++ b/Core/Sources/ChatGPTChatTab/ChatContextMenu.swift @@ -70,12 +70,19 @@ struct ChatContextMenu: View { @ViewBuilder var chatModel: some View { + let allModels = chatModels + [.init( + id: "com.github.copilot", + name: "GitHub Copilot (poc)", + format: .openAI, + info: .init() + )] + Menu("Chat Model") { Button(action: { store.send(.chatModelIdOverrideSelected(nil)) }) { HStack { - if let defaultModel = chatModels + if let defaultModel = allModels .first(where: { $0.id == defaultChatModelId }) { Text("Default (\(defaultModel.name))") @@ -88,7 +95,7 @@ struct ChatContextMenu: View { } } - if let id = store.chatModelIdOverride, !chatModels.map(\.id).contains(id) { + if let id = store.chatModelIdOverride, !allModels.map(\.id).contains(id) { Button(action: { store.send(.chatModelIdOverrideSelected(nil)) }) { @@ -101,7 +108,7 @@ struct ChatContextMenu: View { Divider() - ForEach(chatModels, id: \.id) { model in + ForEach(allModels, id: \.id) { model in Button(action: { store.send(.chatModelIdOverrideSelected(model.id)) }) { @@ -152,26 +159,26 @@ struct ChatContextMenu: View { @ViewBuilder var defaultScopes: some View { Menu("Default Scopes") { + Button(action: { + store.send(.resetDefaultScopesButtonTapped) + }) { + Text("Reset Default Scopes") + } + + Divider() + + ForEach(ChatService.Scope.allCases, id: \.rawValue) { value in Button(action: { - store.send(.resetDefaultScopesButtonTapped) + store.send(.toggleScope(value)) }) { - Text("Reset Default Scopes") - } - - Divider() - - ForEach(ChatService.Scope.allCases, id: \.rawValue) { value in - Button(action: { - store.send(.toggleScope(value)) - }) { - HStack { - Text("@" + value.rawValue) - if store.defaultScopes.contains(value) { - Image(systemName: "checkmark") - } + HStack { + Text("@" + value.rawValue) + if store.defaultScopes.contains(value) { + Image(systemName: "checkmark") } } } + } } } diff --git a/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift index 44d724ae..9bf3a58a 100644 --- a/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift +++ b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift @@ -20,6 +20,7 @@ struct GitHubCopilotView: View { @AppStorage(\.gitHubCopilotProxyPassword) var gitHubCopilotProxyPassword @AppStorage(\.gitHubCopilotUseStrictSSL) var gitHubCopilotUseStrictSSL @AppStorage(\.gitHubCopilotEnterpriseURI) var gitHubCopilotEnterpriseURI + @AppStorage(\.gitHubCopilotPretendIDEToBeVSCode) var pretendIDEToBeVSCode @AppStorage(\.disableGitHubCopilotSettingsAutoRefreshOnAppear) var disableGitHubCopilotSettingsAutoRefreshOnAppear @AppStorage(\.gitHubCopilotLoadKeyChainCertificates) @@ -263,7 +264,7 @@ struct GitHubCopilotView: View { .opacity(isRunningAction ? 0.8 : 1) .disabled(isRunningAction) - Button("Refresh Configuration for Enterprise and Proxy") { + Button("Refresh configurations") { refreshConfiguration() } } @@ -271,7 +272,8 @@ struct GitHubCopilotView: View { SettingsDivider("Advanced") Form { - Toggle("Verbose Log", isOn: $settings.gitHubCopilotVerboseLog) + Toggle("Verbose log", isOn: $settings.gitHubCopilotVerboseLog) + Toggle("Pretend IDE to be VSCode", isOn: $settings.pretendIDEToBeVSCode) } SettingsDivider("Enterprise") @@ -281,7 +283,7 @@ struct GitHubCopilotView: View { text: $settings.gitHubCopilotEnterpriseURI, prompt: Text("Leave it blank if non is available.") ) { - Text("Auth Provider URL") + Text("Auth provider URL") } } @@ -292,18 +294,18 @@ struct GitHubCopilotView: View { text: $settings.gitHubCopilotProxyHost, prompt: Text("xxx.xxx.xxx.xxx, leave it blank to disable proxy.") ) { - Text("Proxy Host") + Text("Proxy host") } TextField(text: $settings.gitHubCopilotProxyPort, prompt: Text("80")) { - Text("Proxy Port") + Text("Proxy port") } TextField(text: $settings.gitHubCopilotProxyUsername) { - Text("Proxy Username") + Text("Proxy username") } SecureField(text: $settings.gitHubCopilotProxyPassword) { - Text("Proxy Password") + Text("Proxy password") } - Toggle("Proxy Strict SSL", isOn: $settings.gitHubCopilotUseStrictSSL) + Toggle("Proxy strict SSL", isOn: $settings.gitHubCopilotUseStrictSSL) } } Spacer() diff --git a/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift b/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift index 1161d81a..974675a4 100644 --- a/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift +++ b/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift @@ -100,17 +100,21 @@ struct ChatSettingsGeneralSectionView: View { "Chat model", selection: $settings.defaultChatFeatureChatModelId ) { - if !settings.chatModels - .contains(where: { $0.id == settings.defaultChatFeatureChatModelId }) - { + let allModels = settings.chatModels + [.init( + id: "com.github.copilot", + name: "GitHub Copilot (poc)", + format: .openAI, + info: .init() + )] + + if !allModels.contains(where: { $0.id == settings.defaultChatFeatureChatModelId }) { Text( - (settings.chatModels.first?.name).map { "\($0) (Default)" } - ?? "No model found" + (allModels.first?.name).map { "\($0) (Default)" } ?? "No model found" ) .tag(settings.defaultChatFeatureChatModelId) } - ForEach(settings.chatModels, id: \.id) { chatModel in + ForEach(allModels, id: \.id) { chatModel in Text(chatModel.name).tag(chatModel.id) } } From 6c03a20844d3854458c6ea81283fa7d42e8328dc Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 30 Jun 2024 23:25:09 +0800 Subject: [PATCH 20/44] Fix that the request may timeout too soon --- .../LanguageServer/GitHubCopilotService.swift | 29 +++++++++++++++-- .../Services/GitHubCopilotChatService.swift | 32 +++++++++---------- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift index 76982a4e..66f68177 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift @@ -37,14 +37,24 @@ public protocol GitHubCopilotSuggestionServiceType { } protocol GitHubCopilotLSP { - func sendRequest(_ endpoint: E) async throws -> E.Response + func sendRequest( + _ endpoint: E, + timeout: TimeInterval? + ) async throws -> E.Response func sendNotification(_ notif: ClientNotification) async throws } +extension GitHubCopilotLSP { + func sendRequest(_ endpoint: E) async throws -> E.Response { + try await sendRequest(endpoint, timeout: nil) + } +} + enum GitHubCopilotError: Error, LocalizedError { case languageServerNotInstalled case languageServerError(ServerError) case failedToInstallStartScript + case chatEndsWithError(String) var errorDescription: String? { switch self { @@ -52,6 +62,8 @@ enum GitHubCopilotError: Error, LocalizedError { return "Language server is not installed." case .failedToInstallStartScript: return "Failed to install start script." + case let .chatEndsWithError(errorMessage): + return "Chat ended with error message: \(errorMessage)" case let .languageServerError(error): switch error { case let .handlerUnavailable(handler): @@ -578,8 +590,19 @@ public final class GitHubCopilotService: GitHubCopilotBaseService, } extension InitializingServer: GitHubCopilotLSP { - func sendRequest(_ endpoint: E) async throws -> E.Response { - try await sendRequest(endpoint.request) + func sendRequest( + _ endpoint: E, + timeout: TimeInterval? = nil + ) async throws -> E.Response { + if let timeout { + return try await withCheckedThrowingContinuation { continuation in + self.sendRequest(endpoint.request, timeout: timeout) { result in + continuation.resume(with: result) + } + } + } else { + return try await sendRequest(endpoint.request) + } } } diff --git a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift b/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift index f52b28d3..a9f9d2af 100644 --- a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift +++ b/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift @@ -45,11 +45,13 @@ public final class GitHubCopilotChatService: BuiltinExtensionChatServiceType { workspaceFolder: workspace.projectURL.path )) - var cont: AsyncThrowingStream.Continuation? = nil let stream = AsyncThrowingStream { continuation in - cont = continuation let startTimestamp = Date() + continuation.onTermination = { _ in + Task { service.unregisterNotificationHandler(id: id) } + } + service.registerNotificationHandler(id: id) { notification, data in // just incase the conversation is stuck, we will cancel it after timeout if Date().timeIntervalSince(startTimestamp) > 60 * 30 { @@ -71,11 +73,9 @@ public final class GitHubCopilotChatService: BuiltinExtensionChatServiceType { if let error = progress.value.error, progress.value.cancellationReason == nil { - continuation.finish(throwing: ServerError.serverError( - code: 0, - message: error, - data: nil - )) + continuation.finish( + throwing: GitHubCopilotError.chatEndsWithError(error) + ) } else { continuation.finish() } @@ -99,22 +99,22 @@ public final class GitHubCopilotChatService: BuiltinExtensionChatServiceType { return false } } - } - do { - let createResponse = try await service.server.sendRequest(request) - cont?.onTermination = { _ in - Task { - service.unregisterNotificationHandler(id: id) + Task { + do { + // this will return when the response is generated. + let createResponse = try await service.server.sendRequest(request, timeout: 120) _ = try await service.server.sendRequest( GitHubCopilotRequest.ConversationDestroy(requestBody: .init( conversationId: createResponse.conversationId )) ) + } catch let error as ServerError { + continuation.finish(throwing: GitHubCopilotError.languageServerError(error)) + } catch { + continuation.finish(throwing: error) } } - } catch { - cont?.finish(throwing: error) } return stream @@ -168,7 +168,7 @@ extension GitHubCopilotChatService { func createNewMessage(references: [RetrievedContent], message: String) -> String { return message } - + struct JSONRPC: Decodable { var jsonrpc: String var method: String From a7335afbe2128171fd25c50a3f20404f7d4b7e8b Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 30 Jun 2024 23:49:09 +0800 Subject: [PATCH 21/44] Fix unit tests --- Tool/Package.swift | 3 +++ .../FetchSuggestionsTests.swift | 10 +++++++--- ...AutoManagedChatGPTMemoryRetrievedContentTests.swift | 9 +++++---- Tool/Tests/OpenAIServiceTests/ChatGPTStreamTests.swift | 1 + Tool/Tests/OpenAIServiceTests/LimitMessagesTests.swift | 5 +++-- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Tool/Package.swift b/Tool/Package.swift index 67d3cc77..94b16163 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -215,6 +215,7 @@ let package = Package( name: "BuiltinExtension", dependencies: [ "SuggestionModel", + "SuggestionProvider", "ChatBasic", "Workspace", "ChatTab", @@ -369,6 +370,7 @@ let package = Package( "TokenEncoder", "Keychain", "BuiltinExtension", + "ChatBasic", .product(name: "JSONRPC", package: "JSONRPC"), .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), .product(name: "GoogleGenerativeAI", package: "generative-ai-swift"), @@ -382,6 +384,7 @@ let package = Package( name: "OpenAIServiceTests", dependencies: [ "OpenAIService", + "ChatBasic", .product( name: "ComposableArchitecture", package: "swift-composable-architecture" diff --git a/Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift b/Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift index 02155ac0..7c462176 100644 --- a/Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift +++ b/Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift @@ -15,10 +15,12 @@ final class FetchSuggestionTests: XCTestCase { func test_process_suggestions_from_server() async throws { struct TestServer: GitHubCopilotLSP { func sendNotification(_: LanguageServerProtocol.ClientNotification) async throws { - throw CancellationError() + return } - func sendRequest(_: E) async throws -> E.Response where E: GitHubCopilotRequestType { + func sendRequest(_: E, timeout: TimeInterval?) async throws -> E.Response + where E: GitHubCopilotRequestType + { return GitHubCopilotRequest.InlineCompletion.Response(items: [ .init( insertText: "Hello World\n", @@ -70,7 +72,9 @@ final class FetchSuggestionTests: XCTestCase { // unimplemented } - func sendRequest(_: E) async throws -> E.Response where E: GitHubCopilotRequestType { + func sendRequest(_: E, timeout: TimeInterval?) async throws -> E.Response + where E: GitHubCopilotRequestType + { return GitHubCopilotRequest.InlineCompletion.Response(items: [ .init( insertText: "Hello World\n", diff --git a/Tool/Tests/OpenAIServiceTests/AutoManagedChatGPTMemoryRetrievedContentTests.swift b/Tool/Tests/OpenAIServiceTests/AutoManagedChatGPTMemoryRetrievedContentTests.swift index 8e1fb855..afa11eda 100644 --- a/Tool/Tests/OpenAIServiceTests/AutoManagedChatGPTMemoryRetrievedContentTests.swift +++ b/Tool/Tests/OpenAIServiceTests/AutoManagedChatGPTMemoryRetrievedContentTests.swift @@ -1,3 +1,4 @@ +import ChatBasic import Foundation import XCTest @@ -20,7 +21,7 @@ class AutoManagedChatGPTMemoryRetrievedContentTests: XCTestCase { func test_retrieved_content_when_the_context_window_is_large_enough() async { let strategy = Strategy() - + let memory = AutoManagedChatGPTMemory( systemPrompt: "", configuration: UserPreferenceChatGPTConfiguration(), @@ -57,7 +58,7 @@ class AutoManagedChatGPTMemoryRetrievedContentTests: XCTestCase { """ let maxTokenCount = await strategy.countToken(.init(role: .user, content: fullContent)) - + let result = await memory.generateRetrievedContentMessage( maxTokenCount: maxTokenCount, strategy: strategy @@ -159,7 +160,7 @@ class AutoManagedChatGPTMemoryRetrievedContentTests: XCTestCase { D """) } - + func test_retrieved_content_when_the_context_window_can_take_only_one_document() async { let strategy = Strategy() @@ -200,7 +201,7 @@ class AutoManagedChatGPTMemoryRetrievedContentTests: XCTestCase { A """) } - + func test_retrieved_content_when_the_context_window_empty() async { let strategy = Strategy() diff --git a/Tool/Tests/OpenAIServiceTests/ChatGPTStreamTests.swift b/Tool/Tests/OpenAIServiceTests/ChatGPTStreamTests.swift index 38bebbbe..5f7c50db 100644 --- a/Tool/Tests/OpenAIServiceTests/ChatGPTStreamTests.swift +++ b/Tool/Tests/OpenAIServiceTests/ChatGPTStreamTests.swift @@ -1,3 +1,4 @@ +import ChatBasic import Dependencies import XCTest @testable import OpenAIService diff --git a/Tool/Tests/OpenAIServiceTests/LimitMessagesTests.swift b/Tool/Tests/OpenAIServiceTests/LimitMessagesTests.swift index 1173494a..3d47d6dc 100644 --- a/Tool/Tests/OpenAIServiceTests/LimitMessagesTests.swift +++ b/Tool/Tests/OpenAIServiceTests/LimitMessagesTests.swift @@ -1,3 +1,4 @@ +import ChatBasic import Foundation import TokenEncoder import XCTest @@ -103,11 +104,11 @@ class MockEncoder: TokenEncoder { struct MockStrategy: AutoManagedChatGPTMemoryStrategy { let encoder = MockEncoder() - func countToken(_ message: OpenAIService.ChatMessage) async -> Int { + func countToken(_ message: ChatBasic.ChatMessage) async -> Int { await encoder.countToken(message) } - func countToken(_: F) async -> Int where F: OpenAIService.ChatGPTFunction { + func countToken(_: F) async -> Int where F: ChatBasic.ChatGPTFunction { 0 } From 74eec7ae26284db1e9ea53b0ac4a754adacc955b Mon Sep 17 00:00:00 2001 From: RoshanNagaram-eng Date: Mon, 24 Jun 2024 10:49:26 -0700 Subject: [PATCH 22/44] Added Indexing Changes for Codeium --- .../HostApp/AccountSettings/CodeiumView.swift | 7 ++ .../CodeiumWorkspacePlugin.swift | 3 +- .../CodeiumInstallationManager.swift | 9 ++ .../CodeiumLanguageServer.swift | 115 ++++++++++++++++++ .../Services/CodeiumService.swift | 10 ++ Tool/Sources/Preferences/Keys.swift | 8 ++ 6 files changed, 150 insertions(+), 2 deletions(-) diff --git a/Core/Sources/HostApp/AccountSettings/CodeiumView.swift b/Core/Sources/HostApp/AccountSettings/CodeiumView.swift index c4da142b..acf5cad1 100644 --- a/Core/Sources/HostApp/AccountSettings/CodeiumView.swift +++ b/Core/Sources/HostApp/AccountSettings/CodeiumView.swift @@ -14,6 +14,7 @@ struct CodeiumView: View { @AppStorage(\.codeiumEnterpriseMode) var codeiumEnterpriseMode @AppStorage(\.codeiumPortalUrl) var codeiumPortalUrl @AppStorage(\.codeiumApiUrl) var codeiumApiUrl + @AppStorage(\.indexEnabled) var indexEnabled init() { isSignedIn = codeiumAuthService.isSignedIn @@ -205,6 +206,12 @@ struct CodeiumView: View { } } } + + SubSection(title: Text("Indexing")) { + Form { + Toggle("Enable Indexing", isOn: $viewModel.indexEnabled) + } + } SubSection(title: Text("Enterprise")) { Form { diff --git a/Tool/Sources/CodeiumService/CodeiumWorkspacePlugin.swift b/Tool/Sources/CodeiumService/CodeiumWorkspacePlugin.swift index 9534f7f1..aef40f4e 100644 --- a/Tool/Sources/CodeiumService/CodeiumWorkspacePlugin.swift +++ b/Tool/Sources/CodeiumService/CodeiumWorkspacePlugin.swift @@ -26,8 +26,7 @@ public final class CodeiumWorkspacePlugin: WorkspacePlugin { let newService = try CodeiumService( projectRootURL: projectRootURL, onServiceLaunched: { - [weak self] in - self?.finishLaunchingService() + }, onServiceTerminated: { // start handled in the service. diff --git a/Tool/Sources/CodeiumService/LanguageServer/CodeiumInstallationManager.swift b/Tool/Sources/CodeiumService/LanguageServer/CodeiumInstallationManager.swift index ddd57777..24e979e8 100644 --- a/Tool/Sources/CodeiumService/LanguageServer/CodeiumInstallationManager.swift +++ b/Tool/Sources/CodeiumService/LanguageServer/CodeiumInstallationManager.swift @@ -20,6 +20,15 @@ public struct CodeiumInstallationManager { } } } + + public func getLatestSupportedVersion() -> String { + if isEnterprise{ + return UserDefaults.shared.value(for: \.codeiumEnterpriseVersion); + } + + return Self.latestSupportedVersion; + + } func getEnterprisePortalVersion() async throws -> String { let enterprisePortalUrl = UserDefaults.shared.value(for: \.codeiumPortalUrl) diff --git a/Tool/Sources/CodeiumService/LanguageServer/CodeiumLanguageServer.swift b/Tool/Sources/CodeiumService/LanguageServer/CodeiumLanguageServer.swift index 21fbab15..85dc29f7 100644 --- a/Tool/Sources/CodeiumService/LanguageServer/CodeiumLanguageServer.swift +++ b/Tool/Sources/CodeiumService/LanguageServer/CodeiumLanguageServer.swift @@ -4,9 +4,11 @@ import LanguageClient import LanguageServerProtocol import Logger import Preferences +import XcodeInspector protocol CodeiumLSP { func sendRequest(_ endpoint: E) async throws -> E.Response + func updateIndexing() async func terminate() } @@ -20,6 +22,7 @@ final class CodeiumLanguageServer { var launchHandler: (() -> Void)? var port: String? var heartbeatTask: Task? + var project_paths: [String] init( languageServerExecutableURL: URL, @@ -33,6 +36,7 @@ final class CodeiumLanguageServer { self.supportURL = supportURL self.terminationHandler = terminationHandler self.launchHandler = launchHandler + self.project_paths = [] process = Process() transport = IOTransport() @@ -60,6 +64,18 @@ final class CodeiumLanguageServer { if isEnterpriseMode { process.arguments?.append("--enterprise_mode") } + + let indexEnabled = UserDefaults.shared.value(for: \.indexEnabled) + if indexEnabled { + let indexingMaxFileSize = UserDefaults.shared.value(for: \.indexingMaxFileSize) + if (indexEnabled) { + process.arguments?.append("--enable_local_search") + process.arguments?.append("--enable_index_service") + process.arguments?.append("--search_max_workspace_file_count") + process.arguments?.append("\(indexingMaxFileSize)") + Logger.codeium.info("Indexing Enabled") + } + } process.currentDirectoryURL = supportURL @@ -177,6 +193,38 @@ extension CodeiumLanguageServer: CodeiumLSP { } } } + + func updateIndexing() async { + let indexEnabled = UserDefaults.shared.value(for: \.indexEnabled) + if !indexEnabled { + return + } + + + let curr_proj_paths = await getProjectPaths() + + // Add all workspaces that are in the curr_proj_paths but not in the previous project paths + for curr_proj_path in curr_proj_paths { + if !self.project_paths.contains(curr_proj_path) && FileManager.default.fileExists(atPath: curr_proj_path) { + _ = try? await self.sendRequest(CodeiumRequest.AddTrackedWorkspace(requestBody: .init( + workspace: curr_proj_path + ))) + } + } + + // Remove all workspaces that are in previous project paths but not in the curr_proj_paths + for proj_path in self.project_paths { + if !curr_proj_paths.contains(proj_path) && FileManager.default.fileExists(atPath: proj_path) { + _ = try? await self.sendRequest(CodeiumRequest.RemoveTrackedWorkspace(requestBody: .init( + workspace: proj_path + ))) + } + } + // These should be identical now + self.project_paths = curr_proj_paths + + } + } final class IOTransport { @@ -272,3 +320,70 @@ final class IOTransport { } } +class WorkspaceParser: NSObject, XMLParserDelegate { + var projectPaths: [String] = [] + var workspaceFileURL: URL + var workspaceBaseURL: URL + + init(workspaceFileURL: URL, workspaceBaseURL: URL) { + self.workspaceFileURL = workspaceFileURL + self.workspaceBaseURL = workspaceBaseURL + } + + func parse() -> [String] { + guard let parser = XMLParser(contentsOf: workspaceFileURL) else { + print("Failed to create XML parser for file: \(workspaceFileURL.path)") + return [] + } + parser.delegate = self + parser.parse() + return projectPaths + } + + // XMLParserDelegate methods + func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) { + if elementName == "FileRef", let location = attributeDict["location"] { + var project_path: String + if location.starts(with: "group:") && pathEndsWithXcodeproj(location) { + let curr_path = String(location.dropFirst("group:".count)) + guard let relative_project_url = URL(string: curr_path) else { + return + } + let relative_base_path = relative_project_url.deletingLastPathComponent() + project_path = (self.workspaceBaseURL.appendingPathComponent(relative_base_path.relativePath)).standardized.path + } else if location.starts(with: "absolute:") && pathEndsWithXcodeproj(location){ + let abs_url = URL(fileURLWithPath: String(location.dropFirst("absolute:".count))) + project_path = abs_url.deletingLastPathComponent().standardized.path + } else { + return + } + if FileManager.default.fileExists(atPath: project_path) { + + projectPaths.append(project_path) + } + } + } + + func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) { + print("Failed to parse XML: \(parseError.localizedDescription)") + } + + func pathEndsWithXcodeproj(_ path: String) -> Bool { + return path.hasSuffix(".xcodeproj") + } + +} + +public func getProjectPaths() async -> [String] { + guard let workspaceURL = await XcodeInspector.shared.safe.realtimeActiveWorkspaceURL else { + return [] + } + + let workspacebaseURL = workspaceURL.deletingLastPathComponent() + + let workspaceContentsURL = workspaceURL.appendingPathComponent("contents.xcworkspacedata") + + let parser = WorkspaceParser(workspaceFileURL: workspaceContentsURL, workspaceBaseURL: workspacebaseURL) + let absolutePaths = parser.parse() + return absolutePaths +} diff --git a/Tool/Sources/CodeiumService/Services/CodeiumService.swift b/Tool/Sources/CodeiumService/Services/CodeiumService.swift index a1c71551..1f44fdd1 100644 --- a/Tool/Sources/CodeiumService/Services/CodeiumService.swift +++ b/Tool/Sources/CodeiumService/Services/CodeiumService.swift @@ -49,6 +49,7 @@ public class CodeiumService { let projectRootURL: URL var server: CodeiumLSP? var heartbeatTask: Task? + var workspaceTask: Task? var requestCounter: UInt64 = 0 var cancellationCounter: UInt64 = 0 let openedDocumentPool = OpenedDocumentPool() @@ -127,6 +128,7 @@ public class CodeiumService { server.terminationHandler = { [weak self] in self?.server = nil self?.heartbeatTask?.cancel() + self?.workspaceTask?.cancel() self?.requestCounter = 0 self?.cancellationCounter = 0 self?.onServiceTerminated() @@ -145,6 +147,14 @@ public class CodeiumService { try await Task.sleep(nanoseconds: 5_000_000_000) } } + + self.workspaceTask = Task { [weak self] in + while true { + try Task.checkCancellation() + _ = await self?.server?.updateIndexing() + try await Task.sleep(nanoseconds: 5_000_000_000) + } + } } self.server = server diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 9fdbc92f..5e168bf3 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -219,6 +219,14 @@ public extension UserDefaultPreferenceKeys { var codeiumEnterpriseVersion: PreferenceKey { .init(defaultValue: "", key: "CodeiumEnterpriseVersion") } + + var indexEnabled: PreferenceKey { + .init(defaultValue: false, key: "indexEnabled") + } + + var indexingMaxFileSize: PreferenceKey { + .init(defaultValue: 5000, key: "indexingMaxFileSize") + } } From 1e0ed33504c5b17d78dc5466a2e06de79f283bee Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 1 Jul 2024 00:05:56 +0800 Subject: [PATCH 23/44] Update preference keys names --- Core/Sources/HostApp/AccountSettings/CodeiumView.swift | 2 +- .../LanguageServer/CodeiumLanguageServer.swift | 6 +++--- Tool/Sources/Preferences/Keys.swift | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Core/Sources/HostApp/AccountSettings/CodeiumView.swift b/Core/Sources/HostApp/AccountSettings/CodeiumView.swift index acf5cad1..217f8167 100644 --- a/Core/Sources/HostApp/AccountSettings/CodeiumView.swift +++ b/Core/Sources/HostApp/AccountSettings/CodeiumView.swift @@ -14,7 +14,7 @@ struct CodeiumView: View { @AppStorage(\.codeiumEnterpriseMode) var codeiumEnterpriseMode @AppStorage(\.codeiumPortalUrl) var codeiumPortalUrl @AppStorage(\.codeiumApiUrl) var codeiumApiUrl - @AppStorage(\.indexEnabled) var indexEnabled + @AppStorage(\.codeiumIndexEnabled) var indexEnabled init() { isSignedIn = codeiumAuthService.isSignedIn diff --git a/Tool/Sources/CodeiumService/LanguageServer/CodeiumLanguageServer.swift b/Tool/Sources/CodeiumService/LanguageServer/CodeiumLanguageServer.swift index 85dc29f7..275e0b19 100644 --- a/Tool/Sources/CodeiumService/LanguageServer/CodeiumLanguageServer.swift +++ b/Tool/Sources/CodeiumService/LanguageServer/CodeiumLanguageServer.swift @@ -65,9 +65,9 @@ final class CodeiumLanguageServer { process.arguments?.append("--enterprise_mode") } - let indexEnabled = UserDefaults.shared.value(for: \.indexEnabled) + let indexEnabled = UserDefaults.shared.value(for: \.codeiumIndexEnabled) if indexEnabled { - let indexingMaxFileSize = UserDefaults.shared.value(for: \.indexingMaxFileSize) + let indexingMaxFileSize = UserDefaults.shared.value(for: \.codeiumIndexingMaxFileSize) if (indexEnabled) { process.arguments?.append("--enable_local_search") process.arguments?.append("--enable_index_service") @@ -195,7 +195,7 @@ extension CodeiumLanguageServer: CodeiumLSP { } func updateIndexing() async { - let indexEnabled = UserDefaults.shared.value(for: \.indexEnabled) + let indexEnabled = UserDefaults.shared.value(for: \.codeiumIndexEnabled) if !indexEnabled { return } diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 5e168bf3..4b668284 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -220,12 +220,12 @@ public extension UserDefaultPreferenceKeys { .init(defaultValue: "", key: "CodeiumEnterpriseVersion") } - var indexEnabled: PreferenceKey { - .init(defaultValue: false, key: "indexEnabled") + var codeiumIndexEnabled: PreferenceKey { + .init(defaultValue: false, key: "CodeiumIndexEnabled") } - var indexingMaxFileSize: PreferenceKey { - .init(defaultValue: 5000, key: "indexingMaxFileSize") + var codeiumIndexingMaxFileSize: PreferenceKey { + .init(defaultValue: 5000, key: "CodeiumIndexingMaxFileSize") } } From 920bba7014194dce5c76ee3a6fa7fb67f3ea8ab3 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 1 Jul 2024 00:10:46 +0800 Subject: [PATCH 24/44] Format files --- .../CodeiumInstallationManager.swift | 13 ++-- .../CodeiumLanguageServer.swift | 76 +++++++++++-------- .../Services/CodeiumService.swift | 14 ++-- Tool/Sources/Preferences/Keys.swift | 35 +++++---- 4 files changed, 74 insertions(+), 64 deletions(-) diff --git a/Tool/Sources/CodeiumService/LanguageServer/CodeiumInstallationManager.swift b/Tool/Sources/CodeiumService/LanguageServer/CodeiumInstallationManager.swift index 24e979e8..e2b25492 100644 --- a/Tool/Sources/CodeiumService/LanguageServer/CodeiumInstallationManager.swift +++ b/Tool/Sources/CodeiumService/LanguageServer/CodeiumInstallationManager.swift @@ -11,7 +11,7 @@ public struct CodeiumInstallationManager { case badURL(String) case invalidResponse case invalidData - + var errorDescription: String? { switch self { case .badURL: return "URL is invalid" @@ -20,14 +20,13 @@ public struct CodeiumInstallationManager { } } } - + public func getLatestSupportedVersion() -> String { - if isEnterprise{ - return UserDefaults.shared.value(for: \.codeiumEnterpriseVersion); + if isEnterprise { + return UserDefaults.shared.value(for: \.codeiumEnterpriseVersion) } - - return Self.latestSupportedVersion; - + + return Self.latestSupportedVersion } func getEnterprisePortalVersion() async throws -> String { diff --git a/Tool/Sources/CodeiumService/LanguageServer/CodeiumLanguageServer.swift b/Tool/Sources/CodeiumService/LanguageServer/CodeiumLanguageServer.swift index 275e0b19..5de10e7f 100644 --- a/Tool/Sources/CodeiumService/LanguageServer/CodeiumLanguageServer.swift +++ b/Tool/Sources/CodeiumService/LanguageServer/CodeiumLanguageServer.swift @@ -36,7 +36,7 @@ final class CodeiumLanguageServer { self.supportURL = supportURL self.terminationHandler = terminationHandler self.launchHandler = launchHandler - self.project_paths = [] + project_paths = [] process = Process() transport = IOTransport() @@ -64,16 +64,16 @@ final class CodeiumLanguageServer { if isEnterpriseMode { process.arguments?.append("--enterprise_mode") } - + let indexEnabled = UserDefaults.shared.value(for: \.codeiumIndexEnabled) if indexEnabled { let indexingMaxFileSize = UserDefaults.shared.value(for: \.codeiumIndexingMaxFileSize) - if (indexEnabled) { - process.arguments?.append("--enable_local_search") - process.arguments?.append("--enable_index_service") - process.arguments?.append("--search_max_workspace_file_count") - process.arguments?.append("\(indexingMaxFileSize)") - Logger.codeium.info("Indexing Enabled") + if indexEnabled { + process.arguments?.append("--enable_local_search") + process.arguments?.append("--enable_index_service") + process.arguments?.append("--search_max_workspace_file_count") + process.arguments?.append("\(indexingMaxFileSize)") + Logger.codeium.info("Indexing Enabled") } } @@ -193,38 +193,39 @@ extension CodeiumLanguageServer: CodeiumLSP { } } } - + func updateIndexing() async { let indexEnabled = UserDefaults.shared.value(for: \.codeiumIndexEnabled) if !indexEnabled { return } - let curr_proj_paths = await getProjectPaths() - + // Add all workspaces that are in the curr_proj_paths but not in the previous project paths for curr_proj_path in curr_proj_paths { - if !self.project_paths.contains(curr_proj_path) && FileManager.default.fileExists(atPath: curr_proj_path) { - _ = try? await self.sendRequest(CodeiumRequest.AddTrackedWorkspace(requestBody: .init( + if !project_paths.contains(curr_proj_path) && FileManager.default + .fileExists(atPath: curr_proj_path) + { + _ = try? await sendRequest(CodeiumRequest.AddTrackedWorkspace(requestBody: .init( workspace: curr_proj_path ))) } } - + // Remove all workspaces that are in previous project paths but not in the curr_proj_paths - for proj_path in self.project_paths { - if !curr_proj_paths.contains(proj_path) && FileManager.default.fileExists(atPath: proj_path) { - _ = try? await self.sendRequest(CodeiumRequest.RemoveTrackedWorkspace(requestBody: .init( + for proj_path in project_paths { + if !curr_proj_paths.contains(proj_path) && FileManager.default + .fileExists(atPath: proj_path) + { + _ = try? await sendRequest(CodeiumRequest.RemoveTrackedWorkspace(requestBody: .init( workspace: proj_path ))) } } // These should be identical now - self.project_paths = curr_proj_paths - + project_paths = curr_proj_paths } - } final class IOTransport { @@ -329,7 +330,7 @@ class WorkspaceParser: NSObject, XMLParserDelegate { self.workspaceFileURL = workspaceFileURL self.workspaceBaseURL = workspaceBaseURL } - + func parse() -> [String] { guard let parser = XMLParser(contentsOf: workspaceFileURL) else { print("Failed to create XML parser for file: \(workspaceFileURL.path)") @@ -339,9 +340,15 @@ class WorkspaceParser: NSObject, XMLParserDelegate { parser.parse() return projectPaths } - + // XMLParserDelegate methods - func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) { + func parser( + _ parser: XMLParser, + didStartElement elementName: String, + namespaceURI: String?, + qualifiedName qName: String?, + attributes attributeDict: [String: String] + ) { if elementName == "FileRef", let location = attributeDict["location"] { var project_path: String if location.starts(with: "group:") && pathEndsWithXcodeproj(location) { @@ -350,28 +357,29 @@ class WorkspaceParser: NSObject, XMLParserDelegate { return } let relative_base_path = relative_project_url.deletingLastPathComponent() - project_path = (self.workspaceBaseURL.appendingPathComponent(relative_base_path.relativePath)).standardized.path - } else if location.starts(with: "absolute:") && pathEndsWithXcodeproj(location){ + project_path = ( + workspaceBaseURL + .appendingPathComponent(relative_base_path.relativePath) + ).standardized.path + } else if location.starts(with: "absolute:") && pathEndsWithXcodeproj(location) { let abs_url = URL(fileURLWithPath: String(location.dropFirst("absolute:".count))) project_path = abs_url.deletingLastPathComponent().standardized.path } else { return } if FileManager.default.fileExists(atPath: project_path) { - projectPaths.append(project_path) } } } - + func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) { print("Failed to parse XML: \(parseError.localizedDescription)") } - + func pathEndsWithXcodeproj(_ path: String) -> Bool { return path.hasSuffix(".xcodeproj") } - } public func getProjectPaths() async -> [String] { @@ -380,10 +388,14 @@ public func getProjectPaths() async -> [String] { } let workspacebaseURL = workspaceURL.deletingLastPathComponent() - + let workspaceContentsURL = workspaceURL.appendingPathComponent("contents.xcworkspacedata") - - let parser = WorkspaceParser(workspaceFileURL: workspaceContentsURL, workspaceBaseURL: workspacebaseURL) + + let parser = WorkspaceParser( + workspaceFileURL: workspaceContentsURL, + workspaceBaseURL: workspacebaseURL + ) let absolutePaths = parser.parse() return absolutePaths } + diff --git a/Tool/Sources/CodeiumService/Services/CodeiumService.swift b/Tool/Sources/CodeiumService/Services/CodeiumService.swift index 1f44fdd1..11346b57 100644 --- a/Tool/Sources/CodeiumService/Services/CodeiumService.swift +++ b/Tool/Sources/CodeiumService/Services/CodeiumService.swift @@ -147,7 +147,7 @@ public class CodeiumService { try await Task.sleep(nanoseconds: 5_000_000_000) } } - + self.workspaceTask = Task { [weak self] in while true { try Task.checkCancellation() @@ -257,7 +257,7 @@ extension CodeiumService: CodeiumSuggestionServiceType { requestCounter += 1 let languageId = languageIdentifierFromFileURL(fileURL) - let relativePath = getRelativePath(of: fileURL) + let relativePath = getRelativePath(of: fileURL) let task = Task { let request = try await CodeiumRequest.GetCompletion(requestBody: .init( @@ -350,7 +350,7 @@ extension CodeiumService: CodeiumSuggestionServiceType { URLQueryItem(name: "ide_name", value: metadata.ide_name), URLQueryItem(name: "ide_version", value: metadata.ide_version), URLQueryItem(name: "web_server_url", value: webServerUrl), - URLQueryItem(name: "ide_telemetry_enabled", value: "true") + URLQueryItem(name: "ide_telemetry_enabled", value: "true"), ] if let url = components.url { @@ -367,7 +367,7 @@ extension CodeiumService: CodeiumSuggestionServiceType { metadata: getMetadata(), completion_id: suggestion.id ))) - } + } public func notifyOpenTextDocument(fileURL: URL, content: String) async throws { let relativePath = getRelativePath(of: fileURL) @@ -392,14 +392,14 @@ extension CodeiumService: CodeiumSuggestionServiceType { } public func notifyOpenWorkspace(workspaceURL: URL) async throws { - _ = try await (try setupServerIfNeeded()).sendRequest( + _ = try await (setupServerIfNeeded()).sendRequest( CodeiumRequest .AddTrackedWorkspace(requestBody: .init(workspace: workspaceURL.path)) ) } public func notifyCloseWorkspace(workspaceURL: URL) async throws { - _ = try await (try setupServerIfNeeded()).sendRequest( + _ = try await (setupServerIfNeeded()).sendRequest( CodeiumRequest .RemoveTrackedWorkspace(requestBody: .init(workspace: workspaceURL.path)) ) @@ -432,7 +432,7 @@ extension CodeiumService: CodeiumSuggestionServiceType { .map(\.url.path), workspace_paths: [workspaceURL.path] )) - _ = try await (try setupServerIfNeeded()).sendRequest(request) + _ = try await (setupServerIfNeeded()).sendRequest(request) } public func terminate() { diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 4b668284..ca6c7aca 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -187,11 +187,11 @@ public extension UserDefaultPreferenceKeys { var runNodeWith: PreferenceKey { .init(defaultValue: .env, key: "RunNodeWith") } - + var gitHubCopilotLoadKeyChainCertificates: PreferenceKey { .init(defaultValue: false, key: "GitHubCopilotLoadKeyChainCertificates") } - + var gitHubCopilotPretendIDEToBeVSCode: PreferenceKey { .init(defaultValue: false, key: "GitHubCopilotPretendIDEToBeVSCode") } @@ -215,7 +215,7 @@ public extension UserDefaultPreferenceKeys { var codeiumApiUrl: PreferenceKey { .init(defaultValue: "", key: "CodeiumApiUrl") } - + var codeiumEnterpriseVersion: PreferenceKey { .init(defaultValue: "", key: "CodeiumEnterpriseVersion") } @@ -223,11 +223,10 @@ public extension UserDefaultPreferenceKeys { var codeiumIndexEnabled: PreferenceKey { .init(defaultValue: false, key: "CodeiumIndexEnabled") } - + var codeiumIndexingMaxFileSize: PreferenceKey { .init(defaultValue: 5000, key: "CodeiumIndexingMaxFileSize") } - } // MARK: - Chat Models @@ -319,7 +318,7 @@ public extension UserDefaultPreferenceKeys { var hideCommonPrecedingSpacesInPromptToCode: PreferenceKey { .init(defaultValue: true, key: "HideCommonPrecedingSpacesInPromptToCode") } - + var wrapCodeInPromptToCode: PreferenceKey { .init(defaultValue: true, key: "WrapCodeInPromptToCode") } @@ -375,23 +374,23 @@ public extension UserDefaultPreferenceKeys { var acceptSuggestionWithTab: PreferenceKey { .init(defaultValue: true, key: "AcceptSuggestionWithTab") } - + var acceptSuggestionWithModifierCommand: PreferenceKey { .init(defaultValue: false, key: "SuggestionWithModifierCommand") } - + var acceptSuggestionWithModifierOption: PreferenceKey { .init(defaultValue: false, key: "SuggestionWithModifierOption") } - + var acceptSuggestionWithModifierControl: PreferenceKey { .init(defaultValue: false, key: "SuggestionWithModifierControl") } - + var acceptSuggestionWithModifierShift: PreferenceKey { .init(defaultValue: false, key: "SuggestionWithModifierShift") } - + var acceptSuggestionWithModifierOnlyForSwift: PreferenceKey { .init(defaultValue: false, key: "SuggestionWithModifierOnlyForSwift") } @@ -403,7 +402,7 @@ public extension UserDefaultPreferenceKeys { var isSuggestionSenseEnabled: PreferenceKey { .init(defaultValue: false, key: "IsSuggestionSenseEnabled") } - + var isSuggestionTypeInTheMiddleEnabled: PreferenceKey { .init(defaultValue: true, key: "IsSuggestionTypeInTheMiddleEnabled") } @@ -506,23 +505,23 @@ public extension UserDefaultPreferenceKeys { var preferredChatModelIdForWebScope: PreferenceKey { .init(defaultValue: "", key: "PreferredChatModelIdForWebScope") } - + var disableFloatOnTopWhenTheChatPanelIsDetached: PreferenceKey { .init(defaultValue: true, key: "DisableFloatOnTopWhenTheChatPanelIsDetached") } - + var keepFloatOnTopIfChatPanelAndXcodeOverlaps: PreferenceKey { .init(defaultValue: true, key: "KeepFloatOnTopIfChatPanelAndXcodeOverlaps") } - + var openChatMode: PreferenceKey { .init(defaultValue: .chatPanel, key: "OpenChatMode") } - + var openChatInBrowserURL: PreferenceKey { .init(defaultValue: "", key: "OpenChatInBrowserURL") } - + var openChatInBrowserInInAppBrowser: PreferenceKey { .init(defaultValue: true, key: "OpenChatInBrowserInInAppBrowser") } @@ -579,7 +578,7 @@ public extension UserDefaultPreferenceKeys { key: "ChatCodeFont" ) } - + var terminalFont: PreferenceKey> { .init( defaultValue: .init(.init(nsFont: .monospacedSystemFont(ofSize: 12, weight: .regular))), From 19fe95fd8d2e0d4662f0b2760bcdf4bac2c2470d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 1 Jul 2024 00:12:42 +0800 Subject: [PATCH 25/44] Fix naming style --- .../CodeiumLanguageServer.swift | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/Tool/Sources/CodeiumService/LanguageServer/CodeiumLanguageServer.swift b/Tool/Sources/CodeiumService/LanguageServer/CodeiumLanguageServer.swift index 5de10e7f..62af56b1 100644 --- a/Tool/Sources/CodeiumService/LanguageServer/CodeiumLanguageServer.swift +++ b/Tool/Sources/CodeiumService/LanguageServer/CodeiumLanguageServer.swift @@ -22,7 +22,7 @@ final class CodeiumLanguageServer { var launchHandler: (() -> Void)? var port: String? var heartbeatTask: Task? - var project_paths: [String] + var projectPaths: [String] init( languageServerExecutableURL: URL, @@ -36,7 +36,7 @@ final class CodeiumLanguageServer { self.supportURL = supportURL self.terminationHandler = terminationHandler self.launchHandler = launchHandler - project_paths = [] + projectPaths = [] process = Process() transport = IOTransport() @@ -200,31 +200,33 @@ extension CodeiumLanguageServer: CodeiumLSP { return } - let curr_proj_paths = await getProjectPaths() + let currentProjectPaths = await getProjectPaths() - // Add all workspaces that are in the curr_proj_paths but not in the previous project paths - for curr_proj_path in curr_proj_paths { - if !project_paths.contains(curr_proj_path) && FileManager.default - .fileExists(atPath: curr_proj_path) + // Add all workspaces that are in the currentProjectPaths but not in the previous project + // paths + for currentProjectPath in currentProjectPaths { + if !projectPaths.contains(currentProjectPath) && FileManager.default + .fileExists(atPath: currentProjectPath) { _ = try? await sendRequest(CodeiumRequest.AddTrackedWorkspace(requestBody: .init( - workspace: curr_proj_path + workspace: currentProjectPath ))) } } - // Remove all workspaces that are in previous project paths but not in the curr_proj_paths - for proj_path in project_paths { - if !curr_proj_paths.contains(proj_path) && FileManager.default - .fileExists(atPath: proj_path) + // Remove all workspaces that are in previous project paths but not in the + // currentProjectPaths + for projectPath in projectPaths { + if !currentProjectPaths.contains(projectPath) && FileManager.default + .fileExists(atPath: projectPath) { _ = try? await sendRequest(CodeiumRequest.RemoveTrackedWorkspace(requestBody: .init( - workspace: proj_path + workspace: projectPath ))) } } // These should be identical now - project_paths = curr_proj_paths + projectPaths = currentProjectPaths } } From f255576b8671086d444c515036df0c9f2ae5597c Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 1 Jul 2024 00:19:56 +0800 Subject: [PATCH 26/44] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index f1d396ca..c95c52aa 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit f1d396caac4704e9ffcad0224cdd341ab3fe5cfb +Subproject commit c95c52aa39427d76ac8a3a856d4f4883ce91e074 From c3c097fccc619da144fe3d4799787132246a238e Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 1 Jul 2024 01:14:03 +0800 Subject: [PATCH 27/44] Bump build number --- Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Version.xcconfig b/Version.xcconfig index 74114724..427e0f8d 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,3 +1,3 @@ APP_VERSION = 0.33.5 -APP_BUILD = 391 +APP_BUILD = 392 From 57480f0bc31e6ed00705c33e471e64f577db9894 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 1 Jul 2024 21:43:54 +0800 Subject: [PATCH 28/44] Rename SuggestionModel to SuggestionBasic --- Core/Package.swift | 14 ++++----- .../CustomCommandTemplateProcessor.swift | 2 +- .../AccountSettings/BingSearchView.swift | 2 +- .../AccountSettings/GitHubCopilotView.swift | 2 +- ...stionFeatureDisabledLanguageListView.swift | 2 +- .../OpenAIPromptToCodeService.swift | 2 +- .../PreviewPromptToCodeService.swift | 2 +- .../PromptToCodeServiceType.swift | 2 +- Core/Sources/Service/GUI/ChatTabFactory.swift | 2 +- .../GraphicalUserInterfaceController.swift | 2 +- .../Service/GUI/WidgetDataSource.swift | 2 +- .../PseudoCommandHandler.swift | 2 +- .../SuggestionCommandHandler.swift | 2 +- .../WindowBaseCommandHandler.swift | 2 +- .../PresentInWindowSuggestionPresenter.swift | 2 +- .../SuggestionInjector.swift | 2 +- .../SuggestionService/SuggestionService.swift | 8 ++--- .../CursorPositionTracker.swift | 2 +- .../CircularWidgetFeature.swift | 2 +- .../FeatureReducers/PromptToCode.swift | 2 +- .../FeatureReducers/PromptToCodeGroup.swift | 2 +- .../CodeBlockSuggestionPanel.swift | 2 +- .../PromptToCodePanel.swift | 2 +- .../Sources/SuggestionWidget/WidgetView.swift | 2 +- Core/Tests/ServiceTests/Environment.swift | 6 ++-- .../ExtractSelectedCodeTests.swift | 2 +- ...FilespaceSuggestionInvalidationTests.swift | 2 +- .../AcceptSuggestionTests.swift | 2 +- .../ProposeSuggestionTests.swift | 2 +- .../RejectSuggestionTests.swift | 2 +- .../AcceptPromptToCodeCommand.swift | 2 +- EditorExtension/AcceptSuggestionCommand.swift | 2 +- EditorExtension/CloseIdleTabsCommand.swift | 2 +- EditorExtension/CustomCommand.swift | 2 +- EditorExtension/GetSuggestionsCommand.swift | 2 +- EditorExtension/Helpers.swift | 2 +- EditorExtension/NextSuggestionCommand.swift | 2 +- EditorExtension/OpenChat.swift | 2 +- .../PrefetchSuggestionsCommand.swift | 2 +- .../PreviousSuggestionCommand.swift | 2 +- EditorExtension/PromptToCodeCommand.swift | 2 +- .../RealtimeSuggestionCommand.swift | 2 +- EditorExtension/RejectSuggestionCommand.swift | 2 +- EditorExtension/SeparatorCommand.swift | 2 +- .../ToggleRealtimeSuggestionsCommand.swift | 2 +- Pro | 2 +- Tool/Package.swift | 30 +++++++++---------- Tool/Sources/ASTParser/ASTParser.swift | 2 +- ...inExtensionSuggestionServiceProvider.swift | 12 ++++---- .../ActiveDocumentChatContextCollector.swift | 2 +- .../GetCodeCodeAroundLineFunction.swift | 2 +- ...cyActiveDocumentChatContextCollector.swift | 2 +- .../ReadableCursorRange.swift | 2 +- .../LanguageServer/CodeiumModels.swift | 2 +- .../LanguageServer/CodeiumRequest.swift | 2 +- .../CodeiumSupportedLanguage.swift | 2 +- .../Services/CodeiumService.swift | 2 +- .../Services/CodeiumSuggestionService.swift | 6 ++-- .../ActiveDocumentContext.swift | 2 +- .../FocusedCodeFinder/FocusedCodeFinder.swift | 2 +- .../KnownLanguageFocusedCodeFinder.swift | 8 ++--- .../ObjectiveC/ObjectiveCCodeFinder.swift | 2 +- ...bjectiveCScopeHierarchySyntaxVisitor.swift | 2 +- .../Swift/SwiftFocusedCodeFinder.swift | 2 +- .../SwiftScopeHierarchySyntaxVisitor.swift | 2 +- .../UnknownLanguageFocusCodeFinder.swift | 2 +- .../LanguageServer/GitHubCopilotRequest.swift | 2 +- .../LanguageServer/GitHubCopilotService.swift | 2 +- .../GitHubCopilotSuggestionService.swift | 6 ++-- .../SyntaxHighlighting.swift | 2 +- .../CodeSuggestion.swift | 0 .../EditorInformation.swift | 0 .../ExportedFromLSP.swift | 0 .../LanguageIdentifierFromFilePath.swift | 0 .../Modification.swift | 0 .../String+LineEnding.swift | 0 ...rocessingSuggestionServiceMiddleware.swift | 2 +- .../SuggestionProvider.swift | 2 +- .../SuggestionServiceMiddleware.swift | 2 +- Tool/Sources/Workspace/Filespace.swift | 2 +- Tool/Sources/Workspace/Workspace.swift | 2 +- .../Filespace+SuggestionService.swift | 2 +- .../SuggestionWorkspacePlugin.swift | 2 +- .../Workspace+SuggestionService.swift | 2 +- Tool/Sources/XPCShared/Models.swift | 2 +- .../XPCShared/XPCServiceProtocol.swift | 2 +- .../Sources/XcodeInspector/SourceEditor.swift | 2 +- .../XcodeInspector/XcodeInspector.swift | 2 +- .../ObjectiveCFocusedCodeFinderTests.swift | 2 +- .../SwiftFocusedCodeFinderTests.swift | 2 +- ...nknownLanguageFocusedCodeFinderTests.swift | 2 +- .../BreakLinePerformanceTests.swift | 2 +- .../LineAnnotationParsingTests.swift | 2 +- .../ModificationTests.swift | 2 +- .../TextExtrationFromCodeTests.swift | 2 +- ...singSuggestionServiceMiddlewareTests.swift | 2 +- .../EditorRangeConversionTests.swift | 2 +- 97 files changed, 128 insertions(+), 128 deletions(-) rename Tool/Sources/{SuggestionModel => SuggestionBasic}/CodeSuggestion.swift (100%) rename Tool/Sources/{SuggestionModel => SuggestionBasic}/EditorInformation.swift (100%) rename Tool/Sources/{SuggestionModel => SuggestionBasic}/ExportedFromLSP.swift (100%) rename Tool/Sources/{SuggestionModel => SuggestionBasic}/LanguageIdentifierFromFilePath.swift (100%) rename Tool/Sources/{SuggestionModel => SuggestionBasic}/Modification.swift (100%) rename Tool/Sources/{SuggestionModel => SuggestionBasic}/String+LineEnding.swift (100%) rename Tool/Tests/{SuggestionModelTests => SuggestionBasicTests}/BreakLinePerformanceTests.swift (90%) rename Tool/Tests/{SuggestionModelTests => SuggestionBasicTests}/LineAnnotationParsingTests.swift (92%) rename Tool/Tests/{SuggestionModelTests => SuggestionBasicTests}/ModificationTests.swift (97%) rename Tool/Tests/{SuggestionModelTests => SuggestionBasicTests}/TextExtrationFromCodeTests.swift (99%) diff --git a/Core/Package.swift b/Core/Package.swift index adef784a..89e3bd1b 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -110,7 +110,7 @@ let package = Package( dependencies: [ .product(name: "XPCShared", package: "Tool"), .product(name: "SuggestionProvider", package: "Tool"), - .product(name: "SuggestionModel", package: "Tool"), + .product(name: "SuggestionBasic", package: "Tool"), .product(name: "Logger", package: "Tool"), .product(name: "Preferences", package: "Tool"), ].pro([ @@ -132,7 +132,7 @@ let package = Package( .product(name: "Workspace", package: "Tool"), .product(name: "UserDefaultsObserver", package: "Tool"), .product(name: "AppMonitoring", package: "Tool"), - .product(name: "SuggestionModel", package: "Tool"), + .product(name: "SuggestionBasic", package: "Tool"), .product(name: "ChatTab", package: "Tool"), .product(name: "Logger", package: "Tool"), .product(name: "OpenAIService", package: "Tool"), @@ -153,7 +153,7 @@ let package = Package( "SuggestionInjector", .product(name: "XPCShared", package: "Tool"), .product(name: "SuggestionProvider", package: "Tool"), - .product(name: "SuggestionModel", package: "Tool"), + .product(name: "SuggestionBasic", package: "Tool"), .product(name: "Preferences", package: "Tool"), ] ), @@ -169,7 +169,7 @@ let package = Package( .product(name: "SuggestionProvider", package: "Tool"), .product(name: "Toast", package: "Tool"), .product(name: "SharedUIComponents", package: "Tool"), - .product(name: "SuggestionModel", package: "Tool"), + .product(name: "SuggestionBasic", package: "Tool"), .product(name: "MarkdownUI", package: "swift-markdown-ui"), .product(name: "OpenAIService", package: "Tool"), .product(name: "Preferences", package: "Tool"), @@ -187,7 +187,7 @@ let package = Package( dependencies: [ .product(name: "UserDefaultsObserver", package: "Tool"), .product(name: "Preferences", package: "Tool"), - .product(name: "SuggestionModel", package: "Tool"), + .product(name: "SuggestionBasic", package: "Tool"), .product(name: "SuggestionProvider", package: "Tool") ].pro([ "ProExtension", @@ -195,7 +195,7 @@ let package = Package( ), .target( name: "SuggestionInjector", - dependencies: [.product(name: "SuggestionModel", package: "Tool")] + dependencies: [.product(name: "SuggestionBasic", package: "Tool")] ), .testTarget( name: "SuggestionInjectorTests", @@ -208,7 +208,7 @@ let package = Package( name: "PromptToCodeService", dependencies: [ .product(name: "FocusedCodeFinder", package: "Tool"), - .product(name: "SuggestionModel", package: "Tool"), + .product(name: "SuggestionBasic", package: "Tool"), .product(name: "OpenAIService", package: "Tool"), .product(name: "AppMonitoring", package: "Tool"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), diff --git a/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift b/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift index d9dae12e..2a54d320 100644 --- a/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift +++ b/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift @@ -1,6 +1,6 @@ import AppKit import Foundation -import SuggestionModel +import SuggestionBasic import XcodeInspector public struct CustomCommandTemplateProcessor { diff --git a/Core/Sources/HostApp/AccountSettings/BingSearchView.swift b/Core/Sources/HostApp/AccountSettings/BingSearchView.swift index 408dbd55..7504e828 100644 --- a/Core/Sources/HostApp/AccountSettings/BingSearchView.swift +++ b/Core/Sources/HostApp/AccountSettings/BingSearchView.swift @@ -2,7 +2,7 @@ import AppKit import Client import OpenAIService import Preferences -import SuggestionModel +import SuggestionBasic import SwiftUI final class BingSearchViewSettings: ObservableObject { diff --git a/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift index 9bf3a58a..8fd049c1 100644 --- a/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift +++ b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift @@ -3,7 +3,7 @@ import Client import GitHubCopilotService import Preferences import SharedUIComponents -import SuggestionModel +import SuggestionBasic import SwiftUI struct GitHubCopilotView: View { diff --git a/Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionFeatureDisabledLanguageListView.swift b/Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionFeatureDisabledLanguageListView.swift index 60bb661b..41fa9fb4 100644 --- a/Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionFeatureDisabledLanguageListView.swift +++ b/Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionFeatureDisabledLanguageListView.swift @@ -1,4 +1,4 @@ -import SuggestionModel +import SuggestionBasic import SwiftUI import SharedUIComponents diff --git a/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift b/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift index 6a009aee..ca39a2fc 100644 --- a/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift +++ b/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift @@ -1,7 +1,7 @@ import Foundation import OpenAIService import Preferences -import SuggestionModel +import SuggestionBasic import XcodeInspector public final class OpenAIPromptToCodeService: PromptToCodeServiceType { diff --git a/Core/Sources/PromptToCodeService/PreviewPromptToCodeService.swift b/Core/Sources/PromptToCodeService/PreviewPromptToCodeService.swift index cff916f3..c6062ec7 100644 --- a/Core/Sources/PromptToCodeService/PreviewPromptToCodeService.swift +++ b/Core/Sources/PromptToCodeService/PreviewPromptToCodeService.swift @@ -1,5 +1,5 @@ import Foundation -import SuggestionModel +import SuggestionBasic public final class PreviewPromptToCodeService: PromptToCodeServiceType { public init() {} diff --git a/Core/Sources/PromptToCodeService/PromptToCodeServiceType.swift b/Core/Sources/PromptToCodeService/PromptToCodeServiceType.swift index 25d3cc6a..609af1d4 100644 --- a/Core/Sources/PromptToCodeService/PromptToCodeServiceType.swift +++ b/Core/Sources/PromptToCodeService/PromptToCodeServiceType.swift @@ -1,6 +1,6 @@ import Dependencies import Foundation -import SuggestionModel +import SuggestionBasic public protocol PromptToCodeServiceType { func modifyCode( diff --git a/Core/Sources/Service/GUI/ChatTabFactory.swift b/Core/Sources/Service/GUI/ChatTabFactory.swift index f438d4c1..dc8f3271 100644 --- a/Core/Sources/Service/GUI/ChatTabFactory.swift +++ b/Core/Sources/Service/GUI/ChatTabFactory.swift @@ -4,7 +4,7 @@ import ChatService import ChatTab import Foundation import PromptToCodeService -import SuggestionModel +import SuggestionBasic import SuggestionWidget import XcodeInspector diff --git a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift index a8475631..396145f9 100644 --- a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift +++ b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift @@ -7,7 +7,7 @@ import ChatTab import ComposableArchitecture import Dependencies import Preferences -import SuggestionModel +import SuggestionBasic import SuggestionWidget #if canImport(ProChatTabs) diff --git a/Core/Sources/Service/GUI/WidgetDataSource.swift b/Core/Sources/Service/GUI/WidgetDataSource.swift index 6233a6e5..77dd8993 100644 --- a/Core/Sources/Service/GUI/WidgetDataSource.swift +++ b/Core/Sources/Service/GUI/WidgetDataSource.swift @@ -7,7 +7,7 @@ import Foundation import GitHubCopilotService import OpenAIService import PromptToCodeService -import SuggestionModel +import SuggestionBasic import SuggestionWidget @MainActor diff --git a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift index c99e5efb..6afc5956 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift @@ -7,7 +7,7 @@ import Logger import PlusFeatureFlag import Preferences import SuggestionInjector -import SuggestionModel +import SuggestionBasic import Toast import Workspace import WorkspaceSuggestionService diff --git a/Core/Sources/Service/SuggestionCommandHandler/SuggestionCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/SuggestionCommandHandler.swift index 4c07b5bb..3d612e82 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/SuggestionCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/SuggestionCommandHandler.swift @@ -1,4 +1,4 @@ -import SuggestionModel +import SuggestionBasic import XPCShared protocol SuggestionCommandHandler { diff --git a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift index d493dded..eb58241b 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift @@ -6,7 +6,7 @@ import LanguageServerProtocol import Logger import OpenAIService import SuggestionInjector -import SuggestionModel +import SuggestionBasic import SuggestionWidget import UserNotifications import Workspace diff --git a/Core/Sources/Service/SuggestionPresenter/PresentInWindowSuggestionPresenter.swift b/Core/Sources/Service/SuggestionPresenter/PresentInWindowSuggestionPresenter.swift index e2568f9f..ce2eb039 100644 --- a/Core/Sources/Service/SuggestionPresenter/PresentInWindowSuggestionPresenter.swift +++ b/Core/Sources/Service/SuggestionPresenter/PresentInWindowSuggestionPresenter.swift @@ -1,7 +1,7 @@ import ChatService import Foundation import OpenAIService -import SuggestionModel +import SuggestionBasic import SuggestionWidget struct PresentInWindowSuggestionPresenter { diff --git a/Core/Sources/SuggestionInjector/SuggestionInjector.swift b/Core/Sources/SuggestionInjector/SuggestionInjector.swift index 046e79b4..a7fcedaf 100644 --- a/Core/Sources/SuggestionInjector/SuggestionInjector.swift +++ b/Core/Sources/SuggestionInjector/SuggestionInjector.swift @@ -1,5 +1,5 @@ import Foundation -import SuggestionModel +import SuggestionBasic // NOTE: Every lines from Xcode Extension has a line break at its end, even the last line. // NOTE: Copilot's completion always start at character 0, no matter where the cursor is. diff --git a/Core/Sources/SuggestionService/SuggestionService.swift b/Core/Sources/SuggestionService/SuggestionService.swift index e34b37ce..2d6cc2da 100644 --- a/Core/Sources/SuggestionService/SuggestionService.swift +++ b/Core/Sources/SuggestionService/SuggestionService.swift @@ -5,7 +5,7 @@ import enum CopilotForXcodeKit.SuggestionServiceError import Foundation import GitHubCopilotService import Preferences -import SuggestionModel +import SuggestionBasic import SuggestionProvider import UserDefaultsObserver import Workspace @@ -63,7 +63,7 @@ public extension SuggestionService { func getSuggestions( _ request: SuggestionRequest, workspaceInfo: CopilotForXcodeKit.WorkspaceInfo - ) async throws -> [SuggestionModel.CodeSuggestion] { + ) async throws -> [SuggestionBasic.CodeSuggestion] { do { var getSuggestion = suggestionProvider.getSuggestions(_:workspaceInfo:) let configuration = await configuration @@ -89,14 +89,14 @@ public extension SuggestionService { } func notifyAccepted( - _ suggestion: SuggestionModel.CodeSuggestion, + _ suggestion: SuggestionBasic.CodeSuggestion, workspaceInfo: CopilotForXcodeKit.WorkspaceInfo ) async { await suggestionProvider.notifyAccepted(suggestion, workspaceInfo: workspaceInfo) } func notifyRejected( - _ suggestions: [SuggestionModel.CodeSuggestion], + _ suggestions: [SuggestionBasic.CodeSuggestion], workspaceInfo: CopilotForXcodeKit.WorkspaceInfo ) async { await suggestionProvider.notifyRejected(suggestions, workspaceInfo: workspaceInfo) diff --git a/Core/Sources/SuggestionWidget/CursorPositionTracker.swift b/Core/Sources/SuggestionWidget/CursorPositionTracker.swift index ef5d1155..35f74326 100644 --- a/Core/Sources/SuggestionWidget/CursorPositionTracker.swift +++ b/Core/Sources/SuggestionWidget/CursorPositionTracker.swift @@ -1,7 +1,7 @@ import Combine import Foundation import Perception -import SuggestionModel +import SuggestionBasic import XcodeInspector @Perceptible diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/CircularWidgetFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/CircularWidgetFeature.swift index 40e95e62..51b7d918 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/CircularWidgetFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/CircularWidgetFeature.swift @@ -1,7 +1,7 @@ import ActiveApplicationMonitor import ComposableArchitecture import Preferences -import SuggestionModel +import SuggestionBasic import SwiftUI @Reducer diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCode.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCode.swift index 02c3b797..9ba5cad3 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCode.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCode.swift @@ -4,7 +4,7 @@ import CustomAsyncAlgorithms import Dependencies import Foundation import PromptToCodeService -import SuggestionModel +import SuggestionBasic public struct PromptToCodeAcceptHandlerDependencyKey: DependencyKey { public static let liveValue: (PromptToCode.State) -> Void = { _ in diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift index ad644677..b9617798 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift @@ -1,7 +1,7 @@ import ComposableArchitecture import Foundation import PromptToCodeService -import SuggestionModel +import SuggestionBasic import XcodeInspector @Reducer diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift index 3e2c64be..a0125683 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift @@ -1,7 +1,7 @@ import Combine import Perception import SharedUIComponents -import SuggestionModel +import SuggestionBasic import SwiftUI import XcodeInspector diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift index 275ea26f..682d9c79 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift @@ -1,7 +1,7 @@ import ComposableArchitecture import MarkdownUI import SharedUIComponents -import SuggestionModel +import SuggestionBasic import SwiftUI struct PromptToCodePanel: View { diff --git a/Core/Sources/SuggestionWidget/WidgetView.swift b/Core/Sources/SuggestionWidget/WidgetView.swift index 64443570..42837b00 100644 --- a/Core/Sources/SuggestionWidget/WidgetView.swift +++ b/Core/Sources/SuggestionWidget/WidgetView.swift @@ -1,7 +1,7 @@ import ActiveApplicationMonitor import ComposableArchitecture import Preferences -import SuggestionModel +import SuggestionBasic import SwiftUI struct WidgetView: View { diff --git a/Core/Tests/ServiceTests/Environment.swift b/Core/Tests/ServiceTests/Environment.swift index 6df58cca..ee78f322 100644 --- a/Core/Tests/ServiceTests/Environment.swift +++ b/Core/Tests/ServiceTests/Environment.swift @@ -2,7 +2,7 @@ import AppKit import Client import Foundation import GitHubCopilotService -import SuggestionModel +import SuggestionBasic import Workspace import XCTest import XPCShared @@ -50,11 +50,11 @@ class MockSuggestionService: GitHubCopilotSuggestionServiceType { fileURL: URL, content: String, originalContent: String, - cursorPosition: SuggestionModel.CursorPosition, + cursorPosition: SuggestionBasic.CursorPosition, tabSize: Int, indentSize: Int, usesTabsForIndentation: Bool - ) async throws -> [SuggestionModel.CodeSuggestion] { + ) async throws -> [SuggestionBasic.CodeSuggestion] { completions } diff --git a/Core/Tests/ServiceTests/ExtractSelectedCodeTests.swift b/Core/Tests/ServiceTests/ExtractSelectedCodeTests.swift index 31dea382..c5bd977c 100644 --- a/Core/Tests/ServiceTests/ExtractSelectedCodeTests.swift +++ b/Core/Tests/ServiceTests/ExtractSelectedCodeTests.swift @@ -1,4 +1,4 @@ -import SuggestionModel +import SuggestionBasic import XCTest @testable import Service @testable import XPCShared diff --git a/Core/Tests/ServiceTests/FilespaceSuggestionInvalidationTests.swift b/Core/Tests/ServiceTests/FilespaceSuggestionInvalidationTests.swift index 26841b41..29a71e69 100644 --- a/Core/Tests/ServiceTests/FilespaceSuggestionInvalidationTests.swift +++ b/Core/Tests/ServiceTests/FilespaceSuggestionInvalidationTests.swift @@ -1,5 +1,5 @@ import Foundation -import SuggestionModel +import SuggestionBasic import XCTest @testable import Service diff --git a/Core/Tests/SuggestionInjectorTests/AcceptSuggestionTests.swift b/Core/Tests/SuggestionInjectorTests/AcceptSuggestionTests.swift index 6bd1cb4b..1f93b021 100644 --- a/Core/Tests/SuggestionInjectorTests/AcceptSuggestionTests.swift +++ b/Core/Tests/SuggestionInjectorTests/AcceptSuggestionTests.swift @@ -1,4 +1,4 @@ -import SuggestionModel +import SuggestionBasic import XCTest @testable import SuggestionInjector diff --git a/Core/Tests/SuggestionInjectorTests/ProposeSuggestionTests.swift b/Core/Tests/SuggestionInjectorTests/ProposeSuggestionTests.swift index 754eb90f..62d744e3 100644 --- a/Core/Tests/SuggestionInjectorTests/ProposeSuggestionTests.swift +++ b/Core/Tests/SuggestionInjectorTests/ProposeSuggestionTests.swift @@ -1,4 +1,4 @@ -//import SuggestionModel +//import SuggestionBasic //import XCTest // //@testable import SuggestionInjector diff --git a/Core/Tests/SuggestionInjectorTests/RejectSuggestionTests.swift b/Core/Tests/SuggestionInjectorTests/RejectSuggestionTests.swift index 54b70d3c..dd6915df 100644 --- a/Core/Tests/SuggestionInjectorTests/RejectSuggestionTests.swift +++ b/Core/Tests/SuggestionInjectorTests/RejectSuggestionTests.swift @@ -1,4 +1,4 @@ -//import SuggestionModel +//import SuggestionBasic //import XCTest // //@testable import SuggestionInjector diff --git a/EditorExtension/AcceptPromptToCodeCommand.swift b/EditorExtension/AcceptPromptToCodeCommand.swift index f7d7171c..51bea4a4 100644 --- a/EditorExtension/AcceptPromptToCodeCommand.swift +++ b/EditorExtension/AcceptPromptToCodeCommand.swift @@ -1,6 +1,6 @@ import Client import Foundation -import SuggestionModel +import SuggestionBasic import XcodeKit class AcceptPromptToCodeCommand: NSObject, XCSourceEditorCommand, CommandType { diff --git a/EditorExtension/AcceptSuggestionCommand.swift b/EditorExtension/AcceptSuggestionCommand.swift index 81882ed9..a1ea71f6 100644 --- a/EditorExtension/AcceptSuggestionCommand.swift +++ b/EditorExtension/AcceptSuggestionCommand.swift @@ -1,5 +1,5 @@ import Client -import SuggestionModel +import SuggestionBasic import Foundation import XcodeKit import XPCShared diff --git a/EditorExtension/CloseIdleTabsCommand.swift b/EditorExtension/CloseIdleTabsCommand.swift index b2dde69a..0e9537ee 100644 --- a/EditorExtension/CloseIdleTabsCommand.swift +++ b/EditorExtension/CloseIdleTabsCommand.swift @@ -1,6 +1,6 @@ import Client import Foundation -import SuggestionModel +import SuggestionBasic import XcodeKit class CloseIdleTabsCommand: NSObject, XCSourceEditorCommand, CommandType { diff --git a/EditorExtension/CustomCommand.swift b/EditorExtension/CustomCommand.swift index 916c0db3..0a43a51d 100644 --- a/EditorExtension/CustomCommand.swift +++ b/EditorExtension/CustomCommand.swift @@ -1,6 +1,6 @@ import Client import Foundation -import SuggestionModel +import SuggestionBasic import XcodeKit class CustomCommand: NSObject, XCSourceEditorCommand, CommandType { diff --git a/EditorExtension/GetSuggestionsCommand.swift b/EditorExtension/GetSuggestionsCommand.swift index f1138270..6be1c417 100644 --- a/EditorExtension/GetSuggestionsCommand.swift +++ b/EditorExtension/GetSuggestionsCommand.swift @@ -1,6 +1,6 @@ import Client import Foundation -import SuggestionModel +import SuggestionBasic import XcodeKit class GetSuggestionsCommand: NSObject, XCSourceEditorCommand, CommandType { diff --git a/EditorExtension/Helpers.swift b/EditorExtension/Helpers.swift index 924927a8..8851c279 100644 --- a/EditorExtension/Helpers.swift +++ b/EditorExtension/Helpers.swift @@ -1,4 +1,4 @@ -import SuggestionModel +import SuggestionBasic import Foundation import XcodeKit import XPCShared diff --git a/EditorExtension/NextSuggestionCommand.swift b/EditorExtension/NextSuggestionCommand.swift index 401fdcd3..f07f4017 100644 --- a/EditorExtension/NextSuggestionCommand.swift +++ b/EditorExtension/NextSuggestionCommand.swift @@ -1,6 +1,6 @@ import Client import Foundation -import SuggestionModel +import SuggestionBasic import XcodeKit class NextSuggestionCommand: NSObject, XCSourceEditorCommand, CommandType { diff --git a/EditorExtension/OpenChat.swift b/EditorExtension/OpenChat.swift index 375f58a2..fccdc3fe 100644 --- a/EditorExtension/OpenChat.swift +++ b/EditorExtension/OpenChat.swift @@ -1,5 +1,5 @@ import Client -import SuggestionModel +import SuggestionBasic import Foundation import XcodeKit diff --git a/EditorExtension/PrefetchSuggestionsCommand.swift b/EditorExtension/PrefetchSuggestionsCommand.swift index 73878fbb..bc43c40e 100644 --- a/EditorExtension/PrefetchSuggestionsCommand.swift +++ b/EditorExtension/PrefetchSuggestionsCommand.swift @@ -1,5 +1,5 @@ import Client -import SuggestionModel +import SuggestionBasic import Foundation import XcodeKit diff --git a/EditorExtension/PreviousSuggestionCommand.swift b/EditorExtension/PreviousSuggestionCommand.swift index e2b1a47e..61894bab 100644 --- a/EditorExtension/PreviousSuggestionCommand.swift +++ b/EditorExtension/PreviousSuggestionCommand.swift @@ -1,6 +1,6 @@ import Client import Foundation -import SuggestionModel +import SuggestionBasic import XcodeKit class PreviousSuggestionCommand: NSObject, XCSourceEditorCommand, CommandType { diff --git a/EditorExtension/PromptToCodeCommand.swift b/EditorExtension/PromptToCodeCommand.swift index 852bedb6..13e4f3be 100644 --- a/EditorExtension/PromptToCodeCommand.swift +++ b/EditorExtension/PromptToCodeCommand.swift @@ -1,5 +1,5 @@ import Client -import SuggestionModel +import SuggestionBasic import Foundation import XcodeKit diff --git a/EditorExtension/RealtimeSuggestionCommand.swift b/EditorExtension/RealtimeSuggestionCommand.swift index b2da0296..ed0473d5 100644 --- a/EditorExtension/RealtimeSuggestionCommand.swift +++ b/EditorExtension/RealtimeSuggestionCommand.swift @@ -1,5 +1,5 @@ import Client -import SuggestionModel +import SuggestionBasic import Foundation import XcodeKit diff --git a/EditorExtension/RejectSuggestionCommand.swift b/EditorExtension/RejectSuggestionCommand.swift index c19dcf5a..f9f370c9 100644 --- a/EditorExtension/RejectSuggestionCommand.swift +++ b/EditorExtension/RejectSuggestionCommand.swift @@ -1,6 +1,6 @@ import Client import Foundation -import SuggestionModel +import SuggestionBasic import XcodeKit class RejectSuggestionCommand: NSObject, XCSourceEditorCommand, CommandType { diff --git a/EditorExtension/SeparatorCommand.swift b/EditorExtension/SeparatorCommand.swift index ba1ff882..79e4b138 100644 --- a/EditorExtension/SeparatorCommand.swift +++ b/EditorExtension/SeparatorCommand.swift @@ -1,5 +1,5 @@ import Client -import SuggestionModel +import SuggestionBasic import Foundation import XcodeKit diff --git a/EditorExtension/ToggleRealtimeSuggestionsCommand.swift b/EditorExtension/ToggleRealtimeSuggestionsCommand.swift index 44fe1883..ab226a0b 100644 --- a/EditorExtension/ToggleRealtimeSuggestionsCommand.swift +++ b/EditorExtension/ToggleRealtimeSuggestionsCommand.swift @@ -1,5 +1,5 @@ import Client -import SuggestionModel +import SuggestionBasic import Foundation import XcodeKit diff --git a/Pro b/Pro index c95c52aa..d1df4fa6 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit c95c52aa39427d76ac8a3a856d4f4883ce91e074 +Subproject commit d1df4fa600db5faa029fb328335fd8ac9fc1ae1b diff --git a/Tool/Package.swift b/Tool/Package.swift index 94b16163..1bb87347 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -20,7 +20,7 @@ let package = Package( name: "ChatContextCollector", targets: ["ChatContextCollector", "ActiveDocumentChatContextCollector"] ), - .library(name: "SuggestionModel", targets: ["SuggestionModel"]), + .library(name: "SuggestionBasic", targets: ["SuggestionBasic"]), .library(name: "ASTParser", targets: ["ASTParser"]), .library(name: "FocusedCodeFinder", targets: ["FocusedCodeFinder"]), .library(name: "Toast", targets: ["Toast"]), @@ -79,7 +79,7 @@ let package = Package( targets: [ // MARK: - Helpers - .target(name: "XPCShared", dependencies: ["SuggestionModel", "Logger"]), + .target(name: "XPCShared", dependencies: ["SuggestionBasic", "Logger"]), .target(name: "Configs"), @@ -151,7 +151,7 @@ let package = Package( ), .target( - name: "SuggestionModel", + name: "SuggestionBasic", dependencies: [ "LanguageClient", .product(name: "Parsing", package: "swift-parsing"), @@ -167,8 +167,8 @@ let package = Package( ), .testTarget( - name: "SuggestionModelTests", - dependencies: ["SuggestionModel"] + name: "SuggestionBasicTests", + dependencies: ["SuggestionBasic"] ), .target( @@ -195,7 +195,7 @@ let package = Package( name: "XcodeInspector", dependencies: [ "AXExtension", - "SuggestionModel", + "SuggestionBasic", "AXNotificationStream", "Logger", "Toast", @@ -214,7 +214,7 @@ let package = Package( .target( name: "BuiltinExtension", dependencies: [ - "SuggestionModel", + "SuggestionBasic", "SuggestionProvider", "ChatBasic", "Workspace", @@ -229,7 +229,7 @@ let package = Package( dependencies: [ "Highlightr", "Preferences", - "SuggestionModel", + "SuggestionBasic", "DebounceFunction", .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), ] @@ -237,7 +237,7 @@ let package = Package( .testTarget(name: "SharedUIComponentsTests", dependencies: ["SharedUIComponents"]), .target(name: "ASTParser", dependencies: [ - "SuggestionModel", + "SuggestionBasic", .product(name: "SwiftTreeSitter", package: "SwiftTreeSitter"), .product(name: "TreeSitterObjC", package: "tree-sitter-objc"), ]), @@ -249,7 +249,7 @@ let package = Package( dependencies: [ "GitIgnoreCheck", "UserDefaultsObserver", - "SuggestionModel", + "SuggestionBasic", "Logger", "Preferences", "XcodeInspector", @@ -271,7 +271,7 @@ let package = Package( dependencies: [ "Preferences", "ASTParser", - "SuggestionModel", + "SuggestionBasic", .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftParser", package: "swift-syntax"), ] @@ -310,7 +310,7 @@ let package = Package( .target(name: "BingSearchService"), .target(name: "SuggestionProvider", dependencies: [ - "SuggestionModel", + "SuggestionBasic", "UserDefaultsObserver", "Preferences", "Logger", @@ -325,7 +325,7 @@ let package = Package( name: "GitHubCopilotService", dependencies: [ "LanguageClient", - "SuggestionModel", + "SuggestionBasic", "ChatBasic", "Logger", "Preferences", @@ -350,7 +350,7 @@ let package = Package( dependencies: [ "LanguageClient", "Keychain", - "SuggestionModel", + "SuggestionBasic", "Preferences", "Terminal", "XcodeInspector", @@ -407,7 +407,7 @@ let package = Package( .target( name: "ChatContextCollector", dependencies: [ - "SuggestionModel", + "SuggestionBasic", "ChatBasic", "OpenAIService", ] diff --git a/Tool/Sources/ASTParser/ASTParser.swift b/Tool/Sources/ASTParser/ASTParser.swift index dd11d709..1f66fa85 100644 --- a/Tool/Sources/ASTParser/ASTParser.swift +++ b/Tool/Sources/ASTParser/ASTParser.swift @@ -1,4 +1,4 @@ -import SuggestionModel +import SuggestionBasic import SwiftTreeSitter import tree_sitter import TreeSitterObjC diff --git a/Tool/Sources/BuiltinExtension/BuiltinExtensionSuggestionServiceProvider.swift b/Tool/Sources/BuiltinExtension/BuiltinExtensionSuggestionServiceProvider.swift index bc8f5ae1..f6234ddf 100644 --- a/Tool/Sources/BuiltinExtension/BuiltinExtensionSuggestionServiceProvider.swift +++ b/Tool/Sources/BuiltinExtension/BuiltinExtensionSuggestionServiceProvider.swift @@ -2,7 +2,7 @@ import CopilotForXcodeKit import Foundation import Logger import Preferences -import SuggestionModel +import SuggestionBasic import SuggestionProvider public final class BuiltinExtensionSuggestionServiceProvider< @@ -42,7 +42,7 @@ public final class BuiltinExtensionSuggestionServiceProvider< public func getSuggestions( _ request: SuggestionProvider.SuggestionRequest, workspaceInfo: CopilotForXcodeKit.WorkspaceInfo - ) async throws -> [SuggestionModel.CodeSuggestion] { + ) async throws -> [SuggestionBasic.CodeSuggestion] { guard let service else { Logger.service.error("Builtin suggestion service not found.") throw BuiltinExtensionSuggestionServiceNotFoundError() @@ -80,7 +80,7 @@ public final class BuiltinExtensionSuggestionServiceProvider< } public func notifyAccepted( - _ suggestion: SuggestionModel.CodeSuggestion, + _ suggestion: SuggestionBasic.CodeSuggestion, workspaceInfo: CopilotForXcodeKit.WorkspaceInfo ) async { guard let service else { @@ -91,7 +91,7 @@ public final class BuiltinExtensionSuggestionServiceProvider< } public func notifyRejected( - _ suggestions: [SuggestionModel.CodeSuggestion], + _ suggestions: [SuggestionBasic.CodeSuggestion], workspaceInfo: CopilotForXcodeKit.WorkspaceInfo ) async { guard let service else { @@ -123,7 +123,7 @@ extension SuggestionProvider.SuggestionRequest { } } -extension SuggestionModel.CodeSuggestion { +extension SuggestionBasic.CodeSuggestion { var converted: CopilotForXcodeKit.CodeSuggestion { .init( id: id, @@ -147,7 +147,7 @@ extension SuggestionModel.CodeSuggestion { } extension CopilotForXcodeKit.CodeSuggestion { - var converted: SuggestionModel.CodeSuggestion { + var converted: SuggestionBasic.CodeSuggestion { .init( id: id, text: text, diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift index 297b8521..8ca58f6c 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift @@ -7,7 +7,7 @@ import Foundation import GitIgnoreCheck import OpenAIService import Preferences -import SuggestionModel +import SuggestionBasic import XcodeInspector public final class ActiveDocumentChatContextCollector: ChatContextCollector { diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/GetCodeCodeAroundLineFunction.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/GetCodeCodeAroundLineFunction.swift index 31d14ba1..7ed337cc 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/GetCodeCodeAroundLineFunction.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/GetCodeCodeAroundLineFunction.swift @@ -2,7 +2,7 @@ import ASTParser import ChatBasic import Foundation import OpenAIService -import SuggestionModel +import SuggestionBasic struct GetCodeCodeAroundLineFunction: ChatGPTFunction { struct Arguments: Codable { diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift index c0590b00..1ba9f334 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift @@ -2,7 +2,7 @@ import ChatContextCollector import Foundation import OpenAIService import Preferences -import SuggestionModel +import SuggestionBasic import XcodeInspector public struct LegacyActiveDocumentChatContextCollector: ChatContextCollector { diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ReadableCursorRange.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ReadableCursorRange.swift index 9d3dfaa0..732601fe 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ReadableCursorRange.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ReadableCursorRange.swift @@ -1,4 +1,4 @@ -import SuggestionModel +import SuggestionBasic extension CursorPosition { var text: String { diff --git a/Tool/Sources/CodeiumService/LanguageServer/CodeiumModels.swift b/Tool/Sources/CodeiumService/LanguageServer/CodeiumModels.swift index 9137130d..d6af5d25 100644 --- a/Tool/Sources/CodeiumService/LanguageServer/CodeiumModels.swift +++ b/Tool/Sources/CodeiumService/LanguageServer/CodeiumModels.swift @@ -1,7 +1,7 @@ import Foundation import JSONRPC import LanguageServerProtocol -import SuggestionModel +import SuggestionBasic struct CodeiumCompletion: Codable { var completionId: String diff --git a/Tool/Sources/CodeiumService/LanguageServer/CodeiumRequest.swift b/Tool/Sources/CodeiumService/LanguageServer/CodeiumRequest.swift index 89b8adea..24ef5561 100644 --- a/Tool/Sources/CodeiumService/LanguageServer/CodeiumRequest.swift +++ b/Tool/Sources/CodeiumService/LanguageServer/CodeiumRequest.swift @@ -1,7 +1,7 @@ import Foundation import JSONRPC import LanguageServerProtocol -import SuggestionModel +import SuggestionBasic protocol CodeiumRequestType { associatedtype Response: Codable diff --git a/Tool/Sources/CodeiumService/LanguageServer/CodeiumSupportedLanguage.swift b/Tool/Sources/CodeiumService/LanguageServer/CodeiumSupportedLanguage.swift index 3ffe57bc..659b3805 100644 --- a/Tool/Sources/CodeiumService/LanguageServer/CodeiumSupportedLanguage.swift +++ b/Tool/Sources/CodeiumService/LanguageServer/CodeiumSupportedLanguage.swift @@ -1,5 +1,5 @@ import Foundation -import SuggestionModel +import SuggestionBasic enum CodeiumSupportedLanguage: Int, Codable { case unspecified = 0 diff --git a/Tool/Sources/CodeiumService/Services/CodeiumService.swift b/Tool/Sources/CodeiumService/Services/CodeiumService.swift index 11346b57..ba05bae0 100644 --- a/Tool/Sources/CodeiumService/Services/CodeiumService.swift +++ b/Tool/Sources/CodeiumService/Services/CodeiumService.swift @@ -3,7 +3,7 @@ import Foundation import LanguageClient import LanguageServerProtocol import Logger -import SuggestionModel +import SuggestionBasic import XcodeInspector public protocol CodeiumSuggestionServiceType { diff --git a/Tool/Sources/CodeiumService/Services/CodeiumSuggestionService.swift b/Tool/Sources/CodeiumService/Services/CodeiumSuggestionService.swift index 1b9b3477..33c6db0e 100644 --- a/Tool/Sources/CodeiumService/Services/CodeiumSuggestionService.swift +++ b/Tool/Sources/CodeiumService/Services/CodeiumSuggestionService.swift @@ -1,6 +1,6 @@ import CopilotForXcodeKit import Foundation -import SuggestionModel +import SuggestionBasic import Workspace public final class CodeiumSuggestionService: SuggestionServiceType { @@ -57,7 +57,7 @@ public final class CodeiumSuggestionService: SuggestionServiceType { } static func convert( - _ suggestion: SuggestionModel.CodeSuggestion + _ suggestion: SuggestionBasic.CodeSuggestion ) -> CopilotForXcodeKit.CodeSuggestion { .init( id: suggestion.id, @@ -81,7 +81,7 @@ public final class CodeiumSuggestionService: SuggestionServiceType { static func convert( _ suggestion: CopilotForXcodeKit.CodeSuggestion - ) -> SuggestionModel.CodeSuggestion { + ) -> SuggestionBasic.CodeSuggestion { .init( id: suggestion.id, text: suggestion.text, diff --git a/Tool/Sources/FocusedCodeFinder/ActiveDocumentContext.swift b/Tool/Sources/FocusedCodeFinder/ActiveDocumentContext.swift index 245b2d16..b5fe90cf 100644 --- a/Tool/Sources/FocusedCodeFinder/ActiveDocumentContext.swift +++ b/Tool/Sources/FocusedCodeFinder/ActiveDocumentContext.swift @@ -1,5 +1,5 @@ import Foundation -import SuggestionModel +import SuggestionBasic public struct ActiveDocumentContext { public var documentURL: URL diff --git a/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift index 175fd6e6..f3048335 100644 --- a/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift @@ -1,5 +1,5 @@ import Foundation -import SuggestionModel +import SuggestionBasic public struct CodeContext: Equatable { public typealias ScopeContext = ActiveDocumentContext.FocusedContext.Context diff --git a/Tool/Sources/FocusedCodeFinder/KnownLanguageFocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/KnownLanguageFocusedCodeFinder.swift index c78257b4..1571a770 100644 --- a/Tool/Sources/FocusedCodeFinder/KnownLanguageFocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/KnownLanguageFocusedCodeFinder.swift @@ -1,6 +1,6 @@ import Foundation import Preferences -import SuggestionModel +import SuggestionBasic public typealias KnownLanguageFocusedCodeFinder = BaseKnownLanguageFocusedCodeFinder & @@ -50,7 +50,7 @@ public protocol KnownLanguageFocusedCodeFinderType: FocusedCodeFinderType { func collectContextNodes( in document: Document, tree: Tree, - containingRange: SuggestionModel.CursorRange, + containingRange: SuggestionBasic.CursorRange, textProvider: @escaping TextProvider, rangeConverter: @escaping RangeConverter ) -> ContextInfo @@ -70,7 +70,7 @@ public protocol KnownLanguageFocusedCodeFinderType: FocusedCodeFinderType { public extension KnownLanguageFocusedCodeFinderType { func findFocusedCode( in document: Document, - containingRange range: SuggestionModel.CursorRange + containingRange range: SuggestionBasic.CursorRange ) -> CodeContext { guard let tree = parseSyntaxTree(from: document) else { return .empty } @@ -145,7 +145,7 @@ extension KnownLanguageFocusedCodeFinderType { func extractFocusedCode( in codeRange: CursorRange, in document: Document, - containingRange range: SuggestionModel.CursorRange + containingRange range: SuggestionBasic.CursorRange ) -> (code: String, lines: [String], codeRange: CursorRange) { var codeRange = codeRange let codeInCodeRange = EditorInformation.code( diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift index 0fa7dda2..1c72906e 100644 --- a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift @@ -1,7 +1,7 @@ import ASTParser import Foundation import Preferences -import SuggestionModel +import SuggestionBasic import SwiftTreeSitter public enum TreeSitterTextPosition { diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCScopeHierarchySyntaxVisitor.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCScopeHierarchySyntaxVisitor.swift index 23186bf8..66761894 100644 --- a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCScopeHierarchySyntaxVisitor.swift +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCScopeHierarchySyntaxVisitor.swift @@ -1,7 +1,7 @@ import ASTParser import Foundation import Preferences -import SuggestionModel +import SuggestionBasic import SwiftTreeSitter final class ObjectiveCScopeHierarchySyntaxVisitor: ASTTreeVisitor { diff --git a/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift index e8a452cb..6676008d 100644 --- a/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift @@ -1,7 +1,7 @@ import ASTParser import Foundation import Preferences -import SuggestionModel +import SuggestionBasic import SwiftParser import SwiftSyntax diff --git a/Tool/Sources/FocusedCodeFinder/Swift/SwiftScopeHierarchySyntaxVisitor.swift b/Tool/Sources/FocusedCodeFinder/Swift/SwiftScopeHierarchySyntaxVisitor.swift index d353bf73..bdd224d1 100644 --- a/Tool/Sources/FocusedCodeFinder/Swift/SwiftScopeHierarchySyntaxVisitor.swift +++ b/Tool/Sources/FocusedCodeFinder/Swift/SwiftScopeHierarchySyntaxVisitor.swift @@ -1,7 +1,7 @@ import ASTParser import Foundation import Preferences -import SuggestionModel +import SuggestionBasic import SwiftParser import SwiftSyntax diff --git a/Tool/Sources/FocusedCodeFinder/UnknownLanguageFocusCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/UnknownLanguageFocusCodeFinder.swift index 3ce18ab1..53b12095 100644 --- a/Tool/Sources/FocusedCodeFinder/UnknownLanguageFocusCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/UnknownLanguageFocusCodeFinder.swift @@ -1,6 +1,6 @@ import Foundation import Preferences -import SuggestionModel +import SuggestionBasic /// Used when the language is not supported by the app /// or that the code is too long to be returned by a focused code finder. diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift index 074ffc44..1b80058c 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift @@ -1,7 +1,7 @@ import Foundation import JSONRPC import LanguageServerProtocol -import SuggestionModel +import SuggestionBasic struct GitHubCopilotDoc: Codable { var source: String diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift index 66f68177..9a4b83e4 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift @@ -5,7 +5,7 @@ import LanguageClient import LanguageServerProtocol import Logger import Preferences -import SuggestionModel +import SuggestionBasic public protocol GitHubCopilotAuthServiceType { func checkStatus() async throws -> GitHubCopilotAccountStatus diff --git a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotSuggestionService.swift b/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotSuggestionService.swift index 25967c1a..f9f8a9b5 100644 --- a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotSuggestionService.swift +++ b/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotSuggestionService.swift @@ -1,6 +1,6 @@ import CopilotForXcodeKit import Foundation -import SuggestionModel +import SuggestionBasic import Workspace public final class GitHubCopilotSuggestionService: SuggestionServiceType { @@ -59,7 +59,7 @@ public final class GitHubCopilotSuggestionService: SuggestionServiceType { } static func convert( - _ suggestion: SuggestionModel.CodeSuggestion + _ suggestion: SuggestionBasic.CodeSuggestion ) -> CopilotForXcodeKit.CodeSuggestion { .init( id: suggestion.id, @@ -83,7 +83,7 @@ public final class GitHubCopilotSuggestionService: SuggestionServiceType { static func convert( _ suggestion: CopilotForXcodeKit.CodeSuggestion - ) -> SuggestionModel.CodeSuggestion { + ) -> SuggestionBasic.CodeSuggestion { .init( id: suggestion.id, text: suggestion.text, diff --git a/Tool/Sources/SharedUIComponents/SyntaxHighlighting.swift b/Tool/Sources/SharedUIComponents/SyntaxHighlighting.swift index 6e096a05..3ec0ea66 100644 --- a/Tool/Sources/SharedUIComponents/SyntaxHighlighting.swift +++ b/Tool/Sources/SharedUIComponents/SyntaxHighlighting.swift @@ -1,7 +1,7 @@ import AppKit import Foundation import Highlightr -import SuggestionModel +import SuggestionBasic import SwiftUI public enum CodeHighlighting { diff --git a/Tool/Sources/SuggestionModel/CodeSuggestion.swift b/Tool/Sources/SuggestionBasic/CodeSuggestion.swift similarity index 100% rename from Tool/Sources/SuggestionModel/CodeSuggestion.swift rename to Tool/Sources/SuggestionBasic/CodeSuggestion.swift diff --git a/Tool/Sources/SuggestionModel/EditorInformation.swift b/Tool/Sources/SuggestionBasic/EditorInformation.swift similarity index 100% rename from Tool/Sources/SuggestionModel/EditorInformation.swift rename to Tool/Sources/SuggestionBasic/EditorInformation.swift diff --git a/Tool/Sources/SuggestionModel/ExportedFromLSP.swift b/Tool/Sources/SuggestionBasic/ExportedFromLSP.swift similarity index 100% rename from Tool/Sources/SuggestionModel/ExportedFromLSP.swift rename to Tool/Sources/SuggestionBasic/ExportedFromLSP.swift diff --git a/Tool/Sources/SuggestionModel/LanguageIdentifierFromFilePath.swift b/Tool/Sources/SuggestionBasic/LanguageIdentifierFromFilePath.swift similarity index 100% rename from Tool/Sources/SuggestionModel/LanguageIdentifierFromFilePath.swift rename to Tool/Sources/SuggestionBasic/LanguageIdentifierFromFilePath.swift diff --git a/Tool/Sources/SuggestionModel/Modification.swift b/Tool/Sources/SuggestionBasic/Modification.swift similarity index 100% rename from Tool/Sources/SuggestionModel/Modification.swift rename to Tool/Sources/SuggestionBasic/Modification.swift diff --git a/Tool/Sources/SuggestionModel/String+LineEnding.swift b/Tool/Sources/SuggestionBasic/String+LineEnding.swift similarity index 100% rename from Tool/Sources/SuggestionModel/String+LineEnding.swift rename to Tool/Sources/SuggestionBasic/String+LineEnding.swift diff --git a/Tool/Sources/SuggestionProvider/PostProcessingSuggestionServiceMiddleware.swift b/Tool/Sources/SuggestionProvider/PostProcessingSuggestionServiceMiddleware.swift index 83453e17..e69e29d2 100644 --- a/Tool/Sources/SuggestionProvider/PostProcessingSuggestionServiceMiddleware.swift +++ b/Tool/Sources/SuggestionProvider/PostProcessingSuggestionServiceMiddleware.swift @@ -1,5 +1,5 @@ import Foundation -import SuggestionModel +import SuggestionBasic public struct PostProcessingSuggestionServiceMiddleware: SuggestionServiceMiddleware { public init() {} diff --git a/Tool/Sources/SuggestionProvider/SuggestionProvider.swift b/Tool/Sources/SuggestionProvider/SuggestionProvider.swift index 623e3ad5..24265613 100644 --- a/Tool/Sources/SuggestionProvider/SuggestionProvider.swift +++ b/Tool/Sources/SuggestionProvider/SuggestionProvider.swift @@ -3,7 +3,7 @@ import struct CopilotForXcodeKit.SuggestionServiceConfiguration import struct CopilotForXcodeKit.WorkspaceInfo import Foundation import Preferences -import SuggestionModel +import SuggestionBasic import UserDefaultsObserver public struct SuggestionRequest { diff --git a/Tool/Sources/SuggestionProvider/SuggestionServiceMiddleware.swift b/Tool/Sources/SuggestionProvider/SuggestionServiceMiddleware.swift index 13408894..dcbfba5e 100644 --- a/Tool/Sources/SuggestionProvider/SuggestionServiceMiddleware.swift +++ b/Tool/Sources/SuggestionProvider/SuggestionServiceMiddleware.swift @@ -1,6 +1,6 @@ import Foundation import Logger -import SuggestionModel +import SuggestionBasic public protocol SuggestionServiceMiddleware { typealias Next = (SuggestionRequest) async throws -> [CodeSuggestion] diff --git a/Tool/Sources/Workspace/Filespace.swift b/Tool/Sources/Workspace/Filespace.swift index 1ce25d0e..db88abef 100644 --- a/Tool/Sources/Workspace/Filespace.swift +++ b/Tool/Sources/Workspace/Filespace.swift @@ -1,7 +1,7 @@ import Dependencies import Foundation import GitIgnoreCheck -import SuggestionModel +import SuggestionBasic public protocol FilespacePropertyKey { associatedtype Value diff --git a/Tool/Sources/Workspace/Workspace.swift b/Tool/Sources/Workspace/Workspace.swift index 2184c779..6579dd72 100644 --- a/Tool/Sources/Workspace/Workspace.swift +++ b/Tool/Sources/Workspace/Workspace.swift @@ -1,6 +1,6 @@ import Foundation import Preferences -import SuggestionModel +import SuggestionBasic import UserDefaultsObserver import XcodeInspector diff --git a/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift b/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift index 6abdba10..ec35158f 100644 --- a/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift +++ b/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift @@ -1,5 +1,5 @@ import Foundation -import SuggestionModel +import SuggestionBasic import Workspace public struct FilespaceSuggestionSnapshot: Equatable { diff --git a/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift b/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift index 7d60d924..ac53d042 100644 --- a/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift +++ b/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift @@ -1,7 +1,7 @@ import BuiltinExtension import Foundation import Preferences -import SuggestionModel +import SuggestionBasic import SuggestionProvider import UserDefaultsObserver import Workspace diff --git a/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift b/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift index 7ffe7835..2fbaddd3 100644 --- a/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift +++ b/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift @@ -1,5 +1,5 @@ import Foundation -import SuggestionModel +import SuggestionBasic import SuggestionProvider import Workspace import XPCShared diff --git a/Tool/Sources/XPCShared/Models.swift b/Tool/Sources/XPCShared/Models.swift index 26782ee8..f83317cf 100644 --- a/Tool/Sources/XPCShared/Models.swift +++ b/Tool/Sources/XPCShared/Models.swift @@ -1,5 +1,5 @@ import Foundation -import SuggestionModel +import SuggestionBasic public struct EditorContent: Codable { public struct Selection: Codable { diff --git a/Tool/Sources/XPCShared/XPCServiceProtocol.swift b/Tool/Sources/XPCShared/XPCServiceProtocol.swift index e659db45..96120576 100644 --- a/Tool/Sources/XPCShared/XPCServiceProtocol.swift +++ b/Tool/Sources/XPCShared/XPCServiceProtocol.swift @@ -1,5 +1,5 @@ import Foundation -import SuggestionModel +import SuggestionBasic @objc(XPCServiceProtocol) public protocol XPCServiceProtocol { diff --git a/Tool/Sources/XcodeInspector/SourceEditor.swift b/Tool/Sources/XcodeInspector/SourceEditor.swift index d6627b2b..09c7da04 100644 --- a/Tool/Sources/XcodeInspector/SourceEditor.swift +++ b/Tool/Sources/XcodeInspector/SourceEditor.swift @@ -3,7 +3,7 @@ import AsyncPassthroughSubject import AXNotificationStream import Foundation import Logger -import SuggestionModel +import SuggestionBasic /// Representing a source editor inside Xcode. public class SourceEditor { diff --git a/Tool/Sources/XcodeInspector/XcodeInspector.swift b/Tool/Sources/XcodeInspector/XcodeInspector.swift index 96187b30..51defded 100644 --- a/Tool/Sources/XcodeInspector/XcodeInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeInspector.swift @@ -5,7 +5,7 @@ import Combine import Foundation import Logger import Preferences -import SuggestionModel +import SuggestionBasic import Toast public extension Notification.Name { diff --git a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift index c9b55764..af9d3c02 100644 --- a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift @@ -1,5 +1,5 @@ import Foundation -import SuggestionModel +import SuggestionBasic import XCTest @testable import FocusedCodeFinder diff --git a/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift index 5cebe53b..666b614c 100644 --- a/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift @@ -1,5 +1,5 @@ import Foundation -import SuggestionModel +import SuggestionBasic import XCTest @testable import FocusedCodeFinder diff --git a/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift index bdc00be9..fa975db9 100644 --- a/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift @@ -1,5 +1,5 @@ import Foundation -import SuggestionModel +import SuggestionBasic import XCTest @testable import FocusedCodeFinder diff --git a/Tool/Tests/SuggestionModelTests/BreakLinePerformanceTests.swift b/Tool/Tests/SuggestionBasicTests/BreakLinePerformanceTests.swift similarity index 90% rename from Tool/Tests/SuggestionModelTests/BreakLinePerformanceTests.swift rename to Tool/Tests/SuggestionBasicTests/BreakLinePerformanceTests.swift index 62683392..6f2ff5a7 100644 --- a/Tool/Tests/SuggestionModelTests/BreakLinePerformanceTests.swift +++ b/Tool/Tests/SuggestionBasicTests/BreakLinePerformanceTests.swift @@ -1,6 +1,6 @@ import Foundation import XCTest -@testable import SuggestionModel +@testable import SuggestionBasic final class BreakLinePerformanceTests: XCTestCase { func test_breakLines() { diff --git a/Tool/Tests/SuggestionModelTests/LineAnnotationParsingTests.swift b/Tool/Tests/SuggestionBasicTests/LineAnnotationParsingTests.swift similarity index 92% rename from Tool/Tests/SuggestionModelTests/LineAnnotationParsingTests.swift rename to Tool/Tests/SuggestionBasicTests/LineAnnotationParsingTests.swift index 1471aa33..8ff71f9c 100644 --- a/Tool/Tests/SuggestionModelTests/LineAnnotationParsingTests.swift +++ b/Tool/Tests/SuggestionBasicTests/LineAnnotationParsingTests.swift @@ -1,7 +1,7 @@ import Foundation import XCTest -@testable import SuggestionModel +@testable import SuggestionBasic class LineAnnotationParsingTests: XCTestCase { func test_parse_line_annotation() { diff --git a/Tool/Tests/SuggestionModelTests/ModificationTests.swift b/Tool/Tests/SuggestionBasicTests/ModificationTests.swift similarity index 97% rename from Tool/Tests/SuggestionModelTests/ModificationTests.swift rename to Tool/Tests/SuggestionBasicTests/ModificationTests.swift index a2296824..0de61490 100644 --- a/Tool/Tests/SuggestionModelTests/ModificationTests.swift +++ b/Tool/Tests/SuggestionBasicTests/ModificationTests.swift @@ -1,6 +1,6 @@ import XCTest -@testable import SuggestionModel +@testable import SuggestionBasic final class ModificationTests: XCTestCase { func test_nsmutablearray_deleting_an_element() { diff --git a/Tool/Tests/SuggestionModelTests/TextExtrationFromCodeTests.swift b/Tool/Tests/SuggestionBasicTests/TextExtrationFromCodeTests.swift similarity index 99% rename from Tool/Tests/SuggestionModelTests/TextExtrationFromCodeTests.swift rename to Tool/Tests/SuggestionBasicTests/TextExtrationFromCodeTests.swift index 3261de88..7b1fa007 100644 --- a/Tool/Tests/SuggestionModelTests/TextExtrationFromCodeTests.swift +++ b/Tool/Tests/SuggestionBasicTests/TextExtrationFromCodeTests.swift @@ -1,6 +1,6 @@ import Foundation import XCTest -@testable import SuggestionModel +@testable import SuggestionBasic final class TextExtrationFromCodeTests: XCTestCase { func test_empty_selection() { diff --git a/Tool/Tests/SuggestionProviderTests/PostProcessingSuggestionServiceMiddlewareTests.swift b/Tool/Tests/SuggestionProviderTests/PostProcessingSuggestionServiceMiddlewareTests.swift index da2e60fd..378bfcf5 100644 --- a/Tool/Tests/SuggestionProviderTests/PostProcessingSuggestionServiceMiddlewareTests.swift +++ b/Tool/Tests/SuggestionProviderTests/PostProcessingSuggestionServiceMiddlewareTests.swift @@ -1,5 +1,5 @@ import Foundation -import SuggestionModel +import SuggestionBasic import XCTest @testable import SuggestionProvider diff --git a/Tool/Tests/XcodeInspectorTests/EditorRangeConversionTests.swift b/Tool/Tests/XcodeInspectorTests/EditorRangeConversionTests.swift index dd61336c..d62d0c0b 100644 --- a/Tool/Tests/XcodeInspectorTests/EditorRangeConversionTests.swift +++ b/Tool/Tests/XcodeInspectorTests/EditorRangeConversionTests.swift @@ -1,5 +1,5 @@ import Foundation -import SuggestionModel +import SuggestionBasic import XCTest @testable import XcodeInspector From 096569f8abda791cc9c12351c05df51a3ff27273 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 2 Jul 2024 21:34:55 +0800 Subject: [PATCH 29/44] Add missing non-stream implemetation --- ...iltinExtensionChatCompletionsService.swift | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/Tool/Sources/OpenAIService/APIs/BuiltinExtensionChatCompletionsService.swift b/Tool/Sources/OpenAIService/APIs/BuiltinExtensionChatCompletionsService.swift index 9c5f4dbb..2e5edeac 100644 --- a/Tool/Sources/OpenAIService/APIs/BuiltinExtensionChatCompletionsService.swift +++ b/Tool/Sources/OpenAIService/APIs/BuiltinExtensionChatCompletionsService.swift @@ -33,7 +33,26 @@ actor BuiltinExtensionChatCompletionsService { extension BuiltinExtensionChatCompletionsService: ChatCompletionsAPI { func callAsFunction() async throws -> ChatCompletionResponseBody { - fatalError() + let stream: AsyncThrowingStream = + try await callAsFunction() + + var id: String? = nil + var model = "" + var content = "" + for try await chunk in stream { + if let chunkId = chunk.id { id = chunkId } + if model.isEmpty, let chunkModel = chunk.model { model = chunkModel } + content.append(chunk.message?.content ?? "") + } + + return .init( + id: id, + object: "", + model: model, + message: .init(role: .assistant, content: content), + otherChoices: [], + finishReason: "" + ) } } @@ -42,8 +61,8 @@ extension BuiltinExtensionChatCompletionsService: ChatCompletionsStreamAPI { ) async throws -> AsyncThrowingStream { let service = try getChatService() let (message, history) = extractMessageAndHistory(from: requestBody) - guard let workspaceURL = XcodeInspector.shared.realtimeActiveWorkspaceURL, - let projectURL = XcodeInspector.shared.realtimeActiveProjectURL + guard let workspaceURL = await XcodeInspector.shared.safe.realtimeActiveWorkspaceURL, + let projectURL = await XcodeInspector.shared.safe.realtimeActiveProjectURL else { throw CancellationError() } let stream = await service.sendMessage( message, @@ -54,12 +73,12 @@ extension BuiltinExtensionChatCompletionsService: ChatCompletionsStreamAPI { projectURL: projectURL ) ) - + let responseID = UUID().uuidString return stream.map { text in ChatCompletionsStreamDataChunk( - id: nil, + id: responseID, object: nil, - model: nil, + model: "github-copilot", message: .init( role: .assistant, content: text, From a2edca4c1e5f9da39f8292fc9ee6e77b7e8efe55 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 2 Jul 2024 21:59:08 +0800 Subject: [PATCH 30/44] Add utility chat model --- .../Chat/ChatSettingsGeneralSectionView.swift | 22 +++++++++++++++++++ Pro | 2 +- .../Chains/CombineAnswersChain.swift | 4 +++- .../Chains/RefineDocumentChain.swift | 8 +++++-- .../RelevantInformationExtractionChain.swift | 6 ++++- Tool/Sources/Preferences/Keys.swift | 4 ++++ 6 files changed, 41 insertions(+), 5 deletions(-) diff --git a/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift b/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift index 974675a4..9675c7f7 100644 --- a/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift +++ b/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift @@ -16,6 +16,7 @@ struct ChatSettingsGeneralSectionView: View { @AppStorage(\.chatCodeFont) var chatCodeFont @AppStorage(\.defaultChatFeatureChatModelId) var defaultChatFeatureChatModelId + @AppStorage(\.preferredChatModelIdForUtilities) var utilityChatModelId @AppStorage(\.defaultChatSystemPrompt) var defaultChatSystemPrompt @AppStorage(\.chatSearchPluginMaxIterations) var chatSearchPluginMaxIterations @AppStorage(\.defaultChatFeatureEmbeddingModelId) var defaultChatFeatureEmbeddingModelId @@ -119,6 +120,27 @@ struct ChatSettingsGeneralSectionView: View { } } + Picker( + "Utility chat model", + selection: $settings.utilityChatModelId + ) { + Text("Use the default model").tag("") + + if !settings.chatModels.contains(where: { $0.id == settings.utilityChatModelId }), + !settings.utilityChatModelId.isEmpty + { + Text( + (settings.chatModels.first?.name).map { "\($0) (Default)" } + ?? "No Model Found" + ) + .tag(settings.utilityChatModelId) + } + + ForEach(settings.chatModels, id: \.id) { chatModel in + Text(chatModel.name).tag(chatModel.id) + } + } + Picker( "Embedding model", selection: $settings.defaultChatFeatureEmbeddingModelId diff --git a/Pro b/Pro index d1df4fa6..406c8513 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit d1df4fa600db5faa029fb328335fd8ac9fc1ae1b +Subproject commit 406c851384e082086e765b04c5b14ecf28febda3 diff --git a/Tool/Sources/LangChain/Chains/CombineAnswersChain.swift b/Tool/Sources/LangChain/Chains/CombineAnswersChain.swift index d199b4f2..3838713c 100644 --- a/Tool/Sources/LangChain/Chains/CombineAnswersChain.swift +++ b/Tool/Sources/LangChain/Chains/CombineAnswersChain.swift @@ -1,6 +1,7 @@ import Foundation import Logger import OpenAIService +import Preferences public class CombineAnswersChain: Chain { public struct Input: Decodable { @@ -16,7 +17,8 @@ public class CombineAnswersChain: Chain { public let chatModelChain: ChatModelChain public init( - configuration: ChatGPTConfiguration = UserPreferenceChatGPTConfiguration(), + configuration: ChatGPTConfiguration = + UserPreferenceChatGPTConfiguration(chatModelKey: \.preferredChatModelIdForUtilities), extraInstructions: String = "" ) { chatModelChain = .init( diff --git a/Tool/Sources/LangChain/Chains/RefineDocumentChain.swift b/Tool/Sources/LangChain/Chains/RefineDocumentChain.swift index b46974cd..81d060e0 100644 --- a/Tool/Sources/LangChain/Chains/RefineDocumentChain.swift +++ b/Tool/Sources/LangChain/Chains/RefineDocumentChain.swift @@ -1,6 +1,7 @@ +import ChatBasic import Foundation import OpenAIService -import ChatBasic +import Preferences public final class RefineDocumentChain: Chain { public struct Input { @@ -76,7 +77,10 @@ public final class RefineDocumentChain: Chain { func buildChatModel() -> ChatModelChain { .init( chatModel: OpenAIChat( - configuration: UserPreferenceChatGPTConfiguration().overriding { + configuration: UserPreferenceChatGPTConfiguration( + chatModelKey: \.preferredChatModelIdForUtilities + ) + .overriding { $0.temperature = 0 $0.runFunctionsAutomatically = false }, diff --git a/Tool/Sources/LangChain/Chains/RelevantInformationExtractionChain.swift b/Tool/Sources/LangChain/Chains/RelevantInformationExtractionChain.swift index 4d7d358f..29f8b73e 100644 --- a/Tool/Sources/LangChain/Chains/RelevantInformationExtractionChain.swift +++ b/Tool/Sources/LangChain/Chains/RelevantInformationExtractionChain.swift @@ -1,6 +1,7 @@ import ChatBasic import Foundation import OpenAIService +import Preferences public final class RelevantInformationExtractionChain: Chain { public struct Input { @@ -52,7 +53,10 @@ public final class RelevantInformationExtractionChain: Chain { func buildChatModel() -> ChatModelChain { .init( chatModel: OpenAIChat( - configuration: UserPreferenceChatGPTConfiguration().overriding { + configuration: UserPreferenceChatGPTConfiguration( + chatModelKey: \.preferredChatModelIdForUtilities + ) + .overriding { $0.temperature = 0.5 $0.runFunctionsAutomatically = false }, diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index ca6c7aca..23e63062 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -505,6 +505,10 @@ public extension UserDefaultPreferenceKeys { var preferredChatModelIdForWebScope: PreferenceKey { .init(defaultValue: "", key: "PreferredChatModelIdForWebScope") } + + var preferredChatModelIdForUtilities: PreferenceKey { + .init(defaultValue: "", key: "PreferredChatModelIdForUtilities") + } var disableFloatOnTopWhenTheChatPanelIsDetached: PreferenceKey { .init(defaultValue: true, key: "DisableFloatOnTopWhenTheChatPanelIsDetached") From 3e59129485a2f50580b08cffcb60af2d90e74757 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 2 Jul 2024 22:21:24 +0800 Subject: [PATCH 31/44] Update --- Core/Sources/ChatService/DynamicContextController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/Sources/ChatService/DynamicContextController.swift b/Core/Sources/ChatService/DynamicContextController.swift index c6adb9a4..11ae9753 100644 --- a/Core/Sources/ChatService/DynamicContextController.swift +++ b/Core/Sources/ChatService/DynamicContextController.swift @@ -60,7 +60,7 @@ final class DynamicContextController { let idIndexMap = chatModels.enumerated().reduce(into: [String: Int]()) { $0[$1.element.id] = $1.offset } - return ids.sorted(by: { + return ids.filter { !$0.isEmpty }.sorted(by: { let lhs = idIndexMap[$0] ?? Int.max let rhs = idIndexMap[$1] ?? Int.max return lhs < rhs From 2d62073125f213ac3b0894c542bbb717845bbdb4 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 2 Jul 2024 22:40:51 +0800 Subject: [PATCH 32/44] Try to inject system prompt --- .../Services/GitHubCopilotChatService.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift b/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift index a9f9d2af..0247dfaf 100644 --- a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift +++ b/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift @@ -129,6 +129,14 @@ extension GitHubCopilotChatService { var currentTurn = Turn(request: "", response: nil) var turns: [Turn] = [] + let systemPrompt = history + .filter { $0.role == .system }.compactMap(\.content) + .joined(separator: "\n\n") + + if !systemPrompt.isEmpty { + turns.append(.init(request: "[System Prompt]\n\(systemPrompt)", response: "OK!")) + } + for i in firstIndexOfUserMessage.. Date: Tue, 2 Jul 2024 23:10:03 +0800 Subject: [PATCH 33/44] Add GitHub Copilot chat (poc) to scope preferred models --- .../Chat/ChatSettingsScopeSectionView.swift | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsScopeSectionView.swift b/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsScopeSectionView.swift index d2d5a862..9e39656b 100644 --- a/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsScopeSectionView.swift +++ b/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsScopeSectionView.swift @@ -96,6 +96,13 @@ struct ChatSettingsScopeSectionView: View { Toggle(isOn: $settings.enableSenseScopeByDefaultInChatContext) { Text("Enable by default") } + + let allModels = settings.chatModels + [.init( + id: "com.github.copilot", + name: "GitHub Copilot (poc)", + format: .openAI, + info: .init() + )] Picker( "Preferred chat model", @@ -103,20 +110,20 @@ struct ChatSettingsScopeSectionView: View { ) { Text("Use the default model").tag("") - if !settings.chatModels + if !allModels .contains(where: { $0.id == settings.preferredChatModelIdForSenseScope }), !settings.preferredChatModelIdForSenseScope.isEmpty { Text( - (settings.chatModels.first?.name).map { "\($0) (Default)" } + (allModels.first?.name).map { "\($0) (Default)" } ?? "No model found" ) .tag(settings.preferredChatModelIdForSenseScope) } - ForEach(settings.chatModels, id: \.id) { chatModel in + ForEach(allModels, id: \.id) { chatModel in Text(chatModel.name).tag(chatModel.id) } } @@ -151,6 +158,13 @@ struct ChatSettingsScopeSectionView: View { Toggle(isOn: $settings.enableProjectScopeByDefaultInChatContext) { Text("Enable by default") } + + let allModels = settings.chatModels + [.init( + id: "com.github.copilot", + name: "GitHub Copilot (poc)", + format: .openAI, + info: .init() + )] Picker( "Preferred chat model", @@ -158,20 +172,20 @@ struct ChatSettingsScopeSectionView: View { ) { Text("Use the default model").tag("") - if !settings.chatModels + if !allModels .contains(where: { $0.id == settings.preferredChatModelIdForProjectScope }), !settings.preferredChatModelIdForProjectScope.isEmpty { Text( - (settings.chatModels.first?.name).map { "\($0) (Default)" } + (allModels.first?.name).map { "\($0) (Default)" } ?? "No Model Found" ) .tag(settings.preferredChatModelIdForProjectScope) } - ForEach(settings.chatModels, id: \.id) { chatModel in + ForEach(allModels, id: \.id) { chatModel in Text(chatModel.name).tag(chatModel.id) } } @@ -188,6 +202,13 @@ struct ChatSettingsScopeSectionView: View { Toggle(isOn: $settings.enableWebScopeByDefaultInChatContext) { Text("Enable @web scope by default in chat context.") } + + let allModels = settings.chatModels + [.init( + id: "com.github.copilot", + name: "GitHub Copilot (poc)", + format: .openAI, + info: .init() + )] Picker( "Preferred chat model", @@ -195,20 +216,20 @@ struct ChatSettingsScopeSectionView: View { ) { Text("Use the default model").tag("") - if !settings.chatModels + if !allModels .contains(where: { $0.id == settings.preferredChatModelIdForWebScope }), !settings.preferredChatModelIdForWebScope.isEmpty { Text( - (settings.chatModels.first?.name).map { "\($0) (Default)" } + (allModels.first?.name).map { "\($0) (Default)" } ?? "No model found" ) .tag(settings.preferredChatModelIdForWebScope) } - ForEach(settings.chatModels, id: \.id) { chatModel in + ForEach(allModels, id: \.id) { chatModel in Text(chatModel.name).tag(chatModel.id) } } From 2882c2464ac06ba5228ce4b3e50c4294aa90aa92 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 2 Jul 2024 23:10:42 +0800 Subject: [PATCH 34/44] Disable GitHub Copilot chat model for every place that requires a utility chat model --- .../Configuration/UserPreferenceChatGPTConfiguration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift index 119445ef..f8a3b009 100644 --- a/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift @@ -25,7 +25,7 @@ public struct UserPreferenceChatGPTConfiguration: ChatGPTConfiguration { } let id = UserDefaults.shared.value(for: \.defaultChatFeatureChatModelId) - if id == "com.github.copilot" { + if id == "com.github.copilot", chatModelKey != \.preferredChatModelIdForUtilities { return .init(id: id, name: "GitHub Copilot", format: .openAI, info: .init()) } return models.first { $0.id == id } From a17e7f589be0309d2169bd280ce64e32d7179609 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 2 Jul 2024 23:10:51 +0800 Subject: [PATCH 35/44] Bump build number --- Version.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Version.xcconfig b/Version.xcconfig index 427e0f8d..f264c7c1 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,3 +1,3 @@ APP_VERSION = 0.33.5 -APP_BUILD = 392 +APP_BUILD = 393 From 348dc9a4ccce6164ce4f94be969c86609f4ea129 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 2 Jul 2024 23:43:26 +0800 Subject: [PATCH 36/44] Update appcast.xml --- appcast.xml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/appcast.xml b/appcast.xml index f215af04..d0e27a93 100644 --- a/appcast.xml +++ b/appcast.xml @@ -2,14 +2,25 @@ Copilot for Xcode + + 0.33.5 + Tue, 02 Jul 2024 23:07:42 +0800 + beta + 393 + 0.33.5 + 12.0 + https://github.com/intitni/CopilotForXcode/releases/tag/0.33.5.beta + + 0.33.5 Mon, 01 Jul 2024 01:11:30 +0800 392 + beta 0.33.5 12.0 - https://github.com/intitni/CopilotForXcode/releases/tag/0.33.5 - + https://github.com/intitni/CopilotForXcode/releases/tag/0.33.5.beta + 0.33.4 From 16295104023b6d68168c9837d5410dd35fc08aec Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 1 Jul 2024 00:25:03 +0800 Subject: [PATCH 37/44] Update LICENSE --- LICENSE | 712 ++---------------------------------------------------- README.md | 3 +- 2 files changed, 22 insertions(+), 693 deletions(-) diff --git a/LICENSE b/LICENSE index bdeb59bb..636cb078 100644 --- a/LICENSE +++ b/LICENSE @@ -1,691 +1,21 @@ -# Copilot for Xcode Open Source License - -This license is a combination of the GPLv3 and some additional agreements. - -Features that requires a Plus license key are not included in this project, and are not open source. - -As a contributor, you agree that your contributed code: -a. may be subject to a more permissive open-source license in the future. -b. can be used for commercial purposes. - -With the GPLv3 and these supplementary agreements, anyone can freely use, modify, and distribute the project, provided that: -- For commercial redistribution or commercial forks of this project, please contact us for authorization. - -Copyright (c) 2023 Shangxin Guo - ----------- - - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. +MIT License + +Copyright (c) 2024 Shangxin Guo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index f162bdad..1c54c24a 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ A [recommended setup](https://github.com/intitni/CopilotForXcode/issues/14) that | Command | Key Binding | | ------------------- | ------------------------------------------------------ | -| Accept Suggestions | `⌥}` (Or accept with Tab if Plus license is available) | +| Accept Suggestions | `⌥}` | | Reject Suggestion | `⌥{` | | Next Suggestion | `⌥>` | | Previous Suggestion | `⌥<` | @@ -366,7 +366,6 @@ The currently available Plus features include: - `@sense` scope in chat and prompt to code to include relevant information of the focusing code. - Terminal tab in chat panel. - Unlimited chat/embedding models. -- Tab to accept suggestions. - Persisted chat panel. - Browser tab in chat panel. - Unlimited custom commands. From d80d931bd6e9b65af622618490eb6a20c8e84ae4 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 3 Jul 2024 02:19:17 +0800 Subject: [PATCH 38/44] Move KeybindingManager to Core --- Core/Package.swift | 20 ++ ...SuggestionSettingsGeneralSectionView.swift | 24 +- .../KeyBindingManager/KeyBindingManager.swift | 29 ++ .../TabToAcceptSuggestion.swift | 309 ++++++++++++++++++ Core/Sources/Service/Service.swift | 13 +- .../TabToAcceptSuggestionTests.swift | 153 +++++++++ Pro | 2 +- TestPlan.xctestplan | 21 +- 8 files changed, 545 insertions(+), 26 deletions(-) create mode 100644 Core/Sources/KeyBindingManager/KeyBindingManager.swift create mode 100644 Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift create mode 100644 Core/Tests/KeyBindingManagerTests/TabToAcceptSuggestionTests.swift diff --git a/Core/Package.swift b/Core/Package.swift index 89e3bd1b..9b501cb1 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -101,6 +101,7 @@ let package = Package( // quick hack to support custom UserDefaults // https://github.com/sindresorhus/KeyboardShortcuts .package(url: "https://github.com/intitni/KeyboardShortcuts", branch: "main"), + .package(url: "https://github.com/intitni/CGEventOverride", from: "1.2.1"), ].pro, targets: [ // MARK: - Main @@ -127,6 +128,7 @@ let package = Package( "ServiceUpdateMigration", "ChatGPTChatTab", "PlusFeatureFlag", + "KeyBindingManager", .product(name: "XPCShared", package: "Tool"), .product(name: "SuggestionProvider", package: "Tool"), .product(name: "Workspace", package: "Tool"), @@ -377,6 +379,24 @@ let package = Package( ], path: "Sources/ChatContextCollectors/SystemInfoChatContextCollector" ), + + // MARK: Key Binding + + .target( + name: "KeyBindingManager", + dependencies: [ + .product(name: "Workspace", package: "Tool"), + .product(name: "Preferences", package: "Tool"), + .product(name: "Logger", package: "Tool"), + .product(name: "CGEventOverride", package: "CGEventOverride"), + .product(name: "AppMonitoring", package: "Tool"), + .product(name: "UserDefaultsObserver", package: "Tool"), + ] + ), + .testTarget( + name: "KeyBindingManagerTests", + dependencies: ["KeyBindingManager"] + ), ] ) diff --git a/Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionSettingsGeneralSectionView.swift b/Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionSettingsGeneralSectionView.swift index ff710f5b..c8cee457 100644 --- a/Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionSettingsGeneralSectionView.swift +++ b/Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionSettingsGeneralSectionView.swift @@ -172,28 +172,24 @@ struct SuggestionSettingsGeneralSectionView: View { Text("Real-time suggestion") } - #if canImport(ProHostApp) - WithFeatureEnabled(\.tabToAcceptSuggestion) { - Toggle(isOn: $settings.acceptSuggestionWithTab) { - HStack { - Text("Accept suggestion with Tab") + Toggle(isOn: $settings.acceptSuggestionWithTab) { + HStack { + Text("Accept suggestion with Tab") - Button(action: { - isTabToAcceptSuggestionModifierViewOpen = true - }) { - Image(systemName: "gearshape.fill") - } - .buttonStyle(.plain) + Button(action: { + isTabToAcceptSuggestionModifierViewOpen = true + }) { + Image(systemName: "gearshape.fill") } - }.sheet(isPresented: $isTabToAcceptSuggestionModifierViewOpen) { - TabToAcceptSuggestionModifierView() + .buttonStyle(.plain) } + }.sheet(isPresented: $isTabToAcceptSuggestionModifierViewOpen) { + TabToAcceptSuggestionModifierView() } Toggle(isOn: $settings.dismissSuggestionWithEsc) { Text("Dismiss suggestion with ESC") } - #endif HStack { Toggle(isOn: $settings.disableSuggestionFeatureGlobally) { diff --git a/Core/Sources/KeyBindingManager/KeyBindingManager.swift b/Core/Sources/KeyBindingManager/KeyBindingManager.swift new file mode 100644 index 00000000..c3d6ed31 --- /dev/null +++ b/Core/Sources/KeyBindingManager/KeyBindingManager.swift @@ -0,0 +1,29 @@ +import Foundation +import Workspace + +public final class KeyBindingManager { + let tabToAcceptSuggestion: TabToAcceptSuggestion + + public init( + workspacePool: WorkspacePool, + acceptSuggestion: @escaping () -> Void, + dismissSuggestion: @escaping () -> Void + ) { + tabToAcceptSuggestion = .init( + workspacePool: workspacePool, + acceptSuggestion: acceptSuggestion, + dismissSuggestion: dismissSuggestion + ) + } + + public func start() { + tabToAcceptSuggestion.start() + } + + @MainActor + public func stopForExit() { + tabToAcceptSuggestion.stopForExit() + } +} + + diff --git a/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift b/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift new file mode 100644 index 00000000..b27d2171 --- /dev/null +++ b/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift @@ -0,0 +1,309 @@ +import ActiveApplicationMonitor +import AppKit +import CGEventOverride +import Foundation +import Logger +import Preferences +import SuggestionBasic +import UserDefaultsObserver +import Workspace +import XcodeInspector + +final class TabToAcceptSuggestion { + let hook: CGEventHookType = CGEventHook(eventsOfInterest: [.keyDown]) { message in + Logger.service.debug("TabToAcceptSuggestion: \(message)") + } + + let workspacePool: WorkspacePool + let acceptSuggestion: () -> Void + let dismissSuggestion: () -> Void + private var CGEventObservationTask: Task? + private var isObserving: Bool { CGEventObservationTask != nil } + private let userDefaultsObserver = UserDefaultsObserver( + object: UserDefaults.shared, forKeyPaths: [ + UserDefaultPreferenceKeys().acceptSuggestionWithTab.key, + UserDefaultPreferenceKeys().dismissSuggestionWithEsc.key, + ], context: nil + ) + private var stoppedForExit = false + + struct ObservationKey: Hashable {} + + var canTapToAcceptSuggestion: Bool { + UserDefaults.shared.value(for: \.acceptSuggestionWithTab) + } + + var canEscToDismissSuggestion: Bool { + UserDefaults.shared.value(for: \.dismissSuggestionWithEsc) + } + + @MainActor + func stopForExit() { + Logger.debug.info("TabToAcceptSuggestion: stopForExit") + stoppedForExit = true + stopObservation() + } + + init( + workspacePool: WorkspacePool, + acceptSuggestion: @escaping () -> Void, + dismissSuggestion: @escaping () -> Void + ) { + _ = ThreadSafeAccessToXcodeInspector.shared + self.workspacePool = workspacePool + self.acceptSuggestion = acceptSuggestion + self.dismissSuggestion = dismissSuggestion + + hook.add( + .init( + eventsOfInterest: [.keyDown], + convert: { [weak self] _, _, event in + self?.handleEvent(event) ?? .unchanged + } + ), + forKey: ObservationKey() + ) + } + + func start() { + Logger.debug.info("TabToAcceptSuggestion: start") + Task { [weak self] in + for await _ in ActiveApplicationMonitor.shared.createInfoStream() { + guard let self else { return } + try Task.checkCancellation() + Logger.debug.info("TabToAcceptSuggestion: Active app changed") + Task { @MainActor in + if ActiveApplicationMonitor.shared.activeXcode != nil { + self.startObservation() + } else { + self.stopObservation() + } + } + } + } + + userDefaultsObserver.onChange = { [weak self] in + guard let self else { return } + Task { @MainActor in + Logger.debug.info("TabToAcceptSuggestion: Settings changed") + if self.canTapToAcceptSuggestion || self.canEscToDismissSuggestion { + self.startObservation() + } else { + self.stopObservation() + } + } + } + } + + @MainActor + func startObservation() { + Logger.debug.info("TabToAcceptSuggestion: startObservation") + guard !stoppedForExit else { return } + guard canTapToAcceptSuggestion || canEscToDismissSuggestion else { return } + hook.activateIfPossible() + } + + @MainActor + func stopObservation() { + Logger.debug.info("TabToAcceptSuggestion: stopObservation") + hook.deactivate() + } + + func handleEvent(_ event: CGEvent) -> CGEventManipulation.Result { + Logger.debug.info("TabToAcceptSuggestion: handleEvent") + let keycode = Int(event.getIntegerValueField(.keyboardEventKeycode)) + let tab = 48 + let esc = 53 + + switch keycode { + case tab: + Logger.debug.info( + "TabToAcceptSuggestion: Tab detected, flags: \(event.flags), permission: \(canTapToAcceptSuggestion)" + ) + + guard let fileURL = ThreadSafeAccessToXcodeInspector.shared.activeDocumentURL + else { + Logger.debug.info("TabToAcceptSuggestion: Active file not found") + return .unchanged + } + + let language = languageIdentifierFromFileURL(fileURL) + + func checkKeybinding() -> Bool { + if event.flags.contains(.maskHelp) { return false } + + let shouldCheckModifiers = if UserDefaults.shared + .value(for: \.acceptSuggestionWithModifierOnlyForSwift) + { + language == .builtIn(.swift) + } else { + true + } + + if shouldCheckModifiers { + if event.flags.contains(.maskShift) != UserDefaults.shared + .value(for: \.acceptSuggestionWithModifierShift) + { + return false + } + if event.flags.contains(.maskControl) != UserDefaults.shared + .value(for: \.acceptSuggestionWithModifierControl) + { + return false + } + if event.flags.contains(.maskAlternate) != UserDefaults.shared + .value(for: \.acceptSuggestionWithModifierOption) + { + return false + } + if event.flags.contains(.maskCommand) != UserDefaults.shared + .value(for: \.acceptSuggestionWithModifierCommand) + { + return false + } + } else { + if event.flags.contains(.maskShift) { return false } + if event.flags.contains(.maskControl) { return false } + if event.flags.contains(.maskAlternate) { return false } + if event.flags.contains(.maskCommand) { return false } + } + + return true + } + + guard + checkKeybinding(), + canTapToAcceptSuggestion + else { + Logger.debug.info("TabToAcceptSuggestion: Tab is invalid") + return .unchanged + } + + guard ThreadSafeAccessToXcodeInspector.shared.activeXcode != nil + else { + Logger.debug.info("TabToAcceptSuggestion: Xcode not found") + return .unchanged + } + guard let editor = ThreadSafeAccessToXcodeInspector.shared.focusedEditor + else { + Logger.debug.info("TabToAcceptSuggestion: Editor not found") + return .unchanged + } + guard let filespace = workspacePool.fetchFilespaceIfExisted(fileURL: fileURL) + else { + Logger.debug.info("TabToAcceptSuggestion: Filespace not found: \(fileURL)") + return .unchanged + } + guard let presentingSuggestion = filespace.presentingSuggestion + else { + Logger.debug.info("TabToAcceptSuggestion: Suggestion not found") + return .unchanged + } + + let editorContent = editor.getContent() + + let shouldAcceptSuggestion = Self.checkIfAcceptSuggestion( + lines: editorContent.lines, + cursorPosition: editorContent.cursorPosition, + codeMetadata: filespace.codeMetadata, + presentingSuggestionText: presentingSuggestion.text + ) + + if shouldAcceptSuggestion { + Logger.debug.info("TabToAcceptSuggestion: Perform accept suggestion") + acceptSuggestion() + return .discarded + } else { + Logger.debug.info("TabToAcceptSuggestion: Do not accept the suggestion") + return .unchanged + } + case esc: + guard + !event.flags.contains(.maskShift), + !event.flags.contains(.maskControl), + !event.flags.contains(.maskAlternate), + !event.flags.contains(.maskCommand), + !event.flags.contains(.maskHelp), + canEscToDismissSuggestion + else { return .unchanged } + + guard + let fileURL = ThreadSafeAccessToXcodeInspector.shared.activeDocumentURL, + ThreadSafeAccessToXcodeInspector.shared.activeXcode != nil, + let filespace = workspacePool.fetchFilespaceIfExisted(fileURL: fileURL), + filespace.presentingSuggestion != nil + else { return .unchanged } + + dismissSuggestion() + return .discarded + default: + return .unchanged + } + } +} + +extension TabToAcceptSuggestion { + static func checkIfAcceptSuggestion( + lines: [String], + cursorPosition: CursorPosition, + codeMetadata: FilespaceCodeMetadata, + presentingSuggestionText: String + ) -> Bool { + let line = cursorPosition.line + guard line >= 0, line < lines.endIndex else { + Logger.debug.info("TabToAcceptSuggestion: Suggestion position invalid") + return true + } + let col = cursorPosition.character + let prefixEndIndex = lines[line].utf16.index( + lines[line].utf16.startIndex, + offsetBy: col, + limitedBy: lines[line].utf16.endIndex + ) ?? lines[line].utf16.endIndex + let prefix = String(lines[line][.. = [] + + init() { + let inspector = XcodeInspector.shared + + inspector.$activeDocumentURL.receive(on: DispatchQueue.main).sink { [weak self] newValue in + self?.activeDocumentURL = newValue + }.store(in: &cancellable) + + inspector.$activeXcode.receive(on: DispatchQueue.main).sink { [weak self] newValue in + self?.activeXcode = newValue + }.store(in: &cancellable) + + inspector.$focusedEditor.receive(on: DispatchQueue.main).sink { [weak self] newValue in + self?.focusedEditor = newValue + }.store(in: &cancellable) + } +} + diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift index 04b8bd9c..b37b404e 100644 --- a/Core/Sources/Service/Service.swift +++ b/Core/Sources/Service/Service.swift @@ -11,7 +11,7 @@ import Workspace import WorkspaceSuggestionService import XcodeInspector import XPCShared - +import KeyBindingManager #if canImport(ProService) import ProService #endif @@ -32,6 +32,7 @@ public final class Service { public let realtimeSuggestionController = RealtimeSuggestionController() public let scheduledCleaner: ScheduledCleaner let globalShortcutManager: GlobalShortcutManager + let keyBindingManager: KeyBindingManager #if canImport(ProService) let proService: ProService @@ -62,9 +63,8 @@ public final class Service { } self.workspacePool = workspacePool globalShortcutManager = .init(guiController: guiController) - - #if canImport(ProService) - proService = ProService( + keyBindingManager = .init( + workspacePool: workspacePool, acceptSuggestion: { Task { await PseudoCommandHandler().acceptSuggestion() } }, @@ -72,6 +72,9 @@ public final class Service { Task { await PseudoCommandHandler().dismissSuggestion() } } ) + + #if canImport(ProService) + proService = ProService() #endif scheduledCleaner.service = self @@ -87,6 +90,7 @@ public final class Service { #endif DependencyUpdater().update() globalShortcutManager.start() + keyBindingManager.start() Task { await XcodeInspector.shared.safe.$activeDocumentURL @@ -105,6 +109,7 @@ public final class Service { @MainActor public func prepareForExit() async { Logger.service.info("Prepare for exit.") + keyBindingManager.stopForExit() #if canImport(ProService) proService.prepareForExit() #endif diff --git a/Core/Tests/KeyBindingManagerTests/TabToAcceptSuggestionTests.swift b/Core/Tests/KeyBindingManagerTests/TabToAcceptSuggestionTests.swift new file mode 100644 index 00000000..b84e8ac0 --- /dev/null +++ b/Core/Tests/KeyBindingManagerTests/TabToAcceptSuggestionTests.swift @@ -0,0 +1,153 @@ +import Foundation +import XCTest + +@testable import Workspace +@testable import KeyBindingManager + +class TabToAcceptSuggestionTests: XCTestCase { + func test_should_accept_if_line_invalid() { + XCTAssertTrue( + TabToAcceptSuggestion.checkIfAcceptSuggestion( + lines: """ + struct Cat { + var name: String + var age: Int + } + """.breakLines(), + cursorPosition: .init(line: 4, character: 4), + codeMetadata: .init(), + presentingSuggestionText: "Hello" + ) + ) + + XCTAssertTrue( + TabToAcceptSuggestion.checkIfAcceptSuggestion( + lines: """ + struct Cat { + var name: String + var age: Int + } + """.breakLines(), + cursorPosition: .init(line: -1, character: 4), + codeMetadata: .init(), + presentingSuggestionText: "Hello" + ) + ) + } + + func test_should_not_accept_if_tab_does_not_invalidate_the_suggestion() { + XCTAssertFalse( + TabToAcceptSuggestion.checkIfAcceptSuggestion( + lines: """ + struct Cat { + + var age: Int + } + """.breakLines(), + cursorPosition: .init(line: 1, character: 0), + codeMetadata: .init(tabSize: 4, indentSize: 4, usesTabsForIndentation: false), + presentingSuggestionText: " var name: String" + ) + ) + + XCTAssertFalse( + TabToAcceptSuggestion.checkIfAcceptSuggestion( + lines: """ + struct 🐱 { + + var 🎇: Int + } + """.breakLines(), + cursorPosition: .init(line: 1, character: 0), + codeMetadata: .init(tabSize: 4, indentSize: 4, usesTabsForIndentation: false), + presentingSuggestionText: " var 🎇: String" + ) + ) + + XCTAssertFalse( + TabToAcceptSuggestion.checkIfAcceptSuggestion( + lines: """ + struct Cat { + + var age: Int + } + """.breakLines(), + cursorPosition: .init(line: 1, character: 0), + codeMetadata: .init(tabSize: 2, indentSize: 2, usesTabsForIndentation: false), + presentingSuggestionText: " var name: String" + ) + ) + + XCTAssertFalse( + TabToAcceptSuggestion.checkIfAcceptSuggestion( + lines: """ + struct Cat { + + \tvar age: Int + } + """.breakLines(), + cursorPosition: .init(line: 1, character: 0), + codeMetadata: .init(tabSize: 4, indentSize: 1, usesTabsForIndentation: true), + presentingSuggestionText: "\tvar name: String" + ) + ) + } + + func test_should_accept_if_tab_invalidates_the_suggestion() { + XCTAssertTrue( + TabToAcceptSuggestion.checkIfAcceptSuggestion( + lines: """ + struct Cat { + \(" ") + var age: Int + } + """.breakLines(), + cursorPosition: .init(line: 1, character: 1), + codeMetadata: .init(tabSize: 4, indentSize: 4, usesTabsForIndentation: false), + presentingSuggestionText: " var name: String" + ) + ) + + XCTAssertTrue( + TabToAcceptSuggestion.checkIfAcceptSuggestion( + lines: """ + struct 🐱 { + \(" ") + var 🎇: Int + } + """.breakLines(), + cursorPosition: .init(line: 1, character: 1), + codeMetadata: .init(tabSize: 4, indentSize: 4, usesTabsForIndentation: false), + presentingSuggestionText: " var 🎇: String" + ) + ) + + XCTAssertTrue( + TabToAcceptSuggestion.checkIfAcceptSuggestion( + lines: """ + struct Cat { + \(" ") + var age: Int + } + """.breakLines(), + cursorPosition: .init(line: 1, character: 1), + codeMetadata: .init(tabSize: 2, indentSize: 2, usesTabsForIndentation: false), + presentingSuggestionText: " var name: String" + ) + ) + + XCTAssertTrue( + TabToAcceptSuggestion.checkIfAcceptSuggestion( + lines: """ + struct Cat { + \t + \tvar age: Int + } + """.breakLines(), + cursorPosition: .init(line: 1, character: 1), + codeMetadata: .init(tabSize: 4, indentSize: 1, usesTabsForIndentation: true), + presentingSuggestionText: "\tvar name: String" + ) + ) + } +} diff --git a/Pro b/Pro index 406c8513..64775a2e 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 406c851384e082086e765b04c5b14ecf28febda3 +Subproject commit 64775a2e32aa245a5a531751471ddbbad97e06e8 diff --git a/TestPlan.xctestplan b/TestPlan.xctestplan index cefad736..1d83d1c6 100644 --- a/TestPlan.xctestplan +++ b/TestPlan.xctestplan @@ -78,13 +78,6 @@ "name" : "TokenEncoderTests" } }, - { - "target" : { - "containerPath" : "container:Tool", - "identifier" : "SuggestionModelTests", - "name" : "SuggestionModelTests" - } - }, { "target" : { "containerPath" : "container:Tool", @@ -147,6 +140,20 @@ "identifier" : "SuggestionProviderTests", "name" : "SuggestionProviderTests" } + }, + { + "target" : { + "containerPath" : "container:Core", + "identifier" : "KeyBindingManagerTests", + "name" : "KeyBindingManagerTests" + } + }, + { + "target" : { + "containerPath" : "container:Tool", + "identifier" : "SuggestionBasicTests", + "name" : "SuggestionBasicTests" + } } ], "version" : 1 From c5f8f108832fcf69523a647e6933fc09cdc97b55 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 3 Jul 2024 02:38:30 +0800 Subject: [PATCH 39/44] Move theme syncronization to Core --- Core/Package.swift | 14 + .../Chat/ChatSettingsGeneralSectionView.swift | 4 - .../PromptToCodeSettingsView.swift | 4 - ...SuggestionSettingsGeneralSectionView.swift | 4 - .../CodeHighlightThemePicker.swift | 71 ++++ Core/Sources/Service/Service.swift | 5 +- .../HighlightJSThemeTemplate.swift | 107 ++++++ .../HighlightrThemeManager.swift | 89 +++++ .../PreferenceKey+Theme.swift | 27 ++ .../XcodeThemeController.swift | 248 ++++++++++++++ .../XcodeThemeParser.swift | 321 ++++++++++++++++++ Pro | 2 +- 12 files changed, 882 insertions(+), 14 deletions(-) create mode 100644 Core/Sources/HostApp/SharedComponents/CodeHighlightThemePicker.swift create mode 100644 Core/Sources/XcodeThemeController/HighlightJSThemeTemplate.swift create mode 100644 Core/Sources/XcodeThemeController/HighlightrThemeManager.swift create mode 100644 Core/Sources/XcodeThemeController/PreferenceKey+Theme.swift create mode 100644 Core/Sources/XcodeThemeController/XcodeThemeController.swift create mode 100644 Core/Sources/XcodeThemeController/XcodeThemeParser.swift diff --git a/Core/Package.swift b/Core/Package.swift index 9b501cb1..d7120b57 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -102,6 +102,7 @@ let package = Package( // https://github.com/sindresorhus/KeyboardShortcuts .package(url: "https://github.com/intitni/KeyboardShortcuts", branch: "main"), .package(url: "https://github.com/intitni/CGEventOverride", from: "1.2.1"), + .package(url: "https://github.com/intitni/Highlightr", branch: "master"), ].pro, targets: [ // MARK: - Main @@ -129,6 +130,7 @@ let package = Package( "ChatGPTChatTab", "PlusFeatureFlag", "KeyBindingManager", + "XcodeThemeController", .product(name: "XPCShared", package: "Tool"), .product(name: "SuggestionProvider", package: "Tool"), .product(name: "Workspace", package: "Tool"), @@ -397,6 +399,18 @@ let package = Package( name: "KeyBindingManagerTests", dependencies: ["KeyBindingManager"] ), + + // MARK: Theming + + .target( + name: "XcodeThemeController", + dependencies: [ + .product(name: "Preferences", package: "Tool"), + .product(name: "AppMonitoring", package: "Tool"), + .product(name: "Highlightr", package: "Highlightr"), + ] + ), + ] ) diff --git a/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift b/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift index 9675c7f7..3484e34d 100644 --- a/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift +++ b/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift @@ -235,12 +235,8 @@ struct ChatSettingsGeneralSectionView: View { Text("Wrap text in code block") } - #if canImport(ProHostApp) - CodeHighlightThemePicker(scenario: .chat) - #endif - Toggle(isOn: $settings.disableFloatOnTopWhenTheChatPanelIsDetached) { Text("Disable always-on-top when the chat panel is detached") } diff --git a/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift index bb35f475..87403d1d 100644 --- a/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift @@ -95,11 +95,7 @@ struct PromptToCodeSettingsView: View { Text("Wrap code") } - #if canImport(ProHostApp) - CodeHighlightThemePicker(scenario: .promptToCode) - - #endif FontPicker(font: $settings.font) { Text("Font") diff --git a/Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionSettingsGeneralSectionView.swift b/Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionSettingsGeneralSectionView.swift index c8cee457..30bf0650 100644 --- a/Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionSettingsGeneralSectionView.swift +++ b/Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionSettingsGeneralSectionView.swift @@ -245,12 +245,8 @@ struct SuggestionSettingsGeneralSectionView: View { Text("Hide common preceding spaces") } - #if canImport(ProHostApp) - CodeHighlightThemePicker(scenario: .suggestion) - #endif - FontPicker(font: $settings.font) { Text("Font") } diff --git a/Core/Sources/HostApp/SharedComponents/CodeHighlightThemePicker.swift b/Core/Sources/HostApp/SharedComponents/CodeHighlightThemePicker.swift new file mode 100644 index 00000000..9c20b7dd --- /dev/null +++ b/Core/Sources/HostApp/SharedComponents/CodeHighlightThemePicker.swift @@ -0,0 +1,71 @@ +import Foundation +import Preferences +import SwiftUI + +public struct CodeHighlightThemePicker: View { + public enum Scenario { + case suggestion + case promptToCode + case chat + } + + let scenario: Scenario + + public init(scenario: Scenario) { + self.scenario = scenario + } + + public var body: some View { + switch scenario { + case .suggestion: + SuggestionThemePicker() + case .promptToCode: + PromptToCodeThemePicker() + case .chat: + ChatThemePicker() + } + } + + struct SuggestionThemePicker: View { + @AppStorage(\.syncSuggestionHighlightTheme) var sync: Bool + var body: some View { + SyncToggle(sync: $sync) + } + } + + struct PromptToCodeThemePicker: View { + @AppStorage(\.syncPromptToCodeHighlightTheme) var sync: Bool + var body: some View { + SyncToggle(sync: $sync) + } + } + + struct ChatThemePicker: View { + @AppStorage(\.syncChatCodeHighlightTheme) var sync: Bool + var body: some View { + SyncToggle(sync: $sync) + } + } + + struct SyncToggle: View { + @Binding var sync: Bool + + var body: some View { + VStack(alignment: .leading) { + Toggle(isOn: $sync) { + Text("Sync color scheme with Xcode") + } + + Text("To refresh the theme, you must activate the extension service app once.") + .font(.footnote) + .foregroundColor(.secondary) + } + } + } +} + +#Preview { + @State var sync = false + return CodeHighlightThemePicker.SyncToggle(sync: $sync) +} + diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift index b37b404e..35ca6a50 100644 --- a/Core/Sources/Service/Service.swift +++ b/Core/Sources/Service/Service.swift @@ -4,14 +4,15 @@ import Combine import Dependencies import Foundation import GitHubCopilotService +import KeyBindingManager import Logger import SuggestionService import Toast import Workspace import WorkspaceSuggestionService import XcodeInspector +import XcodeThemeController import XPCShared -import KeyBindingManager #if canImport(ProService) import ProService #endif @@ -33,6 +34,7 @@ public final class Service { public let scheduledCleaner: ScheduledCleaner let globalShortcutManager: GlobalShortcutManager let keyBindingManager: KeyBindingManager + let xcodeThemeController: XcodeThemeController = .init() #if canImport(ProService) let proService: ProService @@ -85,6 +87,7 @@ public final class Service { scheduledCleaner.start() realtimeSuggestionController.start() guiController.start() + xcodeThemeController.start() #if canImport(ProService) proService.start() #endif diff --git a/Core/Sources/XcodeThemeController/HighlightJSThemeTemplate.swift b/Core/Sources/XcodeThemeController/HighlightJSThemeTemplate.swift new file mode 100644 index 00000000..40e14b66 --- /dev/null +++ b/Core/Sources/XcodeThemeController/HighlightJSThemeTemplate.swift @@ -0,0 +1,107 @@ +import Foundation + +func buildHighlightJSTheme(_ theme: XcodeTheme) -> String { + /// The source value is an `r g b a` string, for example: `0.5 0.5 0.2 1` + + return """ + .hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: \(theme.backgroundColor.hexString); + color: \(theme.plainTextColor.hexString); + } + .xml .hljs-meta { + color: \(theme.marksColor.hexString); + } + .hljs-comment, + .hljs-quote { + color: \(theme.commentColor.hexString); + } + .hljs-tag, + .hljs-keyword, + .hljs-selector-tag, + .hljs-literal, + .hljs-name { + color: \(theme.keywordsColor.hexString); + } + .hljs-attribute { + color: \(theme.attributesColor.hexString); + } + .hljs-variable, + .hljs-template-variable { + color: \(theme.otherPropertiesAndGlobalsColor.hexString); + } + .hljs-code, + .hljs-string, + .hljs-meta-string { + color: \(theme.stringsColor.hexString); + } + .hljs-regexp { + color: \(theme.regexLiteralsColor.hexString); + } + .hljs-link { + color: \(theme.urlsColor.hexString); + } + .hljs-title { + color: \(theme.headingColor.hexString); + } + .hljs-symbol, + .hljs-bullet { + color: \(theme.attributesColor.hexString); + } + .hljs-number { + color: \(theme.numbersColor.hexString); + } + .hljs-section { + color: \(theme.marksColor.hexString); + } + .hljs-meta { + color: \(theme.keywordsColor.hexString); + } + .hljs-type, + .hljs-built_in, + .hljs-builtin-name { + color: \(theme.otherTypeNamesColor.hexString); + } + .hljs-class .hljs-title, + .hljs-title .class_ { + color: \(theme.typeDeclarationsColor.hexString); + } + .hljs-function .hljs-title, + .hljs-title .function_ { + color: \(theme.otherDeclarationsColor.hexString); + } + .hljs-params { + color: \(theme.otherDeclarationsColor.hexString); + } + .hljs-attr { + color: \(theme.attributesColor.hexString); + } + .hljs-subst { + color: \(theme.plainTextColor.hexString); + } + .hljs-formula { + background-color: \(theme.selectionColor.hexString); + font-style: italic; + } + .hljs-addition { + background-color: #baeeba; + } + .hljs-deletion { + background-color: #ffc8bd; + } + .hljs-selector-id, + .hljs-selector-class { + color: \(theme.plainTextColor.hexString); + } + .hljs-doctag, + .hljs-strong { + font-weight: bold; + } + .hljs-emphasis { + font-style: italic; + } + """ +} + diff --git a/Core/Sources/XcodeThemeController/HighlightrThemeManager.swift b/Core/Sources/XcodeThemeController/HighlightrThemeManager.swift new file mode 100644 index 00000000..f5536e3c --- /dev/null +++ b/Core/Sources/XcodeThemeController/HighlightrThemeManager.swift @@ -0,0 +1,89 @@ +import Foundation +import Highlightr +import Preferences + +public class HighlightrThemeManager: ThemeManager { + let defaultManager: ThemeManager + + weak var controller: XcodeThemeController? + + public init(defaultManager: ThemeManager, controller: XcodeThemeController) { + self.defaultManager = defaultManager + self.controller = controller + } + + public func theme(for name: String) -> Theme? { + let syncSuggestionTheme = UserDefaults.shared.value(for: \.syncSuggestionHighlightTheme) + let syncPromptToCodeTheme = UserDefaults.shared.value(for: \.syncPromptToCodeHighlightTheme) + let syncChatTheme = UserDefaults.shared.value(for: \.syncChatCodeHighlightTheme) + + lazy var defaultLight = Theme(themeString: defaultLightTheme) + lazy var defaultDark = Theme(themeString: defaultDarkTheme) + + switch name { + case "suggestion-light": + guard syncSuggestionTheme, let theme = theme(lightMode: true) else { + return defaultLight + } + return theme + case "suggestion-dark": + guard syncSuggestionTheme, let theme = theme(lightMode: false) else { + return defaultDark + } + return theme + case "promptToCode-light": + guard syncPromptToCodeTheme, let theme = theme(lightMode: true) else { + return defaultLight + } + return theme + case "promptToCode-dark": + guard syncPromptToCodeTheme, let theme = theme(lightMode: false) else { + return defaultDark + } + return theme + case "chat-light": + guard syncChatTheme, let theme = theme(lightMode: true) else { + return defaultLight + } + return theme + case "chat-dark": + guard syncChatTheme, let theme = theme(lightMode: false) else { + return defaultDark + } + return theme + case "light": + return defaultLight + case "dark": + return defaultDark + default: + return defaultLight + } + } + + func theme(lightMode: Bool) -> Theme? { + guard let controller else { return nil } + guard let directories = controller.createSupportDirectoriesIfNeeded() else { return nil } + + let themeURL: URL = if lightMode { + directories.themeDirectory.appendingPathComponent("highlightjs-light") + } else { + directories.themeDirectory.appendingPathComponent("highlightjs-dark") + } + + if let themeString = try? String(contentsOf: themeURL) { + return Theme(themeString: themeString) + } + + controller.syncXcodeThemeIfNeeded() + + if let themeString = try? String(contentsOf: themeURL) { + return Theme(themeString: themeString) + } + + return nil + } +} + +let defaultLightTheme = ".hljs{display:block;overflow-x:auto;padding:0.5em;background:#FFFFFFFF;color:#000000D8}.xml .hljs-meta{color:#495460FF}.hljs-comment,.hljs-quote{color:#5D6B79FF}.hljs-tag,.hljs-keyword,.hljs-selector-tag,.hljs-literal,.hljs-name{color:#9A2393FF}.hljs-attribute{color:#805E03FF}.hljs-variable,.hljs-template-variable{color:#6B36A9FF}.hljs-code,.hljs-string,.hljs-meta-string{color:#C31A15FF}.hljs-regexp{color:#000000D8}.hljs-link{color:#0E0EFFFF}.hljs-title{color:#000000FF}.hljs-symbol,.hljs-bullet{color:#805E03FF}.hljs-number{color:#1C00CFFF}.hljs-section{color:#495460FF}.hljs-meta{color:#9A2393FF}.hljs-type,.hljs-built_in,.hljs-builtin-name{color:#3900A0FF}.hljs-class .hljs-title,.hljs-title .class_{color:#0B4F79FF}.hljs-function .hljs-title,.hljs-title .function_{color:#0E67A0FF}.hljs-params{color:#0E67A0FF}.hljs-attr{color:#805E03FF}.hljs-subst{color:#000000D8}.hljs-formula{background-color:#A3CCFEFF;font-style:italic}.hljs-addition{background-color:#baeeba}.hljs-deletion{background-color:#ffc8bd}.hljs-selector-id,.hljs-selector-class{color:#000000D8}.hljs-doctag,.hljs-strong{font-weight:bold}.hljs-emphasis{font-style:italic}" + +let defaultDarkTheme = ".hljs{display:block;overflow-x:auto;padding:0.5em;background:#1F1F23FF;color:#FFFFFFD8}.xml .hljs-meta{color:#91A1B1FF}.hljs-comment,.hljs-quote{color:#6B7985FF}.hljs-tag,.hljs-keyword,.hljs-selector-tag,.hljs-literal,.hljs-name{color:#FC5FA2FF}.hljs-attribute{color:#BF8554FF}.hljs-variable,.hljs-template-variable{color:#A166E5FF}.hljs-code,.hljs-string,.hljs-meta-string{color:#FC695DFF}.hljs-regexp{color:#FFFFFFD8}.hljs-link{color:#5482FEFF}.hljs-title{color:#FFFFFFFF}.hljs-symbol,.hljs-bullet{color:#BF8554FF}.hljs-number{color:#CFBF69FF}.hljs-section{color:#91A1B1FF}.hljs-meta{color:#FC5FA2FF}.hljs-type,.hljs-built_in,.hljs-builtin-name{color:#D0A7FEFF}.hljs-class .hljs-title,.hljs-title .class_{color:#5CD7FEFF}.hljs-function .hljs-title,.hljs-title .function_{color:#41A1BFFF}.hljs-params{color:#41A1BFFF}.hljs-attr{color:#BF8554FF}.hljs-subst{color:#FFFFFFD8}.hljs-formula{background-color:#505A6FFF;font-style:italic}.hljs-addition{background-color:#baeeba}.hljs-deletion{background-color:#ffc8bd}.hljs-selector-id,.hljs-selector-class{color:#FFFFFFD8}.hljs-doctag,.hljs-strong{font-weight:bold}.hljs-emphasis{font-style:italic}" diff --git a/Core/Sources/XcodeThemeController/PreferenceKey+Theme.swift b/Core/Sources/XcodeThemeController/PreferenceKey+Theme.swift new file mode 100644 index 00000000..0d46af1f --- /dev/null +++ b/Core/Sources/XcodeThemeController/PreferenceKey+Theme.swift @@ -0,0 +1,27 @@ +import Foundation +import Preferences + +// MARK: - Theming + +public extension UserDefaultPreferenceKeys { + var lightXcodeThemeName: PreferenceKey { + .init(defaultValue: "", key: "LightXcodeThemeName") + } + + var lightXcodeTheme: PreferenceKey> { + .init(defaultValue: .init(nil), key: "LightXcodeTheme") + } + + var darkXcodeThemeName: PreferenceKey { + .init(defaultValue: "", key: "DarkXcodeThemeName") + } + + var darkXcodeTheme: PreferenceKey> { + .init(defaultValue: .init(nil), key: "LightXcodeTheme") + } + + var lastSyncedHighlightJSThemeCreatedAt: PreferenceKey { + .init(defaultValue: 0, key: "LastSyncedHighlightJSThemeCreatedAt") + } +} + diff --git a/Core/Sources/XcodeThemeController/XcodeThemeController.swift b/Core/Sources/XcodeThemeController/XcodeThemeController.swift new file mode 100644 index 00000000..5cff0ddd --- /dev/null +++ b/Core/Sources/XcodeThemeController/XcodeThemeController.swift @@ -0,0 +1,248 @@ +import AppKit +import Foundation +import Highlightr +import XcodeInspector + +public class XcodeThemeController { + var syncTriggerTask: Task? + + public init(syncTriggerTask: Task? = nil) { + self.syncTriggerTask = syncTriggerTask + } + + public func start() { + let defaultHighlightrThemeManager = Highlightr.themeManager + Highlightr.themeManager = HighlightrThemeManager( + defaultManager: defaultHighlightrThemeManager, + controller: self + ) + + syncXcodeThemeIfNeeded() + + syncTriggerTask?.cancel() + syncTriggerTask = Task { [weak self] in + let notifications = NSWorkspace.shared.notificationCenter + .notifications(named: NSWorkspace.didActivateApplicationNotification) + for await notification in notifications { + try Task.checkCancellation() + guard let app = notification + .userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication + else { continue } + guard app.isCopilotForXcodeExtensionService else { continue } + guard let self else { return } + self.syncXcodeThemeIfNeeded() + } + } + } +} + +extension XcodeThemeController { + func syncXcodeThemeIfNeeded() { + guard UserDefaults.shared.value(for: \.syncSuggestionHighlightTheme) + || UserDefaults.shared.value(for: \.syncPromptToCodeHighlightTheme) + || UserDefaults.shared.value(for: \.syncChatCodeHighlightTheme) + else { return } + guard let directories = createSupportDirectoriesIfNeeded() else { return } + + defer { + UserDefaults.shared.set( + Date().timeIntervalSince1970, + for: \.lastSyncedHighlightJSThemeCreatedAt + ) + } + + let xcodeUserDefaults = UserDefaults(suiteName: "com.apple.dt.Xcode")! + + if let darkThemeName = xcodeUserDefaults + .value(forKey: "XCFontAndColorCurrentDarkTheme") as? String + { + syncXcodeThemeIfNeeded( + xcodeThemeName: darkThemeName, + light: false, + in: directories.themeDirectory + ) + } + + if let lightThemeName = xcodeUserDefaults + .value(forKey: "XCFontAndColorCurrentTheme") as? String + { + syncXcodeThemeIfNeeded( + xcodeThemeName: lightThemeName, + light: true, + in: directories.themeDirectory + ) + } + } + + func syncXcodeThemeIfNeeded( + xcodeThemeName: String, + light: Bool, + in directoryURL: URL + ) { + let targetName = light ? "highlightjs-light" : "highlightjs-dark" + guard let xcodeThemeURL = locateXcodeTheme(named: xcodeThemeName) else { return } + let targetThemeURL = directoryURL.appendingPathComponent(targetName) + let lastSyncTimestamp = UserDefaults.shared + .value(for: \.lastSyncedHighlightJSThemeCreatedAt) + + let shouldSync = { + if light, UserDefaults.shared.value(for: \.lightXcodeTheme) == nil { return true } + if !light, UserDefaults.shared.value(for: \.darkXcodeTheme) == nil { return true } + if light, xcodeThemeName != UserDefaults.shared.value(for: \.lightXcodeThemeName) { + return true + } + if !light, xcodeThemeName != UserDefaults.shared.value(for: \.darkXcodeThemeName) { + return true + } + if !FileManager.default.fileExists(atPath: targetThemeURL.path) { return true } + + let xcodeThemeFileUpdated = { + guard let xcodeThemeModifiedDate = try? xcodeThemeURL + .resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate + else { return true } + return xcodeThemeModifiedDate.timeIntervalSince1970 > lastSyncTimestamp + }() + + if xcodeThemeFileUpdated { return true } + + return false + }() + + if shouldSync { + do { + let theme = try XcodeTheme(fileURL: xcodeThemeURL) + let highlightrTheme = theme.asHighlightJSTheme() + try highlightrTheme.write(to: targetThemeURL, atomically: true, encoding: .utf8) + + Task { @MainActor in + if light { + UserDefaults.shared.set(xcodeThemeName, for: \.lightXcodeThemeName) + UserDefaults.shared.set(.init(theme), for: \.lightXcodeTheme) + UserDefaults.shared.set( + .init(theme.plainTextColor.storable), + for: \.codeForegroundColorLight + ) + UserDefaults.shared.set( + .init(theme.backgroundColor.storable), + for: \.codeBackgroundColorLight + ) + } else { + UserDefaults.shared.set(xcodeThemeName, for: \.darkXcodeThemeName) + UserDefaults.shared.set(.init(theme), for: \.darkXcodeTheme) + UserDefaults.shared.set( + .init(theme.plainTextColor.storable), + for: \.codeForegroundColorDark + ) + UserDefaults.shared.set( + .init(theme.backgroundColor.storable), + for: \.codeBackgroundColorDark + ) + } + } + } catch { + print(error.localizedDescription) + } + } + } + + func locateXcodeTheme(named name: String) -> URL? { + if let customThemeURL = FileManager.default.urls( + for: .libraryDirectory, + in: .userDomainMask + ).first?.appendingPathComponent("Developer/Xcode/UserData/FontAndColorThemes") + .appendingPathComponent(name), + FileManager.default.fileExists(atPath: customThemeURL.path) + { + return customThemeURL + } + + let xcodeURL: URL? = { + // Use the latest running Xcode + if let running = XcodeInspector.shared.latestActiveXcode?.bundleURL { + return running + } + // Use the main Xcode.app + let proposedXcodeURL = URL(fileURLWithPath: "/Applications/Xcode.app") + if FileManager.default.fileExists(atPath: proposedXcodeURL.path) { + return proposedXcodeURL + } + // Look for an Xcode.app + if let applicationsURL = FileManager.default.urls( + for: .applicationDirectory, + in: .localDomainMask + ).first { + struct InfoPlist: Codable { + var CFBundleIdentifier: String + } + + let appBundleIdentifier = "com.apple.dt.Xcode" + let appDirectories = try? FileManager.default.contentsOfDirectory( + at: applicationsURL, + includingPropertiesForKeys: [], + options: .skipsHiddenFiles + ) + for appDirectoryURL in appDirectories ?? [] { + let infoPlistURL = appDirectoryURL.appendingPathComponent("Contents/Info.plist") + if let data = try? Data(contentsOf: infoPlistURL), + let infoPlist = try? PropertyListDecoder().decode( + InfoPlist.self, + from: data + ), + infoPlist.CFBundleIdentifier == appBundleIdentifier + { + return appDirectoryURL + } + } + } + return nil + }() + + if let url = xcodeURL? + .appendingPathComponent("Contents/SharedFrameworks/DVTUserInterfaceKit.framework") + .appendingPathComponent("Versions/A/Resources/FontAndColorThemes") + .appendingPathComponent(name), + FileManager.default.fileExists(atPath: url.path) + { + return url + } + + return nil + } + + func createSupportDirectoriesIfNeeded() -> (supportDirectory: URL, themeDirectory: URL)? { + guard let supportURL = FileManager.default.urls( + for: .applicationSupportDirectory, + in: .userDomainMask + ).first?.appendingPathComponent( + Bundle.main + .object(forInfoDictionaryKey: "APPLICATION_SUPPORT_FOLDER") as! String + ) else { + return nil + } + + let themeURL = supportURL.appendingPathComponent("Themes") + + do { + if !FileManager.default.fileExists(atPath: supportURL.path) { + try FileManager.default.createDirectory( + at: supportURL, + withIntermediateDirectories: true, + attributes: nil + ) + } + + if !FileManager.default.fileExists(atPath: themeURL.path) { + try FileManager.default.createDirectory( + at: themeURL, + withIntermediateDirectories: true, + attributes: nil + ) + } + } catch { + return nil + } + + return (supportURL, themeURL) + } +} + diff --git a/Core/Sources/XcodeThemeController/XcodeThemeParser.swift b/Core/Sources/XcodeThemeController/XcodeThemeParser.swift new file mode 100644 index 00000000..80b13ed5 --- /dev/null +++ b/Core/Sources/XcodeThemeController/XcodeThemeParser.swift @@ -0,0 +1,321 @@ +import Foundation +import Preferences + +public struct XcodeTheme: Codable { + public struct ThemeColor: Codable { + public var red: Double + public var green: Double + public var blue: Double + public var alpha: Double + + public var hexString: String { + let red = Int(self.red * 255) + let green = Int(self.green * 255) + let blue = Int(self.blue * 255) + let alpha = Int(self.alpha * 255) + return String(format: "#%02X%02X%02X%02X", red, green, blue, alpha) + } + + var storable: StorableColor { + .init(red: red, green: green, blue: blue, alpha: alpha) + } + } + + public var plainTextColor: ThemeColor + public var commentColor: ThemeColor + public var documentationMarkupColor: ThemeColor + public var documentationMarkupKeywordColor: ThemeColor + public var marksColor: ThemeColor + public var stringsColor: ThemeColor + public var charactersColor: ThemeColor + public var numbersColor: ThemeColor + public var regexLiteralsColor: ThemeColor + public var regexLiteralNumbersColor: ThemeColor + public var regexLiteralCaptureNamesColor: ThemeColor + public var regexLiteralCharacterClassNamesColor: ThemeColor + public var regexLiteralOperatorsColor: ThemeColor + public var keywordsColor: ThemeColor + public var preprocessorStatementsColor: ThemeColor + public var urlsColor: ThemeColor + public var attributesColor: ThemeColor + public var typeDeclarationsColor: ThemeColor + public var otherDeclarationsColor: ThemeColor + public var projectClassNamesColor: ThemeColor + public var projectFunctionAndMethodNamesColor: ThemeColor + public var projectConstantsColor: ThemeColor + public var projectTypeNamesColor: ThemeColor + public var projectPropertiesAndGlobalsColor: ThemeColor + public var projectPreprocessorMacrosColor: ThemeColor + public var otherClassNamesColor: ThemeColor + public var otherFunctionAndMethodNamesColor: ThemeColor + public var otherConstantsColor: ThemeColor + public var otherTypeNamesColor: ThemeColor + public var otherPropertiesAndGlobalsColor: ThemeColor + public var otherPreprocessorMacrosColor: ThemeColor + public var headingColor: ThemeColor + public var backgroundColor: ThemeColor + public var selectionColor: ThemeColor + public var cursorColor: ThemeColor + public var currentLineColor: ThemeColor + public var invisibleCharactersColor: ThemeColor + public var debuggerConsolePromptColor: ThemeColor + public var debuggerConsoleOutputColor: ThemeColor + public var debuggerConsoleInputColor: ThemeColor + public var executableConsoleOutputColor: ThemeColor + public var executableConsoleInputColor: ThemeColor + + public func asHighlightJSTheme() -> String { + buildHighlightJSTheme(self) + .replacingOccurrences(of: "\n", with: "") + .replacingOccurrences(of: ": ", with: ":") + .replacingOccurrences(of: "} ", with: "}") + .replacingOccurrences(of: " {", with: "{") + .replacingOccurrences(of: ";}", with: "}") + .replacingOccurrences(of: " ", with: "") + } +} + +public extension XcodeTheme { + /// Color scheme locations: + /// ~/Library/Developer/Xcode/UserData/FontAndColorThemes/ + /// Xcode.app/Contents/SharedFrameworks/DVTUserInterfaceKit.framework/Versions/A/Resources/FontAndColorThemes + init(fileURL: URL) throws { + let parser = XcodeThemeParser() + self = try parser.parse(fileURL: fileURL) + } +} + +struct XcodeThemeParser { + enum Error: Swift.Error { + case fileNotFound + case invalidData + } + + func parse(fileURL: URL) throws -> XcodeTheme { + guard let data = try? Data(contentsOf: fileURL) else { + throw Error.fileNotFound + } + + if fileURL.pathExtension == "xccolortheme" { + return try parseXCColorTheme(data) + } else { + throw Error.invalidData + } + } + + func parseXCColorTheme(_ data: Data) throws -> XcodeTheme { + let plist = try? PropertyListSerialization.propertyList( + from: data, + options: .mutableContainers, + format: nil + ) as? [String: Any] + + guard let theme = plist else { throw Error.invalidData } + + /// The source value is an `r g b a` string, for example: `0.5 0.5 0.2 1` + func converColor(source: String) -> XcodeTheme.ThemeColor { + let components = source.split(separator: " ") + let red = (components[0] as NSString).doubleValue + let green = (components[1] as NSString).doubleValue + let blue = (components[2] as NSString).doubleValue + let alpha = (components[3] as NSString).doubleValue + return .init(red: red, green: green, blue: blue, alpha: alpha) + } + + func getThemeValue( + at path: [String], + defaultValue: XcodeTheme.ThemeColor = .init(red: 0, green: 0, blue: 0, alpha: 1) + ) -> XcodeTheme.ThemeColor { + guard !path.isEmpty else { return defaultValue } + let keys = path.dropLast(1) + var currentDict = theme + for key in keys { + guard let value = currentDict[key] as? [String: Any] else { + return defaultValue + } + currentDict = value + } + if let value = currentDict[path.last!] as? String { + return converColor(source: value) + } + return defaultValue + } + + let black = XcodeTheme.ThemeColor(red: 0, green: 0, blue: 0, alpha: 1) + let white = XcodeTheme.ThemeColor(red: 1, green: 1, blue: 1, alpha: 1) + + let xcodeTheme = XcodeTheme( + plainTextColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.plain"], + defaultValue: black + ), + commentColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.comment"], + defaultValue: black + ), + documentationMarkupColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.comment.doc"], + defaultValue: black + ), + documentationMarkupKeywordColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.comment.doc.keyword"], + defaultValue: black + ), + marksColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.mark"], + defaultValue: black + ), + stringsColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.string"], + defaultValue: black + ), + charactersColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.character"], + defaultValue: black + ), + numbersColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.number"], + defaultValue: black + ), + regexLiteralsColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.plain"], + defaultValue: black + ), + regexLiteralNumbersColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.number"], + defaultValue: black + ), + regexLiteralCaptureNamesColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.plain"], + defaultValue: black + ), + regexLiteralCharacterClassNamesColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.plain"], + defaultValue: black + ), + regexLiteralOperatorsColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.plain"], + defaultValue: black + ), + keywordsColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.keyword"], + defaultValue: black + ), + preprocessorStatementsColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.preprocessor"], + defaultValue: black + ), + urlsColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.url"], + defaultValue: black + ), + attributesColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.attribute"], + defaultValue: black + ), + typeDeclarationsColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.declaration.type"], + defaultValue: black + ), + otherDeclarationsColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.declaration.other"], + defaultValue: black + ), + projectClassNamesColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.class"], + defaultValue: black + ), + projectFunctionAndMethodNamesColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.function"], + defaultValue: black + ), + projectConstantsColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.constant"], + defaultValue: black + ), + projectTypeNamesColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.type"], + defaultValue: black + ), + projectPropertiesAndGlobalsColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.variable"], + defaultValue: black + ), + projectPreprocessorMacrosColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.macro"], + defaultValue: black + ), + otherClassNamesColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.class.system"], + defaultValue: black + ), + otherFunctionAndMethodNamesColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.function.system"], + defaultValue: black + ), + otherConstantsColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.constant.system"], + defaultValue: black + ), + otherTypeNamesColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.type.system"], + defaultValue: black + ), + otherPropertiesAndGlobalsColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.variable.system"], + defaultValue: black + ), + otherPreprocessorMacrosColor: getThemeValue( + at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.macro.system"], + defaultValue: black + ), + headingColor: getThemeValue( + at: ["DVTMarkupTextPrimaryHeadingColor"], + defaultValue: black + ), + backgroundColor: getThemeValue( + at: ["DVTSourceTextBackground"], + defaultValue: white + ), + selectionColor: getThemeValue( + at: ["DVTSourceTextSelectionColor"], + defaultValue: black + ), + cursorColor: getThemeValue( + at: ["DVTSourceTextInsertionPointColor"], + defaultValue: black + ), + currentLineColor: getThemeValue( + at: ["DVTSourceTextCurrentLineHighlightColor"], + defaultValue: black + ), + invisibleCharactersColor: getThemeValue( + at: ["DVTSourceTextInvisiblesColor"], + defaultValue: black + ), + debuggerConsolePromptColor: getThemeValue( + at: ["DVTConsoleDebuggerPromptTextColor"], + defaultValue: black + ), + debuggerConsoleOutputColor: getThemeValue( + at: ["DVTConsoleDebuggerOutputTextColor"], + defaultValue: black + ), + debuggerConsoleInputColor: getThemeValue( + at: ["DVTConsoleDebuggerInputTextColor"], + defaultValue: black + ), + executableConsoleOutputColor: getThemeValue( + at: ["DVTConsoleExectuableOutputTextColor"], + defaultValue: black + ), + executableConsoleInputColor: getThemeValue( + at: ["DVTConsoleExectuableInputTextColor"], + defaultValue: black + ) + ) + + return xcodeTheme + } +} + diff --git a/Pro b/Pro index 64775a2e..bef6bfe2 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 64775a2e32aa245a5a531751471ddbbad97e06e8 +Subproject commit bef6bfe206411ef76bda4c451f553b8483b4a7de From f9d49131163ec1219f5376d35a9538e56b5ef52d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 3 Jul 2024 14:48:58 +0800 Subject: [PATCH 40/44] Minor changes to make it build --- .gitmodules | 3 - Copilot for Xcode.xcodeproj/project.pbxproj | 2 - Core/Package.swift | 88 ++++++++----------- Core/Sources/ChatGPTChatTab/Chat.swift | 3 +- Makefile | 25 ------ Pro | 1 - .../ChatTab/CodeiumChatTab.swift | 1 - .../OpenAIService/Memory/ChatGPTMemory.swift | 1 + 8 files changed, 40 insertions(+), 84 deletions(-) delete mode 100644 Makefile delete mode 160000 Pro diff --git a/.gitmodules b/.gitmodules index a091985c..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "Pro"] - path = Pro - url = git@github.com:intitni/CopilotForXcodePro.git diff --git a/Copilot for Xcode.xcodeproj/project.pbxproj b/Copilot for Xcode.xcodeproj/project.pbxproj index d8bc09f9..ad068512 100644 --- a/Copilot for Xcode.xcodeproj/project.pbxproj +++ b/Copilot for Xcode.xcodeproj/project.pbxproj @@ -205,7 +205,6 @@ C8216B772980370100AD38C7 /* ReloadLaunchAgent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReloadLaunchAgent.swift; sourceTree = ""; }; C828B27D2B1F241500E7612A /* ExtensionPoint.appextensionpoint */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = ExtensionPoint.appextensionpoint; sourceTree = ""; }; C82E38492A1F025F00D4EADF /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; - C83E5DED2A38CD8C0071506D /* Makefile */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = ""; }; C8520300293C4D9000460097 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; C8520308293D805800460097 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; C861A6A229E5503F005C41A3 /* PromptToCodeCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromptToCodeCommand.swift; sourceTree = ""; }; @@ -335,7 +334,6 @@ C887BC832965D96000931567 /* DEVELOPMENT.md */, C8520308293D805800460097 /* README.md */, C82E38492A1F025F00D4EADF /* LICENSE */, - C83E5DED2A38CD8C0071506D /* Makefile */, C8F103292A7A365000D28F4F /* launchAgent.plist */, C8738B6D2BE4F3E800609E7F /* bridgeLaunchAgent.plist */, C81E867D296FE4420026E908 /* Version.xcconfig */, diff --git a/Core/Package.swift b/Core/Package.swift index d7120b57..337844ec 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -4,57 +4,6 @@ import Foundation import PackageDescription -// MARK: - Pro - -extension [Target.Dependency] { - func pro(_ targetNames: [String]) -> [Target.Dependency] { - if isProIncluded { - // include the pro package - return self + targetNames.map { Target.Dependency.product(name: $0, package: "Pro") } - } - return self - } -} - -extension [Package.Dependency] { - var pro: [Package.Dependency] { - if isProIncluded { - // include the pro package - return self + [.package(path: "../Pro/Pro")] - } - return self - } -} - -let isProIncluded: Bool = { - func isProIncluded(file: StaticString = #file) -> Bool { - let filePath = "\(file)" - let fileURL = URL(fileURLWithPath: filePath) - let rootURL = fileURL - .deletingLastPathComponent() - .deletingLastPathComponent() - let confURL = rootURL.appendingPathComponent("PLUS") - if !FileManager.default.fileExists(atPath: confURL.path) { - return false - } - do { - if let content = try String( - data: Data(contentsOf: confURL), - encoding: .utf8 - ) { - if content.hasPrefix("YES") { - return true - } - } - return false - } catch { - return false - } - } - - return isProIncluded() -}() - // MARK: - Package let package = Package( @@ -414,3 +363,40 @@ let package = Package( ] ) +extension [Target.Dependency] { + func pro(_ targetNames: [String]) -> [Target.Dependency] { + if isProIncluded { + // include the pro package + return self + targetNames.map { Target.Dependency.product(name: $0, package: "Pro") } + } + return self + } +} + +extension [Package.Dependency] { + var pro: [Package.Dependency] { + if isProIncluded { + // include the pro package + return self + [.package(path: "../../CopilotForXcodePro/Pro")] + } + return self + } +} + +let isProIncluded: Bool = { + func isProIncluded(file: StaticString = #file) -> Bool { + let filePath = "\(file)" + let fileURL = URL(fileURLWithPath: filePath) + let rootURL = fileURL + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + let confURL = rootURL.appendingPathComponent("PLUS") + if !FileManager.default.fileExists(atPath: confURL.path) { + return false + } + return true + } + + return isProIncluded() +}() diff --git a/Core/Sources/ChatGPTChatTab/Chat.swift b/Core/Sources/ChatGPTChatTab/Chat.swift index f3170256..16185f94 100644 --- a/Core/Sources/ChatGPTChatTab/Chat.swift +++ b/Core/Sources/ChatGPTChatTab/Chat.swift @@ -1,3 +1,4 @@ +import ChatBasic import ChatService import ComposableArchitecture import Foundation @@ -48,7 +49,7 @@ public struct DisplayedChatMessage: Equatable { self.id = id self.role = role self.text = text - self.markdownContent = .init(text) + markdownContent = .init(text) self.references = references } } diff --git a/Makefile b/Makefile deleted file mode 100644 index 7d460e78..00000000 --- a/Makefile +++ /dev/null @@ -1,25 +0,0 @@ -GITHUB_URL := https://github.com/intitni/CopilotForXcode/ -ZIPNAME_BASE := Copilot.for.Xcode.app - -setup: - echo "Setup." - -# Usage: make appcast app=path/to/bundle.app tag=1.0.0 [channel=beta] [release=1] -appcast: - $(eval RELEASEDIR := ~/Library/Caches/CopilotForXcodeRelease/$(shell uuidgen)) - $(eval BUNDLENAME := $(shell basename "$(app)")) - $(eval WORKDIR := $(shell dirname "$(app)")) - $(eval ZIPNAME := $(ZIPNAME_BASE)$(if $(channel),.$(channel).$(if $(release),$(release),1))) - $(eval RELEASENOTELINK := $(GITHUB_URL)releases/tag/$(tag)) - mkdir -p $(RELEASEDIR) - cp appcast.xml $(RELEASEDIR)/appcast.xml - cd $(WORKDIR) && ditto -c -k --sequesterRsrc --keepParent "$(BUNDLENAME)" "$(ZIPNAME).zip" - cd $(WORKDIR) && cp "$(ZIPNAME).zip" $(RELEASEDIR)/ - touch $(RELEASEDIR)/$(ZIPNAME).html - echo "" > $(RELEASEDIR)/$(ZIPNAME).html - -Core/.build/artifacts/sparkle/bin/generate_appcast $(RELEASEDIR) --download-url-prefix "$(GITHUB_URL)releases/download/$(tag)/" --release-notes-url-prefix "$(RELEASENOTELINK)" $(if $(channel),--channel "$(channel)") - mv -f $(RELEASEDIR)/appcast.xml . - rm -rf $(RELEASEDIR) - sed -i '' 's/$(ZIPNAME).html/$(tag)/g' appcast.xml - -.PHONY: setup appcast diff --git a/Pro b/Pro deleted file mode 160000 index bef6bfe2..00000000 --- a/Pro +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bef6bfe206411ef76bda4c451f553b8483b4a7de diff --git a/Tool/Sources/CodeiumService/ChatTab/CodeiumChatTab.swift b/Tool/Sources/CodeiumService/ChatTab/CodeiumChatTab.swift index 75e97373..d2dcb704 100644 --- a/Tool/Sources/CodeiumService/ChatTab/CodeiumChatTab.swift +++ b/Tool/Sources/CodeiumService/ChatTab/CodeiumChatTab.swift @@ -2,7 +2,6 @@ import AppKit import ChatTab import Combine import ComposableArchitecture -import LicenseManagement import Logger import Preferences import SwiftUI diff --git a/Tool/Sources/OpenAIService/Memory/ChatGPTMemory.swift b/Tool/Sources/OpenAIService/Memory/ChatGPTMemory.swift index 33300ee8..8f75779b 100644 --- a/Tool/Sources/OpenAIService/Memory/ChatGPTMemory.swift +++ b/Tool/Sources/OpenAIService/Memory/ChatGPTMemory.swift @@ -1,3 +1,4 @@ +import ChatBasic import Foundation public struct ChatGPTPrompt: Equatable { From d6fcbe5a085110e2ebec2c59afcf22cd85ae05f8 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 3 Jul 2024 15:46:43 +0800 Subject: [PATCH 41/44] Update README.md --- README.md | 35 +++-------------------------------- 1 file changed, 3 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 1c54c24a..c51ea964 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ Copilot for Xcode is an Xcode Source Editor Extension that provides GitHub Copil Buy Me A Coffee -[Get a Plus License Key to unlock more features and support this project](https://intii.lemonsqueezy.com) - ## Features - Code Suggestions (powered by GitHub Copilot and Codeium). @@ -32,7 +30,6 @@ Copilot for Xcode is an Xcode Source Editor Extension that provides GitHub Copil - [Managing `CopilotForXcodeExtensionService.app`](#managing-copilotforxcodeextensionserviceapp) - [Update](#update) - [Feature](#feature) -- [Plus Features](#plus-features) - [Limitations](#limitations) - [License](#license) @@ -121,7 +118,8 @@ A [recommended setup](https://github.com/intitni/CopilotForXcode/issues/14) that | Command | Key Binding | | ------------------- | ------------------------------------------------------ | -| Accept Suggestions | `⌥}` | +| Accept Suggestions | `⌥}` or Tab | +| Dismiss Suggestions | Esc | | Reject Suggestion | `⌥{` | | Next Suggestion | `⌥>` | | Previous Suggestion | `⌥<` | @@ -256,7 +254,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 | @@ -353,33 +351,6 @@ You can use the following template arguments in custom commands: | `{{active_editor_file_name}}` | The name of the active file in the editor. | | `{{clipboard}}` | The content in clipboard. | -## Plus Features - -The pre-built binary contains a set of exclusive features that can only be accessed with a Plus license key. To obtain a license key, please visit [this link](https://intii.lemonsqueezy.com). - -These features are included in another repo, and are not open sourced. - -The currently available Plus features include: - -- `@project` scope in chat to include project information in conversations. (experimental) -- Suggestion Cheatsheet that provides relevant content to the suggestion service. (experimental) -- `@sense` scope in chat and prompt to code to include relevant information of the focusing code. -- Terminal tab in chat panel. -- Unlimited chat/embedding models. -- Persisted chat panel. -- Browser tab in chat panel. -- Unlimited custom commands. - -Since the app needs to manage license keys, it will send network request to `https://copilotforxcode-license.intii.com`, - -- when you activate the license key -- when you deactivate the license key -- when you validate the license key manually -- when you open the host app or the service app if a license key is available -- every 24 hours if a license key is available - -The request contains only the license key, the email address (only on activation), and an instance id. You are free to MITM the request to see what data is sent. - ## Limitations - The extension utilizes various tricks to monitor the state of Xcode. It may fail, it may be incorrect, especially when you have multiple Xcode windows running, and maybe even worse when they are in different displays. I am not sure about that though. From 1e57d8dc1de287980d864fc0af07060ea5dcf9b0 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 3 Jul 2024 16:53:58 +0800 Subject: [PATCH 42/44] Remove debug logs --- .../TabToAcceptSuggestion.swift | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift b/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift index b27d2171..f1e154e5 100644 --- a/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift +++ b/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift @@ -30,7 +30,7 @@ final class TabToAcceptSuggestion { struct ObservationKey: Hashable {} var canTapToAcceptSuggestion: Bool { - UserDefaults.shared.value(for: \.acceptSuggestionWithTab) + UserDefaults.shared.value(for: \.acceptSuggestionWithTab) } var canEscToDismissSuggestion: Bool { @@ -39,7 +39,6 @@ final class TabToAcceptSuggestion { @MainActor func stopForExit() { - Logger.debug.info("TabToAcceptSuggestion: stopForExit") stoppedForExit = true stopObservation() } @@ -66,12 +65,10 @@ final class TabToAcceptSuggestion { } func start() { - Logger.debug.info("TabToAcceptSuggestion: start") Task { [weak self] in for await _ in ActiveApplicationMonitor.shared.createInfoStream() { guard let self else { return } try Task.checkCancellation() - Logger.debug.info("TabToAcceptSuggestion: Active app changed") Task { @MainActor in if ActiveApplicationMonitor.shared.activeXcode != nil { self.startObservation() @@ -85,7 +82,6 @@ final class TabToAcceptSuggestion { userDefaultsObserver.onChange = { [weak self] in guard let self else { return } Task { @MainActor in - Logger.debug.info("TabToAcceptSuggestion: Settings changed") if self.canTapToAcceptSuggestion || self.canEscToDismissSuggestion { self.startObservation() } else { @@ -97,7 +93,6 @@ final class TabToAcceptSuggestion { @MainActor func startObservation() { - Logger.debug.info("TabToAcceptSuggestion: startObservation") guard !stoppedForExit else { return } guard canTapToAcceptSuggestion || canEscToDismissSuggestion else { return } hook.activateIfPossible() @@ -105,25 +100,18 @@ final class TabToAcceptSuggestion { @MainActor func stopObservation() { - Logger.debug.info("TabToAcceptSuggestion: stopObservation") hook.deactivate() } func handleEvent(_ event: CGEvent) -> CGEventManipulation.Result { - Logger.debug.info("TabToAcceptSuggestion: handleEvent") let keycode = Int(event.getIntegerValueField(.keyboardEventKeycode)) let tab = 48 let esc = 53 switch keycode { case tab: - Logger.debug.info( - "TabToAcceptSuggestion: Tab detected, flags: \(event.flags), permission: \(canTapToAcceptSuggestion)" - ) - guard let fileURL = ThreadSafeAccessToXcodeInspector.shared.activeDocumentURL else { - Logger.debug.info("TabToAcceptSuggestion: Active file not found") return .unchanged } @@ -175,28 +163,23 @@ final class TabToAcceptSuggestion { checkKeybinding(), canTapToAcceptSuggestion else { - Logger.debug.info("TabToAcceptSuggestion: Tab is invalid") return .unchanged } guard ThreadSafeAccessToXcodeInspector.shared.activeXcode != nil else { - Logger.debug.info("TabToAcceptSuggestion: Xcode not found") return .unchanged } guard let editor = ThreadSafeAccessToXcodeInspector.shared.focusedEditor else { - Logger.debug.info("TabToAcceptSuggestion: Editor not found") return .unchanged } guard let filespace = workspacePool.fetchFilespaceIfExisted(fileURL: fileURL) else { - Logger.debug.info("TabToAcceptSuggestion: Filespace not found: \(fileURL)") return .unchanged } guard let presentingSuggestion = filespace.presentingSuggestion else { - Logger.debug.info("TabToAcceptSuggestion: Suggestion not found") return .unchanged } @@ -210,11 +193,9 @@ final class TabToAcceptSuggestion { ) if shouldAcceptSuggestion { - Logger.debug.info("TabToAcceptSuggestion: Perform accept suggestion") acceptSuggestion() return .discarded } else { - Logger.debug.info("TabToAcceptSuggestion: Do not accept the suggestion") return .unchanged } case esc: @@ -251,7 +232,6 @@ extension TabToAcceptSuggestion { ) -> Bool { let line = cursorPosition.line guard line >= 0, line < lines.endIndex else { - Logger.debug.info("TabToAcceptSuggestion: Suggestion position invalid") return true } let col = cursorPosition.character @@ -273,7 +253,6 @@ extension TabToAcceptSuggestion { // If entering a tab doesn't invalidate the suggestion, just let the user type the tab. // else, accept the suggestion and discard the tab. guard !presentingSuggestionText.hasPrefix(contentAfterTab) else { - Logger.debug.info("TabToAcceptSuggestion: A tab doesn't invalidate the suggestion") return false } return true From 6a60726cf26174ae7cc1ba4e1de5d1d5b57c1c84 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 3 Jul 2024 17:06:45 +0800 Subject: [PATCH 43/44] Update DEVELOPMENT.md --- DEVELOPMENT.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index deb2c132..5b663fbe 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -29,12 +29,13 @@ Most of the logics are implemented inside the package `Core` and `Tool`. 1. Update the xcconfig files, bridgeLaunchAgent.plist, and Tool/Configs/Configurations.swift. 2. Build or archive the Copilot for Xcode target. -3. If Xcode complains that the pro package doesn't exist, please remove the package from the project. -## Testing Source Editor Extension +## Testing Source Editor Extension and Service Just run both the `ExtensionService`, `CommunicationBridge` and the `EditorExtension` Target. Read [Testing Your Source Editor Extension](https://developer.apple.com/documentation/xcodekit/testing_your_source_editor_extension) for more details. +If you are not testing the source editor extension, it's recommended to archive and install a debug version of the Copilot for Xcode and test with the bundled source editor extension. + ## SwiftUI Previews Looks like SwiftUI Previews are not very happy with Objective-C packages when running with app targets. To use previews, please switch schemes to the package product targets. From 72c41a11d5bfac76eac78e4b3c392a344078de0a Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 3 Jul 2024 17:18:41 +0800 Subject: [PATCH 44/44] Update README.md --- README.md | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/README.md b/README.md index c51ea964..a225b90f 100644 --- a/README.md +++ b/README.md @@ -165,15 +165,6 @@ 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". @@ -262,14 +253,6 @@ You can detach the chat panel by simply dragging it away. Once detached, the cha The chat panel allows for chat scope to temporarily control the context of the conversation for the latest message. To use a scope, simply prefix the message with `@scope`. -| Scope | Description | -| :--------: | ---------------------------------------------------------------------------------------- | -| `@file` | Includes the metadata of the editing document and line annotations in the system prompt. | -| `@code` | Includes the focused/selected code and everything from `@file` in the system prompt. | -| `@sense` | Experimental. Read the relevant information of the focused code | -| `@project` | Experimental. Access content of the project | -| `@web` | Allow the bot to search on Bing or query from a web page | - `@code` is on by default, if `Use @code scope by default in chat context.` is on. Otherwise, `@file` will be on by default. To use scopes, you can prefix a message with `@code`. @@ -294,7 +277,6 @@ If you need to end a plugin, you can just type | :--------------------: | ----------------------------------------------------------------------------------------------------------------------------------------- | | `/run` | Runs the command under the project root. | | | Environment variable:
- `PROJECT_ROOT` to get the project root.
- `FILE_PATH` to get the editing file path. | -| `/math` | Solves a math problem in natural language | | `/search` | Search on Bing and summarize the results. You have to setup the Bing Search API in the host app before using it. | | `/shortcut(name)` | Run a shortcut from the Shortcuts.app, and use the following message as the input. | | | If the message is empty, it will use the previous message as input. The output of the shortcut will be printed as a reply from the bot. | @@ -319,10 +301,6 @@ This feature is recommended when you need to update a specific piece of code. So The chat panel allows for chat scope to temporarily control the context of the conversation for the latest message. To use a scope, simply prefix the message with `@scope`. -| Scope | Description | -| :--------: | ---------------------------------------------------------------------------------------- | -| `@sense` | Experimental. Read the relevant information of the focused code | - To use scopes, you can prefix a message with `@sense`. You can use shorthand to represent a scope, such as `@sense`, and enable multiple scopes with `@c+web`.