From c407edd4ab99f31bf0880889d417e0ff4a01fbcd Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 4 Mar 2024 23:03:22 +0800 Subject: [PATCH 01/46] Add suggestion service configuration --- .../SuggestionService/SuggestionService.swift | 13 ++++++++-- Pro | 2 +- Tool/Package.swift | 2 ++ .../CodeiumSuggestionProvider.swift | 8 ++++++ .../GitHubCopilotSuggestionProvider.swift | 13 +++++++++- .../SuggestionProvider.swift | 26 ++++++++++++++++--- .../SuggestionServiceMiddleware.swift | 8 +++++- .../Workspace+SuggestionService.swift | 3 ++- 8 files changed, 65 insertions(+), 10 deletions(-) diff --git a/Core/Sources/SuggestionService/SuggestionService.swift b/Core/Sources/SuggestionService/SuggestionService.swift index fbfb4d1d..d20b3f1b 100644 --- a/Core/Sources/SuggestionService/SuggestionService.swift +++ b/Core/Sources/SuggestionService/SuggestionService.swift @@ -11,6 +11,10 @@ import ProExtension public protocol SuggestionServiceType: SuggestionServiceProvider {} public actor SuggestionService: SuggestionServiceType { + public var configuration: SuggestionServiceConfiguration { + get async { await suggestionProvider.configuration } + } + var middlewares: [SuggestionServiceMiddleware] { SuggestionServiceMiddlewareContainer.middlewares } @@ -50,7 +54,7 @@ public actor SuggestionService: SuggestionServiceType { return provider } #endif - + switch serviceType { case .builtIn(.codeium): return CodeiumSuggestionProvider( @@ -75,10 +79,15 @@ public extension SuggestionService { _ request: SuggestionRequest ) async throws -> [SuggestionModel.CodeSuggestion] { var getSuggestion = suggestionProvider.getSuggestions + let configuration = await configuration for middleware in middlewares.reversed() { getSuggestion = { [getSuggestion] request in - try await middleware.getSuggestion(request, next: getSuggestion) + try await middleware.getSuggestion( + request, + configuration: configuration, + next: getSuggestion + ) } } diff --git a/Pro b/Pro index a26917fd..7e316080 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit a26917fd375041c5ddec59f1d4458c0fdd04df01 +Subproject commit 7e316080c4e2780eab0c56364e34bd4876d66456 diff --git a/Tool/Package.swift b/Tool/Package.swift index bfd91c4b..0f5cb9e8 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -66,6 +66,7 @@ let package = Package( .package(url: "https://github.com/GottaGetSwifty/CodableWrappers", from: "2.0.7"), .package(url: "https://github.com/krzyzanowskim/STTextView", from: "0.8.21"), .package(url: "https://github.com/google/generative-ai-swift", from: "0.4.4"), + .package(url: "https://github.com/intitni/CopilotForXcodeKit", from: "0.4.0"), // TreeSitter .package(url: "https://github.com/intitni/SwiftTreeSitter.git", branch: "main"), @@ -279,6 +280,7 @@ let package = Package( "GitHubCopilotService", "CodeiumService", "UserDefaultsObserver", + .product(name: "CopilotForXcodeKit", package: "CopilotForXcodeKit"), ]), // MARK: - GitHub Copilot diff --git a/Tool/Sources/SuggestionProvider/CodeiumSuggestionProvider.swift b/Tool/Sources/SuggestionProvider/CodeiumSuggestionProvider.swift index e2f7d164..fac572b9 100644 --- a/Tool/Sources/SuggestionProvider/CodeiumSuggestionProvider.swift +++ b/Tool/Sources/SuggestionProvider/CodeiumSuggestionProvider.swift @@ -4,6 +4,14 @@ import Preferences import SuggestionModel public actor CodeiumSuggestionProvider: SuggestionServiceProvider { + public nonisolated var configuration: SuggestionServiceConfiguration { + .init( + acceptsRelevantCodeSnippets: true, + mixRelevantCodeSnippetsInSource: true, + acceptsRelevantSnippetsFromOpenedFiles: false + ) + } + let projectRootURL: URL let onServiceLaunched: (SuggestionServiceProvider) -> Void var codeiumService: CodeiumSuggestionServiceType? diff --git a/Tool/Sources/SuggestionProvider/GitHubCopilotSuggestionProvider.swift b/Tool/Sources/SuggestionProvider/GitHubCopilotSuggestionProvider.swift index 98f4ba46..ca330a2f 100644 --- a/Tool/Sources/SuggestionProvider/GitHubCopilotSuggestionProvider.swift +++ b/Tool/Sources/SuggestionProvider/GitHubCopilotSuggestionProvider.swift @@ -4,11 +4,22 @@ import Preferences import SuggestionModel public actor GitHubCopilotSuggestionProvider: SuggestionServiceProvider { + public nonisolated var configuration: SuggestionServiceConfiguration { + .init( + acceptsRelevantCodeSnippets: true, + mixRelevantCodeSnippetsInSource: true, + acceptsRelevantSnippetsFromOpenedFiles: false + ) + } + let projectRootURL: URL let onServiceLaunched: (SuggestionServiceProvider) -> Void var gitHubCopilotService: GitHubCopilotSuggestionServiceType? - public init(projectRootURL: URL, onServiceLaunched: @escaping (SuggestionServiceProvider) -> Void) { + public init( + projectRootURL: URL, + onServiceLaunched: @escaping (SuggestionServiceProvider) -> Void + ) { self.projectRootURL = projectRootURL self.onServiceLaunched = onServiceLaunched } diff --git a/Tool/Sources/SuggestionProvider/SuggestionProvider.swift b/Tool/Sources/SuggestionProvider/SuggestionProvider.swift index f1af1acd..e59c4a64 100644 --- a/Tool/Sources/SuggestionProvider/SuggestionProvider.swift +++ b/Tool/Sources/SuggestionProvider/SuggestionProvider.swift @@ -1,4 +1,5 @@ -import AppKit +import AppKit +import struct CopilotForXcodeKit.SuggestionServiceConfiguration import Foundation import Preferences import SuggestionModel @@ -13,7 +14,8 @@ public struct SuggestionRequest { public var tabSize: Int public var indentSize: Int public var usesTabsForIndentation: Bool - public var ignoreSpaceOnlySuggestions: Bool + public var ignoreSpaceOnlySuggestions: Bool + public var relevantCodeSnippets: [RelevantCodeSnippet] public init( fileURL: URL, @@ -24,7 +26,8 @@ public struct SuggestionRequest { tabSize: Int, indentSize: Int, usesTabsForIndentation: Bool, - ignoreSpaceOnlySuggestions: Bool + ignoreSpaceOnlySuggestions: Bool, + relevantCodeSnippets: [RelevantCodeSnippet] ) { self.fileURL = fileURL self.relativePath = relativePath @@ -35,12 +38,24 @@ public struct SuggestionRequest { self.indentSize = indentSize self.usesTabsForIndentation = usesTabsForIndentation self.ignoreSpaceOnlySuggestions = ignoreSpaceOnlySuggestions + self.relevantCodeSnippets = relevantCodeSnippets + } +} + +public struct RelevantCodeSnippet: Codable { + public var content: String + public var priority: Int + public var filePath: String + + public init(content: String, priority: Int, filePath: String) { + self.content = content + self.priority = priority + self.filePath = filePath } } public protocol SuggestionServiceProvider { func getSuggestions(_ request: SuggestionRequest) async throws -> [CodeSuggestion] - func notifyAccepted(_ suggestion: CodeSuggestion) async func notifyRejected(_ suggestions: [CodeSuggestion]) async func notifyOpenTextDocument(fileURL: URL, content: String) async throws @@ -49,5 +64,8 @@ public protocol SuggestionServiceProvider { func notifySaveTextDocument(fileURL: URL) async throws func cancelRequest() async func terminate() async + + var configuration: SuggestionServiceConfiguration { get async } } +public typealias SuggestionServiceConfiguration = CopilotForXcodeKit.SuggestionServiceConfiguration diff --git a/Tool/Sources/SuggestionProvider/SuggestionServiceMiddleware.swift b/Tool/Sources/SuggestionProvider/SuggestionServiceMiddleware.swift index f7ba1742..c447131f 100644 --- a/Tool/Sources/SuggestionProvider/SuggestionServiceMiddleware.swift +++ b/Tool/Sources/SuggestionProvider/SuggestionServiceMiddleware.swift @@ -5,7 +5,11 @@ import SuggestionModel public protocol SuggestionServiceMiddleware { typealias Next = (SuggestionRequest) async throws -> [CodeSuggestion] - func getSuggestion(_ request: SuggestionRequest, next: Next) async throws -> [CodeSuggestion] + func getSuggestion( + _ request: SuggestionRequest, + configuration: SuggestionServiceConfiguration, + next: Next + ) async throws -> [CodeSuggestion] } public enum SuggestionServiceMiddlewareContainer { @@ -29,6 +33,7 @@ public struct DisabledLanguageSuggestionServiceMiddleware: SuggestionServiceMidd public func getSuggestion( _ request: SuggestionRequest, + configuration: SuggestionServiceConfiguration, next: Next ) async throws -> [CodeSuggestion] { let language = languageIdentifierFromFileURL(request.fileURL) @@ -50,6 +55,7 @@ public struct DebugSuggestionServiceMiddleware: SuggestionServiceMiddleware { public func getSuggestion( _ request: SuggestionRequest, + configuration: SuggestionServiceConfiguration, next: Next ) async throws -> [CodeSuggestion] { Logger.service.info(""" diff --git a/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift b/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift index 3dd91dda..850eb405 100644 --- a/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift +++ b/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift @@ -64,7 +64,8 @@ public extension Workspace { tabSize: editor.tabSize, indentSize: editor.indentSize, usesTabsForIndentation: editor.usesTabsForIndentation, - ignoreSpaceOnlySuggestions: true + ignoreSpaceOnlySuggestions: true, + relevantCodeSnippets: [] ) ) From 998d413e4da7da6cab949223ecaf4f071cacb107 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 21 Mar 2024 22:33:53 +0800 Subject: [PATCH 02/46] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 7e316080..a48cc1d2 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 7e316080c4e2780eab0c56364e34bd4876d66456 +Subproject commit a48cc1d26ebdcdf41f00567629125de075f219cc From 7e358e579c058e94e288a42c41f428035c9fd216 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 21 Mar 2024 22:42:45 +0800 Subject: [PATCH 03/46] Update --- Tool/Sources/CustomAsyncAlgorithms/TimedDebounce.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tool/Sources/CustomAsyncAlgorithms/TimedDebounce.swift b/Tool/Sources/CustomAsyncAlgorithms/TimedDebounce.swift index 7936b494..df296cc8 100644 --- a/Tool/Sources/CustomAsyncAlgorithms/TimedDebounce.swift +++ b/Tool/Sources/CustomAsyncAlgorithms/TimedDebounce.swift @@ -43,6 +43,8 @@ private actor TimedDebounceFunction { public extension AsyncSequence { /// Debounce, but only if the value is received within a certain time frame. + /// + /// In the future when we drop macOS 12 support we should just use chunked from AsyncAlgorithms. func timedDebounce( for duration: TimeInterval ) -> AsyncThrowingStream { From 9db5e00e4781b7bf3b639397a31b7815a6f60817 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 27 Mar 2024 15:29:24 +0800 Subject: [PATCH 04/46] Udate --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index a48cc1d2..d4ffc33a 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit a48cc1d26ebdcdf41f00567629125de075f219cc +Subproject commit d4ffc33ad03da66271d099136961b707daa03d34 From d2cd0897225ee6b9cba68f439687f6c516dc548b Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 2 Apr 2024 22:47:22 +0800 Subject: [PATCH 05/46] Bump version of Swift async algorithms --- Copilot for Xcode.xcodeproj/project.pbxproj | 2 +- Core/Package.swift | 2 +- Core/Sources/Service/RealtimeSuggestionController.swift | 4 ++-- Tool/Package.swift | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Copilot for Xcode.xcodeproj/project.pbxproj b/Copilot for Xcode.xcodeproj/project.pbxproj index 13ab8477..996d7ae3 100644 --- a/Copilot for Xcode.xcodeproj/project.pbxproj +++ b/Copilot for Xcode.xcodeproj/project.pbxproj @@ -448,7 +448,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1420; + LastSwiftUpdateCheck = 1520; LastUpgradeCheck = 1410; TargetAttributes = { C814588B2939EFDC00135263 = { diff --git a/Core/Package.swift b/Core/Package.swift index 02e7d479..4038db74 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -89,7 +89,7 @@ let package = Package( ], dependencies: [ .package(path: "../Tool"), - .package(url: "https://github.com/apple/swift-async-algorithms", from: "0.1.0"), + .package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"), .package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.1.0"), .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0"), .package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.12.1"), diff --git a/Core/Sources/Service/RealtimeSuggestionController.swift b/Core/Sources/Service/RealtimeSuggestionController.swift index 4d07e62d..edbb4ec7 100644 --- a/Core/Sources/Service/RealtimeSuggestionController.swift +++ b/Core/Sources/Service/RealtimeSuggestionController.swift @@ -81,7 +81,7 @@ public actor RealtimeSuggestionController { } if #available(macOS 13.0, *) { - for await _ in valueChange.throttle(for: .milliseconds(200)) { + for await _ in valueChange._throttle(for: .milliseconds(200)) { if Task.isCancelled { return } await handler() } @@ -103,7 +103,7 @@ public actor RealtimeSuggestionController { } if #available(macOS 13.0, *) { - for await _ in selectedTextChanged.throttle(for: .milliseconds(200)) { + for await _ in selectedTextChanged._throttle(for: .milliseconds(200)) { if Task.isCancelled { return } await handler() } diff --git a/Tool/Package.swift b/Tool/Package.swift index 0f5cb9e8..88b11294 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -52,7 +52,7 @@ let package = Package( // TODO: Update LanguageClient some day. .package(url: "https://github.com/ChimeHQ/LanguageClient", exact: "0.3.1"), .package(url: "https://github.com/ChimeHQ/LanguageServerProtocol", exact: "0.8.0"), - .package(url: "https://github.com/apple/swift-async-algorithms", from: "0.1.0"), + .package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"), .package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.12.1"), .package(url: "https://github.com/ChimeHQ/JSONRPC", exact: "0.6.0"), .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.6.0"), From c374bebc690bcaa8e927072af79386bc2c260ea3 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 2 Apr 2024 23:11:02 +0800 Subject: [PATCH 06/46] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index d4ffc33a..5bd90b0b 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit d4ffc33ad03da66271d099136961b707daa03d34 +Subproject commit 5bd90b0b761ad84aabe5251ed02df2fc3665f2ad From c410167345bd793912d7c6b9c0bbd64abd314078 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 2 Apr 2024 23:11:53 +0800 Subject: [PATCH 07/46] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 5bd90b0b..21e4551b 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 5bd90b0b761ad84aabe5251ed02df2fc3665f2ad +Subproject commit 21e4551b9ea58e6751fbbf164628b10cf94a7417 From 5b88887be305253ae895bd20e34a6e2335f6c342 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 2 Apr 2024 23:13:03 +0800 Subject: [PATCH 08/46] Disable snippets from opened file when not needed --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 21e4551b..dd5f6ce7 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 21e4551b9ea58e6751fbbf164628b10cf94a7417 +Subproject commit dd5f6ce7d37c134173cd92b1d3251a064e58153e From d16d1c7ca18d659c890b10cff6731798307f1744 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 3 Apr 2024 18:07:59 +0800 Subject: [PATCH 09/46] Pass scenario to highlighter --- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 1 + .../CodeBlockSuggestionPanel.swift | 3 ++- .../SuggestionPanelContent/PromptToCodePanel.swift | 3 ++- Pro | 2 +- Tool/Package.swift | 2 +- Tool/Sources/SharedUIComponents/CodeBlock.swift | 7 +++++++ .../Experiment/NewCodeBlock.swift | 8 +++++++- .../SharedUIComponents/SyntaxHighlighting.swift | 13 +++++++++++-- 8 files changed, 32 insertions(+), 7 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index d66c0d25..7b8ee4e3 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -544,6 +544,7 @@ struct ChatCodeSyntaxHighlighter: CodeSyntaxHighlighter { let content = highlightedCodeBlock( code: content, language: language ?? "", + scenario: "chat", brightMode: brightMode, fontSize: fontSize ) diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift index 8ba5d57a..3acb0229 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift @@ -100,7 +100,8 @@ struct CodeBlockSuggestionPanel: View { CodeBlock( code: suggestion.code, language: suggestion.language, - startLineIndex: suggestion.startLineIndex, + startLineIndex: suggestion.startLineIndex, + scenario: "suggestion", colorScheme: colorScheme, fontSize: fontSize, droppingLeadingSpaces: hideCommonPrecedingSpaces diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift index 2414803e..de499d22 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift @@ -275,7 +275,8 @@ extension PromptToCodePanel { CodeBlock( code: viewStore.state.code, language: viewStore.state.language, - startLineIndex: viewStore.state.startLineIndex, + startLineIndex: viewStore.state.startLineIndex, + scenario: "promptToCode", colorScheme: colorScheme, firstLinePrecedingSpaceCount: viewStore.state .firstLinePrecedingSpaceCount, diff --git a/Pro b/Pro index dd5f6ce7..7c3e41fc 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit dd5f6ce7d37c134173cd92b1d3251a064e58153e +Subproject commit 7c3e41fce776f192d9b6cf449f967a0dcb38ad90 diff --git a/Tool/Package.swift b/Tool/Package.swift index 88b11294..72057552 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -57,7 +57,7 @@ let package = Package( .package(url: "https://github.com/ChimeHQ/JSONRPC", exact: "0.6.0"), .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.6.0"), .package(url: "https://github.com/unum-cloud/usearch", from: "0.19.1"), - .package(url: "https://github.com/intitni/Highlightr", branch: "bump-highlight-js-version"), + .package(url: "https://github.com/intitni/Highlightr", branch: "master"), .package( url: "https://github.com/pointfreeco/swift-composable-architecture", from: "0.55.0" diff --git a/Tool/Sources/SharedUIComponents/CodeBlock.swift b/Tool/Sources/SharedUIComponents/CodeBlock.swift index c66a816a..ba55fa34 100644 --- a/Tool/Sources/SharedUIComponents/CodeBlock.swift +++ b/Tool/Sources/SharedUIComponents/CodeBlock.swift @@ -4,6 +4,7 @@ public struct CodeBlock: View { public let code: String public let language: String public let startLineIndex: Int + public let scenario: String public let colorScheme: ColorScheme public let commonPrecedingSpaceCount: Int public let highlightedCode: [NSAttributedString] @@ -15,6 +16,7 @@ public struct CodeBlock: View { code: String, language: String, startLineIndex: Int, + scenario: String, colorScheme: ColorScheme, firstLinePrecedingSpaceCount: Int = 0, fontSize: Double, @@ -23,6 +25,7 @@ public struct CodeBlock: View { self.code = code self.language = language self.startLineIndex = startLineIndex + self.scenario = scenario self.colorScheme = colorScheme self.droppingLeadingSpaces = droppingLeadingSpaces self.firstLinePrecedingSpaceCount = firstLinePrecedingSpaceCount @@ -33,6 +36,7 @@ public struct CodeBlock: View { let result = Self.highlight( code: padding + code, language: language, + scenario: scenario, colorScheme: colorScheme, fontSize: fontSize, droppingLeadingSpaces: droppingLeadingSpaces @@ -75,6 +79,7 @@ public struct CodeBlock: View { static func highlight( code: String, language: String, + scenario: String, colorScheme: ColorScheme, fontSize: Double, droppingLeadingSpaces: Bool @@ -82,6 +87,7 @@ public struct CodeBlock: View { return highlighted( code: code, language: language, + scenario: scenario, brightMode: colorScheme != .dark, droppingLeadingSpaces: droppingLeadingSpaces, fontSize: fontSize @@ -100,6 +106,7 @@ struct CodeBlock_Previews: PreviewProvider { """, language: "swift", startLineIndex: 0, + scenario: "", colorScheme: .dark, firstLinePrecedingSpaceCount: 0, fontSize: 12, diff --git a/Tool/Sources/SharedUIComponents/Experiment/NewCodeBlock.swift b/Tool/Sources/SharedUIComponents/Experiment/NewCodeBlock.swift index 21c53d0b..b52f2d84 100644 --- a/Tool/Sources/SharedUIComponents/Experiment/NewCodeBlock.swift +++ b/Tool/Sources/SharedUIComponents/Experiment/NewCodeBlock.swift @@ -13,6 +13,7 @@ struct _CodeBlock: View { 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: @@ -23,6 +24,7 @@ struct _CodeBlock: View { code: String, language: String, firstLinePrecedingSpaceCount: Int, + scenario: String, colorScheme: ColorScheme, fontSize: Double, droppingLeadingSpaces: Bool, @@ -32,6 +34,7 @@ struct _CodeBlock: View { self.fontSize = fontSize self.colorScheme = colorScheme self.droppingLeadingSpaces = droppingLeadingSpaces + self.scenario = scenario let padding = firstLinePrecedingSpaceCount > 0 ? String(repeating: " ", count: firstLinePrecedingSpaceCount) @@ -39,6 +42,7 @@ struct _CodeBlock: View { let result = Self.highlight( code: padding + code, language: language, + scenario: scenario, colorScheme: colorScheme, fontSize: fontSize, droppingLeadingSpaces: droppingLeadingSpaces @@ -68,13 +72,15 @@ struct _CodeBlock: View { static func highlight( code: String, language: String, + scenario: String, colorScheme: ColorScheme, fontSize: Double, droppingLeadingSpaces: Bool ) -> (code: AttributedString, commonLeadingSpaceCount: Int) { let (lines, commonLeadingSpaceCount) = highlighted( code: code, - language: language, + language: language, + scenario: scenario, brightMode: colorScheme != .dark, droppingLeadingSpaces: droppingLeadingSpaces, fontSize: fontSize, diff --git a/Tool/Sources/SharedUIComponents/SyntaxHighlighting.swift b/Tool/Sources/SharedUIComponents/SyntaxHighlighting.swift index 844a243f..a0b1722a 100644 --- a/Tool/Sources/SharedUIComponents/SyntaxHighlighting.swift +++ b/Tool/Sources/SharedUIComponents/SyntaxHighlighting.swift @@ -7,6 +7,7 @@ import SwiftUI public func highlightedCodeBlock( code: String, language: String, + scenario: String, brightMode: Bool, fontSize: Double ) -> NSAttributedString { @@ -27,7 +28,13 @@ public func highlightedCodeBlock( guard let highlighter = Highlightr() else { return unhighlightedCode() } - highlighter.setTheme(to: brightMode ? "xcode" : "atom-one-dark") + highlighter.setTheme(to: { + let mode = brightMode ? "light" : "dark" + if scenario.isEmpty { + return mode + } + return "\(scenario)-\(mode)" + }()) highlighter.theme.setCodeFont(.monospacedSystemFont(ofSize: fontSize, weight: .regular)) guard let formatted = highlighter.highlight(code, as: language) else { return unhighlightedCode() @@ -41,6 +48,7 @@ public func highlightedCodeBlock( public func highlighted( code: String, language: String, + scenario: String, brightMode: Bool, droppingLeadingSpaces: Bool, fontSize: Double, @@ -48,7 +56,8 @@ public func highlighted( ) -> (code: [NSAttributedString], commonLeadingSpaceCount: Int) { let formatted = highlightedCodeBlock( code: code, - language: language, + language: language, + scenario: scenario, brightMode: brightMode, fontSize: fontSize ) From d5caace1a331368828cac1c5aacded3235ba72c5 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 4 Apr 2024 18:26:08 +0800 Subject: [PATCH 10/46] Support storing codable object in UserDefaults --- Tool/Sources/Preferences/UserDefaults.swift | 44 +++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/Tool/Sources/Preferences/UserDefaults.swift b/Tool/Sources/Preferences/UserDefaults.swift index 1cf3b0b4..19dcf13a 100644 --- a/Tool/Sources/Preferences/UserDefaults.swift +++ b/Tool/Sources/Preferences/UserDefaults.swift @@ -63,6 +63,32 @@ extension Array: RawRepresentable where Element: Codable { } } +public struct UserDefaultsStorageBox: RawRepresentable { + public let value: Element + + public init(_ value: Element) { + self.value = value + } + + public init?(rawValue: String) { + guard let data = rawValue.data(using: .utf8), + let result = try? JSONDecoder().decode(Element.self, from: data) + else { + return nil + } + value = result + } + + public var rawValue: String { + guard let data = try? JSONEncoder().encode(value), + let result = String(data: data, encoding: .utf8) + else { + return "" + } + return result + } +} + public extension UserDefaultsType { // MARK: Normal Types @@ -121,6 +147,16 @@ public extension UserDefaultsType { } return K.Value(rawValue: rawValue) ?? key.defaultValue } + + func value( + for keyPath: KeyPath + ) -> V where K.Value == UserDefaultsStorageBox { + let key = UserDefaultPreferenceKeys()[keyPath: keyPath] + guard let rawValue = value(forKey: key.key) as? String else { + return key.defaultValue.value + } + return (K.Value(rawValue: rawValue) ?? key.defaultValue).value + } func set( _ value: K.Value, @@ -137,6 +173,14 @@ public extension UserDefaultsType { let key = UserDefaultPreferenceKeys()[keyPath: keyPath] set(value.rawValue, forKey: key.key) } + + func set( + _ value: V, + for keyPath: KeyPath + ) where K.Value == UserDefaultsStorageBox { + let key = UserDefaultPreferenceKeys()[keyPath: keyPath] + set(UserDefaultsStorageBox(value).rawValue, forKey: key.key) + } func setupDefaultValue( for keyPath: KeyPath, From 6447b3ca55e70b443cff3f7c9d097e12171a5a94 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 4 Apr 2024 18:26:27 +0800 Subject: [PATCH 11/46] Add preference keys for theme --- Pro | 2 +- Tool/Sources/Preferences/Keys.swift | 32 +++++++++++++++ .../Preferences/Types/StorableColors.swift | 39 +++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 Tool/Sources/Preferences/Types/StorableColors.swift diff --git a/Pro b/Pro index 7c3e41fc..2507d505 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 7c3e41fce776f192d9b6cf449f967a0dcb38ad90 +Subproject commit 2507d5055f1ad05138d41165d050281f0a79d717 diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index a7588692..95cff0cf 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -463,6 +463,38 @@ public extension UserDefaultPreferenceKeys { } } +// MARK: - Theme + +public extension UserDefaultPreferenceKeys { + var syncSuggestionHighlightTheme: PreferenceKey { + .init(defaultValue: false, key: "SyncSuggestionHighlightTheme") + } + + var syncPromptToCodeHighlightTheme: PreferenceKey { + .init(defaultValue: false, key: "SyncPromptToCodeHighlightTheme") + } + + var syncChatCodeHighlightTheme: PreferenceKey { + .init(defaultValue: false, key: "SyncChatCodeHighlightTheme") + } + + var codeForegroundColorLight: PreferenceKey> { + .init(defaultValue: .init(nil), key: "CodeForegroundColorLight") + } + + var codeForegroundColorDark: PreferenceKey> { + .init(defaultValue: .init(nil), key: "CodeForegroundColorDark") + } + + var codeBackgroundColorLight: PreferenceKey> { + .init(defaultValue: .init(nil), key: "CodeBackgroundColorLight") + } + + var codeBackgroundColorDark: PreferenceKey> { + .init(defaultValue: .init(nil), key: "CodeBackgroundColorDark") + } +} + // MARK: - Bing Search public extension UserDefaultPreferenceKeys { diff --git a/Tool/Sources/Preferences/Types/StorableColors.swift b/Tool/Sources/Preferences/Types/StorableColors.swift new file mode 100644 index 00000000..2d7e4f83 --- /dev/null +++ b/Tool/Sources/Preferences/Types/StorableColors.swift @@ -0,0 +1,39 @@ +import Foundation + +public struct StorableColor: Codable { + public var red: Double + public var green: Double + public var blue: Double + public var alpha: Double + + public init(red: Double, green: Double, blue: Double, alpha: Double) { + self.red = red + self.green = green + self.blue = blue + self.alpha = alpha + } +} + +#if canImport(SwiftUI) +import SwiftUI +public extension StorableColor { + var swiftUIColor: SwiftUI.Color { + SwiftUI.Color(.sRGB, red: red, green: green, blue: blue, opacity: alpha) + } +} +#endif + +#if canImport(AppKit) +import AppKit +public extension StorableColor { + var nsColor: NSColor { + NSColor( + srgbRed: CGFloat(red), + green: CGFloat(green), + blue: CGFloat(blue), + alpha: CGFloat(alpha) + ) + } +} +#endif + From adaac4fb32b770b4c23051e7e61e0b2ac751f521 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 4 Apr 2024 18:26:46 +0800 Subject: [PATCH 12/46] Apply theme to UI components --- Core/Sources/ChatGPTChatTab/Styles.swift | 64 +++++++++++++++++-- .../FeatureSettings/ChatSettingsView.swift | 6 ++ .../PromptToCodeSettingsView.swift | 6 ++ .../SuggestionSettingsView.swift | 8 ++- .../CodeBlockSuggestionPanel.swift | 38 +++++++++-- .../PromptToCodePanel.swift | 40 +++++++++++- .../SharedUIComponents/CodeBlock.swift | 19 ++++-- 7 files changed, 163 insertions(+), 18 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/Styles.swift b/Core/Sources/ChatGPTChatTab/Styles.swift index 3f8d40c4..256e1baf 100644 --- a/Core/Sources/ChatGPTChatTab/Styles.swift +++ b/Core/Sources/ChatGPTChatTab/Styles.swift @@ -37,7 +37,26 @@ extension View { var messageBubbleCornerRadius: Double { 8 } func codeBlockLabelStyle() -> some View { - relativeLineSpacing(.em(0.225)) + CodeBlockLabelStyle(content: self) + } + + func codeBlockStyle(_ configuration: CodeBlockConfiguration) -> some View { + CodeBlockStyle(configuration: configuration, content: self) + } +} + +struct CodeBlockLabelStyle: View { + @AppStorage(\.syncChatCodeHighlightTheme) var syncCodeHighlightTheme + @AppStorage(\.codeForegroundColorLight) var codeForegroundColorLight + @AppStorage(\.codeBackgroundColorLight) var codeBackgroundColorLight + @AppStorage(\.codeForegroundColorDark) var codeForegroundColorDark + @AppStorage(\.codeBackgroundColorDark) var codeBackgroundColorDark + + let content: Content + + var body: some View { + content + .relativeLineSpacing(.em(0.225)) .markdownTextStyle { FontFamilyVariant(.monospaced) FontSize(.em(0.85)) @@ -45,14 +64,48 @@ extension View { .padding(16) .padding(.top, 14) } +} - func codeBlockStyle(_ configuration: CodeBlockConfiguration) -> some View { - background(Color(nsColor: .textBackgroundColor).opacity(0.7)) +struct CodeBlockStyle: View { + @AppStorage(\.syncChatCodeHighlightTheme) var syncCodeHighlightTheme + @AppStorage(\.codeForegroundColorLight) var codeForegroundColorLight + @AppStorage(\.codeBackgroundColorLight) var codeBackgroundColorLight + @AppStorage(\.codeForegroundColorDark) var codeForegroundColorDark + @AppStorage(\.codeBackgroundColorDark) var codeBackgroundColorDark + @Environment(\.colorScheme) var colorScheme + + let configuration: CodeBlockConfiguration + let content: Content + + var body: some View { + content + .background({ + if syncCodeHighlightTheme { + if colorScheme == .light, let color = codeBackgroundColorLight.value { + return color.swiftUIColor + } else if let color = codeBackgroundColorDark.value { + return color.swiftUIColor + } + } + + return Color(nsColor: .textBackgroundColor).opacity(0.7) + }() as Color) .clipShape(RoundedRectangle(cornerRadius: 6)) .overlay(alignment: .top) { HStack(alignment: .center) { Text(configuration.language ?? "code") - .foregroundStyle(.tertiary) + .foregroundStyle({ + if syncCodeHighlightTheme { + if colorScheme == .light, + let color = codeForegroundColorLight.value + { + return color.swiftUIColor.opacity(0.5) + } else if let color = codeForegroundColorDark.value { + return color.swiftUIColor.opacity(0.5) + } + } + return Color.secondary.opacity(0.7) + }() as Color) .font(.callout) .padding(.leading, 8) .lineLimit(1) @@ -63,6 +116,9 @@ extension View { } } } + .overlay { + RoundedRectangle(cornerRadius: 6).stroke(Color.primary.opacity(0.05), lineWidth: 1) + } .markdownMargin(top: 4, bottom: 16) } } diff --git a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift index aaeb7af4..d199a751 100644 --- a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift @@ -166,6 +166,12 @@ struct ChatSettingsView: View { Toggle(isOn: $settings.wrapCodeInCodeBlock) { Text("Wrap code in code block") } + + #if canImport(ProHostApp) + + CodeHighlightThemePicker(scenario: .chat) + + #endif } } diff --git a/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift index 6f33d599..e27ab549 100644 --- a/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift @@ -103,6 +103,12 @@ struct PromptToCodeSettingsView: View { Text("pt") } + + #if canImport(ProHostApp) + + CodeHighlightThemePicker(scenario: .promptToCode) + + #endif } ScopeForm() diff --git a/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift index 17761adb..2949e47d 100644 --- a/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift @@ -187,7 +187,7 @@ struct SuggestionSettingsView: View { Text("Accept Suggestion with Tab") } } - + Toggle(isOn: $settings.dismissSuggestionWithEsc) { Text("Dismiss Suggestion with ESC") } @@ -259,6 +259,12 @@ struct SuggestionSettingsView: View { Text("pt") } + + #if canImport(ProHostApp) + + CodeHighlightThemePicker(scenario: .suggestion) + + #endif } } } diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift index 3acb0229..203addc6 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift @@ -8,6 +8,11 @@ struct CodeBlockSuggestionPanel: View { @AppStorage(\.suggestionDisplayCompactMode) var suggestionDisplayCompactMode @AppStorage(\.suggestionPresentationMode) var suggestionPresentationMode @AppStorage(\.hideCommonPrecedingSpacesInSuggestion) var hideCommonPrecedingSpaces + @AppStorage(\.syncSuggestionHighlightTheme) var syncHighlightTheme + @AppStorage(\.codeForegroundColorLight) var codeForegroundColorLight + @AppStorage(\.codeForegroundColorDark) var codeForegroundColorDark + @AppStorage(\.codeBackgroundColorLight) var codeBackgroundColorLight + @AppStorage(\.codeBackgroundColorDark) var codeBackgroundColorDark struct ToolBar: View { @ObservedObject var suggestion: CodeSuggestionProvider @@ -38,7 +43,7 @@ struct CodeBlockSuggestionPanel: View { }) { Text("Dismiss").foregroundStyle(.tertiary).padding(.trailing, 4) }.buttonStyle(.plain) - + Button(action: { suggestion.rejectSuggestion() }) { @@ -100,15 +105,38 @@ struct CodeBlockSuggestionPanel: View { CodeBlock( code: suggestion.code, language: suggestion.language, - startLineIndex: suggestion.startLineIndex, + startLineIndex: suggestion.startLineIndex, scenario: "suggestion", colorScheme: colorScheme, - fontSize: fontSize, - droppingLeadingSpaces: hideCommonPrecedingSpaces + fontSize: fontSize, + droppingLeadingSpaces: hideCommonPrecedingSpaces, + proposedForegroundColor: { + if syncHighlightTheme { + if colorScheme == .light, + let color = codeForegroundColorLight.value?.swiftUIColor + { + return color + } else if let color = codeForegroundColorDark.value?.swiftUIColor { + return color + } + } + return nil + }() ) .frame(maxWidth: .infinity) + .background({ () -> Color in + if syncHighlightTheme { + if colorScheme == .light, + let color = codeBackgroundColorLight.value?.swiftUIColor + { + return color + } else if let color = codeBackgroundColorDark.value?.swiftUIColor { + return color + } + } + return Color.contentBackground + }()) } - .background(Color.contentBackground) if suggestionDisplayCompactMode { CompactToolBar(suggestion: suggestion) diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift index de499d22..e2da22a3 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift @@ -204,7 +204,12 @@ extension PromptToCodePanel { @Environment(\.colorScheme) var colorScheme @AppStorage(\.promptToCodeCodeFontSize) var fontSize @AppStorage(\.hideCommonPrecedingSpacesInPromptToCode) var hideCommonPrecedingSpaces - + @AppStorage(\.syncPromptToCodeHighlightTheme) var syncHighlightTheme + @AppStorage(\.codeForegroundColorLight) var codeForegroundColorLight + @AppStorage(\.codeForegroundColorDark) var codeForegroundColorDark + @AppStorage(\.codeBackgroundColorLight) var codeBackgroundColorLight + @AppStorage(\.codeBackgroundColorDark) var codeBackgroundColorDark + struct CodeContent: Equatable { var code: String var language: String @@ -212,6 +217,32 @@ extension PromptToCodePanel { var firstLinePrecedingSpaceCount: Int var isResponding: Bool } + + var codeForegroundColor: Color? { + if syncHighlightTheme { + if colorScheme == .light, + let color = codeForegroundColorLight.value?.swiftUIColor + { + return color + } else if let color = codeForegroundColorDark.value?.swiftUIColor { + return color + } + } + return nil + } + + var codeBackgroundColor: Color { + if syncHighlightTheme { + if colorScheme == .light, + let color = codeBackgroundColorLight.value?.swiftUIColor + { + return color + } else if let color = codeBackgroundColorDark.value?.swiftUIColor { + return color + } + } + return Color.clear + } var body: some View { CustomScrollView { @@ -243,6 +274,7 @@ extension PromptToCodePanel { .textSelection(.enabled) .markdownTheme(.gitHub.text { BackgroundColor(Color.clear) + ForegroundColor(codeForegroundColor) }) .padding() .frame(maxWidth: .infinity) @@ -266,7 +298,7 @@ extension PromptToCodePanel { ? "Thinking..." : "Enter your requirement to generate code." ) - .foregroundColor(.secondary) + .foregroundColor(codeForegroundColor?.opacity(0.7) ?? .secondary) .padding() .multilineTextAlignment(.center) .frame(maxWidth: .infinity) @@ -281,13 +313,15 @@ extension PromptToCodePanel { firstLinePrecedingSpaceCount: viewStore.state .firstLinePrecedingSpaceCount, fontSize: fontSize, - droppingLeadingSpaces: hideCommonPrecedingSpaces + droppingLeadingSpaces: hideCommonPrecedingSpaces, + proposedForegroundColor:codeForegroundColor ) .frame(maxWidth: .infinity) .scaleEffect(x: 1, y: -1, anchor: .center) } } } + .background(codeBackgroundColor) } .scaleEffect(x: 1, y: -1, anchor: .center) } diff --git a/Tool/Sources/SharedUIComponents/CodeBlock.swift b/Tool/Sources/SharedUIComponents/CodeBlock.swift index ba55fa34..28c14610 100644 --- a/Tool/Sources/SharedUIComponents/CodeBlock.swift +++ b/Tool/Sources/SharedUIComponents/CodeBlock.swift @@ -1,3 +1,4 @@ +import Preferences import SwiftUI public struct CodeBlock: View { @@ -11,6 +12,7 @@ public struct CodeBlock: View { public let firstLinePrecedingSpaceCount: Int public let fontSize: Double public let droppingLeadingSpaces: Bool + public let proposedForegroundColor: Color? public init( code: String, @@ -20,7 +22,8 @@ public struct CodeBlock: View { colorScheme: ColorScheme, firstLinePrecedingSpaceCount: Int = 0, fontSize: Double, - droppingLeadingSpaces: Bool + droppingLeadingSpaces: Bool, + proposedForegroundColor: Color? ) { self.code = code self.language = language @@ -30,6 +33,7 @@ public struct CodeBlock: View { self.droppingLeadingSpaces = droppingLeadingSpaces self.firstLinePrecedingSpaceCount = firstLinePrecedingSpaceCount self.fontSize = fontSize + self.proposedForegroundColor = proposedForegroundColor let padding = firstLinePrecedingSpaceCount > 0 ? String(repeating: " ", count: firstLinePrecedingSpaceCount) : "" @@ -44,6 +48,10 @@ public struct CodeBlock: View { commonPrecedingSpaceCount = result.commonLeadingSpaceCount highlightedCode = result.code } + + var foregroundColor: Color { + proposedForegroundColor ?? (colorScheme == .dark ? .white : .black) + } public var body: some View { VStack(spacing: 2) { @@ -51,10 +59,10 @@ public struct CodeBlock: View { HStack(alignment: .firstTextBaseline, spacing: 4) { Text("\(index + startLineIndex + 1)") .multilineTextAlignment(.trailing) - .foregroundColor(.secondary) + .foregroundColor(foregroundColor.opacity(0.5)) .frame(minWidth: 40) Text(AttributedString(highlightedCode[index])) - .foregroundColor(.white.opacity(0.1)) + .foregroundColor(foregroundColor.opacity(0.3)) .frame(maxWidth: .infinity, alignment: .leading) .multilineTextAlignment(.leading) .lineSpacing(4) @@ -63,7 +71,7 @@ public struct CodeBlock: View { Text("\(commonPrecedingSpaceCount + 1)") .padding(.top, -12) .font(.footnote) - .foregroundStyle(colorScheme == .dark ? .white : .black) + .foregroundStyle(foregroundColor) .opacity(0.3) } } @@ -110,7 +118,8 @@ struct CodeBlock_Previews: PreviewProvider { colorScheme: .dark, firstLinePrecedingSpaceCount: 0, fontSize: 12, - droppingLeadingSpaces: true + droppingLeadingSpaces: true, + proposedForegroundColor: nil ) } } From 5ca085c09cf1bfd60362e5e06ac8c187be989368 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 4 Apr 2024 21:56:36 +0800 Subject: [PATCH 13/46] Tweak the behavior of theme change in chat --- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 4 +- Core/Sources/ChatGPTChatTab/Styles.swift | 152 +++++++++++------- .../ChatGPTChatTab/Views/BotMessage.swift | 12 +- .../ChatGPTChatTab/Views/UserMessage.swift | 12 +- Pro | 2 +- 5 files changed, 96 insertions(+), 86 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index 7b8ee4e3..ca2e238c 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -534,10 +534,12 @@ struct ChatPanel_EmptyChat_Preview: PreviewProvider { struct ChatCodeSyntaxHighlighter: CodeSyntaxHighlighter { let brightMode: Bool let fontSize: Double + let colorChange: Color? - init(brightMode: Bool, fontSize: Double) { + init(brightMode: Bool, fontSize: Double, colorChange: Color?) { self.brightMode = brightMode self.fontSize = fontSize + self.colorChange = colorChange } func highlightCode(_ content: String, language: String?) -> Text { diff --git a/Core/Sources/ChatGPTChatTab/Styles.swift b/Core/Sources/ChatGPTChatTab/Styles.swift index 256e1baf..fe3aff5c 100644 --- a/Core/Sources/ChatGPTChatTab/Styles.swift +++ b/Core/Sources/ChatGPTChatTab/Styles.swift @@ -37,26 +37,7 @@ extension View { var messageBubbleCornerRadius: Double { 8 } func codeBlockLabelStyle() -> some View { - CodeBlockLabelStyle(content: self) - } - - func codeBlockStyle(_ configuration: CodeBlockConfiguration) -> some View { - CodeBlockStyle(configuration: configuration, content: self) - } -} - -struct CodeBlockLabelStyle: View { - @AppStorage(\.syncChatCodeHighlightTheme) var syncCodeHighlightTheme - @AppStorage(\.codeForegroundColorLight) var codeForegroundColorLight - @AppStorage(\.codeBackgroundColorLight) var codeBackgroundColorLight - @AppStorage(\.codeForegroundColorDark) var codeForegroundColorDark - @AppStorage(\.codeBackgroundColorDark) var codeBackgroundColorDark - - let content: Content - - var body: some View { - content - .relativeLineSpacing(.em(0.225)) + relativeLineSpacing(.em(0.225)) .markdownTextStyle { FontFamilyVariant(.monospaced) FontSize(.em(0.85)) @@ -64,48 +45,18 @@ struct CodeBlockLabelStyle: View { .padding(16) .padding(.top, 14) } -} - -struct CodeBlockStyle: View { - @AppStorage(\.syncChatCodeHighlightTheme) var syncCodeHighlightTheme - @AppStorage(\.codeForegroundColorLight) var codeForegroundColorLight - @AppStorage(\.codeBackgroundColorLight) var codeBackgroundColorLight - @AppStorage(\.codeForegroundColorDark) var codeForegroundColorDark - @AppStorage(\.codeBackgroundColorDark) var codeBackgroundColorDark - @Environment(\.colorScheme) var colorScheme - let configuration: CodeBlockConfiguration - let content: Content - - var body: some View { - content - .background({ - if syncCodeHighlightTheme { - if colorScheme == .light, let color = codeBackgroundColorLight.value { - return color.swiftUIColor - } else if let color = codeBackgroundColorDark.value { - return color.swiftUIColor - } - } - - return Color(nsColor: .textBackgroundColor).opacity(0.7) - }() as Color) + func codeBlockStyle( + _ configuration: CodeBlockConfiguration, + backgroundColor: Color, + labelColor: Color + ) -> some View { + background(backgroundColor) .clipShape(RoundedRectangle(cornerRadius: 6)) .overlay(alignment: .top) { HStack(alignment: .center) { Text(configuration.language ?? "code") - .foregroundStyle({ - if syncCodeHighlightTheme { - if colorScheme == .light, - let color = codeForegroundColorLight.value - { - return color.swiftUIColor.opacity(0.5) - } else if let color = codeForegroundColorDark.value { - return color.swiftUIColor.opacity(0.5) - } - } - return Color.secondary.opacity(0.7) - }() as Color) + .foregroundStyle(labelColor) .font(.callout) .padding(.leading, 8) .lineLimit(1) @@ -123,8 +74,69 @@ struct CodeBlockStyle: View { } } +struct ThemedMarkdownText: View { + @AppStorage(\.syncChatCodeHighlightTheme) var syncCodeHighlightTheme + @AppStorage(\.codeForegroundColorLight) var codeForegroundColorLight + @AppStorage(\.codeBackgroundColorLight) var codeBackgroundColorLight + @AppStorage(\.codeForegroundColorDark) var codeForegroundColorDark + @AppStorage(\.codeBackgroundColorDark) var codeBackgroundColorDark + @AppStorage(\.chatFontSize) var chatFontSize + @AppStorage(\.chatCodeFontSize) var chatCodeFontSize + @Environment(\.colorScheme) var colorScheme + + let text: String + + init(_ text: String) { + self.text = text + } + + var body: some View { + Markdown(text) + .textSelection(.enabled) + .markdownTheme(.custom( + fontSize: chatFontSize, + codeBlockBackgroundColor: { + if syncCodeHighlightTheme { + if colorScheme == .light, let color = codeBackgroundColorLight.value { + return color.swiftUIColor + } else if let color = codeBackgroundColorDark.value { + return color.swiftUIColor + } + } + + return Color(nsColor: .textBackgroundColor).opacity(0.7) + }(), + codeBlockLabelColor: { + if syncCodeHighlightTheme { + if colorScheme == .light, + let color = codeForegroundColorLight.value + { + return color.swiftUIColor.opacity(0.5) + } else if let color = codeForegroundColorDark.value { + return color.swiftUIColor.opacity(0.5) + } + } + return Color.secondary.opacity(0.7) + }() + )) + .markdownCodeSyntaxHighlighter( + ChatCodeSyntaxHighlighter( + brightMode: colorScheme != .dark, + fontSize: chatCodeFontSize, + colorChange: colorScheme == .dark + ? codeForegroundColorDark.value?.swiftUIColor + : codeForegroundColorLight.value?.swiftUIColor + ) + ) + } +} + extension MarkdownUI.Theme { - static func custom(fontSize: Double) -> MarkdownUI.Theme { + static func custom( + fontSize: Double, + codeBlockBackgroundColor: Color, + codeBlockLabelColor: Color + ) -> MarkdownUI.Theme { .gitHub.text { ForegroundColor(.primary) BackgroundColor(Color.clear) @@ -136,14 +148,22 @@ extension MarkdownUI.Theme { if wrapCode { configuration.label .codeBlockLabelStyle() - .codeBlockStyle(configuration) + .codeBlockStyle( + configuration, + backgroundColor: codeBlockBackgroundColor, + labelColor: codeBlockLabelColor + ) } else { ScrollView(.horizontal) { configuration.label .codeBlockLabelStyle() } .workaroundForVerticalScrollingBugInMacOS() - .codeBlockStyle(configuration) + .codeBlockStyle( + configuration, + backgroundColor: codeBlockBackgroundColor, + labelColor: codeBlockLabelColor + ) } } } @@ -165,14 +185,22 @@ extension MarkdownUI.Theme { if wrapCode { configuration.label .codeBlockLabelStyle() - .codeBlockStyle(configuration) + .codeBlockStyle( + configuration, + backgroundColor: Color(nsColor: .textBackgroundColor).opacity(0.7), + labelColor: Color.secondary.opacity(0.7) + ) } else { ScrollView(.horizontal) { configuration.label .codeBlockLabelStyle() } .workaroundForVerticalScrollingBugInMacOS() - .codeBlockStyle(configuration) + .codeBlockStyle( + configuration, + backgroundColor: Color(nsColor: .textBackgroundColor).opacity(0.7), + labelColor: Color.secondary.opacity(0.7) + ) } } .table { configuration in diff --git a/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift b/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift index 67457bf1..5d202678 100644 --- a/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift +++ b/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift @@ -11,8 +11,6 @@ struct BotMessage: View { let references: [DisplayedChatMessage.Reference] let chat: StoreOf @Environment(\.colorScheme) var colorScheme - @AppStorage(\.chatFontSize) var chatFontSize - @AppStorage(\.chatCodeFontSize) var chatCodeFontSize @State var isReferencesPresented = false @State var isReferencesHovered = false @@ -45,15 +43,7 @@ struct BotMessage: View { } } - Markdown(text) - .textSelection(.enabled) - .markdownTheme(.custom(fontSize: chatFontSize)) - .markdownCodeSyntaxHighlighter( - ChatCodeSyntaxHighlighter( - brightMode: colorScheme != .dark, - fontSize: chatCodeFontSize - ) - ) + ThemedMarkdownText(text) } .frame(alignment: .trailing) .padding() diff --git a/Core/Sources/ChatGPTChatTab/Views/UserMessage.swift b/Core/Sources/ChatGPTChatTab/Views/UserMessage.swift index c470cc4e..f27e3ed4 100644 --- a/Core/Sources/ChatGPTChatTab/Views/UserMessage.swift +++ b/Core/Sources/ChatGPTChatTab/Views/UserMessage.swift @@ -9,19 +9,9 @@ struct UserMessage: View { let text: String let chat: StoreOf @Environment(\.colorScheme) var colorScheme - @AppStorage(\.chatFontSize) var chatFontSize - @AppStorage(\.chatCodeFontSize) var chatCodeFontSize var body: some View { - Markdown(text) - .textSelection(.enabled) - .markdownTheme(.custom(fontSize: chatFontSize)) - .markdownCodeSyntaxHighlighter( - ChatCodeSyntaxHighlighter( - brightMode: colorScheme != .dark, - fontSize: chatCodeFontSize - ) - ) + ThemedMarkdownText(text) .frame(alignment: .leading) .padding() .background { diff --git a/Pro b/Pro index 2507d505..8f8facdc 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 2507d5055f1ad05138d41165d050281f0a79d717 +Subproject commit 8f8facdc98eacc37fa76defb9be40525e5f6b857 From 7118062a7d70e000742e5e651f67bd18e3ee6f3b Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 4 Apr 2024 22:02:35 +0800 Subject: [PATCH 14/46] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 8f8facdc..59410b2c 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 8f8facdc98eacc37fa76defb9be40525e5f6b857 +Subproject commit 59410b2ca842be13d3d82b0a3f2a3613acaa5ef8 From a77ee2011799a985cf5ae199612b1d7edb58a287 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 4 Apr 2024 22:20:47 +0800 Subject: [PATCH 15/46] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 59410b2c..abe60e8e 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 59410b2ca842be13d3d82b0a3f2a3613acaa5ef8 +Subproject commit abe60e8e9ebf92294144bf00c7c705bcf3f9a40c From 2b70557c42c7a4d276e6f7d698fc1f33a48bc46a Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 5 Apr 2024 14:51:37 +0800 Subject: [PATCH 16/46] Display the error description of a language server error in the host app --- .../GitHubCopilotService.swift | 78 +++++++++++++++++-- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift index e18eb58a..bb3056f2 100644 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift @@ -43,11 +43,43 @@ protocol GitHubCopilotLSP { enum GitHubCopilotError: Error, LocalizedError { case languageServerNotInstalled + case languageServerError(ServerError) var errorDescription: String? { switch self { case .languageServerNotInstalled: return "Language server is not installed." + case let .languageServerError(error): + switch error { + case let .handlerUnavailable(handler): + return "Language server error: Handler \(handler) unavailable" + case let .unhandledMethod(method): + return "Language server error: Unhandled method \(method)" + case let .notificationDispatchFailed(error): + return "Language server error: Notification dispatch failed: \(error)" + case let .requestDispatchFailed(error): + return "Language server error: Request dispatch failed: \(error)" + case let .clientDataUnavailable(error): + return "Language server error: Client data unavailable: \(error)" + case .serverUnavailable: + return "Language server error: Server unavailable, please make sure that:\n1. The path is node is correctly set.\n2. The node is not a shim executable.\n3. the node version is high enough." + case .missingExpectedParameter: + return "Language server error: Missing expected parameter" + case .missingExpectedResult: + return "Language server error: Missing expected result" + case let .unableToDecodeRequest(error): + return "Language server error: Unable to decode request: \(error)" + case let .unableToSendRequest(error): + return "Language server error: Unable to send request: \(error)" + case let .unableToSendNotification(error): + return "Language server error: Unable to send notification: \(error)" + case let .serverError(code: code, message: message, data: data): + return "Language server error: Server error: \(code) \(message) \(String(describing: data))" + case .invalidRequest: + return "Language server error: Invalid request" + case .timeout: + return "Language server error: Timeout, please try again later" + } } } } @@ -228,28 +260,58 @@ public final class GitHubCopilotAuthService: GitHubCopilotBaseService, } public func checkStatus() async throws -> GitHubCopilotAccountStatus { - try await server.sendRequest(GitHubCopilotRequest.CheckStatus()).status + do { + return try await server.sendRequest(GitHubCopilotRequest.CheckStatus()).status + } catch let error as ServerError { + throw GitHubCopilotError.languageServerError(error) + } catch { + throw error + } } public func signInInitiate() async throws -> (verificationUri: String, userCode: String) { - let result = try await server.sendRequest(GitHubCopilotRequest.SignInInitiate()) - return (result.verificationUri, result.userCode) + do { + let result = try await server.sendRequest(GitHubCopilotRequest.SignInInitiate()) + return (result.verificationUri, result.userCode) + } catch let error as ServerError { + throw GitHubCopilotError.languageServerError(error) + } catch { + throw error + } } public func signInConfirm(userCode: String) async throws -> (username: String, status: GitHubCopilotAccountStatus) { - let result = try await server - .sendRequest(GitHubCopilotRequest.SignInConfirm(userCode: userCode)) - return (result.user, result.status) + do { + let result = try await server + .sendRequest(GitHubCopilotRequest.SignInConfirm(userCode: userCode)) + return (result.user, result.status) + } catch let error as ServerError { + throw GitHubCopilotError.languageServerError(error) + } catch { + throw error + } } public func signOut() async throws -> GitHubCopilotAccountStatus { - try await server.sendRequest(GitHubCopilotRequest.SignOut()).status + do { + return try await server.sendRequest(GitHubCopilotRequest.SignOut()).status + } catch let error as ServerError { + throw GitHubCopilotError.languageServerError(error) + } catch { + throw error + } } public func version() async throws -> String { - try await server.sendRequest(GitHubCopilotRequest.GetVersion()).version + do { + return try await server.sendRequest(GitHubCopilotRequest.GetVersion()).version + } catch let error as ServerError { + throw GitHubCopilotError.languageServerError(error) + } catch { + throw error + } } } From e9b9cd216198c0994f9455658dd1991ed1b7a844 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 5 Apr 2024 14:54:57 +0800 Subject: [PATCH 17/46] Fix typo --- Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift index bb3056f2..7af99ff1 100644 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift @@ -62,7 +62,7 @@ enum GitHubCopilotError: Error, LocalizedError { case let .clientDataUnavailable(error): return "Language server error: Client data unavailable: \(error)" case .serverUnavailable: - return "Language server error: Server unavailable, please make sure that:\n1. The path is node is correctly set.\n2. The node is not a shim executable.\n3. the node version is high enough." + return "Language server error: Server unavailable, please make sure that:\n1. The path to node is correctly set.\n2. The node is not a shim executable.\n3. the node version is high enough." case .missingExpectedParameter: return "Language server error: Missing expected parameter" case .missingExpectedResult: From 71ce6d5f756ce10cce3a228ad49c680cbdd074d5 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 5 Apr 2024 16:20:58 +0800 Subject: [PATCH 18/46] Lower minimum debounce --- Core/Sources/Service/RealtimeSuggestionController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/Sources/Service/RealtimeSuggestionController.swift b/Core/Sources/Service/RealtimeSuggestionController.swift index edbb4ec7..e2cdf192 100644 --- a/Core/Sources/Service/RealtimeSuggestionController.swift +++ b/Core/Sources/Service/RealtimeSuggestionController.swift @@ -145,7 +145,7 @@ public actor RealtimeSuggestionController { func triggerPrefetchDebounced(force: Bool = false) { inflightPrefetchTask = Task(priority: .utility) { @WorkspaceActor in try? await Task.sleep(nanoseconds: UInt64( - max(UserDefaults.shared.value(for: \.realtimeSuggestionDebounce), 0.25) + max(UserDefaults.shared.value(for: \.realtimeSuggestionDebounce), 0.15) * 1_000_000_000 )) From 8e11ca2294e835597c08b60be328acb83a3df944 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 7 Apr 2024 08:51:25 +0800 Subject: [PATCH 19/46] Add preference keys for fonts --- Tool/Sources/Preferences/Keys.swift | 12 ++++++ .../Preferences/Types/StorableFont.swift | 40 +++++++++++++++++++ Tool/Sources/Preferences/UserDefaults.swift | 28 +++++++++++-- 3 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 Tool/Sources/Preferences/Types/StorableFont.swift diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 838801cc..201d271a 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -493,6 +493,18 @@ public extension UserDefaultPreferenceKeys { var codeBackgroundColorDark: PreferenceKey> { .init(defaultValue: .init(nil), key: "CodeBackgroundColorDark") } + + var suggestionFont: PreferenceKey> { + .init(defaultValue: .init(nil), key: "SuggestionFont") + } + + var promptToCodeFont: PreferenceKey> { + .init(defaultValue: .init(nil), key: "promptToCodeFont") + } + + var chatCodeFont: PreferenceKey> { + .init(defaultValue: .init(nil), key: "chatCodeFont") + } } // MARK: - Bing Search diff --git a/Tool/Sources/Preferences/Types/StorableFont.swift b/Tool/Sources/Preferences/Types/StorableFont.swift new file mode 100644 index 00000000..f6d72dd3 --- /dev/null +++ b/Tool/Sources/Preferences/Types/StorableFont.swift @@ -0,0 +1,40 @@ +import AppKit +import Foundation + +public struct StorableFont: Codable { + public var nsFont: NSFont + + public init(nsFont: NSFont) { + self.nsFont = nsFont + } + + public enum CodingKeys: String, CodingKey { + case nsFont + } + + public init(from decoder: Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let fontData = try container.decode(Data.self, forKey: .nsFont) + guard let nsFont = try NSKeyedUnarchiver.unarchivedObject( + ofClass: NSFont.self, + from: fontData + ) else { + throw DecodingError.dataCorruptedError( + forKey: .nsFont, + in: container, + debugDescription: "Failed to decode NSFont" + ) + } + self.nsFont = nsFont + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let fontData = try NSKeyedArchiver.archivedData( + withRootObject: nsFont, + requiringSecureCoding: false + ) + try container.encode(fontData, forKey: .nsFont) + } +} + diff --git a/Tool/Sources/Preferences/UserDefaults.swift b/Tool/Sources/Preferences/UserDefaults.swift index 19dcf13a..508d0cd1 100644 --- a/Tool/Sources/Preferences/UserDefaults.swift +++ b/Tool/Sources/Preferences/UserDefaults.swift @@ -1,4 +1,5 @@ import AIModel +import AppKit import Configs import Foundation @@ -29,6 +30,27 @@ public extension UserDefaults { for: \.promptToCodeCodeFontSize, defaultValue: shared.value(for: \.suggestionCodeFontSize) ) + shared.setupDefaultValue( + for: \.suggestionFont, + defaultValue: .init(.init(nsFont: .monospacedSystemFont( + ofSize: shared.value(for: \.suggestionCodeFontSize), + weight: .regular + ))) + ) + shared.setupDefaultValue( + for: \.promptToCodeFont, + defaultValue: .init(.init(nsFont: .monospacedSystemFont( + ofSize: shared.value(for: \.promptToCodeCodeFontSize), + weight: .regular + ))) + ) + shared.setupDefaultValue( + for: \.chatCodeFont, + defaultValue: .init(.init(nsFont: .monospacedSystemFont( + ofSize: shared.value(for: \.chatCodeFontSize), + weight: .regular + ))) + ) } } @@ -69,7 +91,7 @@ public struct UserDefaultsStorageBox: RawRepresentable { public init(_ value: Element) { self.value = value } - + public init?(rawValue: String) { guard let data = rawValue.data(using: .utf8), let result = try? JSONDecoder().decode(Element.self, from: data) @@ -147,7 +169,7 @@ public extension UserDefaultsType { } return K.Value(rawValue: rawValue) ?? key.defaultValue } - + func value( for keyPath: KeyPath ) -> V where K.Value == UserDefaultsStorageBox { @@ -173,7 +195,7 @@ public extension UserDefaultsType { let key = UserDefaultPreferenceKeys()[keyPath: keyPath] set(value.rawValue, forKey: key.key) } - + func set( _ value: V, for keyPath: KeyPath From e9525daa6f13d3899492af9c264ebcbb6fab008d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 7 Apr 2024 08:51:30 +0800 Subject: [PATCH 20/46] Add font picker --- .../SharedUIComponents/FontPicker.swift | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 Tool/Sources/SharedUIComponents/FontPicker.swift diff --git a/Tool/Sources/SharedUIComponents/FontPicker.swift b/Tool/Sources/SharedUIComponents/FontPicker.swift new file mode 100644 index 00000000..4cda4e3a --- /dev/null +++ b/Tool/Sources/SharedUIComponents/FontPicker.swift @@ -0,0 +1,69 @@ +import AppKit +import Foundation +import Preferences +import SwiftUI + +public struct FontPicker: View { + @Binding var font: NSFont + @State var fontManagerDelegate: FontManagerDelegate? + + public init(font: Binding) { + _font = font + } + + public var body: some View { + Button { + if NSFontPanel.shared.isVisible { + NSFontPanel.shared.orderOut(nil) + } + + self.fontManagerDelegate = FontManagerDelegate(font: font) { + self.font = $0 + } + NSFontManager.shared.target = self.fontManagerDelegate + NSFontPanel.shared.setPanelFont(self.font, isMultiple: false) + NSFontPanel.shared.orderBack(nil) + } label: { + HStack { + Text(font.fontName) + + Text(" - ") + + Text(font.pointSize, format: .number.precision(.fractionLength(1))) + + Text("pt") + + Spacer().frame(width: 30) + + Image(systemName: "textformat") + .frame(width: 13) + .scaledToFit() + } + } + } + + final class FontManagerDelegate: NSObject { + let font: NSFont + let onSelection: (NSFont) -> Void + init(font: NSFont, onSelection: @escaping (NSFont) -> Void) { + self.font = font + self.onSelection = onSelection + } + + @objc func changeFont(_ sender: NSFontManager) { + onSelection(sender.convert(font)) + } + } +} + +public extension FontPicker { + init(font: Binding) { + _font = Binding( + get: { font.wrappedValue.nsFont }, + set: { font.wrappedValue = StorableFont(nsFont: $0) } + ) + } +} + +#Preview { + FontPicker(font: .constant(.systemFont(ofSize: 15))) + .padding() +} + From b8433c19b24709b1a2229b467ba78c828a04b504 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 7 Apr 2024 11:11:29 +0800 Subject: [PATCH 21/46] Support changing code font of suggestion, chat and prompt to code --- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 8 +- Core/Sources/ChatGPTChatTab/Styles.swift | 4 +- .../FeatureSettings/ChatSettingsView.swift | 15 +--- .../PromptToCodeSettingsView.swift | 21 ++---- .../SuggestionSettingsView.swift | 23 ++---- .../CodeBlockSuggestionPanel.swift | 4 +- .../PromptToCodePanel.swift | 4 +- Pro | 2 +- Tool/Sources/Preferences/Keys.swift | 73 +++++++++++-------- Tool/Sources/Preferences/UserDefaults.swift | 4 +- .../SharedUIComponents/CodeBlock.swift | 16 ++-- .../Experiment/NewCodeBlock.swift | 21 +++--- .../SharedUIComponents/FontPicker.swift | 48 ++++++++---- .../SyntaxHighlighting.swift | 10 +-- 14 files changed, 127 insertions(+), 126 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index ca2e238c..db080dac 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -533,12 +533,12 @@ struct ChatPanel_EmptyChat_Preview: PreviewProvider { struct ChatCodeSyntaxHighlighter: CodeSyntaxHighlighter { let brightMode: Bool - let fontSize: Double + let font: NSFont let colorChange: Color? - init(brightMode: Bool, fontSize: Double, colorChange: Color?) { + init(brightMode: Bool, font: NSFont, colorChange: Color?) { self.brightMode = brightMode - self.fontSize = fontSize + self.font = font self.colorChange = colorChange } @@ -548,7 +548,7 @@ struct ChatCodeSyntaxHighlighter: CodeSyntaxHighlighter { language: language ?? "", scenario: "chat", brightMode: brightMode, - fontSize: fontSize + font: font ) return Text(AttributedString(content)) } diff --git a/Core/Sources/ChatGPTChatTab/Styles.swift b/Core/Sources/ChatGPTChatTab/Styles.swift index fe3aff5c..e654b72e 100644 --- a/Core/Sources/ChatGPTChatTab/Styles.swift +++ b/Core/Sources/ChatGPTChatTab/Styles.swift @@ -81,7 +81,7 @@ struct ThemedMarkdownText: View { @AppStorage(\.codeForegroundColorDark) var codeForegroundColorDark @AppStorage(\.codeBackgroundColorDark) var codeBackgroundColorDark @AppStorage(\.chatFontSize) var chatFontSize - @AppStorage(\.chatCodeFontSize) var chatCodeFontSize + @AppStorage(\.chatCodeFont) var chatCodeFont @Environment(\.colorScheme) var colorScheme let text: String @@ -122,7 +122,7 @@ struct ThemedMarkdownText: View { .markdownCodeSyntaxHighlighter( ChatCodeSyntaxHighlighter( brightMode: colorScheme != .dark, - fontSize: chatCodeFontSize, + font: chatCodeFont.value.nsFont, colorChange: colorScheme == .dark ? codeForegroundColorDark.value?.swiftUIColor : codeForegroundColorLight.value?.swiftUIColor diff --git a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift index d199a751..7683ebf2 100644 --- a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift @@ -13,7 +13,7 @@ struct ChatSettingsView: View { @AppStorage(\.chatGPTTemperature) var chatGPTTemperature @AppStorage(\.chatGPTMaxMessageCount) var chatGPTMaxMessageCount @AppStorage(\.chatFontSize) var chatFontSize - @AppStorage(\.chatCodeFontSize) var chatCodeFontSize + @AppStorage(\.chatCodeFont) var chatCodeFont @AppStorage(\.defaultChatFeatureChatModelId) var defaultChatFeatureChatModelId @AppStorage(\.defaultChatSystemPrompt) var defaultChatSystemPrompt @@ -150,17 +150,8 @@ struct ChatSettingsView: View { Text("pt") } - HStack { - TextField(text: .init(get: { - "\(Int(settings.chatCodeFontSize))" - }, set: { - settings.chatCodeFontSize = Double(Int($0) ?? 0) - })) { - Text("Font size of code block") - } - .textFieldStyle(.roundedBorder) - - Text("pt") + FontPicker(font: $settings.chatCodeFont) { + Text("Font of code") } Toggle(isOn: $settings.wrapCodeInCodeBlock) { diff --git a/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift index e27ab549..630a9bba 100644 --- a/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift @@ -10,8 +10,8 @@ struct PromptToCodeSettingsView: View { final class Settings: ObservableObject { @AppStorage(\.hideCommonPrecedingSpacesInPromptToCode) var hideCommonPrecedingSpaces - @AppStorage(\.promptToCodeCodeFontSize) - var fontSize + @AppStorage(\.promptToCodeCodeFont) + var font @AppStorage(\.promptToCodeGenerateDescription) var promptToCodeGenerateDescription @AppStorage(\.promptToCodeGenerateDescriptionInUserPreferredLanguage) @@ -91,24 +91,15 @@ struct PromptToCodeSettingsView: View { Text("Hide Common Preceding Spaces") } - HStack { - TextField(text: .init(get: { - "\(Int(settings.fontSize))" - }, set: { - settings.fontSize = Double(Int($0) ?? 0) - })) { - Text("Font size of suggestion code") - } - .textFieldStyle(.roundedBorder) - - Text("pt") - } - #if canImport(ProHostApp) CodeHighlightThemePicker(scenario: .promptToCode) #endif + + FontPicker(font: $settings.font) { + Text("Font") + } } ScopeForm() diff --git a/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift index 2949e47d..02c31e54 100644 --- a/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift @@ -46,8 +46,8 @@ struct SuggestionSettingsView: View { var suggestionFeatureEnabledProjectList @AppStorage(\.hideCommonPrecedingSpacesInSuggestion) var hideCommonPrecedingSpacesInSuggestion - @AppStorage(\.suggestionCodeFontSize) - var suggestionCodeFontSize + @AppStorage(\.suggestionCodeFont) + var font @AppStorage(\.suggestionFeatureProvider) var suggestionFeatureProvider @AppStorage(\.suggestionDisplayCompactMode) @@ -247,24 +247,15 @@ struct SuggestionSettingsView: View { Text("Hide Common Preceding Spaces") } - HStack { - TextField(text: .init(get: { - "\(Int(settings.suggestionCodeFontSize))" - }, set: { - settings.suggestionCodeFontSize = Double(Int($0) ?? 0) - })) { - Text("Font size of suggestion code") - } - .textFieldStyle(.roundedBorder) - - Text("pt") - } - #if canImport(ProHostApp) CodeHighlightThemePicker(scenario: .suggestion) - + #endif + + FontPicker(font: $settings.font) { + Text("Font") + } } } } diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift index 203addc6..1b7e715f 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift @@ -4,7 +4,7 @@ import SwiftUI struct CodeBlockSuggestionPanel: View { @ObservedObject var suggestion: CodeSuggestionProvider @Environment(\.colorScheme) var colorScheme - @AppStorage(\.suggestionCodeFontSize) var fontSize + @AppStorage(\.suggestionCodeFont) var codeFont @AppStorage(\.suggestionDisplayCompactMode) var suggestionDisplayCompactMode @AppStorage(\.suggestionPresentationMode) var suggestionPresentationMode @AppStorage(\.hideCommonPrecedingSpacesInSuggestion) var hideCommonPrecedingSpaces @@ -108,7 +108,7 @@ struct CodeBlockSuggestionPanel: View { startLineIndex: suggestion.startLineIndex, scenario: "suggestion", colorScheme: colorScheme, - fontSize: fontSize, + font: codeFont.value.nsFont, droppingLeadingSpaces: hideCommonPrecedingSpaces, proposedForegroundColor: { if syncHighlightTheme { diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift index e2da22a3..65da23cf 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift @@ -202,7 +202,7 @@ extension PromptToCodePanel { struct Content: View { let store: StoreOf @Environment(\.colorScheme) var colorScheme - @AppStorage(\.promptToCodeCodeFontSize) var fontSize + @AppStorage(\.promptToCodeCodeFont) var codeFont @AppStorage(\.hideCommonPrecedingSpacesInPromptToCode) var hideCommonPrecedingSpaces @AppStorage(\.syncPromptToCodeHighlightTheme) var syncHighlightTheme @AppStorage(\.codeForegroundColorLight) var codeForegroundColorLight @@ -312,7 +312,7 @@ extension PromptToCodePanel { colorScheme: colorScheme, firstLinePrecedingSpaceCount: viewStore.state .firstLinePrecedingSpaceCount, - fontSize: fontSize, + font: codeFont.value.nsFont, droppingLeadingSpaces: hideCommonPrecedingSpaces, proposedForegroundColor:codeForegroundColor ) diff --git a/Pro b/Pro index 43e54161..5207bbf1 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 43e54161edb5893f0e89f2d9aaa4a8588f5ffe72 +Subproject commit 5207bbf185ec392d4d6f2bbf9e5d1cb414c2aebd diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 201d271a..7c39a20b 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -93,9 +93,9 @@ public struct UserDefaultPreferenceKeys { defaultValue: false, key: "ShowHideWidgetShortcutGlobally" ) - + // MARK: Update Channel - + public let installBetaBuilds = PreferenceKey( defaultValue: false, key: "InstallBetaBuilds" @@ -163,7 +163,7 @@ public extension UserDefaultPreferenceKeys { var gitHubCopilotProxyPort: PreferenceKey { .init(defaultValue: "", key: "GitHubCopilotProxyPort") } - + var gitHubCopilotEnterpriseURI: PreferenceKey { .init(defaultValue: "", key: "GitHubCopilotEnterpriseURI") } @@ -290,15 +290,15 @@ public extension UserDefaultPreferenceKeys { var promptToCodeEmbeddingModelId: PreferenceKey { .init(defaultValue: "", key: "PromptToCodeEmbeddingModelId") } - + var enableSenseScopeByDefaultInPromptToCode: PreferenceKey { .init(defaultValue: false, key: "EnableSenseScopeByDefaultInPromptToCode") } - + var promptToCodeCodeFontSize: PreferenceKey { .init(defaultValue: 13, key: "PromptToCodeCodeFontSize") } - + var hideCommonPrecedingSpacesInPromptToCode: PreferenceKey { .init(defaultValue: true, key: "HideCommonPrecedingSpacesInPromptToCode") } @@ -310,7 +310,7 @@ public extension UserDefaultPreferenceKeys { var oldSuggestionFeatureProvider: DeprecatedPreferenceKey { .init(defaultValue: .gitHubCopilot, key: "SuggestionFeatureProvider") } - + var suggestionFeatureProvider: PreferenceKey { .init(defaultValue: .builtIn(.gitHubCopilot), key: "NewSuggestionFeatureProvider") } @@ -354,11 +354,11 @@ public extension UserDefaultPreferenceKeys { var acceptSuggestionWithTab: PreferenceKey { .init(defaultValue: true, key: "AcceptSuggestionWithTab") } - + var dismissSuggestionWithEsc: PreferenceKey { .init(defaultValue: true, key: "DismissSuggestionWithEsc") } - + var isSuggestionSenseEnabled: PreferenceKey { .init(defaultValue: false, key: "IsSuggestionSenseEnabled") } @@ -449,15 +449,15 @@ public extension UserDefaultPreferenceKeys { var enableWebScopeByDefaultInChatContext: PreferenceKey { .init(defaultValue: false, key: "EnableWebScopeByDefaultInChatContext") } - + var preferredChatModelIdForSenseScope: PreferenceKey { .init(defaultValue: "", key: "PreferredChatModelIdForSenseScope") } - + var preferredChatModelIdForProjectScope: PreferenceKey { .init(defaultValue: "", key: "PreferredChatModelIdForProjectScope") } - + var preferredChatModelIdForWebScope: PreferenceKey { .init(defaultValue: "", key: "PreferredChatModelIdForWebScope") } @@ -477,33 +477,42 @@ public extension UserDefaultPreferenceKeys { var syncChatCodeHighlightTheme: PreferenceKey { .init(defaultValue: false, key: "SyncChatCodeHighlightTheme") } - + var codeForegroundColorLight: PreferenceKey> { .init(defaultValue: .init(nil), key: "CodeForegroundColorLight") } - + var codeForegroundColorDark: PreferenceKey> { .init(defaultValue: .init(nil), key: "CodeForegroundColorDark") } - + var codeBackgroundColorLight: PreferenceKey> { .init(defaultValue: .init(nil), key: "CodeBackgroundColorLight") } - + var codeBackgroundColorDark: PreferenceKey> { .init(defaultValue: .init(nil), key: "CodeBackgroundColorDark") } - - var suggestionFont: PreferenceKey> { - .init(defaultValue: .init(nil), key: "SuggestionFont") + + var suggestionCodeFont: PreferenceKey> { + .init( + defaultValue: .init(.init(nsFont: .monospacedSystemFont(ofSize: 12, weight: .regular))), + key: "SuggestionCodeFont" + ) } - - var promptToCodeFont: PreferenceKey> { - .init(defaultValue: .init(nil), key: "promptToCodeFont") + + var promptToCodeCodeFont: PreferenceKey> { + .init( + defaultValue: .init(.init(nsFont: .monospacedSystemFont(ofSize: 12, weight: .regular))), + key: "promptToCodeCodeFont" + ) } - - var chatCodeFont: PreferenceKey> { - .init(defaultValue: .init(nil), key: "chatCodeFont") + + var chatCodeFont: PreferenceKey> { + .init( + defaultValue: .init(.init(nsFont: .monospacedSystemFont(ofSize: 12, weight: .regular))), + key: "chatCodeFont" + ) } } @@ -620,11 +629,11 @@ public extension UserDefaultPreferenceKeys { key: "FeatureFlag-DisableGitHubCopilotSettingsAutoRefreshOnAppear" ) } - + var disableGitIgnoreCheck: FeatureFlag { .init(defaultValue: false, key: "FeatureFlag-DisableGitIgnoreCheck") } - + var disableFileContentManipulationByCheatsheet: FeatureFlag { .init(defaultValue: true, key: "FeatureFlag-DisableFileContentManipulationByCheatsheet") } @@ -635,32 +644,32 @@ public extension UserDefaultPreferenceKeys { key: "FeatureFlag-DisableEnhancedWorkspace" ) } - + var restartXcodeInspectorIfAccessibilityAPIIsMalfunctioning: FeatureFlag { .init( defaultValue: false, key: "FeatureFlag-RestartXcodeInspectorIfAccessibilityAPIIsMalfunctioning" ) } - + var restartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer: FeatureFlag { .init( defaultValue: true, key: "FeatureFlag-RestartXcodeInspectorIfAccessibilityAPIIsMalfunctioningNoTimer" ) } - + var toastForTheReasonWhyXcodeInspectorNeedsToBeRestarted: FeatureFlag { .init( defaultValue: false, key: "FeatureFlag-ToastForTheReasonWhyXcodeInspectorNeedsToBeRestarted" ) } - + var observeToAXNotificationWithDefaultMode: FeatureFlag { .init(defaultValue: false, key: "FeatureFlag-observeToAXNotificationWithDefaultMode") } - + var useCloudflareDomainNameForLicenseCheck: FeatureFlag { .init(defaultValue: false, key: "FeatureFlag-UseCloudflareDomainNameForLicenseCheck") } diff --git a/Tool/Sources/Preferences/UserDefaults.swift b/Tool/Sources/Preferences/UserDefaults.swift index 508d0cd1..5052e1a8 100644 --- a/Tool/Sources/Preferences/UserDefaults.swift +++ b/Tool/Sources/Preferences/UserDefaults.swift @@ -31,14 +31,14 @@ public extension UserDefaults { defaultValue: shared.value(for: \.suggestionCodeFontSize) ) shared.setupDefaultValue( - for: \.suggestionFont, + for: \.suggestionCodeFont, defaultValue: .init(.init(nsFont: .monospacedSystemFont( ofSize: shared.value(for: \.suggestionCodeFontSize), weight: .regular ))) ) shared.setupDefaultValue( - for: \.promptToCodeFont, + for: \.promptToCodeCodeFont, defaultValue: .init(.init(nsFont: .monospacedSystemFont( ofSize: shared.value(for: \.promptToCodeCodeFontSize), weight: .regular diff --git a/Tool/Sources/SharedUIComponents/CodeBlock.swift b/Tool/Sources/SharedUIComponents/CodeBlock.swift index 28c14610..5d9884ca 100644 --- a/Tool/Sources/SharedUIComponents/CodeBlock.swift +++ b/Tool/Sources/SharedUIComponents/CodeBlock.swift @@ -10,7 +10,7 @@ public struct CodeBlock: View { public let commonPrecedingSpaceCount: Int public let highlightedCode: [NSAttributedString] public let firstLinePrecedingSpaceCount: Int - public let fontSize: Double + public let font: NSFont public let droppingLeadingSpaces: Bool public let proposedForegroundColor: Color? @@ -21,7 +21,7 @@ public struct CodeBlock: View { scenario: String, colorScheme: ColorScheme, firstLinePrecedingSpaceCount: Int = 0, - fontSize: Double, + font: NSFont, droppingLeadingSpaces: Bool, proposedForegroundColor: Color? ) { @@ -32,7 +32,7 @@ public struct CodeBlock: View { self.colorScheme = colorScheme self.droppingLeadingSpaces = droppingLeadingSpaces self.firstLinePrecedingSpaceCount = firstLinePrecedingSpaceCount - self.fontSize = fontSize + self.font = font self.proposedForegroundColor = proposedForegroundColor let padding = firstLinePrecedingSpaceCount > 0 ? String(repeating: " ", count: firstLinePrecedingSpaceCount) @@ -42,7 +42,7 @@ public struct CodeBlock: View { language: language, scenario: scenario, colorScheme: colorScheme, - fontSize: fontSize, + font: font, droppingLeadingSpaces: droppingLeadingSpaces ) commonPrecedingSpaceCount = result.commonLeadingSpaceCount @@ -79,7 +79,7 @@ public struct CodeBlock: View { } } .foregroundColor(.white) - .font(.system(size: fontSize, design: .monospaced)) + .font(.init(font)) .padding(.leading, 4) .padding([.trailing, .top, .bottom]) } @@ -89,7 +89,7 @@ public struct CodeBlock: View { language: String, scenario: String, colorScheme: ColorScheme, - fontSize: Double, + font: NSFont, droppingLeadingSpaces: Bool ) -> (code: [NSAttributedString], commonLeadingSpaceCount: Int) { return highlighted( @@ -98,7 +98,7 @@ public struct CodeBlock: View { scenario: scenario, brightMode: colorScheme != .dark, droppingLeadingSpaces: droppingLeadingSpaces, - fontSize: fontSize + font: font ) } } @@ -117,7 +117,7 @@ struct CodeBlock_Previews: PreviewProvider { scenario: "", colorScheme: .dark, firstLinePrecedingSpaceCount: 0, - fontSize: 12, + font: .monospacedSystemFont(ofSize: 12, weight: .regular), droppingLeadingSpaces: true, proposedForegroundColor: nil ) diff --git a/Tool/Sources/SharedUIComponents/Experiment/NewCodeBlock.swift b/Tool/Sources/SharedUIComponents/Experiment/NewCodeBlock.swift index b52f2d84..b455b9a0 100644 --- a/Tool/Sources/SharedUIComponents/Experiment/NewCodeBlock.swift +++ b/Tool/Sources/SharedUIComponents/Experiment/NewCodeBlock.swift @@ -8,7 +8,7 @@ private let insetTop = 12 as Double struct _CodeBlock: View { @Binding private var selection: NSRange? @State private var contentHeight: Double = 500 - let fontSize: Double + let font: NSFont let commonPrecedingSpaceCount: Int let highlightedCode: AttributedString let colorScheme: ColorScheme @@ -26,12 +26,12 @@ struct _CodeBlock: View { firstLinePrecedingSpaceCount: Int, scenario: String, colorScheme: ColorScheme, - fontSize: Double, + font: NSFont, droppingLeadingSpaces: Bool, selection: Binding = .constant(nil) ) { _selection = selection - self.fontSize = fontSize + self.font = font self.colorScheme = colorScheme self.droppingLeadingSpaces = droppingLeadingSpaces self.scenario = scenario @@ -44,7 +44,7 @@ struct _CodeBlock: View { language: language, scenario: scenario, colorScheme: colorScheme, - fontSize: fontSize, + font: font, droppingLeadingSpaces: droppingLeadingSpaces ) commonPrecedingSpaceCount = result.commonLeadingSpaceCount @@ -55,7 +55,7 @@ struct _CodeBlock: View { _CodeBlockRepresentable( text: highlightedCode, selection: $selection, - fontSize: fontSize, + font: font, onHeightChange: { height in print("Q", height) contentHeight = height @@ -74,7 +74,7 @@ struct _CodeBlock: View { language: String, scenario: String, colorScheme: ColorScheme, - fontSize: Double, + font: NSFont, droppingLeadingSpaces: Bool ) -> (code: AttributedString, commonLeadingSpaceCount: Int) { let (lines, commonLeadingSpaceCount) = highlighted( @@ -83,7 +83,7 @@ struct _CodeBlock: View { scenario: scenario, brightMode: colorScheme != .dark, droppingLeadingSpaces: droppingLeadingSpaces, - fontSize: fontSize, + font: font, replaceSpacesWithMiddleDots: false ) @@ -105,18 +105,18 @@ private struct _CodeBlockRepresentable: NSViewRepresentable { @Binding private var selection: NSRange? let text: AttributedString - let fontSize: Double + let font: NSFont let onHeightChange: (Double) -> Void init( text: AttributedString, selection: Binding, - fontSize: Double, + font: NSFont, onHeightChange: @escaping (Double) -> Void ) { self.text = text _selection = selection - self.fontSize = fontSize + self.font = font self.onHeightChange = onHeightChange } @@ -188,7 +188,6 @@ private struct _CodeBlockRepresentable: NSViewRepresentable { textView.heightTracksTextView = true } - let font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular) if textView.font != font { textView.font = font } diff --git a/Tool/Sources/SharedUIComponents/FontPicker.swift b/Tool/Sources/SharedUIComponents/FontPicker.swift index 4cda4e3a..2f91c9d0 100644 --- a/Tool/Sources/SharedUIComponents/FontPicker.swift +++ b/Tool/Sources/SharedUIComponents/FontPicker.swift @@ -3,15 +3,32 @@ import Foundation import Preferences import SwiftUI -public struct FontPicker: View { - @Binding var font: NSFont +public struct FontPicker: View { @State var fontManagerDelegate: FontManagerDelegate? + @Binding var font: NSFont + let label: Label - public init(font: Binding) { + public init(font: Binding, @ViewBuilder label: () -> Label) { _font = font + self.label = label() } public var body: some View { + if #available(macOS 13.0, *) { + LabeledContent { + button + } label: { + label + } + } else { + HStack { + label + button + } + } + } + + var button: some View { Button { if NSFontPanel.shared.isVisible { NSFontPanel.shared.orderOut(nil) @@ -25,13 +42,13 @@ public struct FontPicker: View { NSFontPanel.shared.orderBack(nil) } label: { HStack { - Text(font.fontName) - + Text(" - ") - + Text(font.pointSize, format: .number.precision(.fractionLength(1))) - + Text("pt") - + Text(font.fontName) + + Text(" - ") + + Text(font.pointSize, format: .number.precision(.fractionLength(1))) + + Text("pt") + Spacer().frame(width: 30) - + Image(systemName: "textformat") .frame(width: 13) .scaledToFit() @@ -54,16 +71,19 @@ public struct FontPicker: View { } public extension FontPicker { - init(font: Binding) { + init(font: Binding>, @ViewBuilder label: () -> Label) { _font = Binding( - get: { font.wrappedValue.nsFont }, - set: { font.wrappedValue = StorableFont(nsFont: $0) } + get: { font.wrappedValue.value.nsFont }, + set: { font.wrappedValue = .init(StorableFont(nsFont: $0)) } ) + self.label = label() } } #Preview { - FontPicker(font: .constant(.systemFont(ofSize: 15))) - .padding() + FontPicker(font: .constant(.systemFont(ofSize: 15))) { + Text("Font") + } + .padding() } diff --git a/Tool/Sources/SharedUIComponents/SyntaxHighlighting.swift b/Tool/Sources/SharedUIComponents/SyntaxHighlighting.swift index a0b1722a..b6dd0c02 100644 --- a/Tool/Sources/SharedUIComponents/SyntaxHighlighting.swift +++ b/Tool/Sources/SharedUIComponents/SyntaxHighlighting.swift @@ -9,7 +9,7 @@ public func highlightedCodeBlock( language: String, scenario: String, brightMode: Bool, - fontSize: Double + font: NSFont ) -> NSAttributedString { var language = language // Workaround: Highlightr uses a different identifier for Objective-C. @@ -21,7 +21,7 @@ public func highlightedCodeBlock( string: code, attributes: [ .foregroundColor: brightMode ? NSColor.black : NSColor.white, - .font: NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular), + .font: font, ] ) } @@ -35,7 +35,7 @@ public func highlightedCodeBlock( } return "\(scenario)-\(mode)" }()) - highlighter.theme.setCodeFont(.monospacedSystemFont(ofSize: fontSize, weight: .regular)) + highlighter.theme.setCodeFont(font) guard let formatted = highlighter.highlight(code, as: language) else { return unhighlightedCode() } @@ -51,7 +51,7 @@ public func highlighted( scenario: String, brightMode: Bool, droppingLeadingSpaces: Bool, - fontSize: Double, + font: NSFont, replaceSpacesWithMiddleDots: Bool = true ) -> (code: [NSAttributedString], commonLeadingSpaceCount: Int) { let formatted = highlightedCodeBlock( @@ -59,7 +59,7 @@ public func highlighted( language: language, scenario: scenario, brightMode: brightMode, - fontSize: fontSize + font: font ) let middleDotColor = brightMode ? NSColor.black.withAlphaComponent(0.1) From abcaaec5bcf7404fe23841f67553c67df9c399a0 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 7 Apr 2024 21:49:05 +0800 Subject: [PATCH 22/46] Add terminal font preference key --- .../TerminalSettingsView.swift | 27 +++++++++++++++++++ .../Sources/HostApp/FeatureSettingsView.swift | 12 +++++++++ Pro | 2 +- Tool/Sources/Preferences/Keys.swift | 11 ++++++-- 4 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 Core/Sources/HostApp/FeatureSettings/TerminalSettingsView.swift diff --git a/Core/Sources/HostApp/FeatureSettings/TerminalSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/TerminalSettingsView.swift new file mode 100644 index 00000000..43a8a539 --- /dev/null +++ b/Core/Sources/HostApp/FeatureSettings/TerminalSettingsView.swift @@ -0,0 +1,27 @@ +import Preferences +import SharedUIComponents +import SwiftUI + +#if canImport(ProHostApp) +import ProHostApp +#endif + +struct TerminalSettingsView: View { + class Settings: ObservableObject { + @AppStorage(\.terminalFont) var terminalFont + init() {} + } + + @StateObject var settings = Settings() + + var body: some View { + ScrollView { + Form { + FontPicker(font: $settings.terminalFont) { + Text("Font of code") + } + } + } + + } +} diff --git a/Core/Sources/HostApp/FeatureSettingsView.swift b/Core/Sources/HostApp/FeatureSettingsView.swift index cc8616c4..9a076b2c 100644 --- a/Core/Sources/HostApp/FeatureSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettingsView.swift @@ -34,6 +34,18 @@ struct FeatureSettingsView: View { subtitle: "Write code with natural language", image: "paintbrush" ) + +// #if canImport(ProHostApp) +// ScrollView { +// TerminalSettingsView().padding() +// } +// .sidebarItem( +// tag: 3, +// title: "Terminal", +// subtitle: "Terminal chat tab", +// image: "terminal" +// ) +// #endif } } } diff --git a/Pro b/Pro index 5207bbf1..c44d3a07 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 5207bbf185ec392d4d6f2bbf9e5d1cb414c2aebd +Subproject commit c44d3a07c2d3e4976523000c97b2ac4465767608 diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 7c39a20b..780ec9cf 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -504,14 +504,21 @@ public extension UserDefaultPreferenceKeys { var promptToCodeCodeFont: PreferenceKey> { .init( defaultValue: .init(.init(nsFont: .monospacedSystemFont(ofSize: 12, weight: .regular))), - key: "promptToCodeCodeFont" + key: "PromptToCodeCodeFont" ) } var chatCodeFont: PreferenceKey> { .init( defaultValue: .init(.init(nsFont: .monospacedSystemFont(ofSize: 12, weight: .regular))), - key: "chatCodeFont" + key: "ChatCodeFont" + ) + } + + var terminalFont: PreferenceKey> { + .init( + defaultValue: .init(.init(nsFont: .monospacedSystemFont(ofSize: 12, weight: .regular))), + key: "TerminalCodeFont" ) } } From d891b7952223994b40ab36be2b569f799b5be3ed Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 7 Apr 2024 21:57:47 +0800 Subject: [PATCH 23/46] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index c44d3a07..617fca9e 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit c44d3a07c2d3e4976523000c97b2ac4465767608 +Subproject commit 617fca9e1ee480dbb61d72c65265432d7172d1fd From 40fcb2626cccbce29e376e765ea96036f8e56a0b Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 8 Apr 2024 12:50:04 +0800 Subject: [PATCH 24/46] Support changing base URL of Google Generative AI --- .../ChatModelEditView.swift | 4 ++++ Pro | 2 +- Tool/Package.swift | 8 +++++-- Tool/Sources/AIModel/ChatModel.swift | 2 +- .../APIs/GoogleAIChatCompletionsService.swift | 23 ++++++++++++++++--- .../OpenAIService/ChatGPTService.swift | 6 +++-- 6 files changed, 36 insertions(+), 9 deletions(-) diff --git a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift index fe03c453..bcfccfd6 100644 --- a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift +++ b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift @@ -328,6 +328,10 @@ struct ChatModelEditView: View { @ViewBuilder var googleAI: some View { + baseURLTextField(prompt: Text("https://generativelanguage.googleapis.com")) { + Text("/v1") + } + apiKeyNamePicker WithViewStore( diff --git a/Pro b/Pro index 617fca9e..173f79c1 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 617fca9e1ee480dbb61d72c65265432d7172d1fd +Subproject commit 173f79c1ef3b84adccb2ad50d9630171280416b6 diff --git a/Tool/Package.swift b/Tool/Package.swift index 72057552..4fd93b6a 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -65,7 +65,11 @@ 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"), - .package(url: "https://github.com/google/generative-ai-swift", from: "0.4.4"), + // A fork of https://github.com/google/generative-ai-swift to support setting base url. + .package( + url: "https://github.com/intitni/generative-ai-swift", + branch: "support-setting-base-url" + ), .package(url: "https://github.com/intitni/CopilotForXcodeKit", from: "0.4.0"), // TreeSitter @@ -90,7 +94,7 @@ let package = Package( .target( name: "CustomAsyncAlgorithms", dependencies: [ - .product(name: "AsyncAlgorithms", package: "swift-async-algorithms") + .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), ] ), diff --git a/Tool/Sources/AIModel/ChatModel.swift b/Tool/Sources/AIModel/ChatModel.swift index 00e9fc01..99e19da8 100644 --- a/Tool/Sources/AIModel/ChatModel.swift +++ b/Tool/Sources/AIModel/ChatModel.swift @@ -103,7 +103,7 @@ public struct ChatModel: Codable, Equatable, Identifiable { case .googleAI: let baseURL = info.baseURL if baseURL.isEmpty { return "https://generativelanguage.googleapis.com/v1" } - return "\(baseURL)/v1/chat/completions" + return "\(baseURL)/v1" case .ollama: let baseURL = info.baseURL if baseURL.isEmpty { return "http://localhost:11434/api/chat" } diff --git a/Tool/Sources/OpenAIService/APIs/GoogleAIChatCompletionsService.swift b/Tool/Sources/OpenAIService/APIs/GoogleAIChatCompletionsService.swift index 80a5c9b8..4dc9e925 100644 --- a/Tool/Sources/OpenAIService/APIs/GoogleAIChatCompletionsService.swift +++ b/Tool/Sources/OpenAIService/APIs/GoogleAIChatCompletionsService.swift @@ -8,17 +8,20 @@ actor GoogleAIChatCompletionsService: ChatCompletionsAPI, ChatCompletionsStreamA let model: ChatModel var requestBody: ChatCompletionsRequestBody let prompt: ChatGPTPrompt + let baseURL: String init( apiKey: String, model: ChatModel, requestBody: ChatCompletionsRequestBody, - prompt: ChatGPTPrompt + prompt: ChatGPTPrompt, + baseURL: String ) { self.apiKey = apiKey self.model = model self.requestBody = requestBody self.prompt = prompt + self.baseURL = baseURL } func callAsFunction() async throws -> ChatCompletionResponseBody { @@ -27,7 +30,8 @@ actor GoogleAIChatCompletionsService: ChatCompletionsAPI, ChatCompletionsStreamA apiKey: apiKey, generationConfig: .init(GenerationConfig( temperature: requestBody.temperature.map(Float.init) - )) + )), + baseURL: baseURL ) let history = prompt.googleAICompatible.history.map { message in ModelContent(message) @@ -53,6 +57,12 @@ actor GoogleAIChatCompletionsService: ChatCompletionsAPI, ChatCompletionsStreamA throw error case .responseStoppedEarly: throw error + case .promptImageContentError: + throw error + case let .invalidAPIKey(message: message): + throw error + case .unsupportedUserLocation: + throw error } } catch { throw error @@ -67,7 +77,8 @@ actor GoogleAIChatCompletionsService: ChatCompletionsAPI, ChatCompletionsStreamA apiKey: apiKey, generationConfig: .init(GenerationConfig( temperature: requestBody.temperature.map(Float.init) - )) + )), + baseURL: baseURL ) let history = prompt.googleAICompatible.history.map { message in ModelContent(message) @@ -100,6 +111,12 @@ actor GoogleAIChatCompletionsService: ChatCompletionsAPI, ChatCompletionsStreamA continuation.finish(throwing: error) case .responseStoppedEarly: continuation.finish(throwing: error) + case let .promptImageContentError(underlying: underlying): + continuation.finish(throwing: error) + case let .invalidAPIKey(message: message): + continuation.finish(throwing: error) + case .unsupportedUserLocation: + continuation.finish(throwing: error) } } catch { continuation.finish(throwing: error) diff --git a/Tool/Sources/OpenAIService/ChatGPTService.swift b/Tool/Sources/OpenAIService/ChatGPTService.swift index 17847e18..76b39322 100644 --- a/Tool/Sources/OpenAIService/ChatGPTService.swift +++ b/Tool/Sources/OpenAIService/ChatGPTService.swift @@ -95,7 +95,8 @@ public class ChatGPTService: ChatGPTServiceType { apiKey: apiKey, model: model, requestBody: requestBody, - prompt: prompt + prompt: prompt, + baseURL: endpoint.absoluteString ) case .openAI, .openAICompatible, .azureOpenAI: return OpenAIChatCompletionsService( @@ -129,7 +130,8 @@ public class ChatGPTService: ChatGPTServiceType { apiKey: apiKey, model: model, requestBody: requestBody, - prompt: prompt + prompt: prompt, + baseURL: endpoint.absoluteString ) case .openAI, .openAICompatible, .azureOpenAI: return OpenAIChatCompletionsService( From e02a1f57eb41c9936d1fc14aca46b1e2b7da0394 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 8 Apr 2024 14:43:57 +0800 Subject: [PATCH 25/46] Fix endpoint of Google Generative AI --- Tool/Sources/AIModel/ChatModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tool/Sources/AIModel/ChatModel.swift b/Tool/Sources/AIModel/ChatModel.swift index 99e19da8..b99e1772 100644 --- a/Tool/Sources/AIModel/ChatModel.swift +++ b/Tool/Sources/AIModel/ChatModel.swift @@ -102,8 +102,8 @@ public struct ChatModel: Codable, Equatable, Identifiable { return "\(baseURL)/openai/deployments/\(deployment)/chat/completions?api-version=\(version)" case .googleAI: let baseURL = info.baseURL - if baseURL.isEmpty { return "https://generativelanguage.googleapis.com/v1" } - return "\(baseURL)/v1" + if baseURL.isEmpty { return "https://generativelanguage.googleapis.com" } + return "\(baseURL)" case .ollama: let baseURL = info.baseURL if baseURL.isEmpty { return "http://localhost:11434/api/chat" } From d4fb9019e4d365fa0f1c471481dca39b14ff8135 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 10 Apr 2024 12:10:12 +0800 Subject: [PATCH 26/46] Move refreshUpdateTime calls to Workspace --- Tool/Sources/Workspace/Workspace.swift | 39 ++++++++++++++----- .../SuggestionWorkspacePlugin.swift | 6 --- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/Tool/Sources/Workspace/Workspace.swift b/Tool/Sources/Workspace/Workspace.swift index 9facada9..f7adb02b 100644 --- a/Tool/Sources/Workspace/Workspace.swift +++ b/Tool/Sources/Workspace/Workspace.swift @@ -123,24 +123,18 @@ public final class Workspace { fileURL: fileURL, onSave: { [weak self] filespace in guard let self else { return } - for plugin in self.plugins.values { - plugin.didSaveFilespace(filespace) - } + self.didSaveFilespace(filespace) }, onClose: { [weak self] url in guard let self else { return } - for plugin in self.plugins.values { - plugin.didCloseFilespace(url) - } + self.didCloseFilespace(url) } ) if filespaces[fileURL] == nil { filespaces[fileURL] = filespace } if existedFilespace == nil { - for plugin in plugins.values { - plugin.didOpenFilespace(filespace) - } + didOpenFilespace(filespace) } else { filespace.refreshUpdateTime() } @@ -154,10 +148,37 @@ public final class Workspace { @WorkspaceActor public func didUpdateFilespace(fileURL: URL, content: String) { + refreshUpdateTime() guard let filespace = filespaces[fileURL] else { return } + filespace.refreshUpdateTime() for plugin in plugins.values { plugin.didUpdateFilespace(filespace, content: content) } } + + @WorkspaceActor + func didOpenFilespace(_ filespace: Filespace) { + refreshUpdateTime() + openedFileRecoverableStorage.openFile(fileURL: filespace.fileURL) + for plugin in plugins.values { + plugin.didOpenFilespace(filespace) + } + } + + @WorkspaceActor + func didCloseFilespace(_ fileURL: URL) { + for plugin in self.plugins.values { + plugin.didCloseFilespace(fileURL) + } + } + + @WorkspaceActor + func didSaveFilespace(_ filespace: Filespace) { + refreshUpdateTime() + filespace.refreshUpdateTime() + for plugin in plugins.values { + plugin.didSaveFilespace(filespace) + } + } } diff --git a/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift b/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift index 40b85ac6..04e0734a 100644 --- a/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift +++ b/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift @@ -95,8 +95,6 @@ public final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { } public func notifyOpenFile(filespace: Filespace) { - workspace?.refreshUpdateTime() - workspace?.openedFileRecoverableStorage.openFile(fileURL: filespace.fileURL) Task { guard !(await filespace.isGitIgnored) else { return } // check if file size is larger than 15MB, if so, return immediately @@ -114,8 +112,6 @@ public final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { } public func notifyUpdateFile(filespace: Filespace, content: String) { - filespace.refreshUpdateTime() - workspace?.refreshUpdateTime() Task { guard !(await filespace.isGitIgnored) else { return } try await suggestionService?.notifyChangeTextDocument( @@ -126,8 +122,6 @@ public final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { } public func notifySaveFile(filespace: Filespace) { - filespace.refreshUpdateTime() - workspace?.refreshUpdateTime() Task { guard !(await filespace.isGitIgnored) else { return } try await suggestionService?.notifySaveTextDocument(fileURL: filespace.fileURL) From e6e6eebff3cee52021a18bca1cde66f58fe9780d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 10 Apr 2024 12:13:54 +0800 Subject: [PATCH 27/46] Rename to lastUpdateTime --- Tool/Sources/Workspace/Filespace.swift | 6 +++--- Tool/Sources/Workspace/Workspace.swift | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Tool/Sources/Workspace/Filespace.swift b/Tool/Sources/Workspace/Filespace.swift index e6228803..73805221 100644 --- a/Tool/Sources/Workspace/Filespace.swift +++ b/Tool/Sources/Workspace/Filespace.swift @@ -92,10 +92,10 @@ public final class Filespace { // MARK: Life Cycle public var isExpired: Bool { - Environment.now().timeIntervalSince(lastSuggestionUpdateTime) > 60 * 3 + Environment.now().timeIntervalSince(lastUpdateTime) > 60 * 3 } - private(set) var lastSuggestionUpdateTime: Date = Environment.now() + private(set) var lastUpdateTime: Date = Environment.now() private var additionalProperties = FilespacePropertyValues() let fileSaveWatcher: FileSaveWatcher let onClose: (URL) -> Void @@ -154,7 +154,7 @@ public final class Filespace { } public func refreshUpdateTime() { - lastSuggestionUpdateTime = Environment.now() + lastUpdateTime = Environment.now() } @WorkspaceActor diff --git a/Tool/Sources/Workspace/Workspace.swift b/Tool/Sources/Workspace/Workspace.swift index f7adb02b..40c87261 100644 --- a/Tool/Sources/Workspace/Workspace.swift +++ b/Tool/Sources/Workspace/Workspace.swift @@ -72,9 +72,9 @@ public final class Workspace { public let workspaceURL: URL public let projectRootURL: URL public let openedFileRecoverableStorage: OpenedFileRecoverableStorage - public private(set) var lastSuggestionUpdateTime = Environment.now() + public private(set) var lastLastUpdateTime = Environment.now() public var isExpired: Bool { - Environment.now().timeIntervalSince(lastSuggestionUpdateTime) > 60 * 60 * 1 + Environment.now().timeIntervalSince(lastLastUpdateTime) > 60 * 60 * 1 } public private(set) var filespaces = [URL: Filespace]() @@ -113,7 +113,7 @@ public final class Workspace { } public func refreshUpdateTime() { - lastSuggestionUpdateTime = Environment.now() + lastLastUpdateTime = Environment.now() } @WorkspaceActor From 44ab9630a036d9e14e09864a8400c0b82581f339 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 10 Apr 2024 12:14:18 +0800 Subject: [PATCH 28/46] Make lastUpdateTime publicly readable --- Tool/Sources/Workspace/Filespace.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tool/Sources/Workspace/Filespace.swift b/Tool/Sources/Workspace/Filespace.swift index 73805221..b3b74150 100644 --- a/Tool/Sources/Workspace/Filespace.swift +++ b/Tool/Sources/Workspace/Filespace.swift @@ -95,7 +95,7 @@ public final class Filespace { Environment.now().timeIntervalSince(lastUpdateTime) > 60 * 3 } - private(set) var lastUpdateTime: Date = Environment.now() + public private(set) var lastUpdateTime: Date = Environment.now() private var additionalProperties = FilespacePropertyValues() let fileSaveWatcher: FileSaveWatcher let onClose: (URL) -> Void From 133b65916f53b186c4b95eae482762f71b6ecdb0 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 10 Apr 2024 22:08:15 +0800 Subject: [PATCH 29/46] Support getting int value from AXUIElement --- Tool/Sources/AXExtension/AXUIElement.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tool/Sources/AXExtension/AXUIElement.swift b/Tool/Sources/AXExtension/AXUIElement.swift index 58b12d00..d0dc22d0 100644 --- a/Tool/Sources/AXExtension/AXUIElement.swift +++ b/Tool/Sources/AXExtension/AXUIElement.swift @@ -21,6 +21,10 @@ public extension AXUIElement { var value: String { (try? copyValue(key: kAXValueAttribute)) ?? "" } + + var intValue: Int? { + (try? copyValue(key: kAXValueAttribute)) + } var title: String { (try? copyValue(key: kAXTitleAttribute)) ?? "" From f873f33caca591f6fa30325d1fd5a549c668a65a Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 10 Apr 2024 22:08:29 +0800 Subject: [PATCH 30/46] Update --- Core/Package.swift | 4 +++- Pro | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Core/Package.swift b/Core/Package.swift index 4038db74..1e1a1fcd 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -113,7 +113,9 @@ let package = Package( .product(name: "SuggestionModel", package: "Tool"), .product(name: "Logger", package: "Tool"), .product(name: "Preferences", package: "Tool"), - ] + ].pro([ + "ProClient", + ]) ), .target( name: "Service", diff --git a/Pro b/Pro index 173f79c1..f78270ac 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 173f79c1ef3b84adccb2ad50d9630171280416b6 +Subproject commit f78270ac6991d548c4f89973fe5e9ee774a36b6a From 40c1a1e7a36ecae6301e1a8e7d670a77b0ae2c6d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 10 Apr 2024 22:08:40 +0800 Subject: [PATCH 31/46] Add CloseIdleTabsCommand --- Copilot for Xcode.xcodeproj/project.pbxproj | 4 ++++ EditorExtension/CloseIdleTabsCommand.swift | 20 ++++++++++++++++++++ EditorExtension/SourceEditorExtension.swift | 18 +++++++++++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 EditorExtension/CloseIdleTabsCommand.swift diff --git a/Copilot for Xcode.xcodeproj/project.pbxproj b/Copilot for Xcode.xcodeproj/project.pbxproj index 996d7ae3..ad49af50 100644 --- a/Copilot for Xcode.xcodeproj/project.pbxproj +++ b/Copilot for Xcode.xcodeproj/project.pbxproj @@ -43,6 +43,7 @@ C89E75C32A46FB32000DD64F /* AppDelegate+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89E75C22A46FB32000DD64F /* AppDelegate+Menu.swift */; }; C8C8B60929AFA35F00034BEE /* CopilotForXcodeExtensionService.app in Embed XPCService */ = {isa = PBXBuildFile; fileRef = C861E60E2994F6070056CB02 /* CopilotForXcodeExtensionService.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; C8DCF00029CE11D500FDDDD7 /* ChatWithSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8DCEFFF29CE11D500FDDDD7 /* ChatWithSelection.swift */; }; + C8DD9CB12BC673F80036641C /* CloseIdleTabsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8DD9CB02BC673F80036641C /* CloseIdleTabsCommand.swift */; }; C8F1032B2A7A39D700D28F4F /* launchAgent.plist in Copy Launch Agent */ = {isa = PBXBuildFile; fileRef = C8F103292A7A365000D28F4F /* launchAgent.plist */; }; /* End PBXBuildFile section */ @@ -197,6 +198,7 @@ C89E75C22A46FB32000DD64F /* AppDelegate+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Menu.swift"; sourceTree = ""; }; C8CD828229B88006008D044D /* TestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestPlan.xctestplan; sourceTree = ""; }; C8DCEFFF29CE11D500FDDDD7 /* ChatWithSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWithSelection.swift; sourceTree = ""; }; + C8DD9CB02BC673F80036641C /* CloseIdleTabsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseIdleTabsCommand.swift; sourceTree = ""; }; C8F103292A7A365000D28F4F /* launchAgent.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = launchAgent.plist; sourceTree = ""; }; /* End PBXFileReference section */ @@ -264,6 +266,7 @@ C800DBB0294C624D00B04CAC /* PrefetchSuggestionsCommand.swift */, C8758E6F29F04BFF00D29C1C /* CustomCommand.swift */, C8DCEFFF29CE11D500FDDDD7 /* ChatWithSelection.swift */, + C8DD9CB02BC673F80036641C /* CloseIdleTabsCommand.swift */, C861A6A229E5503F005C41A3 /* PromptToCodeCommand.swift */, C81458972939EFDC00135263 /* Info.plist */, C81458982939EFDC00135263 /* EditorExtension.entitlements */, @@ -525,6 +528,7 @@ files = ( C8DCF00029CE11D500FDDDD7 /* ChatWithSelection.swift in Sources */, C81458942939EFDC00135263 /* SourceEditorExtension.swift in Sources */, + C8DD9CB12BC673F80036641C /* CloseIdleTabsCommand.swift in Sources */, C8758E7029F04BFF00D29C1C /* CustomCommand.swift in Sources */, C8758E7229F04CF100D29C1C /* SeparatorCommand.swift in Sources */, C861A6A329E5503F005C41A3 /* PromptToCodeCommand.swift in Sources */, diff --git a/EditorExtension/CloseIdleTabsCommand.swift b/EditorExtension/CloseIdleTabsCommand.swift new file mode 100644 index 00000000..b2dde69a --- /dev/null +++ b/EditorExtension/CloseIdleTabsCommand.swift @@ -0,0 +1,20 @@ +import Client +import Foundation +import SuggestionModel +import XcodeKit + +class CloseIdleTabsCommand: NSObject, XCSourceEditorCommand, CommandType { + var name: String { "Close Idle Tabs" } + + func perform( + with invocation: XCSourceEditorCommandInvocation, + completionHandler: @escaping (Error?) -> Void + ) { + completionHandler(nil) + Task { + let service = try getService() + _ = try await service.postNotification(name: "CloseIdleTabsOfXcodeWindow") + } + } +} + diff --git a/EditorExtension/SourceEditorExtension.swift b/EditorExtension/SourceEditorExtension.swift index 396a8d00..4b3882e2 100644 --- a/EditorExtension/SourceEditorExtension.swift +++ b/EditorExtension/SourceEditorExtension.swift @@ -3,6 +3,10 @@ import Foundation import Preferences import XcodeKit +#if canImport(PreferencesPlus) +import PreferencesPlus +#endif + class SourceEditorExtension: NSObject, XCSourceEditorExtension { var builtin: [[XCSourceEditorCommandDefinitionKey: Any]] { [ @@ -17,6 +21,18 @@ class SourceEditorExtension: NSObject, XCSourceEditorExtension { ].map(makeCommandDefinition) } + var optional: [[XCSourceEditorCommandDefinitionKey: Any]] { + var all = [[XCSourceEditorCommandDefinitionKey: Any]]() + + #if canImport(PreferencesPlus) + if UserDefaults.shared.value(for: \.enableCloseIdleTabCommandInXcodeMenu) { + all.append(CloseIdleTabsCommand().makeCommandDefinition()) + } + #endif + + return all + } + var internalUse: [[XCSourceEditorCommandDefinitionKey: Any]] { [ SeparatorCommand().named("------"), @@ -34,7 +50,7 @@ class SourceEditorExtension: NSObject, XCSourceEditorExtension { } var commandDefinitions: [[XCSourceEditorCommandDefinitionKey: Any]] { - return builtin + custom + internalUse + return builtin + optional + custom + internalUse } func extensionDidFinishLaunching() { From 37fde4522ccd0ee48d9ea23cecab93bff2b76a6d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 10 Apr 2024 22:44:42 +0800 Subject: [PATCH 32/46] Add settings for closing idle tabs --- .../FeatureSettings/XcodeSettingsView.swift | 20 +++++++++++++++++++ .../Sources/HostApp/FeatureSettingsView.swift | 10 ++++++++++ Pro | 2 +- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 Core/Sources/HostApp/FeatureSettings/XcodeSettingsView.swift diff --git a/Core/Sources/HostApp/FeatureSettings/XcodeSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/XcodeSettingsView.swift new file mode 100644 index 00000000..198aae19 --- /dev/null +++ b/Core/Sources/HostApp/FeatureSettings/XcodeSettingsView.swift @@ -0,0 +1,20 @@ +import Foundation +import SharedUIComponents +import SwiftUI + +#if canImport(ProHostApp) +import ProHostApp +#endif + +struct XcodeSettingsView: View { + var body: some View { + VStack { + #if canImport(ProHostApp) + CloseXcodeIdleTabsSettingsView() + #endif + + EmptyView() + } + } +} + diff --git a/Core/Sources/HostApp/FeatureSettingsView.swift b/Core/Sources/HostApp/FeatureSettingsView.swift index 9a076b2c..cd5683f7 100644 --- a/Core/Sources/HostApp/FeatureSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettingsView.swift @@ -35,6 +35,16 @@ struct FeatureSettingsView: View { image: "paintbrush" ) + ScrollView { + XcodeSettingsView().padding() + } + .sidebarItem( + tag: 3, + title: "Xcode", + subtitle: "Xcode related features", + image: "app" + ) + // #if canImport(ProHostApp) // ScrollView { // TerminalSettingsView().padding() diff --git a/Pro b/Pro index f78270ac..45ae2f33 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit f78270ac6991d548c4f89973fe5e9ee774a36b6a +Subproject commit 45ae2f33c112c9bcce918a25f9cc78792b3379f4 From 0b10b54fce7449c0e3910275b483433d5a366da6 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 10 Apr 2024 22:52:27 +0800 Subject: [PATCH 33/46] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 45ae2f33..8113a55c 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 45ae2f33c112c9bcce918a25f9cc78792b3379f4 +Subproject commit 8113a55c49efaa291f065b4785d65079c48c1b13 From 58bb038f814c3fc144b1d4e885d1ade24a58993d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 10 Apr 2024 23:15:38 +0800 Subject: [PATCH 34/46] Fix default value --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 8113a55c..9d76231d 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 8113a55c49efaa291f065b4785d65079c48c1b13 +Subproject commit 9d76231dfe8097b09e814be499756ba564f85da3 From f6c648841d7e5ceb5a926e35037ec928856d6156 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 11 Apr 2024 00:00:24 +0800 Subject: [PATCH 35/46] Update to also use title changed event to detect file change --- Core/Sources/SuggestionWidget/WidgetWindowsController.swift | 2 ++ .../XcodeInspector/Apps/XcodeAppInstanceInspector.swift | 6 +++++- Tool/Sources/XcodeInspector/XcodeWindowInspector.swift | 4 +++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift index 60ff0b18..4ab5c142 100644 --- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift +++ b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift @@ -338,6 +338,8 @@ private extension WidgetWindowsController { case .created, .uiElementDestroyed, .xcodeCompletionPanelChanged, .applicationDeactivated: continue + case .titleChanged: + continue } } } diff --git a/Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift b/Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift index c16b09a0..852a4de7 100644 --- a/Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift +++ b/Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift @@ -12,6 +12,7 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector { } public enum AXNotificationKind { + case titleChanged case applicationActivated case applicationDeactivated case moved @@ -29,6 +30,8 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector { public init?(rawValue: String) { switch rawValue { + case kAXTitleChangedNotification: + self = .titleChanged case kAXApplicationActivatedNotification: self = .applicationActivated case kAXApplicationDeactivatedNotification: @@ -211,6 +214,7 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector { let axNotificationStream = AXNotificationStream( app: runningApplication, notificationNames: + kAXTitleChangedNotification, kAXApplicationActivatedNotification, kAXApplicationDeactivatedNotification, kAXMovedNotification, @@ -233,7 +237,7 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector { guard let self else { return } try Task.checkCancellation() await Task.yield() - + guard let event = AXNotificationKind(rawValue: notification.name) else { continue } diff --git a/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift b/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift index 5878a27a..611bb9f3 100644 --- a/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift @@ -47,7 +47,9 @@ public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector { group.addTask { [weak self] in for await notification in await axNotifications.notifications() { - guard notification.kind == .focusedUIElementChanged else { continue } + guard notification.kind == .focusedUIElementChanged + || notification.kind == .titleChanged + else { continue } guard let self else { return } try Task.checkCancellation() await Task.yield() From e17ce5a608bfedac2d11a4e60117537b23bf1e87 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 11 Apr 2024 00:07:04 +0800 Subject: [PATCH 36/46] Allow all file types to have a filespace --- Tool/Sources/Workspace/Filespace.swift | 3 +++ Tool/Sources/Workspace/WorkspacePool.swift | 5 ----- .../SuggestionWorkspacePlugin.swift | 3 +++ 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Tool/Sources/Workspace/Filespace.swift b/Tool/Sources/Workspace/Filespace.swift index b3b74150..264a3dae 100644 --- a/Tool/Sources/Workspace/Filespace.swift +++ b/Tool/Sources/Workspace/Filespace.swift @@ -76,6 +76,9 @@ public final class Filespace { public let fileURL: URL public private(set) lazy var language: CodeLanguage = languageIdentifierFromFileURL(fileURL) public var codeMetadata: FilespaceCodeMetadata = .init() + public var isTextReadable: Bool { + fileURL.pathExtension != "mlmodel" + } // MARK: Suggestions diff --git a/Tool/Sources/Workspace/WorkspacePool.swift b/Tool/Sources/Workspace/WorkspacePool.swift index 7798ad6e..2b9a0737 100644 --- a/Tool/Sources/Workspace/WorkspacePool.swift +++ b/Tool/Sources/Workspace/WorkspacePool.swift @@ -88,11 +88,6 @@ public class WorkspacePool { public func fetchOrCreateWorkspaceAndFilespace(fileURL: URL) async throws -> (workspace: Workspace, filespace: Filespace) { - let ignoreFileExtensions = ["mlmodel"] - if ignoreFileExtensions.contains(fileURL.pathExtension) { - throw Workspace.UnsupportedFileError(extensionName: fileURL.pathExtension) - } - // If we can get the workspace URL directly. if let currentWorkspaceURL = await XcodeInspector.shared.safe.realtimeActiveWorkspaceURL { if let existed = workspaces[currentWorkspaceURL] { diff --git a/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift b/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift index 04e0734a..3e999628 100644 --- a/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift +++ b/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift @@ -96,6 +96,7 @@ public final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { public func notifyOpenFile(filespace: Filespace) { Task { + guard filespace.isTextReadable else { return } guard !(await filespace.isGitIgnored) else { return } // check if file size is larger than 15MB, if so, return immediately if let attrs = try? FileManager.default @@ -113,6 +114,7 @@ public final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { public func notifyUpdateFile(filespace: Filespace, content: String) { Task { + guard filespace.isTextReadable else { return } guard !(await filespace.isGitIgnored) else { return } try await suggestionService?.notifyChangeTextDocument( fileURL: filespace.fileURL, @@ -123,6 +125,7 @@ public final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { public func notifySaveFile(filespace: Filespace) { Task { + guard filespace.isTextReadable else { return } guard !(await filespace.isGitIgnored) else { return } try await suggestionService?.notifySaveTextDocument(fileURL: filespace.fileURL) } From 1eed5bba1e254d0b5c2987ca376c575873052d6f Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 11 Apr 2024 00:11:29 +0800 Subject: [PATCH 37/46] Move filespace creation on editor change to Service --- .../Service/RealtimeSuggestionController.swift | 8 -------- Core/Sources/Service/Service.swift | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Core/Sources/Service/RealtimeSuggestionController.swift b/Core/Sources/Service/RealtimeSuggestionController.swift index e2cdf192..27965c7a 100644 --- a/Core/Sources/Service/RealtimeSuggestionController.swift +++ b/Core/Sources/Service/RealtimeSuggestionController.swift @@ -43,14 +43,6 @@ public actor RealtimeSuggestionController { } private func handleFocusElementChange(_ sourceEditor: SourceEditor) { - Task { // Notify suggestion service for open file. - try await Task.sleep(nanoseconds: 500_000_000) - guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL - else { return } - _ = try await Service.shared.workspacePool - .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) - } - self.sourceEditor = sourceEditor let notificationsFromEditor = sourceEditor.axNotifications diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift index 08d86cae..d02d8ef3 100644 --- a/Core/Sources/Service/Service.swift +++ b/Core/Sources/Service/Service.swift @@ -1,3 +1,4 @@ +import Combine import Dependencies import Foundation import SuggestionService @@ -33,6 +34,7 @@ public final class Service { #endif @Dependency(\.toast) var toast + var cancellable = Set() private init() { @Dependency(\.workspacePool) var workspacePool @@ -70,6 +72,19 @@ public final class Service { #endif DependencyUpdater().update() globalShortcutManager.start() + + Task { + await XcodeInspector.shared.safe.$activeDocumentURL + .removeDuplicates() + .filter { $0 != .init(fileURLWithPath: "/") } + .compactMap { $0 } + .sink { [weak self] fileURL in + Task { + try await self?.workspacePool + .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) + } + }.store(in: &cancellable) + } } } From 5fa427e20de61d791606f929cca42dc3ddeb2425 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 11 Apr 2024 15:42:13 +0800 Subject: [PATCH 38/46] Fix bugs in idle tab closing --- Pro | 2 +- Tool/Sources/XcodeInspector/XcodeWindowInspector.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Pro b/Pro index 9d76231d..75268bc5 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 9d76231dfe8097b09e814be499756ba564f85da3 +Subproject commit 75268bc52ebe33c1eaf9752f6ffd0a6b947db1cb diff --git a/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift b/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift index 611bb9f3..d3178781 100644 --- a/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift @@ -16,9 +16,9 @@ public class XcodeWindowInspector: ObservableObject { public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector { let app: NSRunningApplication - @Published var documentURL: URL = .init(fileURLWithPath: "/") - @Published var workspaceURL: URL = .init(fileURLWithPath: "/") - @Published var projectRootURL: URL = .init(fileURLWithPath: "/") + @Published public internal(set) var documentURL: URL = .init(fileURLWithPath: "/") + @Published public internal(set) var workspaceURL: URL = .init(fileURLWithPath: "/") + @Published public internal(set) var projectRootURL: URL = .init(fileURLWithPath: "/") private var focusedElementChangedTask: Task? public func refresh() { From 75f5b0f357c2d2d8c52304bf01078289803506c8 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 11 Apr 2024 15:57:11 +0800 Subject: [PATCH 39/46] Update idle tab closing --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 75268bc5..8315f72f 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 75268bc52ebe33c1eaf9752f6ffd0a6b947db1cb +Subproject commit 8315f72ff54e8de21d4a971e06c3858377137066 From 6249d1235df5d0b4128808fd00f9c7f9e776b910 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 14 Apr 2024 17:14:21 +0800 Subject: [PATCH 40/46] Update UI --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 8315f72f..84ceb281 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 8315f72ff54e8de21d4a971e06c3858377137066 +Subproject commit 84ceb281a83d05e7aa60e16e7ad8172a5add76fd From 47086c675fea2c0a565b8f2a3865ed0f8ce9a581 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 14 Apr 2024 17:14:34 +0800 Subject: [PATCH 41/46] Support setting Google AI api version --- .../ChatModelManagement/ChatModelEdit.swift | 22 ++++++------------- .../ChatModelEditView.swift | 10 ++++++--- Tool/Sources/AIModel/ChatModel.swift | 18 ++++++++++++++- .../APIs/GoogleAIChatCompletionsService.swift | 20 +++++++++++------ 4 files changed, 44 insertions(+), 26 deletions(-) diff --git a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEdit.swift b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEdit.swift index 52964392..62eec368 100644 --- a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEdit.swift +++ b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEdit.swift @@ -1,11 +1,11 @@ import AIModel -import Toast import ComposableArchitecture import Dependencies import Keychain import OpenAIService import Preferences import SwiftUI +import Toast struct ChatModelEdit: ReducerProtocol { struct State: Equatable, Identifiable { @@ -16,6 +16,7 @@ struct ChatModelEdit: ReducerProtocol { @BindingState var supportsFunctionCalling: Bool = true @BindingState var modelName: String = "" @BindingState var ollamaKeepAlive: String = "" + @BindingState var apiVersion: String = "" var apiKeyName: String { apiKeySelection.apiKeyName } var baseURL: String { baseURLSelection.baseURL } var isFullURL: Bool { baseURLSelection.isFullURL } @@ -47,6 +48,7 @@ struct ChatModelEdit: ReducerProtocol { toast($0, $1, "ChatModelEdit") } } + @Dependency(\.apiKeyKeychain) var keychain var body: some ReducerProtocol { @@ -77,19 +79,7 @@ struct ChatModelEdit: ReducerProtocol { case .testButtonClicked: guard !state.isTesting else { return .none } state.isTesting = true - let model = ChatModel( - id: state.id, - name: state.name, - format: state.format, - info: .init( - apiKeyName: state.apiKeyName, - baseURL: state.baseURL, - isFullURL: state.isFullURL, - maxTokens: state.maxTokens, - supportsFunctionCalling: state.supportsFunctionCalling, - modelName: state.modelName - ) - ) + let model = ChatModel(state: state) return .run { send in do { let service = ChatGPTService( @@ -194,6 +184,7 @@ extension ChatModelEdit.State { supportsFunctionCalling: model.info.supportsFunctionCalling, modelName: model.info.modelName, ollamaKeepAlive: model.info.ollamaInfo.keepAlive, + apiVersion: model.info.googleGenerativeAIInfo.apiVersion, apiKeySelection: .init( apiKeyName: model.info.apiKeyName, apiKeyManagement: .init(availableAPIKeyNames: [model.info.apiKeyName]) @@ -223,7 +214,8 @@ extension ChatModel { } }(), modelName: state.modelName.trimmingCharacters(in: .whitespacesAndNewlines), - ollamaInfo: .init(keepAlive: state.ollamaKeepAlive) + ollamaInfo: .init(keepAlive: state.ollamaKeepAlive), + googleGenerativeAIInfo: .init(apiVersion: state.apiVersion) ) ) } diff --git a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift index bcfccfd6..2fe21715 100644 --- a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift +++ b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift @@ -331,7 +331,7 @@ struct ChatModelEditView: View { baseURLTextField(prompt: Text("https://generativelanguage.googleapis.com")) { Text("/v1") } - + apiKeyNamePicker WithViewStore( @@ -357,6 +357,10 @@ struct ChatModelEditView: View { } maxTokensTextField + + WithViewStore(store, removeDuplicates: { $0.apiVersion == $1.apiVersion }) { viewStore in + TextField("API Version", text: viewStore.$apiVersion, prompt: Text("v1")) + } } @ViewBuilder @@ -396,7 +400,7 @@ struct ChatModelEditView: View { baseURLTextField(prompt: Text("https://api.anthropic.com")) { Text("/v1/messages") } - + apiKeyNamePicker WithViewStore( @@ -425,7 +429,7 @@ struct ChatModelEditView: View { .frame(width: 20) } } - + maxTokensTextField VStack(alignment: .leading, spacing: 8) { diff --git a/Tool/Sources/AIModel/ChatModel.swift b/Tool/Sources/AIModel/ChatModel.swift index b99e1772..06d0a022 100644 --- a/Tool/Sources/AIModel/ChatModel.swift +++ b/Tool/Sources/AIModel/ChatModel.swift @@ -43,6 +43,15 @@ public struct ChatModel: Codable, Equatable, Identifiable { self.organizationID = organizationID } } + + public struct GoogleGenerativeAIInfo: Codable, Equatable { + @FallbackDecoding + public var apiVersion: String + + public init(apiVersion: String = "") { + self.apiVersion = apiVersion + } + } @FallbackDecoding public var apiKeyName: String @@ -61,6 +70,8 @@ public struct ChatModel: Codable, Equatable, Identifiable { public var openAIInfo: OpenAIInfo @FallbackDecoding public var ollamaInfo: OllamaInfo + @FallbackDecoding + public var googleGenerativeAIInfo: GoogleGenerativeAIInfo public init( apiKeyName: String = "", @@ -70,7 +81,8 @@ public struct ChatModel: Codable, Equatable, Identifiable { supportsFunctionCalling: Bool = true, modelName: String = "", openAIInfo: OpenAIInfo = OpenAIInfo(), - ollamaInfo: OllamaInfo = OllamaInfo() + ollamaInfo: OllamaInfo = OllamaInfo(), + googleGenerativeAIInfo: GoogleGenerativeAIInfo = GoogleGenerativeAIInfo() ) { self.apiKeyName = apiKeyName self.baseURL = baseURL @@ -80,6 +92,7 @@ public struct ChatModel: Codable, Equatable, Identifiable { self.modelName = modelName self.openAIInfo = openAIInfo self.ollamaInfo = ollamaInfo + self.googleGenerativeAIInfo = googleGenerativeAIInfo } } @@ -132,3 +145,6 @@ public struct EmptyChatModelOpenAIInfo: FallbackValueProvider { public static var defaultValue: ChatModel.Info.OpenAIInfo { .init() } } +public struct EmptyChatModelGoogleGenerativeAIInfo: FallbackValueProvider { + public static var defaultValue: ChatModel.Info.GoogleGenerativeAIInfo { .init() } +} diff --git a/Tool/Sources/OpenAIService/APIs/GoogleAIChatCompletionsService.swift b/Tool/Sources/OpenAIService/APIs/GoogleAIChatCompletionsService.swift index 4dc9e925..2770b6e2 100644 --- a/Tool/Sources/OpenAIService/APIs/GoogleAIChatCompletionsService.swift +++ b/Tool/Sources/OpenAIService/APIs/GoogleAIChatCompletionsService.swift @@ -30,8 +30,11 @@ actor GoogleAIChatCompletionsService: ChatCompletionsAPI, ChatCompletionsStreamA apiKey: apiKey, generationConfig: .init(GenerationConfig( temperature: requestBody.temperature.map(Float.init) - )), - baseURL: baseURL + )), + baseURL: baseURL, + requestOptions: model.info.googleGenerativeAIInfo.apiVersion.isEmpty + ? .init() + : .init(apiVersion: model.info.googleGenerativeAIInfo.apiVersion) ) let history = prompt.googleAICompatible.history.map { message in ModelContent(message) @@ -59,7 +62,7 @@ actor GoogleAIChatCompletionsService: ChatCompletionsAPI, ChatCompletionsStreamA throw error case .promptImageContentError: throw error - case let .invalidAPIKey(message: message): + case .invalidAPIKey: throw error case .unsupportedUserLocation: throw error @@ -77,8 +80,11 @@ actor GoogleAIChatCompletionsService: ChatCompletionsAPI, ChatCompletionsStreamA apiKey: apiKey, generationConfig: .init(GenerationConfig( temperature: requestBody.temperature.map(Float.init) - )), - baseURL: baseURL + )), + baseURL: baseURL, + requestOptions: model.info.googleGenerativeAIInfo.apiVersion.isEmpty + ? .init() + : .init(apiVersion: model.info.googleGenerativeAIInfo.apiVersion) ) let history = prompt.googleAICompatible.history.map { message in ModelContent(message) @@ -111,9 +117,9 @@ actor GoogleAIChatCompletionsService: ChatCompletionsAPI, ChatCompletionsStreamA continuation.finish(throwing: error) case .responseStoppedEarly: continuation.finish(throwing: error) - case let .promptImageContentError(underlying: underlying): + case .promptImageContentError: continuation.finish(throwing: error) - case let .invalidAPIKey(message: message): + case .invalidAPIKey: continuation.finish(throwing: error) case .unsupportedUserLocation: continuation.finish(throwing: error) From 374194a5e2f4a8095561311ce8d97efc65103353 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 14 Apr 2024 17:18:45 +0800 Subject: [PATCH 42/46] Add new OpenAI models --- Tool/Sources/Preferences/Types/ChatGPTModel.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Tool/Sources/Preferences/Types/ChatGPTModel.swift b/Tool/Sources/Preferences/Types/ChatGPTModel.swift index 1541f61c..d56ac4a1 100644 --- a/Tool/Sources/Preferences/Types/ChatGPTModel.swift +++ b/Tool/Sources/Preferences/Types/ChatGPTModel.swift @@ -5,11 +5,13 @@ public enum ChatGPTModel: String { case gpt35Turbo16k = "gpt-3.5-turbo-16k" case gpt4 = "gpt-4" case gpt432k = "gpt-4-32k" - case gpt4TurboPreview = "gpt-4-turbo-preview" + case gpt4Turbo = "gpt-4-turbo" case gpt40314 = "gpt-4-0314" case gpt40613 = "gpt-4-0613" case gpt41106Preview = "gpt-4-1106-preview" case gpt4VisionPreview = "gpt-4-vision-preview" + case gpt4TurboPreview = "gpt-4-turbo-preview" + case gpt4Turbo20240409 = "gpt-4-turbo-2024-04-09" case gpt35Turbo0301 = "gpt-3.5-turbo-0301" case gpt35Turbo0613 = "gpt-3.5-turbo-0613" case gpt35Turbo1106 = "gpt-3.5-turbo-1106" @@ -57,12 +59,16 @@ public extension ChatGPTModel { return 128000 case .gpt40125: return 128000 + case .gpt4Turbo: + return 128000 + case .gpt4Turbo20240409: + return 128000 } } var supportsImages: Bool { switch self { - case .gpt4VisionPreview: + case .gpt4VisionPreview, .gpt4Turbo, .gpt4Turbo20240409: return true default: return false From e49b703820a6fd80e5c556880f602fdff8da9b38 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 15 Apr 2024 23:27:30 +0800 Subject: [PATCH 43/46] Update idle tab closing --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 84ceb281..a125a817 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 84ceb281a83d05e7aa60e16e7ad8172a5add76fd +Subproject commit a125a81755df2bb369cd278a667306b719486b74 From 548d8e320b3e156d5f8441510c1cb6d3db21503e Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 15 Apr 2024 23:34:59 +0800 Subject: [PATCH 44/46] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index a125a817..66867275 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit a125a81755df2bb369cd278a667306b719486b74 +Subproject commit 668672752669e512aa5f675d113a3ed9049e85c2 From a904b59b286ce99ef3964f7ffc36a9d32a7b69d4 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 11 Apr 2024 11:11:07 +0800 Subject: [PATCH 45/46] Bump version --- Version.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Version.xcconfig b/Version.xcconfig index b9735f6d..3aa40d04 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,3 +1,3 @@ -APP_VERSION = 0.31.3 -APP_BUILD = 343 +APP_VERSION = 0.32.0 +APP_BUILD = 360 From 5627c031afc83f1ab3d0f8dee0d896b8b65f38a3 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 15 Apr 2024 23:53:59 +0800 Subject: [PATCH 46/46] Update appcast.xml --- appcast.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/appcast.xml b/appcast.xml index 791b3150..65216067 100644 --- a/appcast.xml +++ b/appcast.xml @@ -3,6 +3,18 @@ Copilot for Xcode + + 0.32.0 + Mon, 15 Apr 2024 23:48:22 +0800 + 360 + 0.32.0 + 12.0 + + https://github.com/intitni/CopilotForXcode/releases/tag/0.32.0 + + + + 0.32.0 Thu, 11 Apr 2024 11:31:29 +0800