From 651b639d205bfa4b3ba0dd232992aa6529737043 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 16 Nov 2023 16:38:40 +0800 Subject: [PATCH 01/74] Support retrieval in context aware prompt to code --- .../OpenAIPromptToCodeService.swift | 4 ++-- .../PromptToCodeServiceType.swift | 14 +++++++++----- Core/Sources/Service/GUI/ChatTabFactory.swift | 3 ++- .../WindowBaseCommandHandler.swift | 3 ++- .../FeatureReducers/PromptToCode.swift | 6 +++++- .../FeatureReducers/PromptToCodeGroup.swift | 6 +++++- .../SuggestionPanelContent/PromptToCodePanel.swift | 2 ++ Pro | 2 +- .../SuggestionModel/EditorInformation.swift | 1 + Tool/Sources/SuggestionModel/ExportedFromLSP.swift | 4 ++++ 10 files changed, 33 insertions(+), 12 deletions(-) diff --git a/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift b/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift index b229cf79..d85e8c34 100644 --- a/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift +++ b/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift @@ -33,8 +33,8 @@ public final class OpenAIPromptToCodeService: PromptToCodeServiceType { let editor: EditorInformation = XcodeInspector.shared.focusedEditorContent ?? .init( editorContent: .init( - content: source.allCode, - lines: [], + content: source.content, + lines: source.lines, selections: [source.range], cursorPosition: .outOfScope, lineAnnotations: [] diff --git a/Core/Sources/PromptToCodeService/PromptToCodeServiceType.swift b/Core/Sources/PromptToCodeService/PromptToCodeServiceType.swift index 1716eb12..25d3cc6a 100644 --- a/Core/Sources/PromptToCodeService/PromptToCodeServiceType.swift +++ b/Core/Sources/PromptToCodeService/PromptToCodeServiceType.swift @@ -19,20 +19,23 @@ public struct PromptToCodeSource { public var language: CodeLanguage public var documentURL: URL public var projectRootURL: URL - public var allCode: String + public var content: String + public var lines: [String] public var range: CursorRange public init( language: CodeLanguage, documentURL: URL, projectRootURL: URL, - allCode: String, + content: String, + lines: [String], range: CursorRange ) { self.language = language self.documentURL = documentURL self.projectRootURL = projectRootURL - self.allCode = allCode + self.content = content + self.lines = lines self.range = range } } @@ -74,7 +77,8 @@ extension ContextAwarePromptToCodeService: PromptToCodeServiceType { language: source.language, documentURL: source.documentURL, projectRootURL: source.projectRootURL, - allCode: source.allCode, + content: source.content, + lines: source.lines, range: source.range ), isDetached: isDetached, @@ -86,7 +90,7 @@ extension ContextAwarePromptToCodeService: PromptToCodeServiceType { public struct PromptToCodeServiceFactoryDependencyKey: DependencyKey { public static let liveValue: () -> PromptToCodeServiceType = { - OpenAIPromptToCodeService() + ContextAwarePromptToCodeService() } public static let previewValue: () -> PromptToCodeServiceType = { diff --git a/Core/Sources/Service/GUI/ChatTabFactory.swift b/Core/Sources/Service/GUI/ChatTabFactory.swift index 82114837..0e68fb4b 100644 --- a/Core/Sources/Service/GUI/ChatTabFactory.swift +++ b/Core/Sources/Service/GUI/ChatTabFactory.swift @@ -92,7 +92,8 @@ enum ChatTabFactory { language: .plaintext, documentURL: .init(fileURLWithPath: "/"), projectRootURL: .init(fileURLWithPath: "/"), - allCode: prompt, + content: prompt, + lines: prompt.breakLines(), range: .outOfScope ), isDetached: true, diff --git a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift index d4668628..94884c33 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift @@ -424,7 +424,8 @@ extension WindowBaseCommandHandler { usesTabsForIndentation: filespace.codeMetadata.usesTabsForIndentation ?? false, documentURL: fileURL, projectRootURL: workspace.projectRootURL, - allCode: editor.content, + allCode: editor.content, + allLines: editor.lines, isContinuous: isContinuous, commandName: name, defaultPrompt: newPrompt ?? "", diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCode.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCode.swift index bfac06d0..b093abf5 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCode.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCode.swift @@ -57,6 +57,7 @@ public struct PromptToCode: ReducerProtocol { public var projectRootURL: URL public var documentURL: URL public var allCode: String + public var allLines: [String] public var extraSystemPrompt: String? public var generateDescriptionRequirement: Bool? public var commandName: String? @@ -76,6 +77,7 @@ public struct PromptToCode: ReducerProtocol { projectRootURL: URL, documentURL: URL, allCode: String, + allLines: [String], commandName: String? = nil, description: String = "", isResponding: Bool = false, @@ -101,6 +103,7 @@ public struct PromptToCode: ReducerProtocol { self.projectRootURL = projectRootURL self.documentURL = documentURL self.allCode = allCode + self.allLines = allLines self.extraSystemPrompt = extraSystemPrompt self.generateDescriptionRequirement = generateDescriptionRequirement self.isAttachedToSelectionRange = isAttachedToSelectionRange @@ -165,7 +168,8 @@ public struct PromptToCode: ReducerProtocol { language: copiedState.language, documentURL: copiedState.documentURL, projectRootURL: copiedState.projectRootURL, - allCode: copiedState.allCode, + content: copiedState.allCode, + lines: copiedState.allLines, range: copiedState.selectionRange ?? .outOfScope ), isDetached: !copiedState.isAttachedToSelectionRange, diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift index 9012535a..3ef47a51 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift @@ -31,6 +31,7 @@ public struct PromptToCodeGroup: ReducerProtocol { public var documentURL: URL public var projectRootURL: URL public var allCode: String + public var allLines: [String] public var isContinuous: Bool public var commandName: String? public var defaultPrompt: String @@ -46,6 +47,7 @@ public struct PromptToCodeGroup: ReducerProtocol { documentURL: URL, projectRootURL: URL, allCode: String, + allLines: [String], isContinuous: Bool, commandName: String?, defaultPrompt: String, @@ -60,6 +62,7 @@ public struct PromptToCodeGroup: ReducerProtocol { self.documentURL = documentURL self.projectRootURL = projectRootURL self.allCode = allCode + self.allLines = allLines self.isContinuous = isContinuous self.commandName = commandName self.defaultPrompt = defaultPrompt @@ -100,7 +103,8 @@ public struct PromptToCodeGroup: ReducerProtocol { usesTabsForIndentation: s.usesTabsForIndentation, projectRootURL: s.projectRootURL, documentURL: s.documentURL, - allCode: s.allCode, + allCode: s.allCode, + allLines: s.allLines, commandName: s.commandName, isContinuous: s.isContinuous, selectionRange: s.selectionRange, diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift index b20190aa..195955ed 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift @@ -430,6 +430,7 @@ struct PromptToCodePanel_Preview: PreviewProvider { projectRootURL: URL(fileURLWithPath: "path/to/file.txt"), documentURL: URL(fileURLWithPath: "path/to/file.txt"), allCode: "", + allLines: [], commandName: "Generate Code", description: "Hello world", isResponding: false, @@ -460,6 +461,7 @@ struct PromptToCodePanel_Error_Detached_Preview: PreviewProvider { projectRootURL: URL(fileURLWithPath: "path/to/file.txt"), documentURL: URL(fileURLWithPath: "path/to/file.txt"), allCode: "", + allLines: [], commandName: "Generate Code", description: "Hello world", isResponding: false, diff --git a/Pro b/Pro index ce6f1630..01fc14a8 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit ce6f1630793d46564d658d511d423048edc443f3 +Subproject commit 01fc14a82c7fa7dda747e9976dd6315573db8e8d diff --git a/Tool/Sources/SuggestionModel/EditorInformation.swift b/Tool/Sources/SuggestionModel/EditorInformation.swift index 21b73cd0..40d15c37 100644 --- a/Tool/Sources/SuggestionModel/EditorInformation.swift +++ b/Tool/Sources/SuggestionModel/EditorInformation.swift @@ -85,6 +85,7 @@ public struct EditorInformation { } public static func lines(in code: [String], containing range: CursorRange) -> [String] { + guard !code.isEmpty else { return [] } let startIndex = min(max(0, range.start.line), code.endIndex - 1) let endIndex = min(max(startIndex, range.end.line), code.endIndex - 1) let selectedLines = code[startIndex...endIndex] diff --git a/Tool/Sources/SuggestionModel/ExportedFromLSP.swift b/Tool/Sources/SuggestionModel/ExportedFromLSP.swift index 6bad79d5..3e4b91c0 100644 --- a/Tool/Sources/SuggestionModel/ExportedFromLSP.swift +++ b/Tool/Sources/SuggestionModel/ExportedFromLSP.swift @@ -48,6 +48,10 @@ public struct CursorRange: Codable, Hashable, Sendable, Equatable, CustomStringC return start == end } + public var isOneLine: Bool { + return start.line == end.line + } + public static func == (lhs: CursorRange, rhs: CursorRange) -> Bool { return lhs.start == rhs.start && lhs.end == rhs.end } From ba79efd21f13ba1acc379ea407fffe14c694fa0c Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 16 Nov 2023 16:45:09 +0800 Subject: [PATCH 02/74] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 01fc14a8..14b23331 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 01fc14a82c7fa7dda747e9976dd6315573db8e8d +Subproject commit 14b233313f5d7f9fc5274233cce1e7ab8780fe33 From 6e0895c9f46f7c3cfe30ef660e34aa42df609580 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 16 Nov 2023 19:25:53 +0800 Subject: [PATCH 03/74] Rename FocusCodeFinder to FocusCodeFinderType --- .../Sources/FocusedCodeFinder/ActiveDocumentContext.swift | 2 +- Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift | 4 ++-- Tool/Sources/FocusedCodeFinder/ObjectiveCCodeFinder.swift | 8 ++++++++ .../FocusedCodeFinder/SwiftFocusedCodeFinder.swift | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 Tool/Sources/FocusedCodeFinder/ObjectiveCCodeFinder.swift diff --git a/Tool/Sources/FocusedCodeFinder/ActiveDocumentContext.swift b/Tool/Sources/FocusedCodeFinder/ActiveDocumentContext.swift index f10977d4..43c4a08a 100644 --- a/Tool/Sources/FocusedCodeFinder/ActiveDocumentContext.swift +++ b/Tool/Sources/FocusedCodeFinder/ActiveDocumentContext.swift @@ -92,7 +92,7 @@ public struct ActiveDocumentContext { } public mutating func moveToCodeContainingRange(_ range: CursorRange) { - let finder: FocusedCodeFinder = { + let finder: FocusedCodeFinderType = { switch language { case .builtIn(.swift): return SwiftFocusedCodeFinder() diff --git a/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift index 5203bb51..a37b4f2e 100644 --- a/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift @@ -46,14 +46,14 @@ public struct CodeContext: Equatable { } } -public protocol FocusedCodeFinder { +public protocol FocusedCodeFinderType { func findFocusedCode( containingRange: CursorRange, activeDocumentContext: ActiveDocumentContext ) -> CodeContext } -public struct UnknownLanguageFocusedCodeFinder: FocusedCodeFinder { +public struct UnknownLanguageFocusedCodeFinder: FocusedCodeFinderType { let proposedSearchRange: Int public init(proposedSearchRange: Int) { diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveCCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveCCodeFinder.swift new file mode 100644 index 00000000..57c75a58 --- /dev/null +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveCCodeFinder.swift @@ -0,0 +1,8 @@ +// +// File.swift +// +// +// Created by Shangxin Guo on 2023/11/16. +// + +import Foundation diff --git a/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift index 0da2abe7..4bd49479 100644 --- a/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift @@ -5,7 +5,7 @@ import SuggestionModel import SwiftParser import SwiftSyntax -public struct SwiftFocusedCodeFinder: FocusedCodeFinder { +public struct SwiftFocusedCodeFinder: FocusedCodeFinderType { public let maxFocusedCodeLineCount: Int public init( From 9c47de5660e7e872a56d9035f246183ec3cffa50 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 16 Nov 2023 23:40:33 +0800 Subject: [PATCH 04/74] Simplify interface of focused code finder --- Pro | 2 +- Tool/Package.swift | 1 + .../ActiveDocumentChatContextCollector.swift | 2 +- .../ActiveDocumentContext.swift | 22 +++---- .../FocusedCodeFinder/FocusedCodeFinder.swift | 57 +++++++++++++++---- .../SwiftFocusedCodeFinder.swift | 34 +++++------ 6 files changed, 74 insertions(+), 44 deletions(-) diff --git a/Pro b/Pro index 14b23331..c916c131 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 14b233313f5d7f9fc5274233cce1e7ab8780fe33 +Subproject commit c916c1311f794682370aefaeef427f060073fd43 diff --git a/Tool/Package.swift b/Tool/Package.swift index de775e9a..e1727913 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -228,6 +228,7 @@ let package = Package( dependencies: [ "Preferences", "ASTParser", + "SuggestionModel", .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftParser", package: "swift-syntax"), ] diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift index d1cdeb3c..bdc89d4f 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift @@ -74,7 +74,7 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { func getActiveDocumentContext(_ info: EditorInformation) -> ActiveDocumentContext { var activeDocumentContext = activeDocumentContext ?? .init( - filePath: "", + documentURL: .init(fileURLWithPath: "/"), relativePath: "", language: .builtIn(.swift), fileContent: "", diff --git a/Tool/Sources/FocusedCodeFinder/ActiveDocumentContext.swift b/Tool/Sources/FocusedCodeFinder/ActiveDocumentContext.swift index 43c4a08a..19c49d9d 100644 --- a/Tool/Sources/FocusedCodeFinder/ActiveDocumentContext.swift +++ b/Tool/Sources/FocusedCodeFinder/ActiveDocumentContext.swift @@ -2,7 +2,7 @@ import Foundation import SuggestionModel public struct ActiveDocumentContext { - public var filePath: String + public var documentURL: URL public var relativePath: String public var language: CodeLanguage public var fileContent: String @@ -52,7 +52,7 @@ public struct ActiveDocumentContext { public var focusedContext: FocusedContext? public init( - filePath: String, + documentURL: URL, relativePath: String, language: CodeLanguage, fileContent: String, @@ -63,7 +63,7 @@ public struct ActiveDocumentContext { imports: [String], focusedContext: FocusedContext? = nil ) { - self.filePath = filePath + self.documentURL = documentURL self.relativePath = relativePath self.language = language self.fileContent = fileContent @@ -92,18 +92,12 @@ public struct ActiveDocumentContext { } public mutating func moveToCodeContainingRange(_ range: CursorRange) { - let finder: FocusedCodeFinderType = { - switch language { - case .builtIn(.swift): - return SwiftFocusedCodeFinder() - default: - return UnknownLanguageFocusedCodeFinder(proposedSearchRange: 5) - } - }() - + let finder = FocusedCodeFinder() + let codeContext = finder.findFocusedCode( + in: .init(documentURL: documentURL, content: fileContent, lines: lines), containingRange: range, - activeDocumentContext: self + language: language ) imports = codeContext.imports @@ -141,7 +135,7 @@ public struct ActiveDocumentContext { return false }() - filePath = info.documentURL.path + documentURL = info.documentURL relativePath = info.relativePath language = info.language fileContent = info.editorContent?.content ?? "" diff --git a/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift index a37b4f2e..b5f0b20e 100644 --- a/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift @@ -3,7 +3,7 @@ import SuggestionModel public struct CodeContext: Equatable { public typealias ScopeContext = ActiveDocumentContext.FocusedContext.Context - + public enum Scope: Equatable { case file case top @@ -46,10 +46,45 @@ public struct CodeContext: Equatable { } } +public struct FocusedCodeFinder { + public init() {} + + public struct Document { + var documentURL: URL + var content: String + var lines: [String] + + public init(documentURL: URL, content: String, lines: [String]) { + self.documentURL = documentURL + self.content = content + self.lines = lines + } + } + + public func findFocusedCode( + in document: Document, + containingRange: CursorRange, + language: CodeLanguage + ) -> CodeContext { + let finder: FocusedCodeFinderType = { + switch language { + case .builtIn(.swift): + return SwiftFocusedCodeFinder() + default: + return UnknownLanguageFocusedCodeFinder(proposedSearchRange: 5) + } + }() + + return finder.findFocusedCode(in: document, containingRange: containingRange) + } +} + public protocol FocusedCodeFinderType { + typealias Document = FocusedCodeFinder.Document + func findFocusedCode( - containingRange: CursorRange, - activeDocumentContext: ActiveDocumentContext + in document: Document, + containingRange: CursorRange ) -> CodeContext } @@ -61,15 +96,15 @@ public struct UnknownLanguageFocusedCodeFinder: FocusedCodeFinderType { } public func findFocusedCode( - containingRange: CursorRange, - activeDocumentContext: ActiveDocumentContext + in document: Document, + containingRange: CursorRange ) -> CodeContext { - guard !activeDocumentContext.lines.isEmpty else { return .empty } + guard !document.lines.isEmpty else { return .empty } // when user is not selecting any code. if containingRange.start == containingRange.end { // search up and down for up to `proposedSearchRange * 2 + 1` lines. - let lines = activeDocumentContext.lines + let lines = document.lines let proposedLineCount = proposedSearchRange * 2 + 1 let startLineIndex = max(containingRange.start.line - proposedSearchRange, 0) let endLineIndex = min( @@ -102,13 +137,13 @@ public struct UnknownLanguageFocusedCodeFinder: FocusedCodeFinderType { } let startLine = max(containingRange.start.line, 0) - let endLine = min(containingRange.end.line, activeDocumentContext.lines.count - 1) + let endLine = min(containingRange.end.line, document.lines.count - 1) if endLine < startLine { return .empty } - let focusedLines = activeDocumentContext.lines[startLine...endLine] + let focusedLines = document.lines[startLine...endLine] let contextStartLine = max(startLine - 3, 0) - let contextEndLine = min(endLine + 3, activeDocumentContext.lines.count - 1) + let contextEndLine = min(endLine + 3, document.lines.count - 1) return CodeContext( scope: .top, @@ -116,7 +151,7 @@ public struct UnknownLanguageFocusedCodeFinder: FocusedCodeFinderType { start: .init(line: contextStartLine, character: 0), end: .init( line: contextEndLine, - character: activeDocumentContext.lines[contextEndLine].count + character: document.lines[contextEndLine].count ) ), focusedRange: containingRange, diff --git a/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift index 4bd49479..7870b5b1 100644 --- a/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift @@ -16,15 +16,15 @@ public struct SwiftFocusedCodeFinder: FocusedCodeFinderType { } public func findFocusedCode( - containingRange range: CursorRange, - activeDocumentContext: ActiveDocumentContext + in document: Document, + containingRange range: CursorRange ) -> CodeContext { - let source = activeDocumentContext.fileContent + let source = document.content #warning("TODO: cache the tree") let tree = Parser.parse(source: source) let locationConverter = SourceLocationConverter( - file: activeDocumentContext.filePath, + file: document.documentURL.path, tree: tree ) @@ -52,8 +52,8 @@ public struct SwiftFocusedCodeFinder: FocusedCodeFinderType { node, parentNodes: nodes, tree: tree, - activeDocumentContext: activeDocumentContext, - locationConverter: locationConverter + locationConverter: locationConverter, + in: document ) if context?.canBeUsedAsCodeRange ?? false { focusedNode = node @@ -62,10 +62,7 @@ public struct SwiftFocusedCodeFinder: FocusedCodeFinderType { } guard let focusedNode else { var result = UnknownLanguageFocusedCodeFinder(proposedSearchRange: 8) - .findFocusedCode( - containingRange: range, - activeDocumentContext: activeDocumentContext - ) + .findFocusedCode(in: document, containingRange: range) result.imports = visitor.imports return result } @@ -74,8 +71,11 @@ public struct SwiftFocusedCodeFinder: FocusedCodeFinderType { codeRange = range } - let result = EditorInformation - .code(in: activeDocumentContext.lines, inside: codeRange, ignoreColumns: true) + let result = EditorInformation.code( + in: document.lines, + inside: codeRange, + ignoreColumns: true + ) var code = result.code @@ -108,8 +108,8 @@ public struct SwiftFocusedCodeFinder: FocusedCodeFinderType { node, parentNodes: nodes, tree: tree, - activeDocumentContext: activeDocumentContext, - locationConverter: locationConverter + locationConverter: locationConverter, + in: document ) if let context { @@ -148,15 +148,15 @@ extension SwiftFocusedCodeFinder { _ node: SyntaxProtocol, parentNodes: [SyntaxProtocol], tree: SourceFileSyntax, - activeDocumentContext: ActiveDocumentContext, - locationConverter: SourceLocationConverter + locationConverter: SourceLocationConverter, + in document: Document ) -> (context: ContextInfo?, more: Bool) { func convertRange(_ node: SyntaxProtocol) -> CursorRange { .init(sourceRange: node.sourceRange(converter: locationConverter)) } func extractText(_ node: SyntaxProtocol) -> String { - EditorInformation.code(in: activeDocumentContext.lines, inside: convertRange(node)).code + EditorInformation.code(in: document.lines, inside: convertRange(node)).code } switch node { From 1d89fcc4c03b241f531559362926631829d29c54 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 26 Nov 2023 00:10:56 +0800 Subject: [PATCH 05/74] Remove tree sitter for Swift --- Tool/Package.swift | 7 +------ Tool/Sources/ASTParser/ASTParser.swift | 4 ---- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/Tool/Package.swift b/Tool/Package.swift index e1727913..dec19946 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -63,11 +63,7 @@ let package = Package( .package(url: "https://github.com/GottaGetSwifty/CodableWrappers", from: "2.0.7"), // TreeSitter - .package(url: "https://github.com/ChimeHQ/SwiftTreeSitter", from: "0.7.1"), - .package( - url: "https://github.com/alex-pinkus/tree-sitter-swift", - branch: "with-generated-files" - ), + .package(url: "https://github.com/intitni/SwiftTreeSitter.git", branch: "main"), .package(url: "https://github.com/lukepistrol/tree-sitter-objc", branch: "feature/spm"), ], targets: [ @@ -197,7 +193,6 @@ let package = Package( "SuggestionModel", .product(name: "SwiftTreeSitter", package: "SwiftTreeSitter"), .product(name: "TreeSitterObjC", package: "tree-sitter-objc"), - .product(name: "TreeSitterSwift", package: "tree-sitter-swift"), ]), .testTarget(name: "ASTParserTests", dependencies: ["ASTParser"]), diff --git a/Tool/Sources/ASTParser/ASTParser.swift b/Tool/Sources/ASTParser/ASTParser.swift index 257eb704..dd11d709 100644 --- a/Tool/Sources/ASTParser/ASTParser.swift +++ b/Tool/Sources/ASTParser/ASTParser.swift @@ -2,16 +2,12 @@ import SuggestionModel import SwiftTreeSitter import tree_sitter import TreeSitterObjC -import TreeSitterSwift public enum ParsableLanguage { - case swift case objectiveC var tsLanguage: UnsafeMutablePointer { switch self { - case .swift: - return tree_sitter_swift() case .objectiveC: return tree_sitter_objc() } From 6b040e3f75930bf9c358416589aec5afec66fd62 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 26 Nov 2023 00:11:38 +0800 Subject: [PATCH 06/74] Update syntax tree dump --- Tool/Sources/ASTParser/DumpSyntaxTree.swift | 91 ++++++++------------- 1 file changed, 35 insertions(+), 56 deletions(-) diff --git a/Tool/Sources/ASTParser/DumpSyntaxTree.swift b/Tool/Sources/ASTParser/DumpSyntaxTree.swift index 2cebc92d..185d897e 100644 --- a/Tool/Sources/ASTParser/DumpSyntaxTree.swift +++ b/Tool/Sources/ASTParser/DumpSyntaxTree.swift @@ -1,76 +1,55 @@ import SwiftTreeSitter +import SwiftUI public extension ASTTree { /// Dumps the syntax tree as a string, for debugging purposes. - func dump() -> String { + func dump() -> AttributedString { guard let tree, let root = tree.rootNode else { return "" } - var result = "" + var result: AttributedString = "" - let appendNode: (_ level: Int, _ node: Node) -> Void = { level, node in + let appendNode: (_ level: Int, _ node: Node, _ name: String) -> Void = { + level, node, name in let range = node.pointRange let lowerBoundL = range.lowerBound.row let lowerBoundC = range.lowerBound.column / 2 let upperBoundL = range.upperBound.row let upperBoundC = range.upperBound.column / 2 - let line = - "\(String(repeating: " ", count: level))\(node.nodeType ?? "N/A") [\(lowerBoundL), \(lowerBoundC)] - [\(upperBoundL), \(upperBoundC)]" - result += line + "\n" + let indentation = AttributedString(String(repeating: " ", count: level)) + let nodeInfo = { + if name.isEmpty { + return AttributedString(node.nodeType ?? "N/A", attributes: .init([ + .foregroundColor: NSColor.blue, + ])) + } else { + var string = AttributedString("\(name): ", attributes: .init([ + .foregroundColor: NSColor.brown, + ])) + string.append(AttributedString(node.nodeType ?? "N/A", attributes: .init([ + .foregroundColor: NSColor.blue, + ]))) + return string + } + }() + let rangeText = "[\(lowerBoundL), \(lowerBoundC)] - [\(upperBoundL), \(upperBoundC)]" + + var line: AttributedString = "" + line.append(indentation) + line.append(nodeInfo) + line.append(AttributedString(" \(rangeText)\n")) + + result.append(line) } - guard let node = root.descendant(in: root.byteRange) else { return result } - - appendNode(0, node) - - let cursor = node.treeCursor - let level = 0 - - if cursor.goToFirstChild(for: node.byteRange.lowerBound) == false { - return result - } - - cursor.enumerateCurrentAndDescendents(level: level + 1) { level, node in - appendNode(level, node) - } - - while cursor.goToNextSibling() { - guard let node = cursor.currentNode else { - assertionFailure("no current node when gotoNextSibling succeeded") - break - } - - // once we are past the interesting range, stop - if node.byteRange.lowerBound > root.byteRange.upperBound { - break - } - - cursor.enumerateCurrentAndDescendents(level: level + 1) { level, node in - appendNode(level, node) + func enumerate(_ node: Node, level: Int, name: String) { + appendNode(level, node, name) + for i in 0.. Void) rethrows { - if let node = currentNode { - try block(level, node) - } - - if goToFirstChild() == false { - return - } - - try enumerateCurrentAndDescendents(level: level + 1, block: block) - - while goToNextSibling() { - try enumerateCurrentAndDescendents(level: level + 1, block: block) - } - - let success = gotoParent() - - assert(success) - } -} - From 71bc6e7d195c2cfaeac1bbd45f22e37f9a54b412 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 26 Nov 2023 15:09:23 +0800 Subject: [PATCH 07/74] Fix code finder tests --- Tool/Package.swift | 4 + .../File.swift | 1 + .../SwiftFocusedCodeFinderTests.swift | 227 ++++++++++++------ ...nknownLanguageFocusedCodeFinderTests.swift | 61 +++-- 4 files changed, 180 insertions(+), 113 deletions(-) create mode 100644 Tool/Tests/ActiveDocumentChatContextCollectorTests/File.swift rename Tool/Tests/{ActiveDocumentChatContextCollectorTests => FocusedCodeFinderTests}/SwiftFocusedCodeFinderTests.swift (70%) rename Tool/Tests/{ActiveDocumentChatContextCollectorTests => FocusedCodeFinderTests}/UnknownLanguageFocusedCodeFinderTests.swift (77%) diff --git a/Tool/Package.swift b/Tool/Package.swift index dec19946..0332e8dd 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -228,6 +228,10 @@ let package = Package( .product(name: "SwiftParser", package: "swift-syntax"), ] ), + .testTarget( + name: "FocusedCodeFinderTests", + dependencies: ["FocusedCodeFinder"] + ), // MARK: - Services diff --git a/Tool/Tests/ActiveDocumentChatContextCollectorTests/File.swift b/Tool/Tests/ActiveDocumentChatContextCollectorTests/File.swift new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/Tool/Tests/ActiveDocumentChatContextCollectorTests/File.swift @@ -0,0 +1 @@ + diff --git a/Tool/Tests/ActiveDocumentChatContextCollectorTests/SwiftFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift similarity index 70% rename from Tool/Tests/ActiveDocumentChatContextCollectorTests/SwiftFocusedCodeFinderTests.swift rename to Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift index 88355c9c..3ee1577e 100644 --- a/Tool/Tests/ActiveDocumentChatContextCollectorTests/SwiftFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift @@ -1,24 +1,19 @@ +import FocusedCodeFinder import Foundation import SuggestionModel import XCTest -import FocusedCodeFinder @testable import ActiveDocumentChatContextCollector -final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { - func context(code: String) -> ActiveDocumentContext { - .init( - filePath: "", - relativePath: "", - language: .builtIn(.swift), - fileContent: code, - lines: code.components(separatedBy: "\n").map { "\($0)\n" }, - selectedCode: "", selectionRange: .zero, - lineAnnotations: [], - imports: [] - ) - } +func document(code: String) -> FocusedCodeFinder.Document { + .init( + documentURL: URL(fileURLWithPath: "/"), + content: code, + lines: code.components(separatedBy: "\n").map { "\($0)\n" } + ) +} +final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { func test_collecting_imports() { let code = """ import var Darwin.stderr @@ -32,8 +27,8 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { end: CursorPosition(line: 2, character: 1) ) let context = SwiftFocusedCodeFinder().findFocusedCode( - containingRange: range, - activeDocumentContext: context(code: code) + in: document(code: code), + containingRange: range ) XCTAssertEqual(context.imports, ["Darwin.stderr", "Bar"]) } @@ -55,13 +50,21 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { end: CursorPosition(line: 4, character: 13) ) let context = SwiftFocusedCodeFinder().findFocusedCode( - containingRange: range, - activeDocumentContext: context(code: code) + in: document(code: code), + containingRange: range ) XCTAssertEqual(context, .init( scope: .scope(signature: [ - "public struct A: B, C", - "@ViewBuilder private func f(_ a: String) -> String", + .init( + signature: "public struct A: B, C", + name: "A", + range: .init(startPair: (0, 0), endPair: (8, 1)) + ), + .init( + signature: "@ViewBuilder private func f(_ a: String) -> String", + name: "f", + range: .init(startPair: (2, 0), endPair: (7, 5)) + ), ]), contextRange: .init(startPair: (0, 0), endPair: (8, 1)), focusedRange: .init(startPair: (4, 0), endPair: (4, 13)), @@ -69,7 +72,8 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { let c = 3 """, - imports: [] + imports: [], + includes: [] )) } @@ -91,12 +95,16 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { end: CursorPosition(line: 7, character: 5) ) let context = SwiftFocusedCodeFinder().findFocusedCode( - containingRange: range, - activeDocumentContext: context(code: code) + in: document(code: code), + containingRange: range ) XCTAssertEqual(context, .init( scope: .scope(signature: [ - "@MainActor public struct A: B, C", + .init( + signature: "@MainActor public struct A: B, C", + name: "A", + range: .init(startPair: (0, 0), endPair: (9, 1)) + ), ]), contextRange: .init(startPair: (0, 0), endPair: (9, 1)), focusedRange: .init(startPair: (2, 0), endPair: (7, 5)), @@ -109,7 +117,8 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { let e = 5 """, - imports: [] + imports: [], + includes: [] )) } @@ -128,12 +137,16 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { end: CursorPosition(line: 1, character: 9) ) let context = SwiftFocusedCodeFinder().findFocusedCode( - containingRange: range, - activeDocumentContext: context(code: code) + in: document(code: code), + containingRange: range ) XCTAssertEqual(context, .init( scope: .scope(signature: [ - "@MainActor final public class A: P, K", + .init( + signature: "@MainActor final public class A: P, K", + name: "A", + range: .init(startPair: (0, 0), endPair: (6, 1)) + ), ]), contextRange: .init(startPair: (0, 0), endPair: (6, 1)), focusedRange: .init(startPair: (1, 0), endPair: (1, 9)), @@ -141,7 +154,8 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { var a = 1 """, - imports: [] + imports: [], + includes: [] )) } @@ -160,12 +174,16 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { end: CursorPosition(line: 1, character: 9) ) let context = SwiftFocusedCodeFinder().findFocusedCode( - containingRange: range, - activeDocumentContext: context(code: code) + in: document(code: code), + containingRange: range ) XCTAssertEqual(context, .init( scope: .scope(signature: [ - "public protocol A: Hashable", + .init( + signature: "public protocol A: Hashable", + name: "A", + range: .init(startPair: (1, 0), endPair: (1, 9)) + ), ]), contextRange: .init(startPair: (0, 0), endPair: (6, 1)), focusedRange: .init(startPair: (1, 0), endPair: (1, 9)), @@ -173,7 +191,8 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { func f() """, - imports: [] + imports: [], + includes: [] )) } @@ -192,12 +211,16 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { end: CursorPosition(line: 1, character: 9) ) let context = SwiftFocusedCodeFinder().findFocusedCode( - containingRange: range, - activeDocumentContext: context(code: code) + in: document(code: code), + containingRange: range ) XCTAssertEqual(context, .init( scope: .scope(signature: [ - "private extension A: Equatable", + .init( + signature: "private extension A: Equatable", + name: "A", + range: .init(startPair: (1, 0), endPair: (1, 9)) + ), ]), contextRange: .init(startPair: (0, 0), endPair: (6, 1)), focusedRange: .init(startPair: (1, 0), endPair: (1, 9)), @@ -205,7 +228,8 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { var a = 1 """, - imports: [] + imports: [], + includes: [] )) } @@ -225,12 +249,16 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { end: CursorPosition(line: 2, character: 9) ) let context = SwiftFocusedCodeFinder().findFocusedCode( - containingRange: range, - activeDocumentContext: context(code: code) + in: document(code: code), + containingRange: range ) XCTAssertEqual(context, .init( scope: .scope(signature: [ - "@gloablActor public actor A", + .init( + signature: "@gloablActor public actor A", + name: "A", + range: .init(startPair: (2, 0), endPair: (2, 9)) + ), ]), contextRange: .init(startPair: (0, 0), endPair: (7, 1)), focusedRange: .init(startPair: (2, 0), endPair: (2, 9)), @@ -238,7 +266,8 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { static func f() {} """, - imports: [] + imports: [], + includes: [] )) } @@ -259,12 +288,16 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { end: CursorPosition(line: 3, character: 9) ) let context = SwiftFocusedCodeFinder().findFocusedCode( - containingRange: range, - activeDocumentContext: context(code: code) + in: document(code: code), + containingRange: range ) XCTAssertEqual(context, .init( scope: .scope(signature: [ - "@MainActor public indirect enum A", + .init( + signature: "@MainActor public indirect enum A", + name: "A", + range: .init(startPair: (3, 0), endPair: (3, 9)) + ), ]), contextRange: .init(startPair: (0, 0), endPair: (8, 1)), focusedRange: .init(startPair: (3, 0), endPair: (3, 9)), @@ -272,7 +305,8 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { case a """, - imports: [] + imports: [], + includes: [] )) } @@ -293,13 +327,21 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { end: CursorPosition(line: 2, character: 9) ) let context = SwiftFocusedCodeFinder().findFocusedCode( - containingRange: range, - activeDocumentContext: context(code: code) + in: document(code: code), + containingRange: range ) XCTAssertEqual(context, .init( scope: .scope(signature: [ - "struct A", - "@SomeWrapper public private(set) var a: Int", + .init( + signature: "struct A", + name: "A", + range: .init(startPair: (2, 0), endPair: (2, 9)) + ), + .init( + signature: "@SomeWrapper public private(set) var a: Int", + name: "a", + range: .init(startPair: (1, 0), endPair: (7, 4)) + ), ]), contextRange: .init(startPair: (0, 0), endPair: (8, 1)), focusedRange: .init(startPair: (2, 0), endPair: (2, 9)), @@ -307,7 +349,8 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { let a = 1 """, - imports: [] + imports: [], + includes: [] )) } @@ -317,19 +360,6 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { } final class SwiftFocusedCodeFinder_FocusedCode_Tests: XCTestCase { - func context(code: String) -> ActiveDocumentContext { - .init( - filePath: "", - relativePath: "", - language: .builtIn(.swift), - fileContent: code, - lines: code.components(separatedBy: "\n").map { "\($0)\n" }, - selectedCode: "", selectionRange: .zero, - lineAnnotations: [], - imports: [] - ) - } - func test_get_focused_code_on_top_level_should_fallback_to_unknown_language() { let code = """ @MainActor @@ -341,7 +371,7 @@ final class SwiftFocusedCodeFinder_FocusedCode_Tests: XCTestCase { case d case e } - + func hello() { print("hello") print("hello") @@ -349,8 +379,8 @@ final class SwiftFocusedCodeFinder_FocusedCode_Tests: XCTestCase { """ let range = CursorRange(startPair: (0, 0), endPair: (0, 0)) let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: 1000).findFocusedCode( - containingRange: range, - activeDocumentContext: context(code: code) + in: document(code: code), + containingRange: range ) XCTAssertEqual(context, .init( scope: .top, @@ -366,17 +396,18 @@ final class SwiftFocusedCodeFinder_FocusedCode_Tests: XCTestCase { case d case e } - + func hello() { print("hello") print("hello") } - + """, - imports: [] + imports: [], + includes: [] )) } - + func test_get_focused_code_inside_enum_the_whole_enum_will_be_the_focused_code() { let code = """ @MainActor @@ -391,8 +422,8 @@ final class SwiftFocusedCodeFinder_FocusedCode_Tests: XCTestCase { """ let range = CursorRange(startPair: (3, 0), endPair: (3, 0)) let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: 1000).findFocusedCode( - containingRange: range, - activeDocumentContext: context(code: code) + in: document(code: code), + containingRange: range ) XCTAssertEqual(context, .init( scope: .file, @@ -408,12 +439,13 @@ final class SwiftFocusedCodeFinder_FocusedCode_Tests: XCTestCase { case d case e } - + """, - imports: [] + imports: [], + includes: [] )) } - + func test_get_focused_code_inside_enum_with_limited_max_line_count() { let code = """ @MainActor @@ -428,8 +460,8 @@ final class SwiftFocusedCodeFinder_FocusedCode_Tests: XCTestCase { """ let range = CursorRange(startPair: (3, 0), endPair: (3, 0)) let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: 3).findFocusedCode( - containingRange: range, - activeDocumentContext: context(code: code) + in: document(code: code), + containingRange: range ) XCTAssertEqual(context, .init( scope: .file, @@ -439,9 +471,46 @@ final class SwiftFocusedCodeFinder_FocusedCode_Tests: XCTestCase { indirect enum A { case a case b - + """, - imports: [] + imports: [], + includes: [] )) } } + +final class SwiftFocusedCodeFinder_Import_Tests: XCTestCase { + func test_parsing_imports() { + let code = """ + import OpTop + import Second + import Third + + struct Foo { + + } + + import BelowStructFoo + + class Bar { + + } + + import BelowClassBar + """ + + let range = CursorRange.zero + let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: 3).findFocusedCode( + in: document(code: code), + containingRange: range + ) + XCTAssertEqual(context.imports, [ + "OnTop", + "Second", + "Third", + "BelowStructFoo", + "BelowClassBar", + ]) + } +} + diff --git a/Tool/Tests/ActiveDocumentChatContextCollectorTests/UnknownLanguageFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift similarity index 77% rename from Tool/Tests/ActiveDocumentChatContextCollectorTests/UnknownLanguageFocusedCodeFinderTests.swift rename to Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift index 2e840f5f..291a9f11 100644 --- a/Tool/Tests/ActiveDocumentChatContextCollectorTests/UnknownLanguageFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift @@ -1,100 +1,93 @@ -import XCTest -import Foundation import FocusedCodeFinder +import Foundation +import XCTest @testable import ActiveDocumentChatContextCollector class UnknownLanguageFocusedCodeFinderTests: XCTestCase { - func context(code: String) -> ActiveDocumentContext { - .init( - filePath: "", - relativePath: "", - language: .builtIn(.swift), - fileContent: code, - lines: code.components(separatedBy: "\n").map { "\($0)\n" }, - selectedCode: "", selectionRange: .zero, - lineAnnotations: [], - imports: [] - ) - } - func test_the_code_is_long_enough_for_the_search_range() { let code = stride(from: 0, through: 100, by: 1).map { "\($0)\n" }.joined() let context = UnknownLanguageFocusedCodeFinder(proposedSearchRange: 5) .findFocusedCode( - containingRange: .init(startPair: (50, 0), endPair: (50, 0)), - activeDocumentContext: self.context(code: code) + in: document(code: code), + containingRange: .init(startPair: (50, 0), endPair: (50, 0)) ) XCTAssertEqual(context, .init( scope: .top, contextRange: .init(startPair: (40, 0), endPair: (60, 3)), focusedRange: .init(startPair: (45, 0), endPair: (55, 3)), focusedCode: stride(from: 45, through: 55, by: 1).map { "\($0)\n" }.joined(), - imports: [] + imports: [], + includes: [] )) } - + func test_the_upper_side_is_not_long_enough_expand_the_lower_end() { let code = stride(from: 0, through: 100, by: 1).map { "\($0)\n" }.joined() let context = UnknownLanguageFocusedCodeFinder(proposedSearchRange: 5) .findFocusedCode( - containingRange: .init(startPair: (2, 0), endPair: (2, 0)), - activeDocumentContext: self.context(code: code) + in: document(code: code), + containingRange: .init(startPair: (2, 0), endPair: (2, 0)) ) XCTAssertEqual(context, .init( scope: .top, contextRange: .init(startPair: (0, 0), endPair: (15, 3)), focusedRange: .init(startPair: (0, 0), endPair: (10, 3)), focusedCode: stride(from: 0, through: 10, by: 1).map { "\($0)\n" }.joined(), - imports: [] + imports: [], + includes: [] )) } - + func test_the_lower_side_is_not_long_enough_do_not_expand_the_upper_end() { let code = stride(from: 0, through: 100, by: 1).map { "\($0)\n" }.joined() let context = UnknownLanguageFocusedCodeFinder(proposedSearchRange: 5) .findFocusedCode( - containingRange: .init(startPair: (99, 0), endPair: (99, 0)), - activeDocumentContext: self.context(code: code) + in: document(code: code), + containingRange: .init(startPair: (99, 0), endPair: (99, 0)) ) XCTAssertEqual(context, .init( scope: .top, contextRange: .init(startPair: (89, 0), endPair: (101, 1)), focusedRange: .init(startPair: (94, 0), endPair: (101, 1)), focusedCode: stride(from: 94, through: 100, by: 1).map { "\($0)\n" }.joined() + "\n", - imports: [] + imports: [], + includes: [] )) } - + func test_both_sides_are_just_long_enough() { let code = stride(from: 0, through: 10, by: 1).map { "\($0)\n" }.joined() let context = UnknownLanguageFocusedCodeFinder(proposedSearchRange: 5) .findFocusedCode( - containingRange: .init(startPair: (5, 0), endPair: (5, 0)), - activeDocumentContext: self.context(code: code) + in: document(code: code), + containingRange: .init(startPair: (5, 0), endPair: (5, 0)) ) XCTAssertEqual(context, .init( scope: .top, contextRange: .init(startPair: (0, 0), endPair: (11, 1)), focusedRange: .init(startPair: (0, 0), endPair: (10, 3)), focusedCode: code, - imports: [] + imports: [], + includes: [] )) } - + func test_both_sides_are_not_long_enough() { let code = stride(from: 0, through: 4, by: 1).map { "\($0)\n" }.joined() let context = UnknownLanguageFocusedCodeFinder(proposedSearchRange: 5) .findFocusedCode( - containingRange: .init(startPair: (3, 0), endPair: (3, 0)), - activeDocumentContext: self.context(code: code) + in: document(code: code), + containingRange: .init(startPair: (3, 0), endPair: (3, 0)) ) XCTAssertEqual(context, .init( scope: .top, contextRange: .init(startPair: (0, 0), endPair: (5, 1)), focusedRange: .init(startPair: (0, 0), endPair: (5, 1)), focusedCode: code + "\n", - imports: [] + imports: [], + includes: [] )) } } + From 2b027e9685d67c8a7010e6e16421a58260e420dd Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 26 Nov 2023 15:10:20 +0800 Subject: [PATCH 08/74] Update focused code finder --- Tool/Sources/ASTParser/TreeCursor.swift | 18 +-- .../FocusedCodeFinder/FocusedCodeFinder.swift | 5 +- .../{ => Swift}/SwiftFocusedCodeFinder.swift | 142 +----------------- .../SwiftScopeHierarchySyntaxVisitor.swift | 142 ++++++++++++++++++ 4 files changed, 157 insertions(+), 150 deletions(-) rename Tool/Sources/FocusedCodeFinder/{ => Swift}/SwiftFocusedCodeFinder.swift (77%) create mode 100644 Tool/Sources/FocusedCodeFinder/Swift/SwiftScopeHierarchySyntaxVisitor.swift diff --git a/Tool/Sources/ASTParser/TreeCursor.swift b/Tool/Sources/ASTParser/TreeCursor.swift index 7cade565..26d27cae 100644 --- a/Tool/Sources/ASTParser/TreeCursor.swift +++ b/Tool/Sources/ASTParser/TreeCursor.swift @@ -1,7 +1,7 @@ import Foundation import SwiftTreeSitter -extension TreeCursor { +public extension TreeCursor { /// Deep first search nodes. /// - Parameter skipChildren: Check if children of a `Node` should be skipped. func deepFirstSearch( @@ -13,7 +13,7 @@ extension TreeCursor { // MARK: - Search -protocol Cursor { +public protocol Cursor { associatedtype Node var currentNode: Node? { get } func goToFirstChild() -> Bool @@ -22,32 +22,32 @@ protocol Cursor { } extension TreeCursor: Cursor { - func goToNextSibling() -> Bool { + public func goToNextSibling() -> Bool { gotoNextSibling() } - - func goToParent() -> Bool { + + public func goToParent() -> Bool { gotoParent() } } -struct CursorDeepFirstSearchSequence: Sequence { +public struct CursorDeepFirstSearchSequence: Sequence { let cursor: C let skipChildren: (C.Node) -> Bool - func makeIterator() -> CursorDeepFirstSearchIterator { + public func makeIterator() -> CursorDeepFirstSearchIterator { return CursorDeepFirstSearchIterator( cursor: cursor, skipChildren: skipChildren ) } - struct CursorDeepFirstSearchIterator: IteratorProtocol { + public struct CursorDeepFirstSearchIterator: IteratorProtocol { let cursor: C let skipChildren: (C.Node) -> Bool var isEnded = false - mutating func next() -> C.Node? { + public mutating func next() -> C.Node? { guard !isEnded else { return nil } let currentNode = cursor.currentNode let hasChild = { diff --git a/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift index b5f0b20e..051f2ff0 100644 --- a/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift @@ -26,6 +26,7 @@ public struct CodeContext: Equatable { public var focusedRange: CursorRange public var focusedCode: String public var imports: [String] + public var includes: [String] public static var empty: CodeContext { .init(scope: .file, contextRange: .zero, focusedRange: .zero, focusedCode: "", imports: []) @@ -36,13 +37,15 @@ public struct CodeContext: Equatable { contextRange: CursorRange, focusedRange: CursorRange, focusedCode: String, - imports: [String] + imports: [String], + includes: [String] ) { self.scope = scope self.contextRange = contextRange self.focusedRange = focusedRange self.focusedCode = focusedCode self.imports = imports + self.includes = includes } } diff --git a/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift similarity index 77% rename from Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift rename to Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift index 7870b5b1..1ae64a03 100644 --- a/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift @@ -131,7 +131,8 @@ public struct SwiftFocusedCodeFinder: FocusedCodeFinderType { contextRange: contextRange, focusedRange: codeRange, focusedCode: code, - imports: visitor.imports + imports: visitor.imports, + includes: [] ) } } @@ -465,142 +466,3 @@ extension String { } } -// MARK: - Visitors - -extension SwiftFocusedCodeFinder { - final class SwiftScopeHierarchySyntaxVisitor: SyntaxVisitor { - let tree: SyntaxProtocol - let code: String - let range: CursorRange - let locationConverter: SourceLocationConverter - - var imports: [String] = [] - private var _scopeHierarchy: [SyntaxProtocol] = [] - - /// The nodes containing the current range, sorted from inner to outer. - func findScopeHierarchy(_ node: some SyntaxProtocol) -> [SyntaxProtocol] { - walk(node) - return _scopeHierarchy.sorted { $0.position.utf8Offset > $1.position.utf8Offset } - } - - /// The nodes containing the current range, sorted from inner to outer. - func findScopeHierarchy() -> [SyntaxProtocol] { - walk(tree) - return _scopeHierarchy.sorted { $0.position.utf8Offset > $1.position.utf8Offset } - } - - init( - tree: SyntaxProtocol, - code: String, - range: CursorRange, - locationConverter: SourceLocationConverter - ) { - self.tree = tree - self.code = code - self.range = range - self.locationConverter = locationConverter - super.init(viewMode: .sourceAccurate) - } - - func skipChildrenIfPossible(_ node: SyntaxProtocol) -> SyntaxVisitorContinueKind { - if _scopeHierarchy.count > 5 { return .skipChildren } - if !nodeContainsRange(node) { return .skipChildren } - return .visitChildren - } - - func captureNodeIfPossible(_ node: SyntaxProtocol) -> SyntaxVisitorContinueKind { - if _scopeHierarchy.count > 5 { return .skipChildren } - if !nodeContainsRange(node) { return .skipChildren } - _scopeHierarchy.append(node) - return .visitChildren - } - - func nodeContainsRange(_ node: SyntaxProtocol) -> Bool { - let sourceRange = node.sourceRange(converter: locationConverter) - let cursorRange = CursorRange(sourceRange: sourceRange) - return cursorRange.strictlyContains(range) - } - - // skip if possible - - override func visit(_ node: MemberDeclBlockSyntax) -> SyntaxVisitorContinueKind { - skipChildrenIfPossible(node) - } - - override func visit(_ node: MemberDeclListItemSyntax) -> SyntaxVisitorContinueKind { - skipChildrenIfPossible(node) - } - - // capture if possible - - override func visit(_ node: ImportDeclSyntax) -> SyntaxVisitorContinueKind { - imports.append(node.path.trimmedDescription) - return .skipChildren - } - - override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { - captureNodeIfPossible(node) - } - - override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { - captureNodeIfPossible(node) - } - - override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { - captureNodeIfPossible(node) - } - - override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind { - captureNodeIfPossible(node) - } - - override func visit(_ node: MacroDeclSyntax) -> SyntaxVisitorContinueKind { - captureNodeIfPossible(node) - } - - override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { - captureNodeIfPossible(node) - } - - override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { - captureNodeIfPossible(node) - } - - override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { - captureNodeIfPossible(node) - } - - override func visit(_ node: SubscriptDeclSyntax) -> SyntaxVisitorContinueKind { - captureNodeIfPossible(node) - } - - override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind { - captureNodeIfPossible(node) - } - - override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind { - captureNodeIfPossible(node) - } - - override func visit(_ node: AccessorDeclSyntax) -> SyntaxVisitorContinueKind { - captureNodeIfPossible(node) - } - - override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { - captureNodeIfPossible(node) - } - - override func visit(_ node: DeinitializerDeclSyntax) -> SyntaxVisitorContinueKind { - captureNodeIfPossible(node) - } - - override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind { - captureNodeIfPossible(node) - } - - override func visit(_ node: SwitchCaseSyntax) -> SyntaxVisitorContinueKind { - captureNodeIfPossible(node) - } - } -} - diff --git a/Tool/Sources/FocusedCodeFinder/Swift/SwiftScopeHierarchySyntaxVisitor.swift b/Tool/Sources/FocusedCodeFinder/Swift/SwiftScopeHierarchySyntaxVisitor.swift new file mode 100644 index 00000000..e16ea15d --- /dev/null +++ b/Tool/Sources/FocusedCodeFinder/Swift/SwiftScopeHierarchySyntaxVisitor.swift @@ -0,0 +1,142 @@ +import ASTParser +import Foundation +import Preferences +import SuggestionModel +import SwiftParser +import SwiftSyntax + +final class SwiftScopeHierarchySyntaxVisitor: SyntaxVisitor { + let tree: SyntaxProtocol + let code: String + let range: CursorRange + let locationConverter: SourceLocationConverter + + var imports: [String] = [] + private var _scopeHierarchy: [SyntaxProtocol] = [] + + /// The nodes containing the current range, sorted from inner to outer. + func findScopeHierarchy(_ node: some SyntaxProtocol) -> [SyntaxProtocol] { + walk(node) + return _scopeHierarchy.sorted { $0.position.utf8Offset > $1.position.utf8Offset } + } + + /// The nodes containing the current range, sorted from inner to outer. + func findScopeHierarchy() -> [SyntaxProtocol] { + walk(tree) + return _scopeHierarchy.sorted { $0.position.utf8Offset > $1.position.utf8Offset } + } + + init( + tree: SyntaxProtocol, + code: String, + range: CursorRange, + locationConverter: SourceLocationConverter + ) { + self.tree = tree + self.code = code + self.range = range + self.locationConverter = locationConverter + super.init(viewMode: .sourceAccurate) + } + + func skipChildrenIfPossible(_ node: SyntaxProtocol) -> SyntaxVisitorContinueKind { + if _scopeHierarchy.count > 5 { return .skipChildren } + if !nodeContainsRange(node) { return .skipChildren } + return .visitChildren + } + + func captureNodeIfPossible(_ node: SyntaxProtocol) -> SyntaxVisitorContinueKind { + if _scopeHierarchy.count > 5 { return .skipChildren } + if !nodeContainsRange(node) { return .skipChildren } + _scopeHierarchy.append(node) + return .visitChildren + } + + func nodeContainsRange(_ node: SyntaxProtocol) -> Bool { + let sourceRange = node.sourceRange(converter: locationConverter) + let cursorRange = CursorRange(sourceRange: sourceRange) + return cursorRange.strictlyContains(range) + } + + // skip if possible + + override func visit(_ node: MemberDeclBlockSyntax) -> SyntaxVisitorContinueKind { + skipChildrenIfPossible(node) + } + + override func visit(_ node: MemberDeclListItemSyntax) -> SyntaxVisitorContinueKind { + skipChildrenIfPossible(node) + } + + // capture if possible + + override func visit(_ node: ImportDeclSyntax) -> SyntaxVisitorContinueKind { + imports.append(node.path.trimmedDescription) + return .skipChildren + } + + override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { + captureNodeIfPossible(node) + } + + override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { + captureNodeIfPossible(node) + } + + override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { + captureNodeIfPossible(node) + } + + override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind { + captureNodeIfPossible(node) + } + + override func visit(_ node: MacroDeclSyntax) -> SyntaxVisitorContinueKind { + captureNodeIfPossible(node) + } + + override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { + captureNodeIfPossible(node) + } + + override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { + captureNodeIfPossible(node) + } + + override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { + captureNodeIfPossible(node) + } + + override func visit(_ node: SubscriptDeclSyntax) -> SyntaxVisitorContinueKind { + captureNodeIfPossible(node) + } + + override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind { + captureNodeIfPossible(node) + } + + override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind { + captureNodeIfPossible(node) + } + + override func visit(_ node: AccessorDeclSyntax) -> SyntaxVisitorContinueKind { + captureNodeIfPossible(node) + } + + override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { + captureNodeIfPossible(node) + } + + override func visit(_ node: DeinitializerDeclSyntax) -> SyntaxVisitorContinueKind { + captureNodeIfPossible(node) + } + + override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind { + captureNodeIfPossible(node) + } + + override func visit(_ node: SwitchCaseSyntax) -> SyntaxVisitorContinueKind { + captureNodeIfPossible(node) + } +} + From 3516f5799a71c9c65af1753c8d6955f94d257267 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 26 Nov 2023 15:11:56 +0800 Subject: [PATCH 09/74] Fix imports --- .../FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift | 3 +-- .../UnknownLanguageFocusedCodeFinderTests.swift | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift index 3ee1577e..553392ed 100644 --- a/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift @@ -1,9 +1,8 @@ -import FocusedCodeFinder import Foundation import SuggestionModel import XCTest -@testable import ActiveDocumentChatContextCollector +@testable import FocusedCodeFinder func document(code: String) -> FocusedCodeFinder.Document { .init( diff --git a/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift index 291a9f11..f540bff2 100644 --- a/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift @@ -1,8 +1,8 @@ -import FocusedCodeFinder import Foundation +import SuggestionModel import XCTest -@testable import ActiveDocumentChatContextCollector +@testable import FocusedCodeFinder class UnknownLanguageFocusedCodeFinderTests: XCTestCase { func test_the_code_is_long_enough_for_the_search_range() { From f9c530161b168881abc3e7c325c060720dfdb650 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 26 Nov 2023 22:02:35 +0800 Subject: [PATCH 10/74] Add ObjectiveCFocusedCodeFinder --- Pro | 2 +- Tool/Sources/ASTParser/ASTTreeVisitor.swift | 61 ++++ .../FocusedCodeFinder/FocusedCodeFinder.swift | 92 +---- .../KnownLanguageFocusedCodeFinder.swift | 208 +++++++++++ .../ObjectiveC/ObjectiveCCodeFinder.swift | 334 ++++++++++++++++++ ...bjectiveCScopeHierarchySyntaxVisitor.swift | 127 +++++++ .../ObjectiveC/ObjectiveCSyntax.swift | 132 +++++++ .../ObjectiveCCodeFinder.swift | 8 - .../Swift/SwiftFocusedCodeFinder.swift | 242 ++++--------- .../SwiftScopeHierarchySyntaxVisitor.swift | 9 +- .../UnknownLanguageFocusCodeFinder.swift | 77 ++++ Tool/Sources/Preferences/Keys.swift | 4 +- .../ObjectiveCFocusedCodeFinderTests.swift | 5 + .../SwiftFocusedCodeFinderTests.swift | 19 - 14 files changed, 1040 insertions(+), 280 deletions(-) create mode 100644 Tool/Sources/ASTParser/ASTTreeVisitor.swift create mode 100644 Tool/Sources/FocusedCodeFinder/KnownLanguageFocusedCodeFinder.swift create mode 100644 Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift create mode 100644 Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCScopeHierarchySyntaxVisitor.swift create mode 100644 Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCSyntax.swift delete mode 100644 Tool/Sources/FocusedCodeFinder/ObjectiveCCodeFinder.swift create mode 100644 Tool/Sources/FocusedCodeFinder/UnknownLanguageFocusCodeFinder.swift create mode 100644 Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift diff --git a/Pro b/Pro index c916c131..c0ffa886 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit c916c1311f794682370aefaeef427f060073fd43 +Subproject commit c0ffa88685b35c90643d71a1feb9faa6ce624898 diff --git a/Tool/Sources/ASTParser/ASTTreeVisitor.swift b/Tool/Sources/ASTParser/ASTTreeVisitor.swift new file mode 100644 index 00000000..32043cf8 --- /dev/null +++ b/Tool/Sources/ASTParser/ASTTreeVisitor.swift @@ -0,0 +1,61 @@ +import Foundation +import SwiftTreeSitter + +public enum ASTTreeVisitorContinueKind { + /// The visitor should visit the descendants of the current node. + case visitChildren + /// The visitor should avoid visiting the descendants of the current node. + case skipChildren +} + +// A SwiftSyntax style tree visitor. +open class ASTTreeVisitor { + public let tree: ASTTree + + public init(tree: ASTTree) { + self.tree = tree + } + + public func walk() { + guard let cursor = tree.rootNode?.treeCursor else { return } + visit(cursor) + } + + public func walk(_ node: ASTNode) { + let cursor = node.treeCursor + visit(cursor) + } + + open func visit(_: ASTNode) -> ASTTreeVisitorContinueKind { + // do nothing + return .skipChildren + } + + open func visitPost(_: ASTNode) { + // do nothing + } + + private func visit(_ cursor: TreeCursor) { + guard let currentNode = cursor.currentNode else { return } + let continueKind = visit(currentNode) + + switch continueKind { + case .skipChildren: + visitPost(currentNode) + case .visitChildren: + visitChildren(cursor) + visitPost(currentNode) + } + } + + private func visitChildren(_ cursor: TreeCursor) { + let hasChild = cursor.goToFirstChild() + guard hasChild else { return } + visit(cursor) + while cursor.goToNextSibling() { + visit(cursor) + } + _ = cursor.gotoParent() + } +} + diff --git a/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift index 051f2ff0..6a082944 100644 --- a/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift @@ -29,7 +29,14 @@ public struct CodeContext: Equatable { public var includes: [String] public static var empty: CodeContext { - .init(scope: .file, contextRange: .zero, focusedRange: .zero, focusedCode: "", imports: []) + .init( + scope: .file, + contextRange: .zero, + focusedRange: .zero, + focusedCode: "", + imports: [], + includes: [] + ) } public init( @@ -51,19 +58,19 @@ public struct CodeContext: Equatable { public struct FocusedCodeFinder { public init() {} - + public struct Document { var documentURL: URL var content: String var lines: [String] - + public init(documentURL: URL, content: String, lines: [String]) { self.documentURL = documentURL self.content = content self.lines = lines } } - + public func findFocusedCode( in document: Document, containingRange: CursorRange, @@ -73,6 +80,8 @@ public struct FocusedCodeFinder { switch language { case .builtIn(.swift): return SwiftFocusedCodeFinder() + case .builtIn(.objc), .builtIn(.objcpp), .builtIn(.c): + return ObjectiveCFocusedCodeFinder() default: return UnknownLanguageFocusedCodeFinder(proposedSearchRange: 5) } @@ -84,83 +93,10 @@ public struct FocusedCodeFinder { public protocol FocusedCodeFinderType { typealias Document = FocusedCodeFinder.Document - + func findFocusedCode( in document: Document, containingRange: CursorRange ) -> CodeContext } -public struct UnknownLanguageFocusedCodeFinder: FocusedCodeFinderType { - let proposedSearchRange: Int - - public init(proposedSearchRange: Int) { - self.proposedSearchRange = proposedSearchRange - } - - public func findFocusedCode( - in document: Document, - containingRange: CursorRange - ) -> CodeContext { - guard !document.lines.isEmpty else { return .empty } - - // when user is not selecting any code. - if containingRange.start == containingRange.end { - // search up and down for up to `proposedSearchRange * 2 + 1` lines. - let lines = document.lines - let proposedLineCount = proposedSearchRange * 2 + 1 - let startLineIndex = max(containingRange.start.line - proposedSearchRange, 0) - let endLineIndex = min( - max( - startLineIndex, - min(startLineIndex + proposedLineCount - 1, lines.count - 1) - ), - lines.count - 1 - ) - - guard endLineIndex >= startLineIndex else { return .empty } - let focusedLines = lines[startLineIndex...endLineIndex] - - let contextStartLine = max(startLineIndex - 5, 0) - let contextEndLine = min(endLineIndex + 5, lines.count - 1) - - return .init( - scope: .top, - contextRange: .init( - start: .init(line: contextStartLine, character: 0), - end: .init(line: contextEndLine, character: lines[contextEndLine].count) - ), - focusedRange: .init( - start: .init(line: startLineIndex, character: 0), - end: .init(line: endLineIndex, character: lines[endLineIndex].count) - ), - focusedCode: focusedLines.joined(), - imports: [] - ) - } - - let startLine = max(containingRange.start.line, 0) - let endLine = min(containingRange.end.line, document.lines.count - 1) - - if endLine < startLine { return .empty } - - let focusedLines = document.lines[startLine...endLine] - let contextStartLine = max(startLine - 3, 0) - let contextEndLine = min(endLine + 3, document.lines.count - 1) - - return CodeContext( - scope: .top, - contextRange: .init( - start: .init(line: contextStartLine, character: 0), - end: .init( - line: contextEndLine, - character: document.lines[contextEndLine].count - ) - ), - focusedRange: containingRange, - focusedCode: focusedLines.joined(), - imports: [] - ) - } -} - diff --git a/Tool/Sources/FocusedCodeFinder/KnownLanguageFocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/KnownLanguageFocusedCodeFinder.swift new file mode 100644 index 00000000..09e8abab --- /dev/null +++ b/Tool/Sources/FocusedCodeFinder/KnownLanguageFocusedCodeFinder.swift @@ -0,0 +1,208 @@ +import Foundation +import Preferences +import SuggestionModel + +public typealias KnownLanguageFocusedCodeFinder = + BaseKnownLanguageFocusedCodeFinder & + KnownLanguageFocusedCodeFinderType + +public class BaseKnownLanguageFocusedCodeFinder { + public typealias TextProvider = (TextPosition) -> String + public typealias RangeConverter = (Node) -> CursorRange + + public struct NodeInfo { + var node: Node + var signature: String + var name: String + var canBeUsedAsCodeRange: Bool = true + } + + public struct ContextInfo { + var nodes: [Node] + var includes: [String] + var imports: [String] + } + + public let maxFocusedCodeLineCount: Int + + init( + maxFocusedCodeLineCount: Int = UserDefaults.shared.value(for: \.maxFocusedCodeLineCount) + ) { + self.maxFocusedCodeLineCount = maxFocusedCodeLineCount + } +} + +public protocol KnownLanguageFocusedCodeFinderType: FocusedCodeFinderType { + associatedtype Tree + associatedtype Node + associatedtype TextPosition + typealias Document = FocusedCodeFinder.Document + typealias Finder = BaseKnownLanguageFocusedCodeFinder + typealias NodeInfo = Finder.NodeInfo + typealias ContextInfo = Finder.ContextInfo + typealias TextProvider = Finder.TextProvider + typealias RangeConverter = Finder.RangeConverter + + var maxFocusedCodeLineCount: Int { get } + + func parseSyntaxTree(from document: Document) -> Tree? + + func collectContextNodes( + in document: Document, + tree: Tree, + containingRange: SuggestionModel.CursorRange, + textProvider: @escaping TextProvider, + rangeConverter: @escaping RangeConverter + ) -> ContextInfo + + func contextContainingNode( + _ node: Node, + textProvider: @escaping TextProvider + ) -> (nodeInfo: NodeInfo?, more: Bool) + + func createTextProviderAndRangeConverter( + for document: Document, + tree: Tree + ) -> (TextProvider, RangeConverter) +} + +public extension KnownLanguageFocusedCodeFinderType { + func findFocusedCode( + in document: Document, + containingRange range: SuggestionModel.CursorRange + ) -> CodeContext { + guard let tree = parseSyntaxTree(from: document) else { return .empty } + + let (textProvider, rangeConverter) = createTextProviderAndRangeConverter( + for: document, + tree: tree + ) + var contextInfo = collectContextNodes( + in: document, + tree: tree, + containingRange: range, + textProvider: textProvider, + rangeConverter: rangeConverter + ) + var codeRange: CursorRange + + let noSelection = range.isEmpty + if noSelection { + // use the first scope as code, the second as context + var focusedNode: Node? + while let node = contextInfo.nodes.first { + contextInfo.nodes.removeFirst() + let (nodeInfo, _) = contextContainingNode(node, textProvider: textProvider) + if nodeInfo?.canBeUsedAsCodeRange ?? false { + focusedNode = node + break + } + } + guard let focusedNode else { + // fallback to unknown language focused code finder when no scope found + var result = UnknownLanguageFocusedCodeFinder(proposedSearchRange: 8) + .findFocusedCode(in: document, containingRange: range) + result.imports = contextInfo.imports + result.includes = contextInfo.includes + return result + } + codeRange = rangeConverter(focusedNode) + } else { + // use the selection as code, the first scope as context + codeRange = range + } + + let (code, _, focusedRange) = extractFocusedCode( + in: codeRange, + in: document, + containingRange: range + ) + + let (contextRange, scopeContexts) = extractScopeContext( + contextNodes: contextInfo.nodes, + textProvider: textProvider, + rangeConverter: rangeConverter + ) + + return .init( + scope: scopeContexts.isEmpty ? .file : .scope(signature: scopeContexts), + contextRange: contextRange, + focusedRange: focusedRange, + focusedCode: code, + imports: contextInfo.imports, + includes: contextInfo.includes + ) + } +} + +extension KnownLanguageFocusedCodeFinderType { + func extractFocusedCode( + in codeRange: CursorRange, + in document: Document, + containingRange range: SuggestionModel.CursorRange + ) -> (code: String, lines: [String], codeRange: CursorRange) { + var codeRange = codeRange + let codeInCodeRange = EditorInformation.code( + in: document.lines, + inside: codeRange, + ignoreColumns: true + ) + + var code = codeInCodeRange.code + var lines = codeInCodeRange.lines + + if range.isEmpty, codeInCodeRange.lines.count > maxFocusedCodeLineCount { + // if the focused code is too long, truncate it to be shorter + let centerLine = range.start.line + let relativeCenterLine = centerLine - codeRange.start.line + let startLine = max(0, relativeCenterLine - maxFocusedCodeLineCount / 2) + let endLine = max( + startLine, + min(codeInCodeRange.lines.count - 1, startLine + maxFocusedCodeLineCount - 1) + ) + + lines = Array(codeInCodeRange.lines[startLine...endLine]) + code = lines.joined() + codeRange = .init( + start: .init(line: startLine + codeRange.start.line, character: 0), + end: .init( + line: endLine + codeRange.start.line, + character: codeInCodeRange.lines[endLine].count + ) + ) + } + + return (code, lines, codeRange) + } + + func extractScopeContext( + contextNodes: [Node], + textProvider: @escaping TextProvider, + rangeConverter: @escaping RangeConverter + ) -> (contextRange: CursorRange, scopeContexts: [CodeContext.ScopeContext]) { + var nodes = contextNodes + var contextRange = CursorRange.zero + var signature = [CodeContext.ScopeContext]() + + while let node = nodes.first { + nodes.removeFirst() + let (context, more) = contextContainingNode(node, textProvider: textProvider) + + if let context { + contextRange = rangeConverter(context.node) + signature.insert(.init( + signature: context.signature, + name: context.name, + range: contextRange + ), at: 0) + } + + if !more { + break + } + } + + return (contextRange, signature) + } +} + diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift new file mode 100644 index 00000000..071d0825 --- /dev/null +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift @@ -0,0 +1,334 @@ +import ASTParser +import Foundation +import Preferences +import SuggestionModel +import SwiftTreeSitter + +public enum TreeSitterTextPosition { + case node(ASTNode) + case range(range: NSRange, pointRange: Range) +} + +public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< + ASTTree, + ASTNode, + TreeSitterTextPosition +> { + override public init( + maxFocusedCodeLineCount: Int = UserDefaults.shared.value(for: \.maxFocusedCodeLineCount) + ) { + super.init(maxFocusedCodeLineCount: maxFocusedCodeLineCount) + } + + public func parseSyntaxTree(from document: Document) -> ASTTree? { + let parser = ASTParser(language: .objectiveC) + return parser.parse(document.content) + } + + public func collectContextNodes( + in document: Document, + tree: ASTTree, + containingRange range: CursorRange, + textProvider: @escaping TextProvider, + rangeConverter: @escaping RangeConverter + ) -> ContextInfo { + let visitor = ObjectiveCScopeHierarchySyntaxVisitor( + tree: tree, + code: document.content, + textProvider: { node in + textProvider(.node(node)) + }, + range: range + ) + + let nodes = visitor.findScopeHierarchy() + + return .init(nodes: nodes, includes: visitor.includes, imports: visitor.imports) + } + + public func createTextProviderAndRangeConverter( + for document: Document, + tree: ASTTree + ) -> (TextProvider, RangeConverter) { + ( + { position in + switch position { + case let .node(node): + return document.content.cursorTextProvider(node.range, node.pointRange) ?? "" + case let .range(range, pointRange): + return document.content.cursorTextProvider(range, pointRange) ?? "" + } + }, + { node in + CursorRange(pointRange: node.pointRange) + } + ) + } + + public func contextContainingNode( + _ node: Node, + textProvider: @escaping TextProvider + ) -> (nodeInfo: NodeInfo?, more: Bool) { + switch ObjectiveCNodeType(rawValue: node.nodeType ?? "") { + case .classInterface, .categoryInterface: + return parseClassInterfaceNode(node, textProvider: textProvider) + case .classImplementation, .categoryImplementation: + return parseClassImplementationNode(node, textProvider: textProvider) + case .protocolDeclaration: + return parseProtocolNode(node, textProvider: textProvider) + case .methodDefinition: + return parseMethodDefinitionNode(node, textProvider: textProvider) + case .functionDefinition: + return parseFunctionDefinitionNode(node, textProvider: textProvider) + case .structSpecifier, .enumSpecifier, .nsEnumSpecifier: + return parseTypeSpecifierNode(node, textProvider: textProvider) + case .typeDefinition: + return parseTypedefNode(node, textProvider: textProvider) + default: + return (nil, false) + } + } + + func parseClassInterfaceNode( + _ node: ASTNode, + textProvider: @escaping TextProvider + ) -> (nodeInfo: NodeInfo?, more: Bool) { + var name = "" + var superClass = "" + var category = "" + var protocols = [String]() + let children = node.children + for child in children { + if let nameNode = child.child(byFieldName: "name") { + name = textProvider(.node(nameNode)) + } + if let superClassNode = child.child(byFieldName: "superclass") { + superClass = textProvider(.node(superClassNode)) + } + if let categoryNode = child.child(byFieldName: "category") { + category = textProvider(.node(categoryNode)) + } + if let protocolsNode = child.child(byFieldName: "protocols") { + for protocolNode in protocolsNode.children { + let protocolName = textProvider(.node(protocolNode)) + if !protocolName.isEmpty { + protocols.append(protocolName) + } + } + } + } + + var signature = "@interface \(name)" + if !category.isEmpty { + signature += "(\(category))" + } + if !protocols.isEmpty { + signature += "<\(protocols.joined(separator: ","))>" + } + if !superClass.isEmpty { + signature += ": \(superClass)" + } + + return ( + .init( + node: node, + signature: signature, + name: name, + canBeUsedAsCodeRange: true + ), + false + ) + } + + func parseClassImplementationNode( + _ node: ASTNode, + textProvider: @escaping TextProvider + ) -> (nodeInfo: NodeInfo?, more: Bool) { + var name = "" + var superClass = "" + var category = "" + var protocols = [String]() + let children = node.children + for child in children { + if let nameNode = child.child(byFieldName: "name") { + name = textProvider(.node(nameNode)) + } + if let superClassNode = child.child(byFieldName: "superclass") { + superClass = textProvider(.node(superClassNode)) + } + if let categoryNode = child.child(byFieldName: "category") { + category = textProvider(.node(categoryNode)) + } + if let protocolsNode = child.child(byFieldName: "protocols") { + for protocolNode in protocolsNode.children { + let protocolName = textProvider(.node(protocolNode)) + if !protocolName.isEmpty { + protocols.append(protocolName) + } + } + } + } + + var signature = "@implement \(name)" + if !category.isEmpty { + signature += "(\(category))" + } + if !protocols.isEmpty { + signature += "<\(protocols.joined(separator: ","))>" + } + if !superClass.isEmpty { + signature += ": \(superClass)" + } + return ( + .init( + node: node, + signature: signature, + name: name, + canBeUsedAsCodeRange: true + ), + false + ) + } + + func parseProtocolNode( + _ node: ASTNode, + textProvider: @escaping TextProvider + ) -> (nodeInfo: NodeInfo?, more: Bool) { + var name = "" + var protocols = [String]() + let children = node.children + for child in children { + if let nameNode = child.child(byFieldName: "name") { + name = textProvider(.node(nameNode)) + } + if let protocolsNode = child.child(byFieldName: "protocols") { + for protocolNode in protocolsNode.children { + let protocolName = textProvider(.node(protocolNode)) + if !protocolName.isEmpty { + protocols.append(protocolName) + } + } + } + } + + var signature = "@protocol \(name)" + if !protocols.isEmpty { + signature += "<\(protocols.joined(separator: ","))>" + } + return ( + .init( + node: node, + signature: signature, + name: name, + canBeUsedAsCodeRange: true + ), + false + ) + } + + func parseMethodDefinitionNode( + _ node: ASTNode, + textProvider: @escaping TextProvider + ) -> (nodeInfo: NodeInfo?, more: Bool) { + parseSignatureBeforeBody(node, textProvider: textProvider) + } + + func parseFunctionDefinitionNode( + _ node: ASTNode, + textProvider: @escaping TextProvider + ) -> (nodeInfo: NodeInfo?, more: Bool) { + parseSignatureBeforeBody(node, textProvider: textProvider) + } + + func parseTypeSpecifierNode( + _ node: ASTNode, + textProvider: @escaping TextProvider + ) -> (nodeInfo: NodeInfo?, more: Bool) { + parseSignatureBeforeBody(node, textProvider: textProvider) + } + + func parseTypedefNode( + _ node: ASTNode, + textProvider: @escaping TextProvider + ) -> (nodeInfo: NodeInfo?, more: Bool) { + guard let typeNode = node.child(byFieldName: "type") else { return (nil, false) } + return parseSignatureBeforeBody(typeNode, textProvider: textProvider) + } +} + +// MARK: - Shared Parser + +extension ObjectiveCFocusedCodeFinder { + func parseSignatureBeforeBody( + _ node: ASTNode, + textProvider: @escaping TextProvider + ) -> (nodeInfo: NodeInfo?, more: Bool) { + let definitionRange = CursorRange(pointRange: node.pointRange) + let name = node.contentOfChild(withFieldName: "name", textProvider: textProvider) + let ( + _, + signatureRange, + signaturePointRange + ) = node.extractInformationBeforeNode(withFieldName: "body") + let signature = textProvider(.range(range: signatureRange, pointRange: signaturePointRange)) + .replacingOccurrences(of: "\n", with: "") + .trimmingCharacters(in: .whitespacesAndNewlines) + if signature.isEmpty { return (nil, false) } + return ( + .init( + node: node, + signature: signature, + name: name ?? "N/A", + canBeUsedAsCodeRange: false + ), + false + ) + } +} + +extension ASTNode { + func contentOfChild( + withFieldName name: String, + textProvider: (TreeSitterTextPosition) -> String + ) -> String? { + guard let child = child(byFieldName: name) else { return nil } + return textProvider(.node(child)) + } + + func extractInformationBeforeNode(withFieldName name: String) -> ( + postfixNode: ASTNode?, + range: NSRange, + pointRange: Range + ) { + guard let postfixNode = child(byFieldName: name) else { + return (nil, range, pointRange) + } + + let range = self.range.excluding(postfixNode.range) + let pointRange = self.pointRange.excluding(postfixNode.pointRange) + return (postfixNode, range, pointRange) + } +} + +extension NSRange { + func excluding(_ range: NSRange) -> NSRange { + let start = max(location, range.location) + let end = min(location + length, range.location + range.length) + return NSRange(location: start, length: end - start) + } +} + +extension Range where Bound == Point { + func excluding(_ range: Range) -> Range { + let start = Point( + row: Swift.max(lowerBound.row, range.lowerBound.row), + column: Swift.max(lowerBound.column, range.lowerBound.column) + ) + let end = Point( + row: Swift.min(upperBound.row, range.upperBound.row), + column: Swift.min(upperBound.column, range.upperBound.column) + ) + return Range(uncheckedBounds: (start, end)) + } +} + diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCScopeHierarchySyntaxVisitor.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCScopeHierarchySyntaxVisitor.swift new file mode 100644 index 00000000..3f710b41 --- /dev/null +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCScopeHierarchySyntaxVisitor.swift @@ -0,0 +1,127 @@ +import ASTParser +import Foundation +import Preferences +import SuggestionModel +import SwiftTreeSitter + +final class ObjectiveCScopeHierarchySyntaxVisitor: ASTTreeVisitor { + let range: CursorRange + let code: String + let textProvider: (ASTNode) -> String + var includes: [String] = [] + var imports: [String] = [] + private var _scopeHierarchy: [ASTNode] = [] + + init( + tree: ASTTree, + code: String, + textProvider: @escaping (ASTNode) -> String, + range: CursorRange + ) { + self.range = range + self.code = code + self.textProvider = textProvider + super.init(tree: tree) + } + + /// The nodes containing the current range, sorted from inner to outer. + func findScopeHierarchy(_ node: ASTNode) -> [ASTNode] { + walk(node) + return _scopeHierarchy.sorted { $0.range.location > $1.range.location } + } + + /// The nodes containing the current range, sorted from inner to outer. + func findScopeHierarchy() -> [ASTNode] { + walk() + return _scopeHierarchy.sorted { $0.range.location > $1.range.location } + } + + override func visit(_ node: ASTNode) -> ASTTreeVisitorContinueKind { + let cursorRange = CursorRange(pointRange: node.pointRange) + + switch ObjectiveCNodeType(rawValue: node.nodeType ?? "") { + case .translationUnit: + return .visitChildren + case .preprocInclude: + handlePreprocInclude(node) + return .skipChildren + case .preprocImport: + handlePreprocImport(node) + return .skipChildren + case .moduleImport: + handleModuleImport(node) + return .skipChildren + case .classInterface, .categoryInterface, .protocolDeclaration: + guard cursorRange.contains(range) else { return .skipChildren } + _scopeHierarchy.append(node) + return .skipChildren + case .classImplementation, .categoryImplementation: + guard cursorRange.contains(range) else { return .skipChildren } + _scopeHierarchy.append(node) + return .visitChildren + case .methodDefinition: + guard cursorRange.contains(range) else { return .skipChildren } + _scopeHierarchy.append(node) + return .visitChildren + case .typeDefinition: + guard cursorRange.contains(range) else { return .skipChildren } + _scopeHierarchy.append(node) + return .skipChildren + case .structSpecifier, .enumSpecifier, .nsEnumSpecifier: + guard cursorRange.contains(range) else { return .skipChildren } + _scopeHierarchy.append(node) + return .skipChildren + default: + return .skipChildren + } + } + + override func visitPost(_: ASTNode) {} + + // MARK: Imports + + func handlePreprocInclude(_ node: ASTNode) { + let children = node.children + for child in children { + if let pathNode = child.child(byFieldName: "path") { + let path = textProvider(pathNode) + if !path.isEmpty { + includes.append(path.replacingOccurrences(of: "\"", with: "")) + } + break + } + } + } + + func handlePreprocImport(_ node: ASTNode) { + let children = node.children + for child in children { + if let pathNode = child.child(byFieldName: "path") { + let path = textProvider(pathNode) + if !path.isEmpty { + imports.append( + path + .replacingOccurrences(of: "\"", with: "") + .replacingOccurrences(of: "<", with: "") + .replacingOccurrences(of: ">", with: "") + ) + } + break + } + } + } + + func handleModuleImport(_ node: ASTNode) { + let children = node.children + for child in children { + if let pathNode = child.child(byFieldName: "module") { + let path = textProvider(pathNode) + if !path.isEmpty { + imports.append(path) + } + break + } + } + } +} + diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCSyntax.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCSyntax.swift new file mode 100644 index 00000000..f2eb20ff --- /dev/null +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCSyntax.swift @@ -0,0 +1,132 @@ +import Foundation + +/// https://github.com/merico-dev/tree-sitter-objc/test/corpus/imports.txt +/// https://github.com/merico-dev/tree-sitter-objc/test/corpus/expressions.txt +/// https://github.com/merico-dev/tree-sitter-objc/test/corpus/declarations.txt +/// https://github.com/merico-dev/tree-sitter-objc/node-types.json +enum ObjectiveCNodeType: String { + /// The top most item + case translationUnit = "translation_unit" + /// `#include` + case preprocInclude = "preproc_include" + /// `#import "bar.h"` + case preprocImport = "preproc_import" + /// `@import foo.bar` + case moduleImport = "module_import" + /// ```objc + /// @interface ClassName(Category): SuperClass { + /// type1 iv1; + /// type2 iv2; + /// } + /// @property (readwrite, copy) float value; + /// + (tr)k1:(t1)a1 : (t2)a2 k2: a3; + /// @end + /// ``` + /// + /// will parse into: + /// ``` + /// (translation_unit + /// (class_interface + /// name: (identifier) + /// superclass: (identifier))) < SuperClass + /// protocols: (protocol_reference_list < Protocols + /// (identifier) + /// (identifier)))) + /// (field_declaration < iv1 + /// type: (type_identifier) + /// declarator: (field_identifier)) + /// (field_declaration < iv2 + /// type: (type_identifier) + /// declarator: (field_identifier)))) + /// (property_declaration ...) < property value + /// (method_declaration ...) < method + /// ``` + /// + case classInterface = "class_interface" + /// `@implementation` + case classImplementation = "class_implementation" + /// Similar to class interface. + case categoryInterface = "category_interface" + /// Similar to class implementation. + case categoryImplementation = "category_implementation" + /// Similar to class interface. + case protocolDeclaration = "protocol_declaration" + /// `@protocol ` + case protocolDeclarationList = "protocol_declaration_list" + /// ```objc + /// @class C1, C2; + /// ``` + /// + /// will parse into: + /// ``` + /// (translation_unit + /// (class_declaration_list + /// (identifier) + /// (identifier))) + /// ``` + case classDeclarationList = "class_declaration_list" + /// ``` + /// + (tr)k1: (t1)a1 : (t2)a2 k2: a3; + /// ``` + /// + /// will parse into: + /// ``` + /// (property_declaration + /// (readwrite) + /// (copy) + /// type: (type_identifier) < type + /// name: (identifier)))) < name + /// ``` + case propertyDeclaration = "property_declaration" + /// ```objc + /// + (tr)k1: (t1)a1 : (t2)a2 k2: a3; + /// ``` + /// + /// will parse into: + /// ``` + /// (method_declaration + /// scope: (class_scope) + /// return_type: (type_descriptor + /// type: (type_identifier)) + /// selector: (keyword_selector + /// (keyword_declarator + /// keyword: (identifier) + /// type: (type_descriptor + /// type: (type_identifier)) + /// name: (identifier)) + /// (keyword_declarator + /// type: (type_descriptor + /// type: (type_identifier)) + /// name: (identifier)) + /// (keyword_declarator + /// keyword: (identifier) + /// name: (identifier)))))) + /// ``` + case methodDeclaration = "method_declaration" + /// `- (rt)sel {}` + case methodDefinition = "method_definition" + /// function definitions + case functionDefinition = "function_definition" + /// Names of symbols + case identifier = "identifier" + /// Type identifiers + case typeIdentifier = "type_identifier" + /// Compound statements, such as `{ ... }` + case compoundStatement = "compound_statement" + /// Typedef. + case typeDefinition = "type_definition" + /// `struct {}`. + case structSpecifier = "struct_specifier" + /// `enum {}`. + case enumSpecifier = "enum_specifier" + /// `NS_ENUM {}` and `NS_OPTIONS {}`. + case nsEnumSpecifier = "ns_enum_specifier" + /// fields inside a type definition. + case fieldDeclarationList = "field_declaration_list" +} + +extension ObjectiveCNodeType { + init?(rawValue: String?) { + self.init(rawValue: rawValue ?? "") + } +} diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveCCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveCCodeFinder.swift deleted file mode 100644 index 57c75a58..00000000 --- a/Tool/Sources/FocusedCodeFinder/ObjectiveCCodeFinder.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// File.swift -// -// -// Created by Shangxin Guo on 2023/11/16. -// - -import Foundation diff --git a/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift index 1ae64a03..587a8de3 100644 --- a/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift @@ -5,159 +5,69 @@ import SuggestionModel import SwiftParser import SwiftSyntax -public struct SwiftFocusedCodeFinder: FocusedCodeFinderType { - public let maxFocusedCodeLineCount: Int - - public init( - maxFocusedCodeLineCount: Int = UserDefaults.shared - .value(for: \.maxFocusedCodeLineCount) +public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< + SourceFileSyntax, + SyntaxProtocol, + SyntaxProtocol +> { + override public init( + maxFocusedCodeLineCount: Int = UserDefaults.shared.value(for: \.maxFocusedCodeLineCount) ) { - self.maxFocusedCodeLineCount = maxFocusedCodeLineCount + super.init(maxFocusedCodeLineCount: maxFocusedCodeLineCount) } - public func findFocusedCode( - in document: Document, - containingRange range: CursorRange - ) -> CodeContext { - let source = document.content - #warning("TODO: cache the tree") - let tree = Parser.parse(source: source) - - let locationConverter = SourceLocationConverter( - file: document.documentURL.path, - tree: tree - ) + public func parseSyntaxTree(from document: Document) -> SourceFileSyntax? { + Parser.parse(source: document.content) + } + public func collectContextNodes( + in document: Document, + tree: SourceFileSyntax, + containingRange range: CursorRange, + textProvider: @escaping TextProvider, + rangeConverter: @escaping RangeConverter + ) -> ContextInfo { let visitor = SwiftScopeHierarchySyntaxVisitor( tree: tree, - code: source, + code: document.content, range: range, - locationConverter: locationConverter + rangeConverter: rangeConverter ) - var nodes = visitor.findScopeHierarchy() - - var codeRange: CursorRange - - func convertRange(_ node: SyntaxProtocol) -> CursorRange { - .init(sourceRange: node.sourceRange(converter: locationConverter)) - } - - if range.isEmpty { - // use the first scope as code, the second as context - var focusedNode: SyntaxProtocol? - while let node = nodes.first { - nodes.removeFirst() - let (context, _) = contextContainingNode( - node, - parentNodes: nodes, - tree: tree, - locationConverter: locationConverter, - in: document - ) - if context?.canBeUsedAsCodeRange ?? false { - focusedNode = node - break - } - } - guard let focusedNode else { - var result = UnknownLanguageFocusedCodeFinder(proposedSearchRange: 8) - .findFocusedCode(in: document, containingRange: range) - result.imports = visitor.imports - return result - } - codeRange = convertRange(focusedNode) - } else { - codeRange = range - } - - let result = EditorInformation.code( - in: document.lines, - inside: codeRange, - ignoreColumns: true - ) - - var code = result.code - - if range.isEmpty, result.lines.count > maxFocusedCodeLineCount { - // if the focused code is too long, truncate it to be shorter - let centerLine = range.start.line - let relativeCenterLine = centerLine - codeRange.start.line - let startLine = max(0, relativeCenterLine - maxFocusedCodeLineCount / 2) - let endLine = max( - startLine, - min(result.lines.count - 1, startLine + maxFocusedCodeLineCount - 1) - ) - - code = result.lines[startLine...endLine].joined() - codeRange = .init( - start: .init(line: startLine + codeRange.start.line, character: 0), - end: .init( - line: endLine + codeRange.start.line, - character: result.lines[endLine].count - ) - ) - } - - var contextRange = CursorRange.zero - var signature = [CodeContext.ScopeContext]() - - while let node = nodes.first { - nodes.removeFirst() - let (context, more) = contextContainingNode( - node, - parentNodes: nodes, - tree: tree, - locationConverter: locationConverter, - in: document - ) - - if let context { - contextRange = context.contextRange - signature.insert(.init( - signature: context.signature, - name: context.name, - range: context.contextRange - ), at: 0) - } - - if !more { - break - } - } - + let nodes = visitor.findScopeHierarchy() return .init( - scope: signature.isEmpty ? .file : .scope(signature: signature), - contextRange: contextRange, - focusedRange: codeRange, - focusedCode: code, - imports: visitor.imports, - includes: [] + nodes: nodes, + includes: [], + imports: visitor.imports ) } -} -extension SwiftFocusedCodeFinder { - struct ContextInfo { - var signature: String - var name: String - var contextRange: CursorRange - var canBeUsedAsCodeRange: Bool = true + public func createTextProviderAndRangeConverter( + for document: Document, + tree: SourceFileSyntax + ) -> (TextProvider, RangeConverter) { + let locationConverter = SourceLocationConverter( + file: document.documentURL.path, + tree: tree + ) + return ( + { node in + let range = CursorRange(sourceRange: node.sourceRange(converter: locationConverter)) + return EditorInformation.code(in: document.lines, inside: range).code + }, + { node in + let range = CursorRange(sourceRange: node.sourceRange(converter: locationConverter)) + return range + } + ) } - func contextContainingNode( + public func contextContainingNode( _ node: SyntaxProtocol, - parentNodes: [SyntaxProtocol], - tree: SourceFileSyntax, - locationConverter: SourceLocationConverter, - in document: Document - ) -> (context: ContextInfo?, more: Bool) { - func convertRange(_ node: SyntaxProtocol) -> CursorRange { - .init(sourceRange: node.sourceRange(converter: locationConverter)) - } - + textProvider: @escaping TextProvider + ) -> (nodeInfo: NodeInfo?, more: Bool) { func extractText(_ node: SyntaxProtocol) -> String { - EditorInformation.code(in: document.lines, inside: convertRange(node)).code + textProvider(node) } switch node { @@ -165,83 +75,83 @@ extension SwiftFocusedCodeFinder { let type = node.structKeyword.text let name = node.identifier.text return (.init( + node: node, signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) .replacingOccurrences(of: "\n", with: " "), - name: name, - contextRange: convertRange(node) + name: name ), false) case let node as ClassDeclSyntax: let type = node.classKeyword.text let name = node.identifier.text return (.init( + node: node, signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) .replacingOccurrences(of: "\n", with: " "), - name: name, - contextRange: convertRange(node) + name: name ), false) case let node as EnumDeclSyntax: let type = node.enumKeyword.text let name = node.identifier.text return (.init( + node: node, signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) .replacingOccurrences(of: "\n", with: " "), - name: name, - contextRange: convertRange(node) + name: name ), false) case let node as ActorDeclSyntax: let type = node.actorKeyword.text let name = node.identifier.text return (.init( + node: node, signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) .replacingOccurrences(of: "\n", with: ""), - name: name, - contextRange: convertRange(node) + name: name ), false) case let node as MacroDeclSyntax: let type = node.macroKeyword.text let name = node.identifier.text return (.init( + node: node, signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .replacingOccurrences(of: "\n", with: " "), - name: name, - contextRange: convertRange(node) + name: name ), false) case let node as ProtocolDeclSyntax: let type = node.protocolKeyword.text let name = node.identifier.text return (.init( + node: node, signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) .replacingOccurrences(of: "\n", with: " "), - name: name, - contextRange: convertRange(node) + name: name ), false) case let node as ExtensionDeclSyntax: let type = node.extensionKeyword.text let name = node.extendedType.trimmedDescription return (.init( + node: node, signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) .replacingOccurrences(of: "\n", with: " "), - name: name, - contextRange: convertRange(node) + name: name ), false) case let node as FunctionDeclSyntax: @@ -253,10 +163,10 @@ extension SwiftFocusedCodeFinder { .joined(separator: " ") return (.init( + node: node, signature: "\(type) \(name)\(signature)" .prefixedModifiers(node.modifierAndAttributeText(extractText)), - name: name, - contextRange: convertRange(node) + name: name ), true) case let node as VariableDeclSyntax: @@ -265,11 +175,11 @@ extension SwiftFocusedCodeFinder { let signature = node.bindings.first?.typeAnnotation?.trimmedDescription ?? "" return (.init( + node: node, signature: "\(type) \(name)\(signature.isEmpty ? "" : "\(signature)")" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .replacingOccurrences(of: "\n", with: " "), name: name, - contextRange: convertRange(node), canBeUsedAsCodeRange: false ), true) @@ -278,11 +188,11 @@ extension SwiftFocusedCodeFinder { let signature = keyword return (.init( + node: node, signature: signature .prefixedModifiers(node.modifierAndAttributeText(extractText)) .replacingOccurrences(of: "\n", with: " "), - name: keyword, - contextRange: convertRange(node) + name: keyword ), true) case let node as SubscriptDeclSyntax: @@ -292,59 +202,59 @@ extension SwiftFocusedCodeFinder { let signature = "subscript\(genericPClause)(\(pClause))\(whereClause)" return (.init( + node: node, signature: signature .prefixedModifiers(node.modifierAndAttributeText(extractText)) .replacingOccurrences(of: "\n", with: " "), - name: "subscript", - contextRange: convertRange(node) + name: "subscript" ), true) case let node as InitializerDeclSyntax: let signature = "init" return (.init( + node: node, signature: "\(signature)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .replacingOccurrences(of: "\n", with: " "), - name: "init", - contextRange: convertRange(node) + name: "init" ), true) case let node as DeinitializerDeclSyntax: let signature = "deinit" return (.init( + node: node, signature: signature .prefixedModifiers(node.modifierAndAttributeText(extractText)) .replacingOccurrences(of: "\n", with: " "), - name: "deinit", - contextRange: convertRange(node) + name: "deinit" ), true) case let node as ClosureExprSyntax: let signature = "closure" return (.init( + node: node, signature: signature.replacingOccurrences(of: "\n", with: " "), - name: "closure", - contextRange: convertRange(node) + name: "closure" ), true) case let node as FunctionCallExprSyntax: let signature = "function call" return (.init( + node: node, signature: signature.replacingOccurrences(of: "\n", with: " "), name: "function call", - contextRange: convertRange(node), canBeUsedAsCodeRange: false ), true) case let node as SwitchCaseSyntax: return (.init( + node: node, signature: node.trimmedDescription.replacingOccurrences(of: "\n", with: " "), - name: "switch", - contextRange: convertRange(node) + name: "switch" ), true) default: diff --git a/Tool/Sources/FocusedCodeFinder/Swift/SwiftScopeHierarchySyntaxVisitor.swift b/Tool/Sources/FocusedCodeFinder/Swift/SwiftScopeHierarchySyntaxVisitor.swift index e16ea15d..3da83270 100644 --- a/Tool/Sources/FocusedCodeFinder/Swift/SwiftScopeHierarchySyntaxVisitor.swift +++ b/Tool/Sources/FocusedCodeFinder/Swift/SwiftScopeHierarchySyntaxVisitor.swift @@ -9,7 +9,7 @@ final class SwiftScopeHierarchySyntaxVisitor: SyntaxVisitor { let tree: SyntaxProtocol let code: String let range: CursorRange - let locationConverter: SourceLocationConverter + let rangeConverter: (SyntaxProtocol) -> CursorRange var imports: [String] = [] private var _scopeHierarchy: [SyntaxProtocol] = [] @@ -30,12 +30,12 @@ final class SwiftScopeHierarchySyntaxVisitor: SyntaxVisitor { tree: SyntaxProtocol, code: String, range: CursorRange, - locationConverter: SourceLocationConverter + rangeConverter: @escaping (SyntaxProtocol) -> CursorRange ) { self.tree = tree self.code = code self.range = range - self.locationConverter = locationConverter + self.rangeConverter = rangeConverter super.init(viewMode: .sourceAccurate) } @@ -53,8 +53,7 @@ final class SwiftScopeHierarchySyntaxVisitor: SyntaxVisitor { } func nodeContainsRange(_ node: SyntaxProtocol) -> Bool { - let sourceRange = node.sourceRange(converter: locationConverter) - let cursorRange = CursorRange(sourceRange: sourceRange) + let cursorRange = rangeConverter(node) return cursorRange.strictlyContains(range) } diff --git a/Tool/Sources/FocusedCodeFinder/UnknownLanguageFocusCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/UnknownLanguageFocusCodeFinder.swift new file mode 100644 index 00000000..f5aeaa68 --- /dev/null +++ b/Tool/Sources/FocusedCodeFinder/UnknownLanguageFocusCodeFinder.swift @@ -0,0 +1,77 @@ +import Foundation +import Preferences +import SuggestionModel + +/// Used when the language is not supported by the app +/// or that the code is too long to be returned by a focused code finder. +public struct UnknownLanguageFocusedCodeFinder: FocusedCodeFinderType { + let proposedSearchRange: Int + + public init(proposedSearchRange: Int) { + self.proposedSearchRange = proposedSearchRange + } + + public func findFocusedCode( + in document: Document, + containingRange: CursorRange + ) -> CodeContext { + guard !document.lines.isEmpty else { return .empty } + + // when user is not selecting any code. + if containingRange.start == containingRange.end { + // search up and down for up to `proposedSearchRange * 2 + 1` lines. + let lines = document.lines + let proposedLineCount = proposedSearchRange * 2 + 1 + let startLineIndex = max(containingRange.start.line - proposedSearchRange, 0) + let endLineIndex = max( + startLineIndex, + min(startLineIndex + proposedLineCount - 1, lines.count - 1) + ) + + let focusedLines = lines[startLineIndex...endLineIndex] + + let contextStartLine = max(startLineIndex - 5, 0) + let contextEndLine = min(endLineIndex + 5, lines.count - 1) + + return .init( + scope: .top, + contextRange: .init( + start: .init(line: contextStartLine, character: 0), + end: .init(line: contextEndLine, character: lines[contextEndLine].count) + ), + focusedRange: .init( + start: .init(line: startLineIndex, character: 0), + end: .init(line: endLineIndex, character: lines[endLineIndex].count) + ), + focusedCode: focusedLines.joined(), + imports: [], + includes: [] + ) + } + + let startLine = max(containingRange.start.line, 0) + let endLine = min(containingRange.end.line, document.lines.count - 1) + + if endLine < startLine { return .empty } + + let focusedLines = document.lines[startLine...endLine] + let contextStartLine = max(startLine - 3, 0) + let contextEndLine = min(endLine + 3, document.lines.count - 1) + + return CodeContext( + scope: .top, + contextRange: .init( + start: .init(line: contextStartLine, character: 0), + end: .init( + line: contextEndLine, + character: document.lines[contextEndLine].count + ) + ), + focusedRange: containingRange, + focusedCode: focusedLines.joined(), + imports: [], + includes: [] + ) + } +} + diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 7f49a153..c6b276d4 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -379,9 +379,7 @@ public extension UserDefaultPreferenceKeys { defaultValue: """ You are an AI programming assistant. Your reply should be concise, clear, informative and logical. - You MUST reply in the format of markdown. - You MUST embed every code you provide in a markdown code block. - You MUST add the programming language name at the start of the markdown code block. + Your reply should be formatted in Markdown. If you are asked to help perform a task, you MUST think step-by-step, then describe each step concisely. If you are asked to explain code, you MUST explain it step-by-step in a ordered list concisely. Make your answer short and structured. diff --git a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift new file mode 100644 index 00000000..3b563b76 --- /dev/null +++ b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift @@ -0,0 +1,5 @@ +import Foundation +import SuggestionModel +import XCTest + +@testable import FocusedCodeFinder diff --git a/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift index 553392ed..047f3f2b 100644 --- a/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift @@ -13,25 +13,6 @@ func document(code: String) -> FocusedCodeFinder.Document { } final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { - func test_collecting_imports() { - let code = """ - import var Darwin.stderr - public struct A: B, C { - let a = 1 - } - import Bar - """ - let range = CursorRange( - start: CursorPosition(line: 2, character: 0), - end: CursorPosition(line: 2, character: 1) - ) - let context = SwiftFocusedCodeFinder().findFocusedCode( - in: document(code: code), - containingRange: range - ) - XCTAssertEqual(context.imports, ["Darwin.stderr", "Bar"]) - } - func test_selecting_a_line_inside_the_function_the_scope_should_be_the_function() { let code = """ public struct A: B, C { From 3e807b06c381ae2770ef721e5ea52be69fcb9f67 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 26 Nov 2023 22:39:34 +0800 Subject: [PATCH 11/74] Update --- .../ObjectiveC/ObjectiveCCodeFinder.swift | 1 - .../ObjectiveCScopeHierarchySyntaxVisitor.swift | 8 ++++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift index 071d0825..ec688cee 100644 --- a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift @@ -263,7 +263,6 @@ extension ObjectiveCFocusedCodeFinder { _ node: ASTNode, textProvider: @escaping TextProvider ) -> (nodeInfo: NodeInfo?, more: Bool) { - let definitionRange = CursorRange(pointRange: node.pointRange) let name = node.contentOfChild(withFieldName: "name", textProvider: textProvider) let ( _, diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCScopeHierarchySyntaxVisitor.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCScopeHierarchySyntaxVisitor.swift index 3f710b41..71328099 100644 --- a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCScopeHierarchySyntaxVisitor.swift +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCScopeHierarchySyntaxVisitor.swift @@ -54,7 +54,7 @@ final class ObjectiveCScopeHierarchySyntaxVisitor: ASTTreeVisitor { case .classInterface, .categoryInterface, .protocolDeclaration: guard cursorRange.contains(range) else { return .skipChildren } _scopeHierarchy.append(node) - return .skipChildren + return .visitChildren case .classImplementation, .categoryImplementation: guard cursorRange.contains(range) else { return .skipChildren } _scopeHierarchy.append(node) @@ -62,7 +62,7 @@ final class ObjectiveCScopeHierarchySyntaxVisitor: ASTTreeVisitor { case .methodDefinition: guard cursorRange.contains(range) else { return .skipChildren } _scopeHierarchy.append(node) - return .visitChildren + return .skipChildren case .typeDefinition: guard cursorRange.contains(range) else { return .skipChildren } _scopeHierarchy.append(node) @@ -71,6 +71,10 @@ final class ObjectiveCScopeHierarchySyntaxVisitor: ASTTreeVisitor { guard cursorRange.contains(range) else { return .skipChildren } _scopeHierarchy.append(node) return .skipChildren + case .functionDefinition: + guard cursorRange.contains(range) else { return .skipChildren } + _scopeHierarchy.append(node) + return .skipChildren default: return .skipChildren } From 975a1df942f69a6c1d29ba4e701bd65ca1e49e91 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 26 Nov 2023 22:49:31 +0800 Subject: [PATCH 12/74] Fix SwiftFocusedCodeFinderTests --- .../SwiftFocusedCodeFinderTests.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift index 047f3f2b..cfee3265 100644 --- a/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift @@ -43,7 +43,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { .init( signature: "@ViewBuilder private func f(_ a: String) -> String", name: "f", - range: .init(startPair: (2, 0), endPair: (7, 5)) + range: .init(startPair: (1, 4), endPair: (7, 5)) ), ]), contextRange: .init(startPair: (0, 0), endPair: (8, 1)), @@ -162,7 +162,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { .init( signature: "public protocol A: Hashable", name: "A", - range: .init(startPair: (1, 0), endPair: (1, 9)) + range: .init(startPair: (0, 0), endPair: (6, 1)) ), ]), contextRange: .init(startPair: (0, 0), endPair: (6, 1)), @@ -199,7 +199,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { .init( signature: "private extension A: Equatable", name: "A", - range: .init(startPair: (1, 0), endPair: (1, 9)) + range: .init(startPair: (0, 0), endPair: (6, 1)) ), ]), contextRange: .init(startPair: (0, 0), endPair: (6, 1)), @@ -237,7 +237,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { .init( signature: "@gloablActor public actor A", name: "A", - range: .init(startPair: (2, 0), endPair: (2, 9)) + range: .init(startPair: (0, 0), endPair: (7, 1)) ), ]), contextRange: .init(startPair: (0, 0), endPair: (7, 1)), @@ -276,7 +276,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { .init( signature: "@MainActor public indirect enum A", name: "A", - range: .init(startPair: (3, 0), endPair: (3, 9)) + range: .init(startPair: (0, 0), endPair: (8, 1)) ), ]), contextRange: .init(startPair: (0, 0), endPair: (8, 1)), @@ -315,12 +315,12 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { .init( signature: "struct A", name: "A", - range: .init(startPair: (2, 0), endPair: (2, 9)) + range: .init(startPair: (0, 0), endPair: (8, 1)) ), .init( signature: "@SomeWrapper public private(set) var a: Int", name: "a", - range: .init(startPair: (1, 0), endPair: (7, 4)) + range: .init(startPair: (1, 4), endPair: (7, 5)) ), ]), contextRange: .init(startPair: (0, 0), endPair: (8, 1)), @@ -462,7 +462,7 @@ final class SwiftFocusedCodeFinder_FocusedCode_Tests: XCTestCase { final class SwiftFocusedCodeFinder_Import_Tests: XCTestCase { func test_parsing_imports() { let code = """ - import OpTop + import OnTop import Second import Third From 72035e7b4643007b522e2b5cfeb13d5455c4781d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 27 Nov 2023 00:42:15 +0800 Subject: [PATCH 13/74] Fix parseSignatureBeforeBody --- .../ObjectiveC/ObjectiveCCodeFinder.swift | 37 +++++---- .../ObjectiveCFocusedCodeFinderTests.swift | 77 +++++++++++++++++++ 2 files changed, 95 insertions(+), 19 deletions(-) diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift index ec688cee..8b18a22f 100644 --- a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift @@ -19,7 +19,7 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< ) { super.init(maxFocusedCodeLineCount: maxFocusedCodeLineCount) } - + public func parseSyntaxTree(from document: Document) -> ASTTree? { let parser = ASTParser(language: .objectiveC) return parser.parse(document.content) @@ -40,9 +40,9 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< }, range: range ) - + let nodes = visitor.findScopeHierarchy() - + return .init(nodes: nodes, includes: visitor.includes, imports: visitor.imports) } @@ -230,7 +230,7 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< _ node: ASTNode, textProvider: @escaping TextProvider ) -> (nodeInfo: NodeInfo?, more: Bool) { - parseSignatureBeforeBody(node, textProvider: textProvider) + parseSignatureBeforeBody(node, fieldNameForName: "selector", textProvider: textProvider) } func parseFunctionDefinitionNode( @@ -261,9 +261,10 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< extension ObjectiveCFocusedCodeFinder { func parseSignatureBeforeBody( _ node: ASTNode, + fieldNameForName: String = "name", textProvider: @escaping TextProvider ) -> (nodeInfo: NodeInfo?, more: Bool) { - let name = node.contentOfChild(withFieldName: "name", textProvider: textProvider) + let name = node.contentOfChild(withFieldName: fieldNameForName, textProvider: textProvider) let ( _, signatureRange, @@ -303,30 +304,28 @@ extension ASTNode { return (nil, range, pointRange) } - let range = self.range.excluding(postfixNode.range) - let pointRange = self.pointRange.excluding(postfixNode.pointRange) + let range = self.range.subtracting(postfixNode.range) + let pointRange = self.pointRange.subtracting(postfixNode.pointRange) return (postfixNode, range, pointRange) } } extension NSRange { - func excluding(_ range: NSRange) -> NSRange { - let start = max(location, range.location) - let end = min(location + length, range.location + range.length) + func subtracting(_ range: NSRange) -> NSRange { + let start = lowerBound + let end = Swift.max(lowerBound, Swift.min(upperBound, range.lowerBound)) return NSRange(location: start, length: end - start) } } extension Range where Bound == Point { - func excluding(_ range: Range) -> Range { - let start = Point( - row: Swift.max(lowerBound.row, range.lowerBound.row), - column: Swift.max(lowerBound.column, range.lowerBound.column) - ) - let end = Point( - row: Swift.min(upperBound.row, range.upperBound.row), - column: Swift.min(upperBound.column, range.upperBound.column) - ) + func subtracting(_ range: Range) -> Range { + let start = lowerBound + let end = if range.lowerBound >= upperBound { + upperBound + } else { + Swift.max(range.lowerBound, lowerBound) + } return Range(uncheckedBounds: (start, end)) } } diff --git a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift index 3b563b76..68b1a32b 100644 --- a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift @@ -3,3 +3,80 @@ import SuggestionModel import XCTest @testable import FocusedCodeFinder + +final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { + func test_selecting_a_line_inside_the_method_the_scope_should_be_the_method() { + let code = """ + @implementation Foo + - (void)foo { + NSInteger foo = 0; + NSLog(@"Hello"); + NSLog(@"World"); + } + @end + """ + let range = CursorRange( + start: CursorPosition(line: 2, character: 0), + end: CursorPosition(line: 2, character: 4) + ) + let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + in: document(code: code), + containingRange: range + ) + XCTAssertEqual(context, .init( + scope: .scope(signature: [ + .init( + signature: "@implementation Foo", + name: "Foo", + range: .init(startPair: (0, 0), endPair: (6, 4)) + ), + .init( + signature: "- (void)foo", + name: "foo", + range: .init(startPair: (1, 0), endPair: (5, 1)) + ), + ]), + contextRange: .init(startPair: (1, 0), endPair: (5, 1)), + focusedRange: range, + focusedCode: """ + NSInteger foo = 0; + + """, + imports: [], + includes: [] + )) + } + + func test_selecting_a_line_inside_a_function_the_scope_should_be_the_function() { + let code = """ + void foo() { + NSInteger foo = 0; + NSLog(@"Hello"); + NSLog(@"World"); + } + """ + let range = CursorRange(startPair: (2, 0), endPair: (2, 4)) + let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + in: document(code: code), + containingRange: range + ) + XCTAssertEqual(context, .init( + scope: .scope(signature: [ + .init( + signature: "void foo()", + name: "foo", + range: .init(startPair: (0, 0), endPair: (4, 1)) + ), + ]), + contextRange: .init(startPair: (0, 0), endPair: (4, 1)), + focusedRange: range, + focusedCode: """ + NSLog(@"Hello"); + + """, + imports: [], + includes: [] + )) + } +} + From 7dcd1160c0c86ee304bab88704469a4fb78be73d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 27 Nov 2023 01:20:58 +0800 Subject: [PATCH 14/74] Simplify contextContainingNode --- .../KnownLanguageFocusedCodeFinder.swift | 10 +- .../ObjectiveC/ObjectiveCCodeFinder.swift | 189 +++++++++--------- .../Swift/SwiftFocusedCodeFinder.swift | 68 +++---- .../ObjectiveCFocusedCodeFinderTests.swift | 2 +- 4 files changed, 131 insertions(+), 138 deletions(-) diff --git a/Tool/Sources/FocusedCodeFinder/KnownLanguageFocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/KnownLanguageFocusedCodeFinder.swift index 09e8abab..95d73c46 100644 --- a/Tool/Sources/FocusedCodeFinder/KnownLanguageFocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/KnownLanguageFocusedCodeFinder.swift @@ -58,7 +58,7 @@ public protocol KnownLanguageFocusedCodeFinderType: FocusedCodeFinderType { func contextContainingNode( _ node: Node, textProvider: @escaping TextProvider - ) -> (nodeInfo: NodeInfo?, more: Bool) + ) -> NodeInfo? func createTextProviderAndRangeConverter( for document: Document, @@ -92,7 +92,7 @@ public extension KnownLanguageFocusedCodeFinderType { var focusedNode: Node? while let node = contextInfo.nodes.first { contextInfo.nodes.removeFirst() - let (nodeInfo, _) = contextContainingNode(node, textProvider: textProvider) + let nodeInfo = contextContainingNode(node, textProvider: textProvider) if nodeInfo?.canBeUsedAsCodeRange ?? false { focusedNode = node break @@ -186,7 +186,7 @@ extension KnownLanguageFocusedCodeFinderType { while let node = nodes.first { nodes.removeFirst() - let (context, more) = contextContainingNode(node, textProvider: textProvider) + let context = contextContainingNode(node, textProvider: textProvider) if let context { contextRange = rangeConverter(context.node) @@ -196,10 +196,6 @@ extension KnownLanguageFocusedCodeFinderType { range: contextRange ), at: 0) } - - if !more { - break - } } return (contextRange, signature) diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift index 8b18a22f..2e816a01 100644 --- a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift @@ -68,7 +68,7 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< public func contextContainingNode( _ node: Node, textProvider: @escaping TextProvider - ) -> (nodeInfo: NodeInfo?, more: Bool) { + ) -> NodeInfo? { switch ObjectiveCNodeType(rawValue: node.nodeType ?? "") { case .classInterface, .categoryInterface: return parseClassInterfaceNode(node, textProvider: textProvider) @@ -85,39 +85,35 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< case .typeDefinition: return parseTypedefNode(node, textProvider: textProvider) default: - return (nil, false) + return nil } } func parseClassInterfaceNode( _ node: ASTNode, textProvider: @escaping TextProvider - ) -> (nodeInfo: NodeInfo?, more: Bool) { + ) -> NodeInfo? { var name = "" var superClass = "" var category = "" var protocols = [String]() - let children = node.children - for child in children { - if let nameNode = child.child(byFieldName: "name") { - name = textProvider(.node(nameNode)) - } - if let superClassNode = child.child(byFieldName: "superclass") { - superClass = textProvider(.node(superClassNode)) - } - if let categoryNode = child.child(byFieldName: "category") { - category = textProvider(.node(categoryNode)) - } - if let protocolsNode = child.child(byFieldName: "protocols") { - for protocolNode in protocolsNode.children { - let protocolName = textProvider(.node(protocolNode)) - if !protocolName.isEmpty { - protocols.append(protocolName) - } + if let nameNode = node.child(byFieldName: "name") { + name = textProvider(.node(nameNode)) + } + if let superClassNode = node.child(byFieldName: "superclass") { + superClass = textProvider(.node(superClassNode)) + } + if let categoryNode = node.child(byFieldName: "category") { + category = textProvider(.node(categoryNode)) + } + if let protocolsNode = node.child(byFieldName: "protocols") { + for protocolNode in protocolsNode.children { + let protocolName = textProvider(.node(protocolNode)) + if !protocolName.isEmpty { + protocols.append(protocolName) } } } - var signature = "@interface \(name)" if !category.isEmpty { signature += "(\(category))" @@ -129,47 +125,41 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< signature += ": \(superClass)" } - return ( - .init( - node: node, - signature: signature, - name: name, - canBeUsedAsCodeRange: true - ), - false + return .init( + node: node, + signature: signature, + name: name, + canBeUsedAsCodeRange: true ) } func parseClassImplementationNode( _ node: ASTNode, textProvider: @escaping TextProvider - ) -> (nodeInfo: NodeInfo?, more: Bool) { + ) -> NodeInfo? { var name = "" var superClass = "" var category = "" var protocols = [String]() - let children = node.children - for child in children { - if let nameNode = child.child(byFieldName: "name") { - name = textProvider(.node(nameNode)) - } - if let superClassNode = child.child(byFieldName: "superclass") { - superClass = textProvider(.node(superClassNode)) - } - if let categoryNode = child.child(byFieldName: "category") { - category = textProvider(.node(categoryNode)) - } - if let protocolsNode = child.child(byFieldName: "protocols") { - for protocolNode in protocolsNode.children { - let protocolName = textProvider(.node(protocolNode)) - if !protocolName.isEmpty { - protocols.append(protocolName) - } + if let nameNode = node.child(byFieldName: "name") { + name = textProvider(.node(nameNode)) + } + if let superClassNode = node.child(byFieldName: "superclass") { + superClass = textProvider(.node(superClassNode)) + } + if let categoryNode = node.child(byFieldName: "category") { + category = textProvider(.node(categoryNode)) + } + if let protocolsNode = node.child(byFieldName: "protocols") { + for protocolNode in protocolsNode.children { + let protocolName = textProvider(.node(protocolNode)) + if !protocolName.isEmpty { + protocols.append(protocolName) } } } - var signature = "@implement \(name)" + var signature = "@implementation \(name)" if !category.isEmpty { signature += "(\(category))" } @@ -179,34 +169,28 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< if !superClass.isEmpty { signature += ": \(superClass)" } - return ( - .init( - node: node, - signature: signature, - name: name, - canBeUsedAsCodeRange: true - ), - false + return .init( + node: node, + signature: signature, + name: name, + canBeUsedAsCodeRange: true ) } func parseProtocolNode( _ node: ASTNode, textProvider: @escaping TextProvider - ) -> (nodeInfo: NodeInfo?, more: Bool) { + ) -> NodeInfo? { var name = "" var protocols = [String]() - let children = node.children - for child in children { - if let nameNode = child.child(byFieldName: "name") { - name = textProvider(.node(nameNode)) - } - if let protocolsNode = child.child(byFieldName: "protocols") { - for protocolNode in protocolsNode.children { - let protocolName = textProvider(.node(protocolNode)) - if !protocolName.isEmpty { - protocols.append(protocolName) - } + if let nameNode = node.child(byFieldName: "name") { + name = textProvider(.node(nameNode)) + } + if let protocolsNode = node.child(byFieldName: "protocols") { + for protocolNode in protocolsNode.children { + let protocolName = textProvider(.node(protocolNode)) + if !protocolName.isEmpty { + protocols.append(protocolName) } } } @@ -215,44 +199,60 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< if !protocols.isEmpty { signature += "<\(protocols.joined(separator: ","))>" } - return ( - .init( - node: node, - signature: signature, - name: name, - canBeUsedAsCodeRange: true - ), - false + return .init( + node: node, + signature: signature, + name: name, + canBeUsedAsCodeRange: true ) } func parseMethodDefinitionNode( _ node: ASTNode, textProvider: @escaping TextProvider - ) -> (nodeInfo: NodeInfo?, more: Bool) { + ) -> NodeInfo? { parseSignatureBeforeBody(node, fieldNameForName: "selector", textProvider: textProvider) } - func parseFunctionDefinitionNode( + func parseTypeSpecifierNode( _ node: ASTNode, textProvider: @escaping TextProvider - ) -> (nodeInfo: NodeInfo?, more: Bool) { + ) -> NodeInfo? { parseSignatureBeforeBody(node, textProvider: textProvider) } - func parseTypeSpecifierNode( + func parseTypedefNode( _ node: ASTNode, textProvider: @escaping TextProvider - ) -> (nodeInfo: NodeInfo?, more: Bool) { - parseSignatureBeforeBody(node, textProvider: textProvider) + ) -> NodeInfo? { + guard let typeNode = node.child(byFieldName: "type") else { return nil } + return parseSignatureBeforeBody(typeNode, textProvider: textProvider) } - func parseTypedefNode( + func parseFunctionDefinitionNode( _ node: ASTNode, textProvider: @escaping TextProvider - ) -> (nodeInfo: NodeInfo?, more: Bool) { - guard let typeNode = node.child(byFieldName: "type") else { return (nil, false) } - return parseSignatureBeforeBody(typeNode, textProvider: textProvider) + ) -> NodeInfo? { + let declaratorNode = node.child(byFieldName: "declarator") + let name = declaratorNode?.contentOfChild( + withFieldName: "declarator", + textProvider: textProvider + ) + let ( + _, + signatureRange, + signaturePointRange + ) = node.extractInformationBeforeNode(withFieldName: "body") + let signature = textProvider(.range(range: signatureRange, pointRange: signaturePointRange)) + .replacingOccurrences(of: "\n", with: "") + .trimmingCharacters(in: .whitespacesAndNewlines) + if signature.isEmpty { return nil } + return .init( + node: node, + signature: signature, + name: name ?? "N/A", + canBeUsedAsCodeRange: false + ) } } @@ -263,7 +263,7 @@ extension ObjectiveCFocusedCodeFinder { _ node: ASTNode, fieldNameForName: String = "name", textProvider: @escaping TextProvider - ) -> (nodeInfo: NodeInfo?, more: Bool) { + ) -> NodeInfo? { let name = node.contentOfChild(withFieldName: fieldNameForName, textProvider: textProvider) let ( _, @@ -273,15 +273,12 @@ extension ObjectiveCFocusedCodeFinder { let signature = textProvider(.range(range: signatureRange, pointRange: signaturePointRange)) .replacingOccurrences(of: "\n", with: "") .trimmingCharacters(in: .whitespacesAndNewlines) - if signature.isEmpty { return (nil, false) } - return ( - .init( - node: node, - signature: signature, - name: name ?? "N/A", - canBeUsedAsCodeRange: false - ), - false + if signature.isEmpty { return nil } + return .init( + node: node, + signature: signature, + name: name ?? "N/A", + canBeUsedAsCodeRange: false ) } } diff --git a/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift index 587a8de3..e3bfcb42 100644 --- a/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift @@ -65,7 +65,7 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< public func contextContainingNode( _ node: SyntaxProtocol, textProvider: @escaping TextProvider - ) -> (nodeInfo: NodeInfo?, more: Bool) { + ) -> NodeInfo? { func extractText(_ node: SyntaxProtocol) -> String { textProvider(node) } @@ -74,85 +74,85 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< case let node as StructDeclSyntax: let type = node.structKeyword.text let name = node.identifier.text - return (.init( + return .init( node: node, signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) .replacingOccurrences(of: "\n", with: " "), name: name - ), false) + ) case let node as ClassDeclSyntax: let type = node.classKeyword.text let name = node.identifier.text - return (.init( + return .init( node: node, signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) .replacingOccurrences(of: "\n", with: " "), name: name - ), false) + ) case let node as EnumDeclSyntax: let type = node.enumKeyword.text let name = node.identifier.text - return (.init( + return .init( node: node, signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) .replacingOccurrences(of: "\n", with: " "), name: name - ), false) + ) case let node as ActorDeclSyntax: let type = node.actorKeyword.text let name = node.identifier.text - return (.init( + return .init( node: node, signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) .replacingOccurrences(of: "\n", with: ""), name: name - ), false) + ) case let node as MacroDeclSyntax: let type = node.macroKeyword.text let name = node.identifier.text - return (.init( + return .init( node: node, signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .replacingOccurrences(of: "\n", with: " "), name: name - ), false) + ) case let node as ProtocolDeclSyntax: let type = node.protocolKeyword.text let name = node.identifier.text - return (.init( + return .init( node: node, signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) .replacingOccurrences(of: "\n", with: " "), name: name - ), false) + ) case let node as ExtensionDeclSyntax: let type = node.extensionKeyword.text let name = node.extendedType.trimmedDescription - return (.init( + return .init( node: node, signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) .replacingOccurrences(of: "\n", with: " "), name: name - ), false) + ) case let node as FunctionDeclSyntax: let type = node.funcKeyword.text @@ -162,38 +162,38 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .joined(separator: " ") - return (.init( + return .init( node: node, signature: "\(type) \(name)\(signature)" .prefixedModifiers(node.modifierAndAttributeText(extractText)), name: name - ), true) + ) case let node as VariableDeclSyntax: let type = node.bindingSpecifier.trimmedDescription let name = node.bindings.first?.pattern.trimmedDescription ?? "" let signature = node.bindings.first?.typeAnnotation?.trimmedDescription ?? "" - return (.init( + return .init( node: node, signature: "\(type) \(name)\(signature.isEmpty ? "" : "\(signature)")" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .replacingOccurrences(of: "\n", with: " "), name: name, canBeUsedAsCodeRange: false - ), true) + ) case let node as AccessorDeclSyntax: let keyword = node.accessorSpecifier.text let signature = keyword - return (.init( + return .init( node: node, signature: signature .prefixedModifiers(node.modifierAndAttributeText(extractText)) .replacingOccurrences(of: "\n", with: " "), name: keyword - ), true) + ) case let node as SubscriptDeclSyntax: let genericPClause = node.genericWhereClause?.trimmedDescription ?? "" @@ -201,64 +201,64 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< let whereClause = node.genericWhereClause?.trimmedDescription ?? "" let signature = "subscript\(genericPClause)(\(pClause))\(whereClause)" - return (.init( + return .init( node: node, signature: signature .prefixedModifiers(node.modifierAndAttributeText(extractText)) .replacingOccurrences(of: "\n", with: " "), name: "subscript" - ), true) + ) case let node as InitializerDeclSyntax: let signature = "init" - return (.init( + return .init( node: node, signature: "\(signature)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .replacingOccurrences(of: "\n", with: " "), name: "init" - ), true) + ) case let node as DeinitializerDeclSyntax: let signature = "deinit" - return (.init( + return .init( node: node, signature: signature .prefixedModifiers(node.modifierAndAttributeText(extractText)) .replacingOccurrences(of: "\n", with: " "), name: "deinit" - ), true) + ) case let node as ClosureExprSyntax: let signature = "closure" - return (.init( + return .init( node: node, signature: signature.replacingOccurrences(of: "\n", with: " "), name: "closure" - ), true) + ) case let node as FunctionCallExprSyntax: let signature = "function call" - return (.init( + return .init( node: node, signature: signature.replacingOccurrences(of: "\n", with: " "), name: "function call", canBeUsedAsCodeRange: false - ), true) + ) case let node as SwitchCaseSyntax: - return (.init( + return .init( node: node, signature: node.trimmedDescription.replacingOccurrences(of: "\n", with: " "), name: "switch" - ), true) + ) default: - return (nil, true) + return nil } } diff --git a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift index 68b1a32b..f3ecfa60 100644 --- a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift @@ -36,7 +36,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { range: .init(startPair: (1, 0), endPair: (5, 1)) ), ]), - contextRange: .init(startPair: (1, 0), endPair: (5, 1)), + contextRange: .init(startPair: (0, 0), endPair: (6, 4)), focusedRange: range, focusedCode: """ NSInteger foo = 0; From 3a0b5e57a71a0cac4639797bb7d7c6ca83578d12 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 27 Nov 2023 12:50:04 +0800 Subject: [PATCH 15/74] Fix super class and protocol conformance parsing --- .../ObjectiveC/ObjectiveCCodeFinder.swift | 69 ++++++++++----- ...bjectiveCScopeHierarchySyntaxVisitor.swift | 12 +-- .../ObjectiveC/ObjectiveCSyntax.swift | 6 +- .../ObjectiveCFocusedCodeFinderTests.swift | 88 +++++++++++++++++-- 4 files changed, 143 insertions(+), 32 deletions(-) diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift index 2e816a01..2514964d 100644 --- a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift @@ -100,26 +100,41 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< if let nameNode = node.child(byFieldName: "name") { name = textProvider(.node(nameNode)) } - if let superClassNode = node.child(byFieldName: "superclass") { - superClass = textProvider(.node(superClassNode)) - } if let categoryNode = node.child(byFieldName: "category") { category = textProvider(.node(categoryNode)) } - if let protocolsNode = node.child(byFieldName: "protocols") { - for protocolNode in protocolsNode.children { - let protocolName = textProvider(.node(protocolNode)) - if !protocolName.isEmpty { - protocols.append(protocolName) + + for i in 0.." + signature += "<\(protocols.joined(separator: ", "))>" } if !superClass.isEmpty { signature += ": \(superClass)" @@ -144,27 +159,41 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< if let nameNode = node.child(byFieldName: "name") { name = textProvider(.node(nameNode)) } - if let superClassNode = node.child(byFieldName: "superclass") { - superClass = textProvider(.node(superClassNode)) - } if let categoryNode = node.child(byFieldName: "category") { category = textProvider(.node(categoryNode)) } - if let protocolsNode = node.child(byFieldName: "protocols") { - for protocolNode in protocolsNode.children { - let protocolName = textProvider(.node(protocolNode)) - if !protocolName.isEmpty { - protocols.append(protocolName) + + for i in 0.." + signature += "<\(protocols.joined(separator: ", "))>" } if !superClass.isEmpty { signature += ": \(superClass)" diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCScopeHierarchySyntaxVisitor.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCScopeHierarchySyntaxVisitor.swift index 71328099..6dfe5b45 100644 --- a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCScopeHierarchySyntaxVisitor.swift +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCScopeHierarchySyntaxVisitor.swift @@ -52,27 +52,27 @@ final class ObjectiveCScopeHierarchySyntaxVisitor: ASTTreeVisitor { handleModuleImport(node) return .skipChildren case .classInterface, .categoryInterface, .protocolDeclaration: - guard cursorRange.contains(range) else { return .skipChildren } + guard cursorRange.strictlyContains(range) else { return .skipChildren } _scopeHierarchy.append(node) return .visitChildren case .classImplementation, .categoryImplementation: - guard cursorRange.contains(range) else { return .skipChildren } + guard cursorRange.strictlyContains(range) else { return .skipChildren } _scopeHierarchy.append(node) return .visitChildren case .methodDefinition: - guard cursorRange.contains(range) else { return .skipChildren } + guard cursorRange.strictlyContains(range) else { return .skipChildren } _scopeHierarchy.append(node) return .skipChildren case .typeDefinition: - guard cursorRange.contains(range) else { return .skipChildren } + guard cursorRange.strictlyContains(range) else { return .skipChildren } _scopeHierarchy.append(node) return .skipChildren case .structSpecifier, .enumSpecifier, .nsEnumSpecifier: - guard cursorRange.contains(range) else { return .skipChildren } + guard cursorRange.strictlyContains(range) else { return .skipChildren } _scopeHierarchy.append(node) return .skipChildren case .functionDefinition: - guard cursorRange.contains(range) else { return .skipChildren } + guard cursorRange.strictlyContains(range) else { return .skipChildren } _scopeHierarchy.append(node) return .skipChildren default: diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCSyntax.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCSyntax.swift index f2eb20ff..eb561624 100644 --- a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCSyntax.swift +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCSyntax.swift @@ -121,8 +121,12 @@ enum ObjectiveCNodeType: String { case enumSpecifier = "enum_specifier" /// `NS_ENUM {}` and `NS_OPTIONS {}`. case nsEnumSpecifier = "ns_enum_specifier" - /// fields inside a type definition. + /// Fields inside a type definition. case fieldDeclarationList = "field_declaration_list" + /// Protocols that a type conforms. + case protocolQualifiers = "protocol_qualifiers" + /// Superclass of a type. + case superclassReference = "superclass_reference" } extension ObjectiveCNodeType { diff --git a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift index f3ecfa60..12710c2a 100644 --- a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift @@ -8,7 +8,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { func test_selecting_a_line_inside_the_method_the_scope_should_be_the_method() { let code = """ @implementation Foo - - (void)foo { + - (void)fooWith:(NSInteger)foo { NSInteger foo = 0; NSLog(@"Hello"); NSLog(@"World"); @@ -31,8 +31,8 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { range: .init(startPair: (0, 0), endPair: (6, 4)) ), .init( - signature: "- (void)foo", - name: "foo", + signature: "- (void)fooWith:(NSInteger)foo", + name: "fooWith:(NSInteger)foo", range: .init(startPair: (1, 0), endPair: (5, 1)) ), ]), @@ -49,7 +49,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { func test_selecting_a_line_inside_a_function_the_scope_should_be_the_function() { let code = """ - void foo() { + void foo(char name[]) { NSInteger foo = 0; NSLog(@"Hello"); NSLog(@"World"); @@ -63,7 +63,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { XCTAssertEqual(context, .init( scope: .scope(signature: [ .init( - signature: "void foo()", + signature: "void foo(char name[])", name: "foo", range: .init(startPair: (0, 0), endPair: (4, 1)) ), @@ -78,5 +78,83 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { includes: [] )) } + + func test_selecting_a_method_inside_an_implementation_the_scope_should_be_the_implementation() { + let code = """ + @implementation Foo (Category) + - (void)fooWith:(NSInteger)foo { + NSInteger foo = 0; + NSLog(@"Hello"); + NSLog(@"World"); + } + @end + """ + let range = CursorRange( + start: CursorPosition(line: 1, character: 0), + end: CursorPosition(line: 5, character: 1) + ) + let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + in: document(code: code), + containingRange: range + ) + XCTAssertEqual(context, .init( + scope: .scope(signature: [ + .init( + signature: "@implementation Foo (Category)", + name: "Foo", + range: .init(startPair: (0, 0), endPair: (6, 4)) + ), + ]), + contextRange: .init(startPair: (0, 0), endPair: (6, 4)), + focusedRange: range, + focusedCode: """ + - (void)fooWith:(NSInteger)foo { + NSInteger foo = 0; + NSLog(@"Hello"); + NSLog(@"World"); + } + + """, + imports: [], + includes: [] + )) + } + + func test_selecting_a_line_inside_an_interface_the_scope_should_be_the_interface() { + let code = """ + @interface Foo: NSObject + - (void)fooWith:(NSInteger)foo; + - (void)fooWith:(NSInteger)foo; + - (void)fooWith:(NSInteger)foo; + @end + """ + let range = CursorRange( + start: CursorPosition(line: 1, character: 0), + end: CursorPosition(line: 3, character: 31) + ) + let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + in: document(code: code), + containingRange: range + ) + XCTAssertEqual(context, .init( + scope: .scope(signature: [ + .init( + signature: "@interface Foo: NSObject", + name: "Foo", + range: .init(startPair: (0, 0), endPair: (4, 4)) + ), + ]), + contextRange: .init(startPair: (0, 0), endPair: (4, 4)), + focusedRange: range, + focusedCode: """ + - (void)fooWith:(NSInteger)foo; + - (void)fooWith:(NSInteger)foo; + - (void)fooWith:(NSInteger)foo; + + """, + imports: [], + includes: [] + )) + } } From b17d28252c7a6378d757a7cb0457c93cb4cd3f28 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 27 Nov 2023 15:42:16 +0800 Subject: [PATCH 16/74] Add parseDeclarationInterfaceNode to parse all declaration contexts --- .../ObjectiveC/ObjectiveCCodeFinder.swift | 173 ++++++------------ .../ObjectiveC/ObjectiveCSyntax.swift | 73 ++------ .../ObjectiveCFocusedCodeFinderTests.swift | 89 ++++++++- 3 files changed, 146 insertions(+), 189 deletions(-) diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift index 2514964d..effa8c1a 100644 --- a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift @@ -71,11 +71,11 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< ) -> NodeInfo? { switch ObjectiveCNodeType(rawValue: node.nodeType ?? "") { case .classInterface, .categoryInterface: - return parseClassInterfaceNode(node, textProvider: textProvider) + return parseDeclarationInterfaceNode(node, textProvider: textProvider) case .classImplementation, .categoryImplementation: - return parseClassImplementationNode(node, textProvider: textProvider) + return parseDeclarationInterfaceNode(node, textProvider: textProvider) case .protocolDeclaration: - return parseProtocolNode(node, textProvider: textProvider) + return parseDeclarationInterfaceNode(node, textProvider: textProvider) case .methodDefinition: return parseMethodDefinitionNode(node, textProvider: textProvider) case .functionDefinition: @@ -89,16 +89,23 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< } } - func parseClassInterfaceNode( + func parseDeclarationInterfaceNode( _ node: ASTNode, textProvider: @escaping TextProvider ) -> NodeInfo? { var name = "" - var superClass = "" var category = "" - var protocols = [String]() + /// Attributes, declaration kind, and name. + var prefix = "" + /// Generics, super class, etc. + var extra = "" + if let nameNode = node.child(byFieldName: "name") { name = textProvider(.node(nameNode)) + prefix = textProvider(.range( + range: node.range.notSurpassing(nameNode.range), + pointRange: node.pointRange.notSurpassing(nameNode.pointRange) + )) } if let categoryNode = node.child(byFieldName: "category") { category = textProvider(.node(categoryNode)) @@ -107,127 +114,37 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< for i in 0.." - } - if !superClass.isEmpty { - signature += ": \(superClass)" - } - - return .init( - node: node, - signature: signature, - name: name, - canBeUsedAsCodeRange: true - ) - } - - func parseClassImplementationNode( - _ node: ASTNode, - textProvider: @escaping TextProvider - ) -> NodeInfo? { - var name = "" - var superClass = "" - var category = "" - var protocols = [String]() - if let nameNode = node.child(byFieldName: "name") { - name = textProvider(.node(nameNode)) - } - if let categoryNode = node.child(byFieldName: "category") { - category = textProvider(.node(categoryNode)) - } - - for i in 0.." - } - if !superClass.isEmpty { - signature += ": \(superClass)" - } - return .init( - node: node, - signature: signature, - name: name, - canBeUsedAsCodeRange: true - ) - } - func parseProtocolNode( - _ node: ASTNode, - textProvider: @escaping TextProvider - ) -> NodeInfo? { - var name = "" - var protocols = [String]() - if let nameNode = node.child(byFieldName: "name") { - name = textProvider(.node(nameNode)) - } - if let protocolsNode = node.child(byFieldName: "protocols") { - for protocolNode in protocolsNode.children { - let protocolName = textProvider(.node(protocolNode)) - if !protocolName.isEmpty { - protocols.append(protocolName) - } - } - } - - var signature = "@protocol \(name)" - if !protocols.isEmpty { - signature += "<\(protocols.joined(separator: ","))>" - } return .init( node: node, signature: signature, @@ -235,7 +152,7 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< canBeUsedAsCodeRange: true ) } - + func parseMethodDefinitionNode( _ node: ASTNode, textProvider: @escaping TextProvider @@ -273,7 +190,7 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< signaturePointRange ) = node.extractInformationBeforeNode(withFieldName: "body") let signature = textProvider(.range(range: signatureRange, pointRange: signaturePointRange)) - .replacingOccurrences(of: "\n", with: "") + .replacingOccurrences(of: "\n", with: " ") .trimmingCharacters(in: .whitespacesAndNewlines) if signature.isEmpty { return nil } return .init( @@ -342,6 +259,12 @@ extension NSRange { let end = Swift.max(lowerBound, Swift.min(upperBound, range.lowerBound)) return NSRange(location: start, length: end - start) } + + func notSurpassing(_ range: NSRange) -> NSRange { + let start = lowerBound + let end = Swift.max(lowerBound, Swift.min(upperBound, range.upperBound)) + return NSRange(location: start, length: end - start) + } } extension Range where Bound == Point { @@ -354,5 +277,15 @@ extension Range where Bound == Point { } return Range(uncheckedBounds: (start, end)) } + + func notSurpassing(_ range: Range) -> Range { + let start = lowerBound + let end = if range.lowerBound >= upperBound { + upperBound + } else { + Swift.max(range.upperBound, lowerBound) + } + return Range(uncheckedBounds: (start, end)) + } } diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCSyntax.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCSyntax.swift index eb561624..98f5307a 100644 --- a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCSyntax.swift +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCSyntax.swift @@ -1,9 +1,10 @@ import Foundation -/// https://github.com/merico-dev/tree-sitter-objc/test/corpus/imports.txt -/// https://github.com/merico-dev/tree-sitter-objc/test/corpus/expressions.txt -/// https://github.com/merico-dev/tree-sitter-objc/test/corpus/declarations.txt -/// https://github.com/merico-dev/tree-sitter-objc/node-types.json +/// https://github.com/lukepistrol/tree-sitter-objc/blob/feature/spm/test/corpus/imports.txt +/// https://github.com/lukepistrol/tree-sitter-objc/blob/feature/spm/test/corpus/expressions.txt +/// https://github.com/lukepistrol/tree-sitter-objc/blob/feature/spm/test/corpus/declarations.txt +/// https://github.com/lukepistrol/tree-sitter-objc/blob/feature/spm/node-types.json +/// Some of the test cases are actually incorrect? enum ObjectiveCNodeType: String { /// The top most item case translationUnit = "translation_unit" @@ -22,26 +23,6 @@ enum ObjectiveCNodeType: String { /// + (tr)k1:(t1)a1 : (t2)a2 k2: a3; /// @end /// ``` - /// - /// will parse into: - /// ``` - /// (translation_unit - /// (class_interface - /// name: (identifier) - /// superclass: (identifier))) < SuperClass - /// protocols: (protocol_reference_list < Protocols - /// (identifier) - /// (identifier)))) - /// (field_declaration < iv1 - /// type: (type_identifier) - /// declarator: (field_identifier)) - /// (field_declaration < iv2 - /// type: (type_identifier) - /// declarator: (field_identifier)))) - /// (property_declaration ...) < property value - /// (method_declaration ...) < method - /// ``` - /// case classInterface = "class_interface" /// `@implementation` case classImplementation = "class_implementation" @@ -56,52 +37,14 @@ enum ObjectiveCNodeType: String { /// ```objc /// @class C1, C2; /// ``` - /// - /// will parse into: - /// ``` - /// (translation_unit - /// (class_declaration_list - /// (identifier) - /// (identifier))) - /// ``` case classDeclarationList = "class_declaration_list" /// ``` /// + (tr)k1: (t1)a1 : (t2)a2 k2: a3; /// ``` - /// - /// will parse into: - /// ``` - /// (property_declaration - /// (readwrite) - /// (copy) - /// type: (type_identifier) < type - /// name: (identifier)))) < name - /// ``` case propertyDeclaration = "property_declaration" /// ```objc /// + (tr)k1: (t1)a1 : (t2)a2 k2: a3; /// ``` - /// - /// will parse into: - /// ``` - /// (method_declaration - /// scope: (class_scope) - /// return_type: (type_descriptor - /// type: (type_identifier)) - /// selector: (keyword_selector - /// (keyword_declarator - /// keyword: (identifier) - /// type: (type_descriptor - /// type: (type_identifier)) - /// name: (identifier)) - /// (keyword_declarator - /// type: (type_descriptor - /// type: (type_identifier)) - /// name: (identifier)) - /// (keyword_declarator - /// keyword: (identifier) - /// name: (identifier)))))) - /// ``` case methodDeclaration = "method_declaration" /// `- (rt)sel {}` case methodDefinition = "method_definition" @@ -127,6 +70,12 @@ enum ObjectiveCNodeType: String { case protocolQualifiers = "protocol_qualifiers" /// Superclass of a type. case superclassReference = "superclass_reference" + /// The generic type arguments. + case parameterizedClassTypeArguments = "parameterized_class_type_arguments" + /// `__GENERICS` in category interface and implementation. + case genericsTypeReference = "generics_type_reference" + /// `IB_DESIGNABLE`, etc. The typo is from the original source. + case classInterfaceAttributeSpecifier = "class_interface_attribute_sepcifier" } extension ObjectiveCNodeType { diff --git a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift index 12710c2a..bbad8c52 100644 --- a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift @@ -81,6 +81,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { func test_selecting_a_method_inside_an_implementation_the_scope_should_be_the_implementation() { let code = """ + __attribute__((objc_nonlazy_class)) @implementation Foo (Category) - (void)fooWith:(NSInteger)foo { NSInteger foo = 0; @@ -90,8 +91,8 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { @end """ let range = CursorRange( - start: CursorPosition(line: 1, character: 0), - end: CursorPosition(line: 5, character: 1) + start: CursorPosition(line: 2, character: 0), + end: CursorPosition(line: 6, character: 1) ) let context = ObjectiveCFocusedCodeFinder().findFocusedCode( in: document(code: code), @@ -100,12 +101,12 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { XCTAssertEqual(context, .init( scope: .scope(signature: [ .init( - signature: "@implementation Foo (Category)", + signature: "__attribute__((objc_nonlazy_class)) @implementation Foo (Category)", name: "Foo", - range: .init(startPair: (0, 0), endPair: (6, 4)) + range: .init(startPair: (0, 0), endPair: (7, 4)) ), ]), - contextRange: .init(startPair: (0, 0), endPair: (6, 4)), + contextRange: .init(startPair: (0, 0), endPair: (7, 4)), focusedRange: range, focusedCode: """ - (void)fooWith:(NSInteger)foo { @@ -122,7 +123,81 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { func test_selecting_a_line_inside_an_interface_the_scope_should_be_the_interface() { let code = """ - @interface Foo: NSObject + @interface ViewController >: NSObject + - (void)fooWith:(NSInteger)foo; + - (void)fooWith:(NSInteger)foo; + - (void)fooWith:(NSInteger)foo; + @end + """ + let range = CursorRange( + start: CursorPosition(line: 1, character: 0), + end: CursorPosition(line: 3, character: 31) + ) + let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + in: document(code: code), + containingRange: range + ) + XCTAssertEqual(context, .init( + scope: .scope(signature: [ + .init( + signature: "@interface ViewController>: NSObject", + name: "ViewController", + range: .init(startPair: (0, 0), endPair: (4, 4)) + ), + ]), + contextRange: .init(startPair: (0, 0), endPair: (4, 4)), + focusedRange: range, + focusedCode: """ + - (void)fooWith:(NSInteger)foo; + - (void)fooWith:(NSInteger)foo; + - (void)fooWith:(NSInteger)foo; + + """, + imports: [], + includes: [] + )) + } + + func test_selecting_a_line_inside_an_interface_category_the_scope_should_be_the_interface() { + let code = """ + @interface __GENERICS(NSArray, ObjectType) (BlocksKit) + - (void)fooWith:(NSInteger)foo; + - (void)fooWith:(NSInteger)foo; + - (void)fooWith:(NSInteger)foo; + @end + """ + let range = CursorRange( + start: CursorPosition(line: 1, character: 0), + end: CursorPosition(line: 3, character: 31) + ) + let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + in: document(code: code), + containingRange: range + ) + XCTAssertEqual(context, .init( + scope: .scope(signature: [ + .init( + signature: "@interface __GENERICS(NSArray, ObjectType) (BlocksKit)", + name: "NSArray", + range: .init(startPair: (0, 0), endPair: (4, 4)) + ), + ]), + contextRange: .init(startPair: (0, 0), endPair: (4, 4)), + focusedRange: range, + focusedCode: """ + - (void)fooWith:(NSInteger)foo; + - (void)fooWith:(NSInteger)foo; + - (void)fooWith:(NSInteger)foo; + + """, + imports: [], + includes: [] + )) + } + + func test_selecting_a_line_inside_a_protocol_the_scope_should_be_the_protocol() { + let code = """ + @protocol Foo - (void)fooWith:(NSInteger)foo; - (void)fooWith:(NSInteger)foo; - (void)fooWith:(NSInteger)foo; @@ -139,7 +214,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { XCTAssertEqual(context, .init( scope: .scope(signature: [ .init( - signature: "@interface Foo: NSObject", + signature: "@protocol Foo", name: "Foo", range: .init(startPair: (0, 0), endPair: (4, 4)) ), From 2749e2e6683b4a03d590d215f639c8781a510646 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 27 Nov 2023 16:13:40 +0800 Subject: [PATCH 17/74] Fix parsing includes and imports --- .../ObjectiveC/ObjectiveCCodeFinder.swift | 6 +- ...bjectiveCScopeHierarchySyntaxVisitor.swift | 47 ++---- .../ObjectiveCFocusedCodeFinderTests.swift | 135 ++++++++++++++++++ 3 files changed, 155 insertions(+), 33 deletions(-) diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift index effa8c1a..46ffed77 100644 --- a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift @@ -172,7 +172,11 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< textProvider: @escaping TextProvider ) -> NodeInfo? { guard let typeNode = node.child(byFieldName: "type") else { return nil } - return parseSignatureBeforeBody(typeNode, textProvider: textProvider) + guard var result = parseSignatureBeforeBody(typeNode, textProvider: textProvider) + else { return nil } + result.signature = "typedef \(result.signature)" + result.node = node + return result } func parseFunctionDefinitionNode( diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCScopeHierarchySyntaxVisitor.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCScopeHierarchySyntaxVisitor.swift index 6dfe5b45..23186bf8 100644 --- a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCScopeHierarchySyntaxVisitor.swift +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCScopeHierarchySyntaxVisitor.swift @@ -23,13 +23,13 @@ final class ObjectiveCScopeHierarchySyntaxVisitor: ASTTreeVisitor { self.textProvider = textProvider super.init(tree: tree) } - + /// The nodes containing the current range, sorted from inner to outer. func findScopeHierarchy(_ node: ASTNode) -> [ASTNode] { walk(node) return _scopeHierarchy.sorted { $0.range.location > $1.range.location } } - + /// The nodes containing the current range, sorted from inner to outer. func findScopeHierarchy() -> [ASTNode] { walk() @@ -70,7 +70,7 @@ final class ObjectiveCScopeHierarchySyntaxVisitor: ASTTreeVisitor { case .structSpecifier, .enumSpecifier, .nsEnumSpecifier: guard cursorRange.strictlyContains(range) else { return .skipChildren } _scopeHierarchy.append(node) - return .skipChildren + return .skipChildren case .functionDefinition: guard cursorRange.strictlyContains(range) else { return .skipChildren } _scopeHierarchy.append(node) @@ -85,45 +85,28 @@ final class ObjectiveCScopeHierarchySyntaxVisitor: ASTTreeVisitor { // MARK: Imports func handlePreprocInclude(_ node: ASTNode) { - let children = node.children - for child in children { - if let pathNode = child.child(byFieldName: "path") { - let path = textProvider(pathNode) - if !path.isEmpty { - includes.append(path.replacingOccurrences(of: "\"", with: "")) - } - break + if let pathNode = node.child(byFieldName: "path") { + let path = textProvider(pathNode) + if !path.isEmpty { + includes.append(path) } } } func handlePreprocImport(_ node: ASTNode) { - let children = node.children - for child in children { - if let pathNode = child.child(byFieldName: "path") { - let path = textProvider(pathNode) - if !path.isEmpty { - imports.append( - path - .replacingOccurrences(of: "\"", with: "") - .replacingOccurrences(of: "<", with: "") - .replacingOccurrences(of: ">", with: "") - ) - } - break + if let pathNode = node.child(byFieldName: "path") { + let path = textProvider(pathNode) + if !path.isEmpty { + imports.append(path) } } } func handleModuleImport(_ node: ASTNode) { - let children = node.children - for child in children { - if let pathNode = child.child(byFieldName: "module") { - let path = textProvider(pathNode) - if !path.isEmpty { - imports.append(path) - } - break + if let pathNode = node.child(byFieldName: "module") { + let path = textProvider(pathNode) + if !path.isEmpty { + imports.append(path) } } } diff --git a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift index bbad8c52..c6366015 100644 --- a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift @@ -231,5 +231,140 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { includes: [] )) } + + func test_selecting_a_line_inside_a_struct_the_scope_should_be_the_struct() { + let code = """ + struct Foo { + NSInteger foo; + NSInteger bar; + NSInteger baz; + } + """ + let range = CursorRange( + start: CursorPosition(line: 1, character: 0), + end: CursorPosition(line: 3, character: 31) + ) + let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + in: document(code: code), + containingRange: range + ) + XCTAssertEqual(context, .init( + scope: .scope(signature: [ + .init( + signature: "struct Foo", + name: "Foo", + range: .init(startPair: (0, 0), endPair: (4, 1)) + ), + ]), + contextRange: .init(startPair: (0, 0), endPair: (4, 1)), + focusedRange: range, + focusedCode: """ + NSInteger foo; + NSInteger bar; + NSInteger baz; + + """, + imports: [], + includes: [] + )) + } + + func test_selecting_a_line_inside_a_enum_the_scope_should_be_the_enum() { + let code = """ + enum Foo { + foo, + bar, + baz + }; + """ + let range = CursorRange( + start: CursorPosition(line: 1, character: 0), + end: CursorPosition(line: 3, character: 31) + ) + let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + in: document(code: code), + containingRange: range + ) + XCTAssertEqual(context, .init( + scope: .scope(signature: [ + .init( + signature: "enum Foo", + name: "Foo", + range: .init(startPair: (0, 0), endPair: (4, 1)) + ), + ]), + contextRange: .init(startPair: (0, 0), endPair: (4, 1)), + focusedRange: range, + focusedCode: """ + foo, + bar, + baz + + """, + imports: [], + includes: [] + )) + } + + func test_selecting_a_line_inside_an_NSEnum_the_scope_should_be_the_enum() { + let code = """ + typedef NS_ENUM(NSInteger, Foo) { + foo, + bar, + baz + }; + """ + let range = CursorRange( + start: CursorPosition(line: 1, character: 0), + end: CursorPosition(line: 3, character: 31) + ) + let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + in: document(code: code), + containingRange: range + ) + XCTAssertEqual(context, .init( + scope: .scope(signature: [ + .init( + signature: "typedef NS_ENUM(NSInteger, Foo)", + name: "Foo", + range: .init(startPair: (0, 0), endPair: (4, 2)) + ), + ]), + contextRange: .init(startPair: (0, 0), endPair: (4, 2)), + focusedRange: range, + focusedCode: """ + foo, + bar, + baz + + """, + imports: [], + includes: [] + )) + } } +final class ObjectiveCFocusedCodeFinder_Imports_Tests: XCTestCase { + func test_parsing_imports() { + let code = """ + #import + @import UIKit; + #import "Foo.h" + #include "Bar.h" + """ + + let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + in: document(code: code), + containingRange: .zero + ) + + XCTAssertEqual(context.imports, [ + "", + "UIKit", + "\"Foo.h\"" + ]) + XCTAssertEqual(context.includes, [ + "\"Bar.h\"" + ]) + } +} From f4ea0f670d5473e70e33e53accde3f93d4127adc Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 27 Nov 2023 16:27:52 +0800 Subject: [PATCH 18/74] Fix focused code --- .../ObjectiveC/ObjectiveCCodeFinder.swift | 4 +- .../ObjectiveCFocusedCodeFinderTests.swift | 79 +++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift index 46ffed77..c1c2d240 100644 --- a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift @@ -201,7 +201,7 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< node: node, signature: signature, name: name ?? "N/A", - canBeUsedAsCodeRange: false + canBeUsedAsCodeRange: true ) } } @@ -228,7 +228,7 @@ extension ObjectiveCFocusedCodeFinder { node: node, signature: signature, name: name ?? "N/A", - canBeUsedAsCodeRange: false + canBeUsedAsCodeRange: true ) } } diff --git a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift index c6366015..20e2f277 100644 --- a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift @@ -344,6 +344,85 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { } } +final class ObjectiveCFocusedCodeFinder_Focus_Tests: XCTestCase { + func test_get_focused_code_inside_method_the_method_should_be_the_focused_code() { + let code = """ + @implementation Foo + - (void)fooWith:(NSInteger)foo { + NSInteger foo = 0; + NSLog(@"Hello"); + NSLog(@"World"); + } + @end + """ + let range = CursorRange( + start: CursorPosition(line: 2, character: 0), + end: CursorPosition(line: 2, character: 0) + ) + let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + in: document(code: code), + containingRange: range + ) + XCTAssertEqual(context, .init( + scope: .scope(signature: [ + .init( + signature: "@implementation Foo", + name: "Foo", + range: .init(startPair: (0, 0), endPair: (6, 4)) + ), + ]), + contextRange: .init(startPair: (0, 0), endPair: (6, 4)), + focusedRange: .init(startPair: (1, 0), endPair: (5, 1)), + focusedCode: """ + - (void)fooWith:(NSInteger)foo { + NSInteger foo = 0; + NSLog(@"Hello"); + NSLog(@"World"); + } + + """, + imports: [], + includes: [] + )) + } + + func test_get_focused_code_inside_an_interface_category_the_focused_code_should_be_the_interface() { + let code = """ + @interface __GENERICS(NSArray, ObjectType) (BlocksKit) + - (void)fooWith:(NSInteger)foo; + - (void)fooWith:(NSInteger)foo; + - (void)fooWith:(NSInteger)foo; + @end + + @implementation Foo + @end + """ + let range = CursorRange( + start: CursorPosition(line: 1, character: 0), + end: CursorPosition(line: 1, character: 0) + ) + let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + in: document(code: code), + containingRange: range + ) + XCTAssertEqual(context, .init( + scope: .file, + contextRange: .init(startPair: (0, 0), endPair: (0, 0)), + focusedRange: .init(startPair: (0, 0), endPair: (4, 4)), + focusedCode: """ + @interface __GENERICS(NSArray, ObjectType) (BlocksKit) + - (void)fooWith:(NSInteger)foo; + - (void)fooWith:(NSInteger)foo; + - (void)fooWith:(NSInteger)foo; + @end + + """, + imports: [], + includes: [] + )) + } +} + final class ObjectiveCFocusedCodeFinder_Imports_Tests: XCTestCase { func test_parsing_imports() { let code = """ From e8c3ba44d30b55cc2f2208c8073d722fc1566005 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 27 Nov 2023 16:29:16 +0800 Subject: [PATCH 19/74] Update --- Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift index 6a082944..9c0e7987 100644 --- a/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift @@ -81,6 +81,7 @@ public struct FocusedCodeFinder { case .builtIn(.swift): return SwiftFocusedCodeFinder() case .builtIn(.objc), .builtIn(.objcpp), .builtIn(.c): + #warning("TODO: Implement C++ focused code finder, use it for C and metal shading language") return ObjectiveCFocusedCodeFinder() default: return UnknownLanguageFocusedCodeFinder(proposedSearchRange: 5) From 6d22d48b3e23ba1f53abdccf5c55609fb7b33888 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 28 Nov 2023 15:00:30 +0800 Subject: [PATCH 20/74] Add AutoresizingCustomTextEditor --- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 28 ++++------- .../PromptToCodePanel.swift | 30 ++++-------- Tool/Package.swift | 2 + .../SharedUIComponents/CustomTextEditor.swift | 47 +++++++++++++++++++ 4 files changed, 66 insertions(+), 41 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index af42e170..577642b8 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -540,26 +540,14 @@ struct ChatPanelInputArea: View { removeDuplicates: { $0.typedMessage == $1.typedMessage && $0.focusedField == $1.focusedField }) { viewStore in - ZStack(alignment: .center) { - // a hack to support dynamic height of TextEditor - Text( - viewStore.state.typedMessage.isEmpty ? "Hi" : viewStore.state.typedMessage - ).opacity(0) - .font(.system(size: 14)) - .frame(maxWidth: .infinity, maxHeight: 400) - .padding(.top, 1) - .padding(.bottom, 2) - .padding(.horizontal, 4) - - CustomTextEditor( - text: viewStore.$typedMessage, - font: .systemFont(ofSize: 14), - onSubmit: { viewStore.send(.sendButtonTapped) }, - completions: chatAutoCompletion - ) - .padding(.top, 1) - .padding(.bottom, -1) - } + AutoresizingCustomTextEditor( + text: viewStore.$typedMessage, + font: .systemFont(ofSize: 14), + isEditable: true, + maxHeight: 400, + onSubmit: { viewStore.send(.sendButtonTapped) }, + completions: chatAutoCompletion + ) .focused($focusedField, equals: .textField) .bind(viewStore.$focusedField, to: $focusedField) .padding(8) diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift index 9d5ddaaf..31c71b9e 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift @@ -372,27 +372,15 @@ extension PromptToCodePanel { ) } ) { viewStore in - ZStack(alignment: .center) { - // a hack to support dynamic height of TextEditor - Text(viewStore.state.prompt.isEmpty ? "Hi" : viewStore.state.prompt) - .opacity(0) - .font(.system(size: 14)) - .frame(maxWidth: .infinity, maxHeight: 400) - .padding(.top, 1) - .padding(.bottom, 2) - .padding(.horizontal, 4) - - CustomTextEditor( - text: viewStore.$prompt, - font: .systemFont(ofSize: 14), - isEditable: !viewStore.state.isResponding, - onSubmit: { viewStore.send(.modifyCodeButtonTapped) } - ) - .padding(.top, 1) - .padding(.bottom, -1) - .opacity(viewStore.state.isResponding ? 0.5 : 1) - .disabled(viewStore.state.isResponding) - } + AutoresizingCustomTextEditor( + text: viewStore.$prompt, + font: .systemFont(ofSize: 14), + isEditable: !viewStore.state.isResponding, + maxHeight: 400, + onSubmit: { viewStore.send(.modifyCodeButtonTapped) } + ) + .opacity(viewStore.state.isResponding ? 0.5 : 1) + .disabled(viewStore.state.isResponding) .focused($focusField, equals: .textField) .bind(viewStore.$focusField, to: $focusField) } diff --git a/Tool/Package.swift b/Tool/Package.swift index 0332e8dd..6772dd0c 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -61,6 +61,7 @@ let package = Package( ), .package(url: "https://github.com/apple/swift-syntax.git", branch: "main"), .package(url: "https://github.com/GottaGetSwifty/CodableWrappers", from: "2.0.7"), + .package(url: "https://github.com/krzyzanowskim/STTextView", from: "0.8.21"), // TreeSitter .package(url: "https://github.com/intitni/SwiftTreeSitter.git", branch: "main"), @@ -185,6 +186,7 @@ let package = Package( dependencies: [ "Highlightr", "Preferences", + .product(name: "STTextView", package: "STTextView"), ] ), .testTarget(name: "SharedUIComponentsTests", dependencies: ["SharedUIComponents"]), diff --git a/Tool/Sources/SharedUIComponents/CustomTextEditor.swift b/Tool/Sources/SharedUIComponents/CustomTextEditor.swift index 9ce33e6b..d001da8e 100644 --- a/Tool/Sources/SharedUIComponents/CustomTextEditor.swift +++ b/Tool/Sources/SharedUIComponents/CustomTextEditor.swift @@ -1,5 +1,52 @@ import SwiftUI +public struct AutoresizingCustomTextEditor: View { + @Binding public var text: String + public let font: NSFont + public let isEditable: Bool + public let maxHeight: Double + public let onSubmit: () -> Void + public var completions: (_ text: String, _ words: [String], _ range: NSRange) -> [String] + + public init( + text: Binding, + font: NSFont, + isEditable: Bool, + maxHeight: Double, + onSubmit: @escaping () -> Void, + completions: @escaping (_ text: String, _ words: [String], _ range: NSRange) + -> [String] = { _, _, _ in [] } + ) { + _text = text + self.font = font + self.isEditable = isEditable + self.maxHeight = maxHeight + self.onSubmit = onSubmit + self.completions = completions + } + + public var body: some View { + ZStack(alignment: .center) { + // a hack to support dynamic height of TextEditor + Text(text.isEmpty ? "Hi" : text).opacity(0) + .font(.init(font)) + .frame(maxWidth: .infinity, maxHeight: maxHeight) + .padding(.top, 1) + .padding(.bottom, 2) + .padding(.horizontal, 4) + + CustomTextEditor( + text: $text, + font: font, + onSubmit: onSubmit, + completions: completions + ) + .padding(.top, 1) + .padding(.bottom, -1) + } + } +} + public struct CustomTextEditor: NSViewRepresentable { public func makeCoordinator() -> Coordinator { Coordinator(self) From 1305c7278f72cdf1e2e7a8a414cb13af225ba142 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 28 Nov 2023 15:03:45 +0800 Subject: [PATCH 21/74] Rename SuggestionProvider to CodeSuggestionProvider --- Core/Sources/Service/GUI/WidgetDataSource.swift | 2 +- .../FeatureReducers/PanelFeature.swift | 4 ++-- .../FeatureReducers/SharedPanelFeature.swift | 2 +- .../FeatureReducers/SuggestionPanelFeature.swift | 2 +- ...ovider.swift => CodeSuggestionProvider.swift} | 5 +++-- .../CodeBlockSuggestionPanel.swift | 16 ++++++++-------- .../SuggestionWidgetDataSource.swift | 6 +++--- Pro | 2 +- 8 files changed, 20 insertions(+), 19 deletions(-) rename Core/Sources/SuggestionWidget/Providers/{SuggestionProvider.swift => CodeSuggestionProvider.swift} (89%) diff --git a/Core/Sources/Service/GUI/WidgetDataSource.swift b/Core/Sources/Service/GUI/WidgetDataSource.swift index 7fa244e8..b51001f8 100644 --- a/Core/Sources/Service/GUI/WidgetDataSource.swift +++ b/Core/Sources/Service/GUI/WidgetDataSource.swift @@ -14,7 +14,7 @@ import SuggestionWidget final class WidgetDataSource {} extension WidgetDataSource: SuggestionWidgetDataSource { - func suggestionForFile(at url: URL) async -> SuggestionProvider? { + func suggestionForFile(at url: URL) async -> CodeSuggestionProvider? { for workspace in Service.shared.workspacePool.workspaces.values { if let filespace = workspace.filespaces[url], let suggestion = filespace.presentingSuggestion diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift index 589a8a11..4fd43f94 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift @@ -23,7 +23,7 @@ public struct PanelFeature: ReducerProtocol { public enum Action: Equatable { case presentSuggestion - case presentSuggestionProvider(SuggestionProvider, displayContent: Bool) + case presentSuggestionProvider(CodeSuggestionProvider, displayContent: Bool) case presentError(String) case presentPromptToCode(PromptToCodeGroup.PromptToCodeInitialState) case displayPanelContent @@ -136,7 +136,7 @@ public struct PanelFeature: ReducerProtocol { } } - func fetchSuggestionProvider(fileURL: URL) async -> SuggestionProvider? { + func fetchSuggestionProvider(fileURL: URL) async -> CodeSuggestionProvider? { guard let provider = await suggestionWidgetControllerDependency .suggestionWidgetDataSource? .suggestionForFile(at: fileURL) else { return nil } diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/SharedPanelFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/SharedPanelFeature.swift index 32657955..30d6a1c0 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/SharedPanelFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/SharedPanelFeature.swift @@ -6,7 +6,7 @@ import SwiftUI public struct SharedPanelFeature: ReducerProtocol { public struct Content: Equatable { public var promptToCodeGroup = PromptToCodeGroup.State() - var suggestion: SuggestionProvider? + var suggestion: CodeSuggestionProvider? public var promptToCode: PromptToCode.State? { promptToCodeGroup.activePromptToCode } var error: String? } diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/SuggestionPanelFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/SuggestionPanelFeature.swift index dbd4034e..db6061e8 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/SuggestionPanelFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/SuggestionPanelFeature.swift @@ -4,7 +4,7 @@ import SwiftUI public struct SuggestionPanelFeature: ReducerProtocol { public struct State: Equatable { - var content: SuggestionProvider? + var content: CodeSuggestionProvider? var colorScheme: ColorScheme = .light var alignTopToAnchor = false var isPanelDisplayed: Bool = false diff --git a/Core/Sources/SuggestionWidget/Providers/SuggestionProvider.swift b/Core/Sources/SuggestionWidget/Providers/CodeSuggestionProvider.swift similarity index 89% rename from Core/Sources/SuggestionWidget/Providers/SuggestionProvider.swift rename to Core/Sources/SuggestionWidget/Providers/CodeSuggestionProvider.swift index b821d4a8..61167584 100644 --- a/Core/Sources/SuggestionWidget/Providers/SuggestionProvider.swift +++ b/Core/Sources/SuggestionWidget/Providers/CodeSuggestionProvider.swift @@ -1,8 +1,8 @@ import Foundation import SwiftUI -public final class SuggestionProvider: ObservableObject, Equatable { - public static func == (lhs: SuggestionProvider, rhs: SuggestionProvider) -> Bool { +public final class CodeSuggestionProvider: ObservableObject, Equatable { + public static func == (lhs: CodeSuggestionProvider, rhs: CodeSuggestionProvider) -> Bool { lhs.code == rhs.code && lhs.language == rhs.language } @@ -12,6 +12,7 @@ public final class SuggestionProvider: ObservableObject, Equatable { @Published public var suggestionCount: Int = 0 @Published public var currentSuggestionIndex: Int = 0 @Published public var commonPrecedingSpaceCount = 0 + @Published public var extraInformation: String = "" public var onSelectPreviousSuggestionTapped: () -> Void public var onSelectNextSuggestionTapped: () -> Void diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift index c43c396d..3013e23e 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift @@ -2,13 +2,13 @@ import SharedUIComponents import SwiftUI struct CodeBlockSuggestionPanel: View { - @ObservedObject var suggestion: SuggestionProvider + @ObservedObject var suggestion: CodeSuggestionProvider @Environment(\.colorScheme) var colorScheme @AppStorage(\.suggestionCodeFontSize) var fontSize @AppStorage(\.suggestionDisplayCompactMode) var suggestionDisplayCompactMode struct ToolBar: View { - @ObservedObject var suggestion: SuggestionProvider + @ObservedObject var suggestion: CodeSuggestionProvider var body: some View { HStack { @@ -50,7 +50,7 @@ struct CodeBlockSuggestionPanel: View { } struct CompactToolBar: View { - @ObservedObject var suggestion: SuggestionProvider + @ObservedObject var suggestion: CodeSuggestionProvider var body: some View { HStack { @@ -114,7 +114,7 @@ struct CodeBlockSuggestionPanel: View { struct CodeBlockSuggestionPanel_Dark_Preview: PreviewProvider { static var previews: some View { - CodeBlockSuggestionPanel(suggestion: SuggestionProvider( + CodeBlockSuggestionPanel(suggestion: CodeSuggestionProvider( code: """ LazyVGrid(columns: [GridItem(.fixed(30)), GridItem(.flexible())]) { ForEach(0.. SuggestionProvider? + func suggestionForFile(at url: URL) async -> CodeSuggestionProvider? } struct MockWidgetDataSource: SuggestionWidgetDataSource { - func suggestionForFile(at url: URL) async -> SuggestionProvider? { - return SuggestionProvider( + func suggestionForFile(at url: URL) async -> CodeSuggestionProvider? { + return CodeSuggestionProvider( code: """ func test() { let x = 1 diff --git a/Pro b/Pro index c0ffa886..7b8199f4 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit c0ffa88685b35c90643d71a1feb9faa6ce624898 +Subproject commit 7b8199f4e10217a9349dffec28aa71b7721c8bdd From 02900cbfbb2179f7cd06f423c6cc54b63eb55870 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 28 Nov 2023 23:11:24 +0800 Subject: [PATCH 22/74] Update --- .../Experiment/NewCodeBlock.swift | 289 ++++++++++++++++++ .../SyntaxHighlighting.swift | 45 +-- 2 files changed, 314 insertions(+), 20 deletions(-) create mode 100644 Tool/Sources/SharedUIComponents/Experiment/NewCodeBlock.swift diff --git a/Tool/Sources/SharedUIComponents/Experiment/NewCodeBlock.swift b/Tool/Sources/SharedUIComponents/Experiment/NewCodeBlock.swift new file mode 100644 index 00000000..87e716a9 --- /dev/null +++ b/Tool/Sources/SharedUIComponents/Experiment/NewCodeBlock.swift @@ -0,0 +1,289 @@ +import STTextView +import SwiftUI + +private let insetBottom = 12 as Double +private let insetTop = 12 as Double + +/// This SwiftUI view can be used to view and edit rich text. +struct _CodeBlock: View { + @Binding private var selection: NSRange? + @State private var contentHeight: Double = 500 + let fontSize: Double + let commonPrecedingSpaceCount: Int + let highlightedCode: AttributedString + let colorScheme: ColorScheme + + /// Create a text edit view with a certain text that uses a certain options. + /// - Parameters: + /// - text: The attributed string content + /// - options: Editor options + /// - plugins: Editor plugins + public init( + code: String, + language: String, + firstLinePrecedingSpaceCount: Int, + colorScheme: ColorScheme, + fontSize: Double, + selection: Binding = .constant(nil) + ) { + _selection = selection + self.fontSize = fontSize + self.colorScheme = colorScheme + + let padding = firstLinePrecedingSpaceCount > 0 + ? String(repeating: " ", count: firstLinePrecedingSpaceCount) + : "" + let result = Self.highlight( + code: padding + code, + language: language, + colorScheme: colorScheme, + fontSize: fontSize + ) + commonPrecedingSpaceCount = result.commonLeadingSpaceCount + highlightedCode = result.code + } + + public var body: some View { + _CodeBlockRepresentable( + text: highlightedCode, + selection: $selection, + fontSize: fontSize, + onHeightChange: { height in + print("Q", height) + contentHeight = height + } + ) + .frame(height: contentHeight, alignment: .topLeading) + .background(.background) + .colorScheme(colorScheme) + .onAppear { + print("") + } + } + + static func highlight( + code: String, + language: String, + colorScheme: ColorScheme, + fontSize: Double + ) -> (code: AttributedString, commonLeadingSpaceCount: Int) { + let (lines, commonLeadingSpaceCount) = highlighted( + code: code, + language: language, + brightMode: colorScheme != .dark, + droppingLeadingSpaces: UserDefaults.shared + .value(for: \.hideCommonPrecedingSpacesInSuggestion), + fontSize: fontSize, + replaceSpacesWithMiddleDots: false + ) + + let string = NSMutableAttributedString() + for (index, line) in lines.enumerated() { + string.append(line) + if index < lines.count - 1 { + string.append(NSAttributedString(string: "\n")) + } + } + + return (code: .init(string), commonLeadingSpaceCount: commonLeadingSpaceCount) + } +} + +private struct _CodeBlockRepresentable: NSViewRepresentable { + @Environment(\.isEnabled) private var isEnabled + @Environment(\.lineSpacing) private var lineSpacing + + @Binding private var selection: NSRange? + let text: AttributedString + let fontSize: Double + let onHeightChange: (Double) -> Void + + init( + text: AttributedString, + selection: Binding, + fontSize: Double, + onHeightChange: @escaping (Double) -> Void + ) { + self.text = text + _selection = selection + self.fontSize = fontSize + self.onHeightChange = onHeightChange + } + + func makeNSView(context: Context) -> NSScrollView { + let scrollView = STTextViewFrameObservable.scrollableTextView() + scrollView.contentInsets = .init(top: 0, left: 0, bottom: insetBottom, right: 0) + scrollView.automaticallyAdjustsContentInsets = false + let textView = scrollView.documentView as! STTextView + textView.delegate = context.coordinator + textView.highlightSelectedLine = false + textView.widthTracksTextView = true + textView.heightTracksTextView = true + textView.isEditable = true + + textView.setSelectedRange(NSRange()) + let lineNumberRuler = STLineNumberRulerView(textView: textView) + lineNumberRuler.backgroundColor = .clear + lineNumberRuler.separatorColor = .clear + lineNumberRuler.rulerInsets = .init(leading: 10, trailing: 10) + scrollView.verticalRulerView = lineNumberRuler + let columnNumberRuler = ColumnRuler(textView: textView) + scrollView.horizontalRulerView = columnNumberRuler + scrollView.rulersVisible = true + + context.coordinator.isUpdating = true + textView.setAttributedString(NSAttributedString(text)) + context.coordinator.isUpdating = false + + return scrollView + } + + func updateNSView(_ scrollView: NSScrollView, context: Context) { + context.coordinator.parent = self + + let textView = scrollView.documentView as! STTextViewFrameObservable + + textView.onHeightChange = onHeightChange + textView.showsInvisibleCharacters = true + textView.textContainer.lineBreakMode = .byCharWrapping + + if let columnNumberRuler = scrollView.horizontalRulerView as? ColumnRuler { + columnNumberRuler.columnNumber = 5 + } + + do { + context.coordinator.isUpdating = true + if context.coordinator.isDidChangeText == false { + textView.setAttributedString(.init(text)) + } + context.coordinator.isUpdating = false + context.coordinator.isDidChangeText = false + } + + if textView.selectedRange() != selection, let selection { + textView.setSelectedRange(selection) + } + + if textView.isSelectable != isEnabled { + textView.isSelectable = isEnabled + } + + textView.isEditable = false + + if !textView.widthTracksTextView { + textView.widthTracksTextView = false + } + + if !textView.heightTracksTextView { + textView.heightTracksTextView = true + } + + let font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular) + if textView.font != font { + textView.font = font + } + } + + func makeCoordinator() -> TextCoordinator { + TextCoordinator(parent: self) + } + + private func styledAttributedString(_ typingAttributes: [NSAttributedString.Key: Any]) + -> AttributedString + { + let paragraph = (typingAttributes[.paragraphStyle] as! NSParagraphStyle) + .mutableCopy() as! NSMutableParagraphStyle + if paragraph.lineSpacing != lineSpacing { + paragraph.lineSpacing = lineSpacing + var typingAttributes = typingAttributes + typingAttributes[.paragraphStyle] = paragraph + + let attributeContainer = AttributeContainer(typingAttributes) + var styledText = text + styledText.mergeAttributes(attributeContainer, mergePolicy: .keepNew) + return styledText + } + + return text + } + + class TextCoordinator: STTextViewDelegate { + var parent: _CodeBlockRepresentable + var isUpdating: Bool = false + var isDidChangeText: Bool = false + var enqueuedValue: AttributedString? + + init(parent: _CodeBlockRepresentable) { + self.parent = parent + } + + func textViewDidChangeText(_ notification: Notification) { + guard let textView = notification.object as? STTextView else { + return + } + + (textView as! STTextViewFrameObservable).recalculateSize() + } + + func textViewDidChangeSelection(_ notification: Notification) { + guard let textView = notification.object as? STTextView else { + return + } + + Task { @MainActor in + self.parent.selection = textView.selectedRange() + } + } + } +} + +private class STTextViewFrameObservable: STTextView { + var onHeightChange: ((Double) -> Void)? + func recalculateSize() { + var maxY = 0 as Double + textLayoutManager.enumerateTextLayoutFragments(in: textLayoutManager.documentRange, options: [.ensuresLayout]) { fragment in + print(fragment.layoutFragmentFrame) + maxY = max(maxY, fragment.layoutFragmentFrame.maxY) + return true + } + onHeightChange?(maxY) + } +} + +private final class ColumnRuler: NSRulerView { + var columnNumber: Int = 0 + + private var textView: STTextView? { + clientView as? STTextView + } + + public required init(textView: STTextView, scrollView: NSScrollView? = nil) { + super.init( + scrollView: scrollView ?? textView.enclosingScrollView, + orientation: .verticalRuler + ) + clientView = textView + ruleThickness = insetBottom + } + + @available(*, unavailable) + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_: NSRect) { + guard let context: CGContext = NSGraphicsContext.current?.cgContext else { return } + NSColor.windowBackgroundColor.withAlphaComponent(0.6).setFill() + context.fill(bounds) + + let insetLeft = scrollView?.verticalRulerView?.bounds.width ?? 0 + var drawingBounds = bounds + drawingBounds.origin.x += insetLeft + 4 + let fontSize = 10 as Double + drawingBounds.origin.y = (insetTop - fontSize) / 2 + NSString(string: "\(columnNumber)").draw(in: drawingBounds, withAttributes: [ + .font: NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular), + .foregroundColor: NSColor.tertiaryLabelColor, + ]) + } +} diff --git a/Tool/Sources/SharedUIComponents/SyntaxHighlighting.swift b/Tool/Sources/SharedUIComponents/SyntaxHighlighting.swift index db01ac70..57c4cf50 100644 --- a/Tool/Sources/SharedUIComponents/SyntaxHighlighting.swift +++ b/Tool/Sources/SharedUIComponents/SyntaxHighlighting.swift @@ -42,7 +42,8 @@ public func highlighted( language: String, brightMode: Bool, droppingLeadingSpaces: Bool, - fontSize: Double + fontSize: Double, + replaceSpacesWithMiddleDots: Bool = true ) -> (code: [NSAttributedString], commonLeadingSpaceCount: Int) { let formatted = highlightedCodeBlock( code: code, @@ -56,14 +57,16 @@ public func highlighted( return convertToCodeLines( formatted, middleDotColor: middleDotColor, - droppingLeadingSpaces: droppingLeadingSpaces + droppingLeadingSpaces: droppingLeadingSpaces, + replaceSpacesWithMiddleDots: replaceSpacesWithMiddleDots ) } func convertToCodeLines( _ formattedCode: NSAttributedString, middleDotColor: NSColor, - droppingLeadingSpaces: Bool + droppingLeadingSpaces: Bool, + replaceSpacesWithMiddleDots: Bool = true ) -> (code: [NSAttributedString], commonLeadingSpaceCount: Int) { let input = formattedCode.string func isEmptyLine(_ line: String) -> Bool { @@ -115,24 +118,26 @@ func convertToCodeLines( } } - // use regex to replace all spaces to a middle dot - do { - let regex = try NSRegularExpression(pattern: "[ ]*", options: []) - let result = regex.matches( - in: mutable.string, - range: NSRange(location: 0, length: mutable.mutableString.length) - ) - for r in result { - let range = r.range - mutable.replaceCharacters( - in: range, - with: String(repeating: "·", count: range.length) + if replaceSpacesWithMiddleDots { + // use regex to replace all spaces to a middle dot + do { + let regex = try NSRegularExpression(pattern: "[ ]*", options: []) + let result = regex.matches( + in: mutable.string, + range: NSRange(location: 0, length: mutable.mutableString.length) ) - mutable.addAttributes([ - .foregroundColor: middleDotColor, - ], range: range) - } - } catch {} + for r in result { + let range = r.range + mutable.replaceCharacters( + in: range, + with: String(repeating: "·", count: range.length) + ) + mutable.addAttributes([ + .foregroundColor: middleDotColor, + ], range: range) + } + } catch {} + } output.append(mutable) start += range.length + 1 } From 03882d7e56f3b58a4717e4cb4d59ba0a4461048d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 30 Nov 2023 15:15:16 +0800 Subject: [PATCH 23/74] Update the context system prompt to use role user --- .../Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift index 0a4f36e6..415534cc 100644 --- a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift +++ b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift @@ -88,7 +88,7 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { } var smallestSystemPromptMessage = ChatMessage(role: .system, content: systemPrompt) - var contextSystemPromptMessage = ChatMessage(role: .system, content: contextSystemPrompt) + var contextSystemPromptMessage = ChatMessage(role: .user, content: contextSystemPrompt) let smallestSystemMessageTokenCount = countToken(&smallestSystemPromptMessage) let contextSystemPromptTokenCount = !contextSystemPrompt.isEmpty ? countToken(&contextSystemPromptMessage) From bc39122e0ca7e328887dff1e14abec7d9b772d09 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 30 Nov 2023 16:23:00 +0800 Subject: [PATCH 24/74] Adjust the prompt in chat --- .../Memory/AutoManagedChatGPTMemory.swift | 213 ++++++++++++------ 1 file changed, 148 insertions(+), 65 deletions(-) diff --git a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift index 415534cc..ba3a93a4 100644 --- a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift +++ b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift @@ -67,11 +67,11 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { /// Format: /// ``` /// [System Prompt] priority: high - /// [Retrieved Content] priority: low + /// [Functions] priority: high + /// [Retrieved Content] priority: low /// [Retrieved Content A] /// /// [Retrieved Content B] - /// [Functions] priority: high /// [Message History] priority: medium /// [Context System Prompt] priority: high /// [Latest Message] priority: high @@ -80,18 +80,88 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { maxNumberOfMessages: Int = UserDefaults.shared.value(for: \.chatGPTMaxMessageCount), encoder: TokenEncoder = AutoManagedChatGPTMemory.encoder ) -> [ChatMessage] { - func countToken(_ message: inout ChatMessage) -> Int { - if let count = message.tokensCount { return count } - let count = encoder.countToken(message: message) - message.tokensCount = count - return count + let ( + systemPromptMessage, + contextSystemPromptMessage, + availableTokenCountForMessages, + mandatoryUsage + ) = generateMandatoryMessages(encoder: encoder) + + let ( + historyMessage, + newMessage, + availableTokenCountForRetrievedContent, + messageUsage + ) = generateMessageHistory( + maxNumberOfMessages: maxNumberOfMessages, + maxTokenCount: availableTokenCountForMessages, + encoder: encoder + ) + + let ( + retrievedContentMessage, + _, + retrievedContentUsage, + _ + ) = generateRetrievedContentMessage( + maxTokenCount: availableTokenCountForRetrievedContent, + encoder: encoder + ) + + let allMessages: [ChatMessage] = ( + [systemPromptMessage] + + historyMessage + + [retrievedContentMessage, contextSystemPromptMessage, newMessage] + ).filter { + !($0.content?.isEmpty ?? false) } + #if DEBUG + Logger.service.info(""" + Sending tokens count + - system prompt: \(mandatoryUsage.systemPrompt) + - context system prompt: \(mandatoryUsage.contextSystemPrompt) + - functions: \(mandatoryUsage.functions) + - messages: \(messageUsage) + - retrieved content: \(retrievedContentUsage) + - total: \( + mandatoryUsage.systemPrompt + + mandatoryUsage.contextSystemPrompt + + mandatoryUsage.functions + + messageUsage + + retrievedContentUsage + ) + """) + #endif + + return allMessages + } + + func generateRemainingTokens( + maxNumberOfMessages: Int = UserDefaults.shared.value(for: \.chatGPTMaxMessageCount), + encoder: TokenEncoder = AutoManagedChatGPTMemory.encoder + ) -> Int? { + // It should be fine to just let OpenAI decide. + return nil + } + + func setOnHistoryChangeBlock(_ onChange: @escaping () -> Void) { + onHistoryChange = onChange + } +} + +extension AutoManagedChatGPTMemory { + func generateMandatoryMessages(encoder: TokenEncoder) -> ( + systemPrompt: ChatMessage, + contextSystemPrompt: ChatMessage, + remainingTokenCount: Int, + usage: (systemPrompt: Int, contextSystemPrompt: Int, functions: Int) + ) { var smallestSystemPromptMessage = ChatMessage(role: .system, content: systemPrompt) var contextSystemPromptMessage = ChatMessage(role: .user, content: contextSystemPrompt) - let smallestSystemMessageTokenCount = countToken(&smallestSystemPromptMessage) + let smallestSystemMessageTokenCount = encoder.countToken(&smallestSystemPromptMessage) let contextSystemPromptTokenCount = !contextSystemPrompt.isEmpty - ? countToken(&contextSystemPromptMessage) + ? encoder.countToken(&contextSystemPromptMessage) : 0 let functionTokenCount = functionProvider.functions.reduce(into: 0) { partial, function in @@ -109,46 +179,86 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { + functionTokenCount + 3 // every reply is primed with <|start|>assistant<|message|> + // build messages + /// the available tokens count for other messages and retrieved content let availableTokenCountForMessages = configuration.maxTokens - configuration.minimumReplyTokens - mandatoryContentTokensCount + return ( + smallestSystemPromptMessage, + contextSystemPromptMessage, + availableTokenCountForMessages, + ( + smallestSystemMessageTokenCount, + contextSystemPromptTokenCount, + functionTokenCount + ) + ) + } + + func generateMessageHistory( + maxNumberOfMessages: Int, + maxTokenCount: Int, + encoder: TokenEncoder + ) -> ( + history: [ChatMessage], + newMessage: ChatMessage, + remainingTokenCount: Int, + usage: Int + ) { var messageTokenCount = 0 var allMessages: [ChatMessage] = [] + var newMessage: ChatMessage? for (index, message) in history.enumerated().reversed() { if maxNumberOfMessages > 0, allMessages.count >= maxNumberOfMessages { break } if message.isEmpty { continue } - let tokensCount = countToken(&history[index]) - if tokensCount + messageTokenCount > availableTokenCountForMessages { break } + let tokensCount = encoder.countToken(&history[index]) + if tokensCount + messageTokenCount > maxTokenCount { break } messageTokenCount += tokensCount - allMessages.append(message) + if index == history.endIndex - 1 { + newMessage = message + } else { + allMessages.append(message) + } } - /// the available tokens count for retrieved content - let availableTokenCountForRetrievedContent = min( - availableTokenCountForMessages - messageTokenCount, - configuration.maxTokens / 2 + return ( + allMessages.reversed(), + newMessage ?? .init(role: .user, content: ""), + maxTokenCount - messageTokenCount, + messageTokenCount ) - var retrievedContentTokenCount = 0 + } + func generateRetrievedContentMessage( + maxTokenCount: Int, + encoder: TokenEncoder + ) -> ( + retrievedContent: ChatMessage, + remainingTokenCount: Int, + usage: Int, + includedRetrievedContent: [String] + ) { + var retrievedContentTokenCount = 0 let separator = String(repeating: "=", count: 32) // only 1 token + var message = "" + var includedRetrievedContent = [String]() - var systemPrompt = systemPrompt - - func appendToSystemPrompt(_ text: String) -> Bool { + func appendToMessage(_ text: String) -> Bool { let tokensCount = encoder.countToken(text: text) - if tokensCount + retrievedContentTokenCount > - availableTokenCountForRetrievedContent { return false } + if tokensCount + retrievedContentTokenCount > maxTokenCount { return false } retrievedContentTokenCount += tokensCount - systemPrompt += text + message += text + includedRetrievedContent.append(text) return true } for (index, content) in retrievedContent.filter({ !$0.isEmpty }).enumerated() { if index == 0 { - if !appendToSystemPrompt(""" + if !appendToMessage(""" ## Relevant Content @@ -158,52 +268,18 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { """) { break } } else { - if !appendToSystemPrompt("\n\(separator)\n") { break } + if !appendToMessage("\n\(separator)\n") { break } } - if !appendToSystemPrompt(content) { break } + if !appendToMessage(content) { break } } - if !systemPrompt.isEmpty { - let message = ChatMessage(role: .system, content: systemPrompt) - allMessages.append(message) - } - - if !contextSystemPrompt.isEmpty { - allMessages.insert(contextSystemPromptMessage, at: 1) - } - - #if DEBUG - Logger.service.info(""" - Sending tokens count - - system prompt: \(smallestSystemMessageTokenCount) - - context system prompt: \(contextSystemPromptTokenCount) - - functions: \(functionTokenCount) - - messages: \(messageTokenCount) - - retrieved content: \(retrievedContentTokenCount) - - total: \( - smallestSystemMessageTokenCount - + contextSystemPromptTokenCount - + functionTokenCount - + messageTokenCount - + retrievedContentTokenCount + return ( + .init(role: .user, content: message), + maxTokenCount - retrievedContentTokenCount, + retrievedContentTokenCount, + includedRetrievedContent ) - """) - #endif - - return allMessages.reversed() - } - - func generateRemainingTokens( - maxNumberOfMessages: Int = UserDefaults.shared.value(for: \.chatGPTMaxMessageCount), - encoder: TokenEncoder = AutoManagedChatGPTMemory.encoder - ) -> Int? { - // It should be fine to just let OpenAI decide. - return nil - } - - func setOnHistoryChangeBlock(_ onChange: @escaping () -> Void) { - onHistoryChange = onChange } } @@ -224,5 +300,12 @@ extension TokenEncoder { } return total } + + func countToken(_ message: inout ChatMessage) -> Int { + if let count = message.tokensCount { return count } + let count = countToken(message: message) + message.tokensCount = count + return count + } } From a1c1043119a9c17cad031e1bd40c4e87f4e2df91 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 30 Nov 2023 16:35:43 +0800 Subject: [PATCH 25/74] Rename ChatMessage in chat panel to DisplayedChatMessage --- Core/Sources/ChatGPTChatTab/Chat.swift | 4 ++-- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 4 ++-- .../OpenAIService/Memory/AutoManagedChatGPTMemory.swift | 6 +----- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/Chat.swift b/Core/Sources/ChatGPTChatTab/Chat.swift index 9bfbe68b..767e4965 100644 --- a/Core/Sources/ChatGPTChatTab/Chat.swift +++ b/Core/Sources/ChatGPTChatTab/Chat.swift @@ -4,7 +4,7 @@ import Foundation import OpenAIService import Preferences -public struct ChatMessage: Equatable { +public struct DisplayedChatMessage: Equatable { public enum Role { case user case assistant @@ -29,7 +29,7 @@ struct Chat: ReducerProtocol { struct State: Equatable { var title: String = "Chat" @BindingState var typedMessage = "" - var history: [ChatMessage] = [] + var history: [DisplayedChatMessage] = [] @BindingState var isReceivingMessage = false var chatMenu = ChatMenu.State() @BindingState var focusedField: Field? diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index 577642b8..40fe2711 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -196,7 +196,7 @@ struct ChatPanelMessages: View { struct PinToBottomRelatedState: Equatable { var isReceivingMessage: Bool - var lastMessage: ChatMessage? + var lastMessage: DisplayedChatMessage? } var body: some View { @@ -676,7 +676,7 @@ struct RoundedCorners: Shape { // MARK: - Previews struct ChatPanel_Preview: PreviewProvider { - static let history: [ChatMessage] = [ + static let history: [DisplayedChatMessage] = [ .init( id: "1", role: .user, diff --git a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift index ba3a93a4..b40a738e 100644 --- a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift +++ b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift @@ -259,11 +259,7 @@ extension AutoManagedChatGPTMemory { for (index, content) in retrievedContent.filter({ !$0.isEmpty }).enumerated() { if index == 0 { if !appendToMessage(""" - - - ## Relevant Content - - Below are information related to the conversation, separated by \(separator) + Here are the information you know about the system and the project, separated by \(separator) """) { break } From 9b22c2963381edc2de8c19634f90c24095d8f3a3 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 30 Nov 2023 17:28:44 +0800 Subject: [PATCH 26/74] Add references to chat message --- Core/Sources/ChatGPTChatTab/Chat.swift | 7 +++-- Tool/Sources/OpenAIService/Models.swift | 42 +++++++++++++++++++++---- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/Chat.swift b/Core/Sources/ChatGPTChatTab/Chat.swift index 767e4965..78e9c070 100644 --- a/Core/Sources/ChatGPTChatTab/Chat.swift +++ b/Core/Sources/ChatGPTChatTab/Chat.swift @@ -15,11 +15,13 @@ public struct DisplayedChatMessage: Equatable { public var id: String public var role: Role public var text: String + public var references: [ChatMessage.Reference] = [] - public init(id: String, role: Role, text: String) { + public init(id: String, role: Role, text: String, references: [ChatMessage.Reference]) { self.id = id self.role = role self.text = text + self.references = references } } @@ -247,7 +249,8 @@ struct Chat: ReducerProtocol { case .function: return .function } }(), - text: message.summary ?? message.content ?? "" + text: message.summary ?? message.content ?? "", + references: message.references ) } diff --git a/Tool/Sources/OpenAIService/Models.swift b/Tool/Sources/OpenAIService/Models.swift index 1911f054..54a3709f 100644 --- a/Tool/Sources/OpenAIService/Models.swift +++ b/Tool/Sources/OpenAIService/Models.swift @@ -1,3 +1,4 @@ +import CodableWrappers import Foundation struct Cancellable { @@ -24,9 +25,32 @@ public struct ChatMessage: Equatable, Codable { } } + public struct Reference: Codable, Equatable { + public var title: String + public var subTitle: String + public var uri: String + public var startLine: Int? + public var endLine: Int? + public var metadata: [String: String] + + public init( + title: String, + subTitle: String, + uri: String, + startLine: Int?, + endLine: Int?, + metadata: [String: String] + ) { + self.title = title + self.subTitle = subTitle + self.uri = uri + self.metadata = metadata + } + } + /// The role of a message. public var role: Role - + /// The content of the message, either the chat message, or a result of a function call. public var content: String? { didSet { tokensCount = nil } @@ -41,16 +65,20 @@ public struct ChatMessage: Equatable, Codable { public var name: String? { didSet { tokensCount = nil } } - + /// The summary of a message that is used for display. public var summary: String? - + /// The id of the message. public var id: String - + /// The number of tokens of this message. var tokensCount: Int? - + + /// The references of this message. + @FallbackDecoding> + public var references: [Reference] + /// Is the message considered empty. var isEmpty: Bool { if let content, !content.isEmpty { return false } @@ -66,7 +94,8 @@ public struct ChatMessage: Equatable, Codable { name: String? = nil, functionCall: FunctionCall? = nil, summary: String? = nil, - tokenCount: Int? = nil + tokenCount: Int? = nil, + references: [Reference] = [] ) { self.role = role self.content = content @@ -75,6 +104,7 @@ public struct ChatMessage: Equatable, Codable { self.summary = summary self.id = id tokensCount = tokenCount + self.references = references } } From 94aaadedd7fa05077da28e934caa72d31939a39a Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 30 Nov 2023 23:52:07 +0800 Subject: [PATCH 27/74] Change the definition of a memory --- Core/Sources/ChatGPTChatTab/Chat.swift | 2 +- .../ShortcutChatPlugin.swift | 2 +- .../ShortcutInputChatPlugin.swift | 2 +- Core/Sources/ChatService/ChatService.swift | 8 ++-- ...ContextAwareAutoManagedChatGPTMemory.swift | 12 +---- .../DynamicContextController.swift | 22 ++++++--- .../LangChain/ChatModel/OpenAIChat.swift | 4 +- .../OpenAIService/ChatGPTService.swift | 27 +++++++---- .../Memory/AutoManagedChatGPTMemory.swift | 46 ++++++++----------- .../OpenAIService/Memory/ChatGPTMemory.swift | 38 +++++++++++---- .../Memory/ConversationChatGPTMemory.swift | 11 +++-- .../Memory/EmptyChatGPTMemory.swift | 9 ++-- Tool/Sources/OpenAIService/Models.swift | 9 +++- 13 files changed, 108 insertions(+), 84 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/Chat.swift b/Core/Sources/ChatGPTChatTab/Chat.swift index 78e9c070..be7091cb 100644 --- a/Core/Sources/ChatGPTChatTab/Chat.swift +++ b/Core/Sources/ChatGPTChatTab/Chat.swift @@ -17,7 +17,7 @@ public struct DisplayedChatMessage: Equatable { public var text: String public var references: [ChatMessage.Reference] = [] - public init(id: String, role: Role, text: String, references: [ChatMessage.Reference]) { + public init(id: String, role: Role, text: String, references: [ChatMessage.Reference] = []) { self.id = id self.role = role self.text = text diff --git a/Core/Sources/ChatPlugins/ShortcutChatPlugin/ShortcutChatPlugin.swift b/Core/Sources/ChatPlugins/ShortcutChatPlugin/ShortcutChatPlugin.swift index f882b750..0fc3cc06 100644 --- a/Core/Sources/ChatPlugins/ShortcutChatPlugin/ShortcutChatPlugin.swift +++ b/Core/Sources/ChatPlugins/ShortcutChatPlugin/ShortcutChatPlugin.swift @@ -49,7 +49,7 @@ public actor ShortcutChatPlugin: ChatPlugin { var input = String(content).trimmingCharacters(in: .whitespacesAndNewlines) if input.isEmpty { // if no input detected, use the previous message as input - input = await chatGPTService.memory.messages.last?.content ?? "" + input = await chatGPTService.memory.history.last?.content ?? "" await chatGPTService.memory.appendMessage(.init(role: .user, content: originalMessage)) } else { await chatGPTService.memory.appendMessage(.init(role: .user, content: originalMessage)) diff --git a/Core/Sources/ChatPlugins/ShortcutChatPlugin/ShortcutInputChatPlugin.swift b/Core/Sources/ChatPlugins/ShortcutChatPlugin/ShortcutInputChatPlugin.swift index 76f16677..eeeddc0d 100644 --- a/Core/Sources/ChatPlugins/ShortcutChatPlugin/ShortcutInputChatPlugin.swift +++ b/Core/Sources/ChatPlugins/ShortcutChatPlugin/ShortcutInputChatPlugin.swift @@ -52,7 +52,7 @@ public actor ShortcutInputChatPlugin: ChatPlugin { var input = String(content).trimmingCharacters(in: .whitespacesAndNewlines) if input.isEmpty { // if no input detected, use the previous message as input - input = await chatGPTService.memory.messages.last?.content ?? "" + input = await chatGPTService.memory.history.last?.content ?? "" } do { diff --git a/Core/Sources/ChatService/ChatService.swift b/Core/Sources/ChatService/ChatService.swift index 2cbaee67..2c6fb227 100644 --- a/Core/Sources/ChatService/ChatService.swift +++ b/Core/Sources/ChatService/ChatService.swift @@ -100,15 +100,13 @@ public final class ChatService: ObservableObject { guard !isReceivingMessage else { throw CancellationError() } let handledInPlugin = try await pluginController.handleContent(content) if handledInPlugin { return } + isReceivingMessage = true + defer { isReceivingMessage = false } let stream = try await chatGPTService.send(content: content, summary: nil) - isReceivingMessage = true do { for try await _ in stream {} - isReceivingMessage = false - } catch { - isReceivingMessage = false - } + } catch {} } public func sendAndWait(content: String) async throws -> String { diff --git a/Core/Sources/ChatService/ContextAwareAutoManagedChatGPTMemory.swift b/Core/Sources/ChatService/ContextAwareAutoManagedChatGPTMemory.swift index f4600a79..15564dbd 100644 --- a/Core/Sources/ChatService/ContextAwareAutoManagedChatGPTMemory.swift +++ b/Core/Sources/ChatService/ContextAwareAutoManagedChatGPTMemory.swift @@ -7,14 +7,6 @@ public final class ContextAwareAutoManagedChatGPTMemory: ChatGPTMemory { let functionProvider: ChatFunctionProvider weak var chatService: ChatService? - public var messages: [ChatMessage] { - get async { await memory.messages } - } - - public var remainingTokens: Int? { - get async { await memory.remainingTokens } - } - public var history: [ChatMessage] { get async { await memory.history } } @@ -45,7 +37,7 @@ public final class ContextAwareAutoManagedChatGPTMemory: ChatGPTMemory { await memory.mutateHistory(update) } - public func refresh() async { + public func generatePrompt() async -> ChatGPTPrompt { let content = (await memory.history) .last(where: { $0.role == .user || $0.role == .function })?.content try? await contextController.collectContextInformation( @@ -55,7 +47,7 @@ public final class ContextAwareAutoManagedChatGPTMemory: ChatGPTMemory { """, content: content ?? "" ) - await memory.refresh() + return await memory.generatePrompt() } } diff --git a/Core/Sources/ChatService/DynamicContextController.swift b/Core/Sources/ChatService/DynamicContextController.swift index 2f7cf014..07066467 100644 --- a/Core/Sources/ChatService/DynamicContextController.swift +++ b/Core/Sources/ChatService/DynamicContextController.swift @@ -41,21 +41,21 @@ final class DynamicContextController { var content = content var scopes = Self.parseScopes(&content) scopes.formUnion(defaultScopes) - + let overridingChatModelId = { var ids = [String]() if scopes.contains(.sense) { ids.append(UserDefaults.shared.value(for: \.preferredChatModelIdForSenseScope)) } - + if scopes.contains(.project) { ids.append(UserDefaults.shared.value(for: \.preferredChatModelIdForProjectScope)) } - + if scopes.contains(.web) { ids.append(UserDefaults.shared.value(for: \.preferredChatModelIdForWebScope)) } - + let chatModels = UserDefaults.shared.value(for: \.chatModels) let idIndexMap = chatModels.enumerated().reduce(into: [String: Int]()) { $0[$1.element.id] = $1.offset @@ -66,7 +66,7 @@ final class DynamicContextController { return lhs < rhs }).first }() - + configuration.overriding.modelId = overridingChatModelId functionProvider.removeAll() @@ -108,7 +108,17 @@ final class DynamicContextController { """ await memory.mutateSystemPrompt(contextualSystemPrompt) await memory.mutateContextSystemPrompt(contextSystemPrompt) - await memory.mutateRetrievedContent(contextPrompts.map(\.content)) + await memory.mutateRetrievedContent(contextPrompts.map { + .init( + title: "", + subTitle: "", + uri: "", + content: $0.content, + startLine: nil, + endLine: nil, + metadata: [:] + ) + }) functionProvider.append(functions: contexts.flatMap(\.functions)) } } diff --git a/Tool/Sources/LangChain/ChatModel/OpenAIChat.swift b/Tool/Sources/LangChain/ChatModel/OpenAIChat.swift index af7c52bd..9a436168 100644 --- a/Tool/Sources/LangChain/ChatModel/OpenAIChat.swift +++ b/Tool/Sources/LangChain/ChatModel/OpenAIChat.swift @@ -42,10 +42,10 @@ public struct OpenAIChat: ChatModel { message.append(trunk) callbackManagers.send(CallbackEvents.LLMDidProduceNewToken(info: trunk)) } - return await memory.messages.last ?? .init(role: .assistant, content: "") + return await memory.history.last ?? .init(role: .assistant, content: "") } else { let _ = try await service.sendAndWait(content: "") - return await memory.messages.last ?? .init(role: .assistant, content: "") + return await memory.history.last ?? .init(role: .assistant, content: "") } } } diff --git a/Tool/Sources/OpenAIService/ChatGPTService.swift b/Tool/Sources/OpenAIService/ChatGPTService.swift index b8aa94a0..1e2687b3 100644 --- a/Tool/Sources/OpenAIService/ChatGPTService.swift +++ b/Tool/Sources/OpenAIService/ChatGPTService.swift @@ -93,7 +93,8 @@ public class ChatGPTService: ChatGPTServiceType { content: content, name: nil, functionCall: nil, - summary: summary + summary: summary, + references: [] ) await memory.appendMessage(newMessage) } @@ -218,7 +219,7 @@ extension ChatGPTService { /// Send the memory as prompt to ChatGPT, with stream enabled. func sendMemory() async throws -> AsyncThrowingStream { - await memory.refresh() + let prompt = await memory.generatePrompt() guard let model = configuration.model else { throw ChatGPTServiceError.chatModelNotAvailable @@ -227,7 +228,7 @@ extension ChatGPTService { throw ChatGPTServiceError.endpointIncorrect } - let messages = await memory.messages.map { + let messages = prompt.history.map { CompletionRequestBody.Message( role: $0.role, content: $0.content ?? "", @@ -237,7 +238,7 @@ extension ChatGPTService { } ) } - let remainingTokens = await memory.remainingTokens + let remainingTokens = prompt.remainingTokenCount let requestBody = CompletionRequestBody( model: model.info.modelName, @@ -278,9 +279,13 @@ extension ChatGPTService { return AsyncThrowingStream { continuation in let task = Task { do { + let proposedId = UUID().uuidString + String(Date().timeIntervalSince1970) + await memory.streamMessage( + id: proposedId, + references: prompt.references + ) let (trunks, cancel) = try await api() cancelTask = cancel - let proposedId = UUID().uuidString + String(Date().timeIntervalSince1970) for try await trunk in trunks { try Task.checkCancellation() guard let delta = trunk.choices?.first?.delta else { continue } @@ -336,7 +341,8 @@ extension ChatGPTService { /// Send the memory as prompt to ChatGPT, with stream disabled. func sendMemoryAndWait() async throws -> ChatMessage? { - await memory.refresh() + let proposedId = UUID().uuidString + String(Date().timeIntervalSince1970) + let prompt = await memory.generatePrompt() guard let model = configuration.model else { throw ChatGPTServiceError.chatModelNotAvailable @@ -345,7 +351,7 @@ extension ChatGPTService { throw ChatGPTServiceError.endpointIncorrect } - let messages = await memory.messages.map { + let messages = prompt.history.map { CompletionRequestBody.Message( role: $0.role, content: $0.content ?? "", @@ -355,7 +361,7 @@ extension ChatGPTService { } ) } - let remainingTokens = await memory.remainingTokens + let remainingTokens = prompt.remainingTokenCount let requestBody = CompletionRequestBody( model: model.info.modelName, @@ -397,13 +403,14 @@ extension ChatGPTService { guard let choice = response.choices.first else { return nil } let message = ChatMessage( - id: response.id ?? UUID().uuidString, + id: proposedId, role: choice.message.role, content: choice.message.content, name: choice.message.name, functionCall: choice.message.function_call.map { ChatMessage.FunctionCall(name: $0.name, arguments: $0.arguments ?? "") - } + }, + references: prompt.references ) await memory.appendMessage(message) return message diff --git a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift index b40a738e..f773e443 100644 --- a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift +++ b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift @@ -5,16 +5,14 @@ import TokenEncoder /// A memory that automatically manages the history according to max tokens and max message count. public actor AutoManagedChatGPTMemory: ChatGPTMemory { - public private(set) var messages: [ChatMessage] = [] + public private(set) var history: [ChatMessage] = [] { + didSet { onHistoryChange() } + } public private(set) var remainingTokens: Int? public var systemPrompt: String public var contextSystemPrompt: String - public var retrievedContent: [String] = [] - public var history: [ChatMessage] = [] { - didSet { onHistoryChange() } - } - + public var retrievedContent: [ChatMessage.Reference] = [] public var configuration: ChatGPTConfiguration public var functionProvider: ChatGPTFunctionProvider @@ -46,7 +44,7 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { contextSystemPrompt = newPrompt } - public func mutateRetrievedContent(_ newContent: [String]) { + public func mutateRetrievedContent(_ newContent: [ChatMessage.Reference]) { retrievedContent = newContent } @@ -57,9 +55,8 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { } } - public func refresh() async { - messages = generateSendingHistory() - remainingTokens = generateRemainingTokens() + public func generatePrompt() async -> ChatGPTPrompt { + return generateSendingHistory() } /// https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb @@ -79,7 +76,7 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { func generateSendingHistory( maxNumberOfMessages: Int = UserDefaults.shared.value(for: \.chatGPTMaxMessageCount), encoder: TokenEncoder = AutoManagedChatGPTMemory.encoder - ) -> [ChatMessage] { + ) -> ChatGPTPrompt { let ( systemPromptMessage, contextSystemPromptMessage, @@ -102,7 +99,7 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { retrievedContentMessage, _, retrievedContentUsage, - _ + retrievedContent ) = generateRetrievedContentMessage( maxTokenCount: availableTokenCountForRetrievedContent, encoder: encoder @@ -134,15 +131,7 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { """) #endif - return allMessages - } - - func generateRemainingTokens( - maxNumberOfMessages: Int = UserDefaults.shared.value(for: \.chatGPTMaxMessageCount), - encoder: TokenEncoder = AutoManagedChatGPTMemory.encoder - ) -> Int? { - // It should be fine to just let OpenAI decide. - return nil + return .init(history: allMessages, references: retrievedContent) } func setOnHistoryChangeBlock(_ onChange: @escaping () -> Void) { @@ -240,26 +229,26 @@ extension AutoManagedChatGPTMemory { retrievedContent: ChatMessage, remainingTokenCount: Int, usage: Int, - includedRetrievedContent: [String] + references: [ChatMessage.Reference] ) { var retrievedContentTokenCount = 0 let separator = String(repeating: "=", count: 32) // only 1 token var message = "" - var includedRetrievedContent = [String]() + var references = [ChatMessage.Reference]() func appendToMessage(_ text: String) -> Bool { let tokensCount = encoder.countToken(text: text) if tokensCount + retrievedContentTokenCount > maxTokenCount { return false } retrievedContentTokenCount += tokensCount message += text - includedRetrievedContent.append(text) return true } - for (index, content) in retrievedContent.filter({ !$0.isEmpty }).enumerated() { + for (index, content) in retrievedContent.filter({ !$0.content.isEmpty }).enumerated() { if index == 0 { if !appendToMessage(""" - Here are the information you know about the system and the project, separated by \(separator) + Here are the information you know about the system and the project, \ + separated by \(separator) """) { break } @@ -267,14 +256,15 @@ extension AutoManagedChatGPTMemory { if !appendToMessage("\n\(separator)\n") { break } } - if !appendToMessage(content) { break } + if !appendToMessage(content.content) { break } + references.append(content) } return ( .init(role: .user, content: message), maxTokenCount - retrievedContentTokenCount, retrievedContentTokenCount, - includedRetrievedContent + references ) } } diff --git a/Tool/Sources/OpenAIService/Memory/ChatGPTMemory.swift b/Tool/Sources/OpenAIService/Memory/ChatGPTMemory.swift index 437e99f3..4f4e405d 100644 --- a/Tool/Sources/OpenAIService/Memory/ChatGPTMemory.swift +++ b/Tool/Sources/OpenAIService/Memory/ChatGPTMemory.swift @@ -1,17 +1,33 @@ import Foundation +public struct ChatGPTPrompt { + public var history: [ChatMessage] + public var references: [ChatMessage.Reference] + public var remainingTokenCount: Int? + + public init( + history: [ChatMessage], + references: [ChatMessage.Reference] = [], + remainingTokenCount: Int? = nil + ) { + self.history = history + self.references = references + self.remainingTokenCount = remainingTokenCount + } +} + public protocol ChatGPTMemory { - /// The visible messages to the ChatGPT service. - var messages: [ChatMessage] { get async } - /// The remaining tokens available for the reply. - var remainingTokens: Int? { get async } + /// The message history. + var history: [ChatMessage] { get async } /// Update the message history. func mutateHistory(_ update: (inout [ChatMessage]) -> Void) async - /// Refresh `messages` and `remainingTokens`. - /// Sometimes the message history needs time to generate, in such case, you - /// can use this method to refresh the memory, instead of making variable - /// `messages` and `remainingTokens` computed. - func refresh() async + /// Generate prompt that would be send through the API. + /// + /// A memory should make sure that the history in the prompt + /// doesn't exceed the maximum token count. + /// + /// The history can be different from the actual history. + func generatePrompt() async -> ChatGPTPrompt } public extension ChatGPTMemory { @@ -48,7 +64,8 @@ public extension ChatGPTMemory { role: ChatMessage.Role? = nil, content: String? = nil, functionCall: ChatMessage.FunctionCall? = nil, - summary: String? = nil + summary: String? = nil, + references: [ChatMessage.Reference] = [] ) async { await mutateHistory { history in if let index = history.firstIndex(where: { $0.id == id }) { @@ -91,3 +108,4 @@ public extension ChatGPTMemory { await mutateHistory { $0.removeAll() } } } + diff --git a/Tool/Sources/OpenAIService/Memory/ConversationChatGPTMemory.swift b/Tool/Sources/OpenAIService/Memory/ConversationChatGPTMemory.swift index 74b40969..462beea0 100644 --- a/Tool/Sources/OpenAIService/Memory/ConversationChatGPTMemory.swift +++ b/Tool/Sources/OpenAIService/Memory/ConversationChatGPTMemory.swift @@ -1,17 +1,18 @@ import Foundation public actor ConversationChatGPTMemory: ChatGPTMemory { - public var messages: [ChatMessage] = [] - public var remainingTokens: Int? { nil } + public var history: [ChatMessage] = [] public init(systemPrompt: String, systemMessageId: String = UUID().uuidString) { - messages.append(.init(id: systemMessageId, role: .system, content: systemPrompt)) + history.append(.init(id: systemMessageId, role: .system, content: systemPrompt)) } public func mutateHistory(_ update: (inout [ChatMessage]) -> Void) { - update(&messages) + update(&history) } - public func refresh() async {} + public func generatePrompt() async -> ChatGPTPrompt { + return .init(history: history) + } } diff --git a/Tool/Sources/OpenAIService/Memory/EmptyChatGPTMemory.swift b/Tool/Sources/OpenAIService/Memory/EmptyChatGPTMemory.swift index a07c51e2..c4470ad1 100644 --- a/Tool/Sources/OpenAIService/Memory/EmptyChatGPTMemory.swift +++ b/Tool/Sources/OpenAIService/Memory/EmptyChatGPTMemory.swift @@ -1,15 +1,16 @@ import Foundation public actor EmptyChatGPTMemory: ChatGPTMemory { - public var messages: [ChatMessage] = [] - public var remainingTokens: Int? { nil } + public var history: [ChatMessage] = [] public init() {} public func mutateHistory(_ update: (inout [ChatMessage]) -> Void) { - update(&messages) + update(&history) } - public func refresh() async {} + public func generatePrompt() async -> ChatGPTPrompt { + return .init(history: history) + } } diff --git a/Tool/Sources/OpenAIService/Models.swift b/Tool/Sources/OpenAIService/Models.swift index 54a3709f..04fd24aa 100644 --- a/Tool/Sources/OpenAIService/Models.swift +++ b/Tool/Sources/OpenAIService/Models.swift @@ -9,6 +9,8 @@ struct Cancellable { } public struct ChatMessage: Equatable, Codable { + public typealias ID = String + public enum Role: String, Codable, Equatable { case system case user @@ -29,6 +31,7 @@ public struct ChatMessage: Equatable, Codable { public var title: String public var subTitle: String public var uri: String + public var content: String public var startLine: Int? public var endLine: Int? public var metadata: [String: String] @@ -37,6 +40,7 @@ public struct ChatMessage: Equatable, Codable { title: String, subTitle: String, uri: String, + content: String, startLine: Int?, endLine: Int?, metadata: [String: String] @@ -45,6 +49,9 @@ public struct ChatMessage: Equatable, Codable { self.subTitle = subTitle self.uri = uri self.metadata = metadata + self.content = content + self.startLine = startLine + self.endLine = endLine } } @@ -70,7 +77,7 @@ public struct ChatMessage: Equatable, Codable { public var summary: String? /// The id of the message. - public var id: String + public var id: ID /// The number of tokens of this message. var tokensCount: Int? From 68ea73b89a321947eb8155da115f370b219d0b8d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 1 Dec 2023 00:00:04 +0800 Subject: [PATCH 28/74] Update RetrievedContent in ChatContext --- .../ChatService/DynamicContextController.swift | 16 +++------------- Pro | 2 +- .../ChatContextCollector.swift | 8 ++++---- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/Core/Sources/ChatService/DynamicContextController.swift b/Core/Sources/ChatService/DynamicContextController.swift index 07066467..45c0fe19 100644 --- a/Core/Sources/ChatService/DynamicContextController.swift +++ b/Core/Sources/ChatService/DynamicContextController.swift @@ -97,9 +97,9 @@ final class DynamicContextController { .filter { !$0.isEmpty } .joined(separator: "\n\n") - let contextPrompts = contexts + let retrievedContent = contexts .flatMap(\.retrievedContent) - .filter { !$0.content.isEmpty } + .filter { !$0.document.content.isEmpty } .sorted { $0.priority > $1.priority } let contextualSystemPrompt = """ @@ -108,17 +108,7 @@ final class DynamicContextController { """ await memory.mutateSystemPrompt(contextualSystemPrompt) await memory.mutateContextSystemPrompt(contextSystemPrompt) - await memory.mutateRetrievedContent(contextPrompts.map { - .init( - title: "", - subTitle: "", - uri: "", - content: $0.content, - startLine: nil, - endLine: nil, - metadata: [:] - ) - }) + await memory.mutateRetrievedContent(retrievedContent.map(\.document)) functionProvider.append(functions: contexts.flatMap(\.functions)) } } diff --git a/Pro b/Pro index 7b8199f4..16d6e392 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 7b8199f4e10217a9349dffec28aa71b7721c8bdd +Subproject commit 16d6e392928c739f04ffe6cada00ba208e114192 diff --git a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift index eccd7d03..711620b5 100644 --- a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift @@ -12,11 +12,11 @@ public struct ChatContext { } public struct RetrievedContent { - public var content: String + public var document: ChatMessage.Reference public var priority: Int - - public init(content: String, priority: Int) { - self.content = content + + public init(document: ChatMessage.Reference, priority: Int) { + self.document = document self.priority = priority } } From ba592c29d84afddf9bd9baa066b4f01f04257474 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 1 Dec 2023 15:08:11 +0800 Subject: [PATCH 29/74] Support displaying references in chat panel --- Core/Sources/ChatGPTChatTab/Chat.swift | 27 ++- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 193 ++++++++++++++------ 2 files changed, 163 insertions(+), 57 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/Chat.swift b/Core/Sources/ChatGPTChatTab/Chat.swift index be7091cb..7ed66cce 100644 --- a/Core/Sources/ChatGPTChatTab/Chat.swift +++ b/Core/Sources/ChatGPTChatTab/Chat.swift @@ -5,19 +5,31 @@ import OpenAIService import Preferences public struct DisplayedChatMessage: Equatable { - public enum Role { + public enum Role: Equatable { case user case assistant case function case ignored } + + public struct Reference: Equatable { + public var title: String + public var subtitle: String + public var uri: String + + public init(title: String, subtitle: String, uri: String) { + self.title = title + self.subtitle = subtitle + self.uri = uri + } + } public var id: String public var role: Role public var text: String - public var references: [ChatMessage.Reference] = [] + public var references: [Reference] = [] - public init(id: String, role: Role, text: String, references: [ChatMessage.Reference] = []) { + public init(id: String, role: Role, text: String, references: [Reference]) { self.id = id self.role = role self.text = text @@ -25,6 +37,10 @@ public struct DisplayedChatMessage: Equatable { } } +private var isPreview: Bool { + ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" +} + struct Chat: ReducerProtocol { public typealias MessageID = String @@ -92,6 +108,7 @@ struct Chat: ReducerProtocol { switch action { case .appear: return .run { send in + if isPreview { return } await send(.observeChatService) await send(.historyChanged) await send(.isReceivingMessageChanged) @@ -250,7 +267,9 @@ struct Chat: ReducerProtocol { } }(), text: message.summary ?? message.content ?? "", - references: message.references + references: message.references.map { + .init(title: $0.title, subtitle: $0.subTitle, uri: $0.uri) + } ) } diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index 40fe2711..78c2026a 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -245,14 +245,19 @@ struct ChatHistory: View { )) .padding(.vertical, 4) case .assistant: - BotMessage(id: message.id, text: text, chat: chat) - .listRowInsets(EdgeInsets( - top: 0, - leading: -8, - bottom: 0, - trailing: -8 - )) - .padding(.vertical, 4) + BotMessage( + id: message.id, + text: text, + references: message.references, + chat: chat + ) + .listRowInsets(EdgeInsets( + top: 0, + leading: -8, + bottom: 0, + trailing: -8 + )) + .padding(.vertical, 4) case .function: FunctionMessage(id: message.id, text: text) case .ignored: @@ -427,50 +432,82 @@ private struct UserMessage: View { private struct BotMessage: View { let id: String let text: String + 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 + var body: some View { HStack(alignment: .bottom, spacing: 2) { - Markdown(text) - .textSelection(.enabled) - .markdownTheme(.custom(fontSize: chatFontSize)) - .markdownCodeSyntaxHighlighter( - ChatCodeSyntaxHighlighter( - brightMode: colorScheme != .dark, - fontSize: chatCodeFontSize - ) - ) - .frame(alignment: .trailing) - .padding() - .background { - RoundedCorners(tl: r, tr: r, bl: 0, br: r) - .fill(Color.contentBackground) + VStack(alignment: .leading, spacing: 16) { + if !references.isEmpty { + Button(action: { + isReferencesPresented.toggle() + }, label: { + HStack(spacing: 4) { + Image(systemName: "plus.circle") + Text("Used \(references.count) references") + } + .padding(8) + .background { + RoundedRectangle(cornerRadius: r - 4) + .foregroundStyle(Color(isReferencesHovered ? .black : .clear)) + + } + .overlay { + RoundedRectangle(cornerRadius: r - 4) + .stroke(Color(nsColor: .separatorColor), lineWidth: 1) + } + .foregroundStyle(.secondary) + }) + .buttonStyle(.plain) + .popover(isPresented: $isReferencesPresented, arrowEdge: .trailing) { + ReferenceList(references: references) + } } - .overlay { - RoundedCorners(tl: r, tr: r, bl: 0, br: r) - .stroke(Color(nsColor: .separatorColor), lineWidth: 1) + + Markdown(text) + .textSelection(.enabled) + .markdownTheme(.custom(fontSize: chatFontSize)) + .markdownCodeSyntaxHighlighter( + ChatCodeSyntaxHighlighter( + brightMode: colorScheme != .dark, + fontSize: chatCodeFontSize + ) + ) + } + .frame(alignment: .trailing) + .padding() + .background { + RoundedCorners(tl: r, tr: r, bl: 0, br: r) + .fill(Color.contentBackground) + } + .overlay { + RoundedCorners(tl: r, tr: r, bl: 0, br: r) + .stroke(Color(nsColor: .separatorColor), lineWidth: 1) + } + .padding(.leading, 8) + .shadow(color: .black.opacity(0.1), radius: 2) + .contextMenu { + Button("Copy") { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(text, forType: .string) } - .padding(.leading, 8) - .shadow(color: .black.opacity(0.1), radius: 2) - .contextMenu { - Button("Copy") { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(text, forType: .string) - } - Button("Set as Extra System Prompt") { - chat.send(.setAsExtraPromptButtonTapped(id)) - } + Button("Set as Extra System Prompt") { + chat.send(.setAsExtraPromptButtonTapped(id)) + } - Divider() + Divider() - Button("Delete") { - chat.send(.deleteMessageButtonTapped(id)) - } + Button("Delete") { + chat.send(.deleteMessageButtonTapped(id)) } + } CopyButton { NSPasteboard.general.clearContents() @@ -482,6 +519,26 @@ private struct BotMessage: View { } } +struct ReferenceList: View { + let references: [DisplayedChatMessage.Reference] + var body: some View { + ScrollView { + VStack { + ForEach(0.. hello + > hi + """, + references: [] + ), + .init( + id: "5", + role: .assistant, + text: "Yooo", + references: [] + ), + .init( + id: "4", + role: .user, + text: "Yeeeehh", + references: [] ), - .init(id: "7", role: .ignored, text: "Ignored"), - .init(id: "6", role: .function, text: """ - Searching for something... - - abc - - [def](https://1.com) - > hello - > hi - """), - .init(id: "5", role: .assistant, text: "Yooo"), - .init(id: "4", role: .user, text: "Yeeeehh"), .init( id: "3", role: .user, @@ -718,7 +804,8 @@ struct ChatPanel_Preview: PreviewProvider { ```objectivec - (void)bar {} ``` - """# + """#, + references: [] ), ] From aac293e1a7e25459587900a135b75fd2bd7364d1 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 1 Dec 2023 15:50:53 +0800 Subject: [PATCH 30/74] Break ChatPanel.swift into multiple files --- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 315 ------------------ Core/Sources/ChatGPTChatTab/Styles.swift | 66 +++- .../ChatGPTChatTab/Views/BotMessage.swift | 191 +++++++++++ .../Views/FunctionMessage.swift | 30 ++ .../ChatGPTChatTab/Views/Instructions.swift | 85 +++++ .../ChatGPTChatTab/Views/UserMessage.swift | 86 +++++ 6 files changed, 453 insertions(+), 320 deletions(-) create mode 100644 Core/Sources/ChatGPTChatTab/Views/BotMessage.swift create mode 100644 Core/Sources/ChatGPTChatTab/Views/FunctionMessage.swift create mode 100644 Core/Sources/ChatGPTChatTab/Views/Instructions.swift create mode 100644 Core/Sources/ChatGPTChatTab/Views/UserMessage.swift diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index 78c2026a..dd53fb0d 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -294,265 +294,6 @@ private struct StopRespondingButton: View { } } -private struct Instruction: View { - let chat: StoreOf - - var body: some View { - Group { - Markdown( - """ - You can use plugins to perform various tasks. - - | Plugin Name | Description | - | --- | --- | - | `/run` | Runs a command under the project root | - | `/math` | Solves a math problem in natural language | - | `/search` | Searches on Bing and summarizes the results | - | `/shortcut(name)` | Runs a shortcut from the Shortcuts.app, with the previous message as input | - | `/shortcutInput(name)` | Runs a shortcut and uses its result as a new message | - - To use plugins, you can prefix a message with `/pluginName`. - """ - ) - .modifier(InstructionModifier()) - - Markdown( - """ - You can use scopes to give the bot extra abilities. - - | Scope Name | Abilities | - | --- | --- | - | `@file` | Read the metadata of the editing file | - | `@code` | Read the code and metadata in the editing file | - | `@sense`| Experimental. Read the relevant code of the focused editor | - | `@project` | Experimental. Access content of the project | - | `@web` (beta) | Search on Bing or query from a web page | - - To use scopes, you can prefix a message with `@code`. - - You can use shorthand to represent a scope, such as `@c`, and enable multiple scopes with `@c+web`. - """ - ) - .modifier(InstructionModifier()) - - WithViewStore(chat, observe: \.chatMenu.defaultScopes) { viewStore in - Markdown( - """ - Hello, I am your AI programming assistant. I can identify issues, explain and even improve code. - - \({ - if viewStore.state.isEmpty { - return "No scope is enabled by default" - } else { - let scopes = viewStore.state.map(\.rawValue).sorted() - .joined(separator: ", ") - return "Default scopes: `\(scopes)`" - } - }()) - """ - ) - .modifier(InstructionModifier()) - } - } - } - - struct InstructionModifier: ViewModifier { - @AppStorage(\.chatFontSize) var chatFontSize - - func body(content: Content) -> some View { - content - .textSelection(.enabled) - .markdownTheme(.custom(fontSize: chatFontSize)) - .opacity(0.8) - .frame(maxWidth: .infinity, alignment: .leading) - .padding() - .overlay { - RoundedRectangle(cornerRadius: 8) - .stroke(Color(nsColor: .separatorColor), lineWidth: 1) - } - } - } -} - -private struct UserMessage: View { - let id: String - 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 - ) - ) - .frame(alignment: .leading) - .padding() - .background { - RoundedCorners(tl: r, tr: r, bl: r, br: 0) - .fill(Color.userChatContentBackground) - } - .overlay { - RoundedCorners(tl: r, tr: r, bl: r, br: 0) - .stroke(Color(nsColor: .separatorColor), lineWidth: 1) - } - .padding(.leading) - .padding(.trailing, 8) - .shadow(color: .black.opacity(0.1), radius: 2) - .frame(maxWidth: .infinity, alignment: .trailing) - .contextMenu { - Button("Copy") { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(text, forType: .string) - } - - Button("Send Again") { - chat.send(.resendMessageButtonTapped(id)) - } - - Button("Set as Extra System Prompt") { - chat.send(.setAsExtraPromptButtonTapped(id)) - } - - Divider() - - Button("Delete") { - chat.send(.deleteMessageButtonTapped(id)) - } - } - } -} - -private struct BotMessage: View { - let id: String - let text: String - 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 - - var body: some View { - HStack(alignment: .bottom, spacing: 2) { - VStack(alignment: .leading, spacing: 16) { - if !references.isEmpty { - Button(action: { - isReferencesPresented.toggle() - }, label: { - HStack(spacing: 4) { - Image(systemName: "plus.circle") - Text("Used \(references.count) references") - } - .padding(8) - .background { - RoundedRectangle(cornerRadius: r - 4) - .foregroundStyle(Color(isReferencesHovered ? .black : .clear)) - - } - .overlay { - RoundedRectangle(cornerRadius: r - 4) - .stroke(Color(nsColor: .separatorColor), lineWidth: 1) - } - .foregroundStyle(.secondary) - }) - .buttonStyle(.plain) - .popover(isPresented: $isReferencesPresented, arrowEdge: .trailing) { - ReferenceList(references: references) - } - } - - Markdown(text) - .textSelection(.enabled) - .markdownTheme(.custom(fontSize: chatFontSize)) - .markdownCodeSyntaxHighlighter( - ChatCodeSyntaxHighlighter( - brightMode: colorScheme != .dark, - fontSize: chatCodeFontSize - ) - ) - } - .frame(alignment: .trailing) - .padding() - .background { - RoundedCorners(tl: r, tr: r, bl: 0, br: r) - .fill(Color.contentBackground) - } - .overlay { - RoundedCorners(tl: r, tr: r, bl: 0, br: r) - .stroke(Color(nsColor: .separatorColor), lineWidth: 1) - } - .padding(.leading, 8) - .shadow(color: .black.opacity(0.1), radius: 2) - .contextMenu { - Button("Copy") { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(text, forType: .string) - } - - Button("Set as Extra System Prompt") { - chat.send(.setAsExtraPromptButtonTapped(id)) - } - - Divider() - - Button("Delete") { - chat.send(.deleteMessageButtonTapped(id)) - } - } - - CopyButton { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(text, forType: .string) - } - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.trailing, 2) - } -} - -struct ReferenceList: View { - let references: [DisplayedChatMessage.Reference] - var body: some View { - ScrollView { - VStack { - ForEach(0.. @FocusState var focusedField: Chat.State.Field? @@ -675,62 +416,6 @@ struct ChatPanelInputArea: View { } } -struct RoundedCorners: Shape { - var tl: CGFloat = 0.0 - var tr: CGFloat = 0.0 - var bl: CGFloat = 0.0 - var br: CGFloat = 0.0 - - func path(in rect: CGRect) -> Path { - Path { path in - - let w = rect.size.width - let h = rect.size.height - - // Make sure we do not exceed the size of the rectangle - let tr = min(min(self.tr, h / 2), w / 2) - let tl = min(min(self.tl, h / 2), w / 2) - let bl = min(min(self.bl, h / 2), w / 2) - let br = min(min(self.br, h / 2), w / 2) - - path.move(to: CGPoint(x: w / 2.0, y: 0)) - path.addLine(to: CGPoint(x: w - tr, y: 0)) - path.addArc( - center: CGPoint(x: w - tr, y: tr), - radius: tr, - startAngle: Angle(degrees: -90), - endAngle: Angle(degrees: 0), - clockwise: false - ) - path.addLine(to: CGPoint(x: w, y: h - br)) - path.addArc( - center: CGPoint(x: w - br, y: h - br), - radius: br, - startAngle: Angle(degrees: 0), - endAngle: Angle(degrees: 90), - clockwise: false - ) - path.addLine(to: CGPoint(x: bl, y: h)) - path.addArc( - center: CGPoint(x: bl, y: h - bl), - radius: bl, - startAngle: Angle(degrees: 90), - endAngle: Angle(degrees: 180), - clockwise: false - ) - path.addLine(to: CGPoint(x: 0, y: tl)) - path.addArc( - center: CGPoint(x: tl, y: tl), - radius: tl, - startAngle: Angle(degrees: 180), - endAngle: Angle(degrees: 270), - clockwise: false - ) - path.closeSubpath() - } - } -} - // MARK: - Previews struct ChatPanel_Preview: PreviewProvider { diff --git a/Core/Sources/ChatGPTChatTab/Styles.swift b/Core/Sources/ChatGPTChatTab/Styles.swift index 2fa6c6be..ed1c981c 100644 --- a/Core/Sources/ChatGPTChatTab/Styles.swift +++ b/Core/Sources/ChatGPTChatTab/Styles.swift @@ -34,9 +34,10 @@ extension NSAppearance { } extension View { + var messageBubbleCornerRadius: Double { 8 } + func codeBlockLabelStyle() -> some View { - self - .relativeLineSpacing(.em(0.225)) + relativeLineSpacing(.em(0.225)) .markdownTextStyle { FontFamilyVariant(.monospaced) FontSize(.em(0.85)) @@ -44,10 +45,9 @@ extension View { .padding(16) .padding(.top, 14) } - + func codeBlockStyle(_ configuration: CodeBlockConfiguration) -> some View { - self - .background(Color(nsColor: .textBackgroundColor).opacity(0.7)) + background(Color(nsColor: .textBackgroundColor).opacity(0.7)) .clipShape(RoundedRectangle(cornerRadius: 6)) .overlay(alignment: .top) { HStack(alignment: .center) { @@ -156,3 +156,59 @@ extension View { } } +struct RoundedCorners: Shape { + var tl: CGFloat = 0.0 + var tr: CGFloat = 0.0 + var bl: CGFloat = 0.0 + var br: CGFloat = 0.0 + + func path(in rect: CGRect) -> Path { + Path { path in + + let w = rect.size.width + let h = rect.size.height + + // Make sure we do not exceed the size of the rectangle + let tr = min(min(self.tr, h / 2), w / 2) + let tl = min(min(self.tl, h / 2), w / 2) + let bl = min(min(self.bl, h / 2), w / 2) + let br = min(min(self.br, h / 2), w / 2) + + path.move(to: CGPoint(x: w / 2.0, y: 0)) + path.addLine(to: CGPoint(x: w - tr, y: 0)) + path.addArc( + center: CGPoint(x: w - tr, y: tr), + radius: tr, + startAngle: Angle(degrees: -90), + endAngle: Angle(degrees: 0), + clockwise: false + ) + path.addLine(to: CGPoint(x: w, y: h - br)) + path.addArc( + center: CGPoint(x: w - br, y: h - br), + radius: br, + startAngle: Angle(degrees: 0), + endAngle: Angle(degrees: 90), + clockwise: false + ) + path.addLine(to: CGPoint(x: bl, y: h)) + path.addArc( + center: CGPoint(x: bl, y: h - bl), + radius: bl, + startAngle: Angle(degrees: 90), + endAngle: Angle(degrees: 180), + clockwise: false + ) + path.addLine(to: CGPoint(x: 0, y: tl)) + path.addArc( + center: CGPoint(x: tl, y: tl), + radius: tl, + startAngle: Angle(degrees: 180), + endAngle: Angle(degrees: 270), + clockwise: false + ) + path.closeSubpath() + } + } +} + diff --git a/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift b/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift new file mode 100644 index 00000000..a872a4f3 --- /dev/null +++ b/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift @@ -0,0 +1,191 @@ +import ComposableArchitecture +import Foundation +import MarkdownUI +import SharedUIComponents +import SwiftUI + +struct BotMessage: View { + var r: Double { messageBubbleCornerRadius } + let id: String + let text: String + 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 + + var body: some View { + HStack(alignment: .bottom, spacing: 2) { + VStack(alignment: .leading, spacing: 16) { + if !references.isEmpty { + Button(action: { + isReferencesPresented.toggle() + }, label: { + HStack(spacing: 4) { + Image(systemName: "plus.circle") + Text("Used \(references.count) references") + } + .padding(8) + .background { + RoundedRectangle(cornerRadius: r - 4) + .foregroundStyle(Color(isReferencesHovered ? .black : .clear)) + } + .overlay { + RoundedRectangle(cornerRadius: r - 4) + .stroke(Color(nsColor: .separatorColor), lineWidth: 1) + } + .foregroundStyle(.secondary) + }) + .buttonStyle(.plain) + .popover(isPresented: $isReferencesPresented, arrowEdge: .trailing) { + ReferenceList(references: references) + } + } + + Markdown(text) + .textSelection(.enabled) + .markdownTheme(.custom(fontSize: chatFontSize)) + .markdownCodeSyntaxHighlighter( + ChatCodeSyntaxHighlighter( + brightMode: colorScheme != .dark, + fontSize: chatCodeFontSize + ) + ) + } + .frame(alignment: .trailing) + .padding() + .background { + RoundedCorners(tl: r, tr: r, bl: 0, br: r) + .fill(Color.contentBackground) + } + .overlay { + RoundedCorners(tl: r, tr: r, bl: 0, br: r) + .stroke(Color(nsColor: .separatorColor), lineWidth: 1) + } + .padding(.leading, 8) + .shadow(color: .black.opacity(0.1), radius: 2) + .contextMenu { + Button("Copy") { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(text, forType: .string) + } + + Button("Set as Extra System Prompt") { + chat.send(.setAsExtraPromptButtonTapped(id)) + } + + Divider() + + Button("Delete") { + chat.send(.deleteMessageButtonTapped(id)) + } + } + + CopyButton { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(text, forType: .string) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.trailing, 2) + } +} + +struct ReferenceList: View { + let references: [DisplayedChatMessage.Reference] + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 8) { + ForEach(0.. hello + > hi + """) + .padding() + .fixedSize() +} + diff --git a/Core/Sources/ChatGPTChatTab/Views/Instructions.swift b/Core/Sources/ChatGPTChatTab/Views/Instructions.swift new file mode 100644 index 00000000..2244a236 --- /dev/null +++ b/Core/Sources/ChatGPTChatTab/Views/Instructions.swift @@ -0,0 +1,85 @@ +import ComposableArchitecture +import Foundation +import MarkdownUI +import SwiftUI + +struct Instruction: View { + let chat: StoreOf + + var body: some View { + Group { + Markdown( + """ + You can use plugins to perform various tasks. + + | Plugin Name | Description | + | --- | --- | + | `/run` | Runs a command under the project root | + | `/math` | Solves a math problem in natural language | + | `/search` | Searches on Bing and summarizes the results | + | `/shortcut(name)` | Runs a shortcut from the Shortcuts.app, with the previous message as input | + | `/shortcutInput(name)` | Runs a shortcut and uses its result as a new message | + + To use plugins, you can prefix a message with `/pluginName`. + """ + ) + .modifier(InstructionModifier()) + + Markdown( + """ + You can use scopes to give the bot extra abilities. + + | Scope Name | Abilities | + | --- | --- | + | `@file` | Read the metadata of the editing file | + | `@code` | Read the code and metadata in the editing file | + | `@sense`| Experimental. Read the relevant code of the focused editor | + | `@project` | Experimental. Access content of the project | + | `@web` (beta) | Search on Bing or query from a web page | + + To use scopes, you can prefix a message with `@code`. + + You can use shorthand to represent a scope, such as `@c`, and enable multiple scopes with `@c+web`. + """ + ) + .modifier(InstructionModifier()) + + WithViewStore(chat, observe: \.chatMenu.defaultScopes) { viewStore in + Markdown( + """ + Hello, I am your AI programming assistant. I can identify issues, explain and even improve code. + + \({ + if viewStore.state.isEmpty { + return "No scope is enabled by default" + } else { + let scopes = viewStore.state.map(\.rawValue).sorted() + .joined(separator: ", ") + return "Default scopes: `\(scopes)`" + } + }()) + """ + ) + .modifier(InstructionModifier()) + } + } + } + + struct InstructionModifier: ViewModifier { + @AppStorage(\.chatFontSize) var chatFontSize + + func body(content: Content) -> some View { + content + .textSelection(.enabled) + .markdownTheme(.custom(fontSize: chatFontSize)) + .opacity(0.8) + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .overlay { + RoundedRectangle(cornerRadius: 8) + .stroke(Color(nsColor: .separatorColor), lineWidth: 1) + } + } + } +} + diff --git a/Core/Sources/ChatGPTChatTab/Views/UserMessage.swift b/Core/Sources/ChatGPTChatTab/Views/UserMessage.swift new file mode 100644 index 00000000..2fd67f42 --- /dev/null +++ b/Core/Sources/ChatGPTChatTab/Views/UserMessage.swift @@ -0,0 +1,86 @@ +import ComposableArchitecture +import Foundation +import MarkdownUI +import SwiftUI + +struct UserMessage: View { + var r: Double { messageBubbleCornerRadius } + let id: String + 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 + ) + ) + .frame(alignment: .leading) + .padding() + .background { + RoundedCorners(tl: r, tr: r, bl: r, br: 0) + .fill(Color.userChatContentBackground) + } + .overlay { + RoundedCorners(tl: r, tr: r, bl: r, br: 0) + .stroke(Color(nsColor: .separatorColor), lineWidth: 1) + } + .padding(.leading) + .padding(.trailing, 8) + .shadow(color: .black.opacity(0.1), radius: 2) + .frame(maxWidth: .infinity, alignment: .trailing) + .contextMenu { + Button("Copy") { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(text, forType: .string) + } + + Button("Send Again") { + chat.send(.resendMessageButtonTapped(id)) + } + + Button("Set as Extra System Prompt") { + chat.send(.setAsExtraPromptButtonTapped(id)) + } + + Divider() + + Button("Delete") { + chat.send(.deleteMessageButtonTapped(id)) + } + } + } +} + +#Preview { + UserMessage( + id: "A", + text: #""" + Please buy me a coffee! + | Coffee | Milk | + |--------|------| + | Espresso | No | + | Latte | Yes | + ```swift + func foo() {} + ``` + ```objectivec + - (void)bar {} + ``` + """#, + chat: .init( + initialState: .init(history: [], isReceivingMessage: false), + reducer: Chat(service: .init()) + ) + ) + .padding() + .fixedSize(horizontal: /*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/, vertical: /*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) +} + From be2376a7402fee020eae0d2997db8ffbdb0e2dfa Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 1 Dec 2023 16:44:23 +0800 Subject: [PATCH 31/74] Update to stream references to message --- Tool/Sources/OpenAIService/Memory/ChatGPTMemory.swift | 8 ++++++-- Tool/Sources/OpenAIService/Models.swift | 9 +++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Tool/Sources/OpenAIService/Memory/ChatGPTMemory.swift b/Tool/Sources/OpenAIService/Memory/ChatGPTMemory.swift index 4f4e405d..b25d5ce4 100644 --- a/Tool/Sources/OpenAIService/Memory/ChatGPTMemory.swift +++ b/Tool/Sources/OpenAIService/Memory/ChatGPTMemory.swift @@ -65,7 +65,7 @@ public extension ChatGPTMemory { content: String? = nil, functionCall: ChatMessage.FunctionCall? = nil, summary: String? = nil, - references: [ChatMessage.Reference] = [] + references: [ChatMessage.Reference]? = nil ) async { await mutateHistory { history in if let index = history.firstIndex(where: { $0.id == id }) { @@ -90,6 +90,9 @@ public extension ChatGPTMemory { if let summary { history[index].summary = summary } + if let references { + history[index].references.append(contentsOf: references) + } } else { history.append(.init( id: id, @@ -97,7 +100,8 @@ public extension ChatGPTMemory { content: content, name: nil, functionCall: functionCall, - summary: summary + summary: summary, + references: references ?? [] )) } } diff --git a/Tool/Sources/OpenAIService/Models.swift b/Tool/Sources/OpenAIService/Models.swift index 04fd24aa..ea19c52f 100644 --- a/Tool/Sources/OpenAIService/Models.swift +++ b/Tool/Sources/OpenAIService/Models.swift @@ -34,22 +34,19 @@ public struct ChatMessage: Equatable, Codable { public var content: String public var startLine: Int? public var endLine: Int? - public var metadata: [String: String] public init( title: String, subTitle: String, - uri: String, content: String, + uri: String, startLine: Int?, - endLine: Int?, - metadata: [String: String] + endLine: Int? ) { self.title = title self.subTitle = subTitle - self.uri = uri - self.metadata = metadata self.content = content + self.uri = uri self.startLine = startLine self.endLine = endLine } From 19253976ba4d35ce9f45c84ef7ff0886970b1133 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 1 Dec 2023 16:55:30 +0800 Subject: [PATCH 32/74] Support opening references in Xcode --- Core/Package.swift | 1 + Core/Sources/ChatGPTChatTab/Chat.swift | 29 +++++++++++++++++-- .../ChatGPTChatTab/Views/BotMessage.swift | 10 ++++--- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/Core/Package.swift b/Core/Package.swift index 5e55ae02..d2f3bd54 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -250,6 +250,7 @@ let package = Package( .product(name: "OpenAIService", package: "Tool"), .product(name: "Logger", package: "Tool"), .product(name: "ChatTab", package: "Tool"), + .product(name: "Terminal", package: "Tool"), .product(name: "MarkdownUI", package: "swift-markdown-ui"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), ] diff --git a/Core/Sources/ChatGPTChatTab/Chat.swift b/Core/Sources/ChatGPTChatTab/Chat.swift index 7ed66cce..14395c8d 100644 --- a/Core/Sources/ChatGPTChatTab/Chat.swift +++ b/Core/Sources/ChatGPTChatTab/Chat.swift @@ -3,6 +3,7 @@ import ComposableArchitecture import Foundation import OpenAIService import Preferences +import Terminal public struct DisplayedChatMessage: Equatable { public enum Role: Equatable { @@ -11,12 +12,12 @@ public struct DisplayedChatMessage: Equatable { case function case ignored } - + public struct Reference: Equatable { public var title: String public var subtitle: String public var uri: String - + public init(title: String, subtitle: String, uri: String) { self.title = title self.subtitle = subtitle @@ -69,6 +70,7 @@ struct Chat: ReducerProtocol { case resendMessageButtonTapped(MessageID) case setAsExtraPromptButtonTapped(MessageID) case focusOnTextField + case referenceClicked(DisplayedChatMessage.Reference) case observeChatService case observeHistoryChange @@ -96,6 +98,8 @@ struct Chat: ReducerProtocol { case observeExtraSystemPromptChange(UUID) case observeDefaultScopesChange(UUID) } + + @Dependency(\.openURL) var openURL var body: some ReducerProtocol { BindingReducer() @@ -153,7 +157,26 @@ struct Chat: ReducerProtocol { return .run { _ in await service.setMessageAsExtraPrompt(id: id) } - + + case let .referenceClicked(reference): + let fileURL = URL(fileURLWithPath: reference.uri) + return .run { _ in + if FileManager.default.fileExists(atPath: fileURL.path) { + let terminal = Terminal() + do { + _ = try await terminal.runCommand( + "/bin/bash", + arguments: ["-c", "xed -l 0 \"\(reference.uri)\""], + environment: [:] + ) + } catch { + print(error) + } + } else if let url = URL(string: reference.uri) { + await openURL(url) + } + } + case .focusOnTextField: state.focusedField = .textField return .none diff --git a/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift b/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift index a872a4f3..7470f0bf 100644 --- a/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift +++ b/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift @@ -41,7 +41,7 @@ struct BotMessage: View { }) .buttonStyle(.plain) .popover(isPresented: $isReferencesPresented, arrowEdge: .trailing) { - ReferenceList(references: references) + ReferenceList(references: references, chat: chat) } } @@ -96,14 +96,16 @@ struct BotMessage: View { struct ReferenceList: View { let references: [DisplayedChatMessage.Reference] + let chat: StoreOf + var body: some View { ScrollView { VStack(alignment: .leading, spacing: 8) { ForEach(0.. Date: Fri, 1 Dec 2023 17:00:32 +0800 Subject: [PATCH 33/74] Support jump to specific line of reference file --- Core/Sources/ChatGPTChatTab/Chat.swift | 16 +++++++++---- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 3 ++- .../ChatGPTChatTab/Views/BotMessage.swift | 24 ++++++++++++------- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/Chat.swift b/Core/Sources/ChatGPTChatTab/Chat.swift index 14395c8d..e05d3d95 100644 --- a/Core/Sources/ChatGPTChatTab/Chat.swift +++ b/Core/Sources/ChatGPTChatTab/Chat.swift @@ -17,11 +17,13 @@ public struct DisplayedChatMessage: Equatable { public var title: String public var subtitle: String public var uri: String + public var startLine: Int? - public init(title: String, subtitle: String, uri: String) { + public init(title: String, subtitle: String, uri: String, startLine: Int?) { self.title = title self.subtitle = subtitle self.uri = uri + self.startLine = startLine } } @@ -98,7 +100,7 @@ struct Chat: ReducerProtocol { case observeExtraSystemPromptChange(UUID) case observeDefaultScopesChange(UUID) } - + @Dependency(\.openURL) var openURL var body: some ReducerProtocol { @@ -166,7 +168,8 @@ struct Chat: ReducerProtocol { do { _ = try await terminal.runCommand( "/bin/bash", - arguments: ["-c", "xed -l 0 \"\(reference.uri)\""], + arguments: ["-c", + "xed -l \(reference.startLine ?? 0) \"\(reference.uri)\""], environment: [:] ) } catch { @@ -291,7 +294,12 @@ struct Chat: ReducerProtocol { }(), text: message.summary ?? message.content ?? "", references: message.references.map { - .init(title: $0.title, subtitle: $0.subTitle, uri: $0.uri) + .init( + title: $0.title, + subtitle: $0.subTitle, + uri: $0.uri, + startLine: $0.startLine + ) } ) } diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index dd53fb0d..adaa78b0 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -439,7 +439,8 @@ struct ChatPanel_Preview: PreviewProvider { .init( title: "Hello Hello Hello Hello", subtitle: "Hi Hi Hi Hi", - uri: "https://google.com" + uri: "https://google.com", + startLine: nil ), ] ), diff --git a/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift b/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift index 7470f0bf..0d527b8c 100644 --- a/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift +++ b/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift @@ -142,12 +142,14 @@ struct ReferenceList: View { .init( title: "ReferenceList", subtitle: "/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift:100", - uri: "https://google.com" + uri: "https://google.com", + startLine: nil ), .init( title: "BotMessage.swift:100-102", subtitle: "/Core/Sources/ChatGPTChatTab/Views", - uri: "https://google.com" + uri: "https://google.com", + startLine: nil ), ], chat: .init(initialState: .init(), reducer: Chat(service: .init())) @@ -161,32 +163,38 @@ struct ReferenceList: View { .init( title: "ReferenceList", subtitle: "/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift:100", - uri: "https://google.com" + uri: "https://google.com", + startLine: nil ), .init( title: "BotMessage.swift:100-102", subtitle: "/Core/Sources/ChatGPTChatTab/Views", - uri: "https://google.com" + uri: "https://google.com", + startLine: nil ), .init( title: "ReferenceList", subtitle: "/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift:100", - uri: "https://google.com" + uri: "https://google.com", + startLine: nil ), .init( title: "ReferenceList", subtitle: "/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift:100", - uri: "https://google.com" + uri: "https://google.com", + startLine: nil ), .init( title: "ReferenceList", subtitle: "/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift:100", - uri: "https://google.com" + uri: "https://google.com", + startLine: nil ), .init( title: "ReferenceList", subtitle: "/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift:100", - uri: "https://google.com" + uri: "https://google.com", + startLine: nil ), ], chat: .init(initialState: .init(), reducer: Chat(service: .init()))) } From 5d57f904e48e80e8225aac5114f9ec1b5f441aac Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 1 Dec 2023 17:09:31 +0800 Subject: [PATCH 34/74] Fix the UI of reference list --- .../ChatGPTChatTab/Views/BotMessage.swift | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift b/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift index 0d527b8c..1bf4c316 100644 --- a/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift +++ b/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift @@ -109,8 +109,13 @@ struct ReferenceList: View { }) { HStack(spacing: 8) { Text(reference.title) + .truncationMode(.middle) + .lineLimit(1) Text(reference.subtitle) - .foregroundStyle(.secondary) + .lineLimit(1) + .truncationMode(.middle) + .layoutPriority(/*@START_MENU_TOKEN@*/0/*@END_MENU_TOKEN@*/) + .foregroundStyle(.tertiary) } .padding(.vertical, 6) .padding(.horizontal, 8) @@ -124,8 +129,8 @@ struct ReferenceList: View { } } .padding() - .frame(maxHeight: 500) } + .frame(maxWidth: 500, maxHeight: 500) } } @@ -138,20 +143,12 @@ struct ReferenceList: View { func foo() {} ``` """, - references: [ - .init( - title: "ReferenceList", - subtitle: "/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift:100", - uri: "https://google.com", - startLine: nil - ), - .init( - title: "BotMessage.swift:100-102", - subtitle: "/Core/Sources/ChatGPTChatTab/Views", - uri: "https://google.com", - startLine: nil - ), - ], + references: .init(repeating: .init( + title: "ReferenceList", + subtitle: "/Core/Sources/ChatGPTChatTab/Views/BotMessage.swift:100", + uri: "https://google.com", + startLine: nil + ), count: 20), chat: .init(initialState: .init(), reducer: Chat(service: .init())) ) .padding() From 01bcaad11b63fe5aa4b7f9ec7439aa96f71c654b Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 1 Dec 2023 22:01:32 +0800 Subject: [PATCH 35/74] Fix opening reference --- Core/Sources/ChatGPTChatTab/Chat.swift | 8 +++++--- Pro | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/Chat.swift b/Core/Sources/ChatGPTChatTab/Chat.swift index e05d3d95..46b02d7f 100644 --- a/Core/Sources/ChatGPTChatTab/Chat.swift +++ b/Core/Sources/ChatGPTChatTab/Chat.swift @@ -168,14 +168,16 @@ struct Chat: ReducerProtocol { do { _ = try await terminal.runCommand( "/bin/bash", - arguments: ["-c", - "xed -l \(reference.startLine ?? 0) \"\(reference.uri)\""], + arguments: [ + "-c", + "xed -l \(reference.startLine ?? 0) \"\(reference.uri)\"", + ], environment: [:] ) } catch { print(error) } - } else if let url = URL(string: reference.uri) { + } else if let url = URL(string: reference.uri), url.scheme != nil { await openURL(url) } } diff --git a/Pro b/Pro index 16d6e392..90f1c04e 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 16d6e392928c739f04ffe6cada00ba208e114192 +Subproject commit 90f1c04eb8950b25659fb992f8e7b2e14bd19bf3 From 353c68629abbe209f794a30d25284ab4a0582c80 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 2 Dec 2023 16:18:10 +0800 Subject: [PATCH 36/74] Fix cancellation --- .../FeatureReducers/PromptToCode.swift | 2 +- Tool/Sources/OpenAIService/ChatGPTService.swift | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCode.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCode.swift index efcf8cb0..7aa41536 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCode.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCode.swift @@ -219,7 +219,7 @@ public struct PromptToCode: ReducerProtocol { case .stopRespondingButtonTapped: state.isResponding = false promptToCodeService.stopResponding() - return .none + return .cancel(id: CancellationKey.modifyCode(state.id)) case let .modifyCodeTrunkReceived(code, description): state.code = code diff --git a/Tool/Sources/OpenAIService/ChatGPTService.swift b/Tool/Sources/OpenAIService/ChatGPTService.swift index 1e2687b3..bbaf4276 100644 --- a/Tool/Sources/OpenAIService/ChatGPTService.swift +++ b/Tool/Sources/OpenAIService/ChatGPTService.swift @@ -63,7 +63,7 @@ public class ChatGPTService: ChatGPTServiceType { public var functionProvider: ChatGPTFunctionProvider var uuidGenerator: () -> String = { UUID().uuidString } - var cancelTask: Cancellable? + var runningTask: Task? var buildCompletionStreamAPI: CompletionStreamAPIBuilder = OpenAICompletionStreamAPI.init var buildCompletionAPI: CompletionAPIBuilder = OpenAICompletionAPI.init @@ -204,8 +204,8 @@ public class ChatGPTService: ChatGPTServiceType { } public func stopReceivingMessage() { - cancelTask?() - cancelTask = nil + runningTask?.cancel() + runningTask = nil } } @@ -285,9 +285,11 @@ extension ChatGPTService { references: prompt.references ) let (trunks, cancel) = try await api() - cancelTask = cancel for try await trunk in trunks { - try Task.checkCancellation() + if Task.isCancelled { + cancel() + throw CancellationError() + } guard let delta = trunk.choices?.first?.delta else { continue } // The api will always return a function call with JSON object. @@ -333,6 +335,8 @@ extension ChatGPTService { } } + runningTask = task + continuation.onTermination = { _ in task.cancel() } From 9c3840d6fe0f4f45fbbbb8b440dfd83740496b28 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 2 Dec 2023 16:18:14 +0800 Subject: [PATCH 37/74] Update --- Tool/Sources/Workspace/OpenedFileRocoverableStorage.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Tool/Sources/Workspace/OpenedFileRocoverableStorage.swift b/Tool/Sources/Workspace/OpenedFileRocoverableStorage.swift index d5661941..06833867 100644 --- a/Tool/Sources/Workspace/OpenedFileRocoverableStorage.swift +++ b/Tool/Sources/Workspace/OpenedFileRocoverableStorage.swift @@ -15,7 +15,9 @@ public final class OpenedFileRecoverableStorage { var openedFiles = Set(dict[projectRootURL.path] as? [String] ?? []) openedFiles.insert(fileURL.path) dict[projectRootURL.path] = Array(openedFiles) - userDefault.set(dict, forKey: key) + Task { @MainActor [dict] in + userDefault.set(dict, forKey: key) + } } public func closeFile(fileURL: URL) { @@ -23,7 +25,9 @@ public final class OpenedFileRecoverableStorage { var openedFiles = dict[projectRootURL.path] as? [String] ?? [] openedFiles.removeAll(where: { $0 == fileURL.path }) dict[projectRootURL.path] = openedFiles - userDefault.set(dict, forKey: key) + Task { @MainActor [dict] in + userDefault.set(dict, forKey: key) + } } public var openedFiles: [URL] { From 46da74c7ccc433b1bfdddbd4c15fe98ebf3e65fe Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 2 Dec 2023 16:18:48 +0800 Subject: [PATCH 38/74] Support providing history compose logic from other types --- .../Memory/AutoManagedChatGPTMemory.swift | 60 ++++++++++++------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift index f773e443..4a78aae5 100644 --- a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift +++ b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift @@ -5,9 +5,20 @@ import TokenEncoder /// A memory that automatically manages the history according to max tokens and max message count. public actor AutoManagedChatGPTMemory: ChatGPTMemory { + public struct ComposableMessages { + public var systemPromptMessage: ChatMessage + public var historyMessage: [ChatMessage] + public var retrievedContentMessage: ChatMessage + public var contextSystemPromptMessage: ChatMessage + public var newMessage: ChatMessage + } + + public typealias HistoryComposer = (ComposableMessages) -> [ChatMessage] + public private(set) var history: [ChatMessage] = [] { didSet { onHistoryChange() } } + public private(set) var remainingTokens: Int? public var systemPrompt: String @@ -19,16 +30,36 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { static let encoder: TokenEncoder = TiktokenCl100kBaseTokenEncoder() var onHistoryChange: () -> Void = {} + + let composeHistory: HistoryComposer public init( systemPrompt: String, configuration: ChatGPTConfiguration, - functionProvider: ChatGPTFunctionProvider + functionProvider: ChatGPTFunctionProvider, + composeHistory: @escaping HistoryComposer = { + /// Default Format: + /// ``` + /// [System Prompt] priority: high + /// [Functions] priority: high + /// [Retrieved Content] priority: low + /// [Retrieved Content A] + /// + /// [Retrieved Content B] + /// [Message History] priority: medium + /// [Context System Prompt] priority: high + /// [Latest Message] priority: high + /// ``` + [$0.systemPromptMessage] + + $0.historyMessage + + [$0.retrievedContentMessage, $0.contextSystemPromptMessage, $0.newMessage] + } ) { self.systemPrompt = systemPrompt contextSystemPrompt = "" self.configuration = configuration self.functionProvider = functionProvider + self.composeHistory = composeHistory _ = Self.encoder // force pre-initialize } @@ -60,19 +91,6 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { } /// https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb - /// - /// Format: - /// ``` - /// [System Prompt] priority: high - /// [Functions] priority: high - /// [Retrieved Content] priority: low - /// [Retrieved Content A] - /// - /// [Retrieved Content B] - /// [Message History] priority: medium - /// [Context System Prompt] priority: high - /// [Latest Message] priority: high - /// ``` func generateSendingHistory( maxNumberOfMessages: Int = UserDefaults.shared.value(for: \.chatGPTMaxMessageCount), encoder: TokenEncoder = AutoManagedChatGPTMemory.encoder @@ -105,11 +123,13 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { encoder: encoder ) - let allMessages: [ChatMessage] = ( - [systemPromptMessage] + - historyMessage + - [retrievedContentMessage, contextSystemPromptMessage, newMessage] - ).filter { + let allMessages = composeHistory(.init( + systemPromptMessage: systemPromptMessage, + historyMessage: historyMessage, + retrievedContentMessage: retrievedContentMessage, + contextSystemPromptMessage: contextSystemPromptMessage, + newMessage: newMessage + )).filter { !($0.content?.isEmpty ?? false) } @@ -269,7 +289,7 @@ extension AutoManagedChatGPTMemory { } } -extension TokenEncoder { +public extension TokenEncoder { /// https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb func countToken(message: ChatMessage) -> Int { var total = 3 From 67221f4514519f32fe023da1a8865460bd06280d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 2 Dec 2023 16:21:10 +0800 Subject: [PATCH 39/74] Update --- Core/Sources/ChatGPTChatTab/Chat.swift | 12 ++++++++---- Core/Sources/ChatService/ChatService.swift | 4 +++- Tool/Sources/OpenAIService/ChatGPTService.swift | 1 + 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/Chat.swift b/Core/Sources/ChatGPTChatTab/Chat.swift index 46b02d7f..d5e32804 100644 --- a/Core/Sources/ChatGPTChatTab/Chat.swift +++ b/Core/Sources/ChatGPTChatTab/Chat.swift @@ -99,6 +99,7 @@ struct Chat: ReducerProtocol { case observeSystemPromptChange(UUID) case observeExtraSystemPromptChange(UUID) case observeDefaultScopesChange(UUID) + case sendMessage(UUID) } @Dependency(\.openURL) var openURL @@ -129,16 +130,19 @@ struct Chat: ReducerProtocol { state.typedMessage = "" return .run { _ in try await service.send(content: message) - } + }.cancellable(id: CancelID.sendMessage(id)) case .returnButtonTapped: state.typedMessage += "\n" return .none case .stopRespondingButtonTapped: - return .run { _ in - await service.stopReceivingMessage() - } + return .merge( + .run { _ in + await service.stopReceivingMessage() + }, + .cancel(id: CancelID.sendMessage(id)) + ) case .clearButtonTap: return .run { _ in diff --git a/Core/Sources/ChatService/ChatService.swift b/Core/Sources/ChatService/ChatService.swift index 2c6fb227..60fe7dfa 100644 --- a/Core/Sources/ChatService/ChatService.swift +++ b/Core/Sources/ChatService/ChatService.swift @@ -105,7 +105,9 @@ public final class ChatService: ObservableObject { let stream = try await chatGPTService.send(content: content, summary: nil) do { - for try await _ in stream {} + for try await _ in stream { + try Task.checkCancellation() + } } catch {} } diff --git a/Tool/Sources/OpenAIService/ChatGPTService.swift b/Tool/Sources/OpenAIService/ChatGPTService.swift index bbaf4276..d35af95a 100644 --- a/Tool/Sources/OpenAIService/ChatGPTService.swift +++ b/Tool/Sources/OpenAIService/ChatGPTService.swift @@ -203,6 +203,7 @@ public class ChatGPTService: ChatGPTServiceType { } } + #warning("TODO: remove this and let the concurrency system handle it") public func stopReceivingMessage() { runningTask?.cancel() runningTask = nil From 2b317fd60c4b44c43de2f54be919313dba41833e Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 2 Dec 2023 17:49:09 +0800 Subject: [PATCH 40/74] Update prompt of prompt to code --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 90f1c04e..e1670cc6 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 90f1c04eb8950b25659fb992f8e7b2e14bd19bf3 +Subproject commit e1670cc6630ea78ed55d60ecb74bf6e37b6365cd From b3ae4ec2923e1d54917b25ddb1d50581be6e18b3 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 2 Dec 2023 17:50:01 +0800 Subject: [PATCH 41/74] Fix that context retriever was incorrect when the context is too long --- Pro | 2 +- .../ActiveDocumentChatContextCollector.swift | 3 +- .../ActiveDocumentContext.swift | 19 +++++++++--- .../FocusedCodeFinder/FocusedCodeFinder.swift | 18 ++++++++--- .../KnownLanguageFocusedCodeFinder.swift | 3 +- .../ObjectiveC/ObjectiveCCodeFinder.swift | 10 +++---- .../Swift/SwiftFocusedCodeFinder.swift | 4 +-- .../UnknownLanguageFocusCodeFinder.swift | 30 ++++++++++++------- .../ObjectiveCFocusedCodeFinderTests.swift | 13 +++++++- .../SwiftFocusedCodeFinderTests.swift | 11 +++++++ ...nknownLanguageFocusedCodeFinderTests.swift | 5 ++++ 11 files changed, 86 insertions(+), 32 deletions(-) diff --git a/Pro b/Pro index e1670cc6..d1282a45 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit e1670cc6630ea78ed55d60ecb74bf6e37b6365cd +Subproject commit d1282a456a11956942aabdd78cffb59809e3e2a2 diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift index bdc89d4f..2495ceee 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift @@ -82,7 +82,8 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { selectedCode: "", selectionRange: .outOfScope, lineAnnotations: [], - imports: [] + imports: [], + includes: [] ) activeDocumentContext.update(info) diff --git a/Tool/Sources/FocusedCodeFinder/ActiveDocumentContext.swift b/Tool/Sources/FocusedCodeFinder/ActiveDocumentContext.swift index 19c49d9d..245b2d16 100644 --- a/Tool/Sources/FocusedCodeFinder/ActiveDocumentContext.swift +++ b/Tool/Sources/FocusedCodeFinder/ActiveDocumentContext.swift @@ -11,22 +11,24 @@ public struct ActiveDocumentContext { public var selectionRange: CursorRange public var lineAnnotations: [EditorInformation.LineAnnotation] public var imports: [String] + public var includes: [String] public struct FocusedContext { public struct Context: Equatable { public var signature: String public var name: String public var range: CursorRange - + public init(signature: String, name: String, range: CursorRange) { self.signature = signature self.name = name self.range = range } } - + public var context: [Context] public var contextRange: CursorRange + public var smallestContextRange: CursorRange public var codeRange: CursorRange public var code: String public var lineAnnotations: [EditorInformation.LineAnnotation] @@ -35,6 +37,7 @@ public struct ActiveDocumentContext { public init( context: [Context], contextRange: CursorRange, + smallestContextRange: CursorRange, codeRange: CursorRange, code: String, lineAnnotations: [EditorInformation.LineAnnotation], @@ -42,6 +45,7 @@ public struct ActiveDocumentContext { ) { self.context = context self.contextRange = contextRange + self.smallestContextRange = smallestContextRange self.codeRange = codeRange self.code = code self.lineAnnotations = lineAnnotations @@ -61,6 +65,7 @@ public struct ActiveDocumentContext { selectionRange: CursorRange, lineAnnotations: [EditorInformation.LineAnnotation], imports: [String], + includes: [String], focusedContext: FocusedContext? = nil ) { self.documentURL = documentURL @@ -72,6 +77,7 @@ public struct ActiveDocumentContext { self.selectionRange = selectionRange self.lineAnnotations = lineAnnotations self.imports = imports + self.includes = includes self.focusedContext = focusedContext } @@ -92,8 +98,10 @@ public struct ActiveDocumentContext { } public mutating func moveToCodeContainingRange(_ range: CursorRange) { - let finder = FocusedCodeFinder() - + let finder = FocusedCodeFinder( + maxFocusedCodeLineCount: UserDefaults.shared.value(for: \.maxFocusedCodeLineCount) + ) + let codeContext = finder.findFocusedCode( in: .init(documentURL: documentURL, content: fileContent, lines: lines), containingRange: range, @@ -101,6 +109,7 @@ public struct ActiveDocumentContext { ) imports = codeContext.imports + includes = codeContext.includes let startLine = codeContext.focusedRange.start.line let endLine = codeContext.focusedRange.end.line @@ -117,6 +126,7 @@ public struct ActiveDocumentContext { focusedContext = .init( context: codeContext.scopeContexts, contextRange: codeContext.contextRange, + smallestContextRange: codeContext.smallestContextRange, codeRange: codeContext.focusedRange, code: codeContext.focusedCode, lineAnnotations: matchedAnnotations, @@ -144,6 +154,7 @@ public struct ActiveDocumentContext { selectionRange = info.editorContent?.selections.first ?? .zero lineAnnotations = info.editorContent?.lineAnnotations ?? [] imports = [] + includes = [] if changed { moveToFocusedCode() diff --git a/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift index 9c0e7987..175fd6e6 100644 --- a/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift @@ -23,6 +23,7 @@ public struct CodeContext: Equatable { public var scope: Scope public var contextRange: CursorRange + public var smallestContextRange: CursorRange public var focusedRange: CursorRange public var focusedCode: String public var imports: [String] @@ -32,6 +33,7 @@ public struct CodeContext: Equatable { .init( scope: .file, contextRange: .zero, + smallestContextRange: .zero, focusedRange: .zero, focusedCode: "", imports: [], @@ -42,6 +44,7 @@ public struct CodeContext: Equatable { public init( scope: Scope, contextRange: CursorRange, + smallestContextRange: CursorRange, focusedRange: CursorRange, focusedCode: String, imports: [String], @@ -49,6 +52,7 @@ public struct CodeContext: Equatable { ) { self.scope = scope self.contextRange = contextRange + self.smallestContextRange = smallestContextRange self.focusedRange = focusedRange self.focusedCode = focusedCode self.imports = imports @@ -57,7 +61,11 @@ public struct CodeContext: Equatable { } public struct FocusedCodeFinder { - public init() {} + public let maxFocusedCodeLineCount: Int + + public init(maxFocusedCodeLineCount: Int) { + self.maxFocusedCodeLineCount = maxFocusedCodeLineCount + } public struct Document { var documentURL: URL @@ -79,10 +87,12 @@ public struct FocusedCodeFinder { let finder: FocusedCodeFinderType = { switch language { case .builtIn(.swift): - return SwiftFocusedCodeFinder() + return SwiftFocusedCodeFinder(maxFocusedCodeLineCount: maxFocusedCodeLineCount) case .builtIn(.objc), .builtIn(.objcpp), .builtIn(.c): - #warning("TODO: Implement C++ focused code finder, use it for C and metal shading language") - return ObjectiveCFocusedCodeFinder() + #warning( + "TODO: Implement C++ focused code finder, use it for C and metal shading language" + ) + return ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: maxFocusedCodeLineCount) default: return UnknownLanguageFocusedCodeFinder(proposedSearchRange: 5) } diff --git a/Tool/Sources/FocusedCodeFinder/KnownLanguageFocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/KnownLanguageFocusedCodeFinder.swift index 95d73c46..e47a001f 100644 --- a/Tool/Sources/FocusedCodeFinder/KnownLanguageFocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/KnownLanguageFocusedCodeFinder.swift @@ -126,7 +126,8 @@ public extension KnownLanguageFocusedCodeFinderType { return .init( scope: scopeContexts.isEmpty ? .file : .scope(signature: scopeContexts), - contextRange: contextRange, + contextRange: contextRange, + smallestContextRange: codeRange, focusedRange: focusedRange, focusedCode: code, imports: contextInfo.imports, diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift index c1c2d240..421e3cc7 100644 --- a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift @@ -14,9 +14,7 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< ASTNode, TreeSitterTextPosition > { - override public init( - maxFocusedCodeLineCount: Int = UserDefaults.shared.value(for: \.maxFocusedCodeLineCount) - ) { + override public init(maxFocusedCodeLineCount: Int) { super.init(maxFocusedCodeLineCount: maxFocusedCodeLineCount) } @@ -99,7 +97,7 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< var prefix = "" /// Generics, super class, etc. var extra = "" - + if let nameNode = node.child(byFieldName: "name") { name = textProvider(.node(nameNode)) prefix = textProvider(.range( @@ -135,7 +133,7 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< prefix = prefix.split(separator: "\n") .joined(separator: " ") .trimmingCharacters(in: .whitespacesAndNewlines) - + extra = extra.split(separator: "\n") .joined(separator: " ") .trimmingCharacters(in: .whitespacesAndNewlines) @@ -152,7 +150,7 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< canBeUsedAsCodeRange: true ) } - + func parseMethodDefinitionNode( _ node: ASTNode, textProvider: @escaping TextProvider diff --git a/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift index e3bfcb42..b2b235cc 100644 --- a/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift @@ -10,9 +10,7 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< SyntaxProtocol, SyntaxProtocol > { - override public init( - maxFocusedCodeLineCount: Int = UserDefaults.shared.value(for: \.maxFocusedCodeLineCount) - ) { + override public init(maxFocusedCodeLineCount: Int) { super.init(maxFocusedCodeLineCount: maxFocusedCodeLineCount) } diff --git a/Tool/Sources/FocusedCodeFinder/UnknownLanguageFocusCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/UnknownLanguageFocusCodeFinder.swift index f5aeaa68..3ce18ab1 100644 --- a/Tool/Sources/FocusedCodeFinder/UnknownLanguageFocusCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/UnknownLanguageFocusCodeFinder.swift @@ -28,17 +28,22 @@ public struct UnknownLanguageFocusedCodeFinder: FocusedCodeFinderType { min(startLineIndex + proposedLineCount - 1, lines.count - 1) ) + if lines.endIndex <= endLineIndex { return .empty } + let focusedLines = lines[startLineIndex...endLineIndex] let contextStartLine = max(startLineIndex - 5, 0) let contextEndLine = min(endLineIndex + 5, lines.count - 1) + + let contextRange = CursorRange( + start: .init(line: contextStartLine, character: 0), + end: .init(line: contextEndLine, character: lines[contextEndLine].count) + ) return .init( scope: .top, - contextRange: .init( - start: .init(line: contextStartLine, character: 0), - end: .init(line: contextEndLine, character: lines[contextEndLine].count) - ), + contextRange: contextRange, + smallestContextRange: contextRange, focusedRange: .init( start: .init(line: startLineIndex, character: 0), end: .init(line: endLineIndex, character: lines[endLineIndex].count) @@ -57,16 +62,19 @@ public struct UnknownLanguageFocusedCodeFinder: FocusedCodeFinderType { let focusedLines = document.lines[startLine...endLine] let contextStartLine = max(startLine - 3, 0) let contextEndLine = min(endLine + 3, document.lines.count - 1) + + let contextRange = CursorRange( + start: .init(line: contextStartLine, character: 0), + end: .init( + line: contextEndLine, + character: document.lines[contextEndLine].count + ) + ) return CodeContext( scope: .top, - contextRange: .init( - start: .init(line: contextStartLine, character: 0), - end: .init( - line: contextEndLine, - character: document.lines[contextEndLine].count - ) - ), + contextRange: contextRange, + smallestContextRange: contextRange, focusedRange: containingRange, focusedCode: focusedLines.joined(), imports: [], diff --git a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift index 20e2f277..0c27085f 100644 --- a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift @@ -36,7 +36,8 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { range: .init(startPair: (1, 0), endPair: (5, 1)) ), ]), - contextRange: .init(startPair: (0, 0), endPair: (6, 4)), + contextRange: .init(startPair: (0, 0), endPair: (6, 4)), + smallestContextRange: range, focusedRange: range, focusedCode: """ NSInteger foo = 0; @@ -69,6 +70,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { ), ]), contextRange: .init(startPair: (0, 0), endPair: (4, 1)), + smallestContextRange: range, focusedRange: range, focusedCode: """ NSLog(@"Hello"); @@ -107,6 +109,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { ), ]), contextRange: .init(startPair: (0, 0), endPair: (7, 4)), + smallestContextRange: range, focusedRange: range, focusedCode: """ - (void)fooWith:(NSInteger)foo { @@ -146,6 +149,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { ), ]), contextRange: .init(startPair: (0, 0), endPair: (4, 4)), + smallestContextRange: range, focusedRange: range, focusedCode: """ - (void)fooWith:(NSInteger)foo; @@ -183,6 +187,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { ), ]), contextRange: .init(startPair: (0, 0), endPair: (4, 4)), + smallestContextRange: range, focusedRange: range, focusedCode: """ - (void)fooWith:(NSInteger)foo; @@ -220,6 +225,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { ), ]), contextRange: .init(startPair: (0, 0), endPair: (4, 4)), + smallestContextRange: range, focusedRange: range, focusedCode: """ - (void)fooWith:(NSInteger)foo; @@ -257,6 +263,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { ), ]), contextRange: .init(startPair: (0, 0), endPair: (4, 1)), + smallestContextRange: range, focusedRange: range, focusedCode: """ NSInteger foo; @@ -294,6 +301,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { ), ]), contextRange: .init(startPair: (0, 0), endPair: (4, 1)), + smallestContextRange: range, focusedRange: range, focusedCode: """ foo, @@ -331,6 +339,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { ), ]), contextRange: .init(startPair: (0, 0), endPair: (4, 2)), + smallestContextRange: range, focusedRange: range, focusedCode: """ foo, @@ -372,6 +381,7 @@ final class ObjectiveCFocusedCodeFinder_Focus_Tests: XCTestCase { ), ]), contextRange: .init(startPair: (0, 0), endPair: (6, 4)), + smallestContextRange: range, focusedRange: .init(startPair: (1, 0), endPair: (5, 1)), focusedCode: """ - (void)fooWith:(NSInteger)foo { @@ -408,6 +418,7 @@ final class ObjectiveCFocusedCodeFinder_Focus_Tests: XCTestCase { XCTAssertEqual(context, .init( scope: .file, contextRange: .init(startPair: (0, 0), endPair: (0, 0)), + smallestContextRange: range, focusedRange: .init(startPair: (0, 0), endPair: (4, 4)), focusedCode: """ @interface __GENERICS(NSArray, ObjectType) (BlocksKit) diff --git a/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift index cfee3265..00f56fbe 100644 --- a/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift @@ -47,6 +47,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { ), ]), contextRange: .init(startPair: (0, 0), endPair: (8, 1)), + smallestContextRange: .init(startPair: (4, 0), endPair: (4, 13)), focusedRange: .init(startPair: (4, 0), endPair: (4, 13)), focusedCode: """ let c = 3 @@ -87,6 +88,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { ), ]), contextRange: .init(startPair: (0, 0), endPair: (9, 1)), + smallestContextRange: .init(startPair: (2, 0), endPair: (7, 5)), focusedRange: .init(startPair: (2, 0), endPair: (7, 5)), focusedCode: """ func f() { @@ -129,6 +131,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { ), ]), contextRange: .init(startPair: (0, 0), endPair: (6, 1)), + smallestContextRange: .init(startPair: (1, 0), endPair: (1, 9)), focusedRange: .init(startPair: (1, 0), endPair: (1, 9)), focusedCode: """ var a = 1 @@ -166,6 +169,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { ), ]), contextRange: .init(startPair: (0, 0), endPair: (6, 1)), + smallestContextRange: .init(startPair: (1, 0), endPair: (1, 9)), focusedRange: .init(startPair: (1, 0), endPair: (1, 9)), focusedCode: """ func f() @@ -203,6 +207,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { ), ]), contextRange: .init(startPair: (0, 0), endPair: (6, 1)), + smallestContextRange: .init(startPair: (1, 0), endPair: (1, 9)), focusedRange: .init(startPair: (1, 0), endPair: (1, 9)), focusedCode: """ var a = 1 @@ -241,6 +246,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { ), ]), contextRange: .init(startPair: (0, 0), endPair: (7, 1)), + smallestContextRange: .init(startPair: (2, 0), endPair: (2, 9)), focusedRange: .init(startPair: (2, 0), endPair: (2, 9)), focusedCode: """ static func f() {} @@ -280,6 +286,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { ), ]), contextRange: .init(startPair: (0, 0), endPair: (8, 1)), + smallestContextRange: .init(startPair: (3, 0), endPair: (3, 9)), focusedRange: .init(startPair: (3, 0), endPair: (3, 9)), focusedCode: """ case a @@ -324,6 +331,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { ), ]), contextRange: .init(startPair: (0, 0), endPair: (8, 1)), + smallestContextRange: .init(startPair: (2, 0), endPair: (2, 9)), focusedRange: .init(startPair: (2, 0), endPair: (2, 9)), focusedCode: """ let a = 1 @@ -365,6 +373,7 @@ final class SwiftFocusedCodeFinder_FocusedCode_Tests: XCTestCase { XCTAssertEqual(context, .init( scope: .top, contextRange: .init(startPair: (0, 0), endPair: (13, 2)), + smallestContextRange: .init(startPair: (0, 0), endPair: (13, 2)), focusedRange: .init(startPair: (0, 0), endPair: (13, 2)), focusedCode: """ @MainActor @@ -408,6 +417,7 @@ final class SwiftFocusedCodeFinder_FocusedCode_Tests: XCTestCase { XCTAssertEqual(context, .init( scope: .file, contextRange: .init(startPair: (0, 0), endPair: (0, 0)), + smallestContextRange: .init(startPair: (0, 0), endPair: (8, 1)), focusedRange: .init(startPair: (0, 0), endPair: (8, 1)), focusedCode: """ @MainActor @@ -446,6 +456,7 @@ final class SwiftFocusedCodeFinder_FocusedCode_Tests: XCTestCase { XCTAssertEqual(context, .init( scope: .file, contextRange: .init(startPair: (0, 0), endPair: (0, 0)), + smallestContextRange: .init(startPair: (2, 0), endPair: (4, 11)), focusedRange: .init(startPair: (2, 0), endPair: (4, 11)), focusedCode: """ indirect enum A { diff --git a/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift index f540bff2..80febfc1 100644 --- a/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift @@ -15,6 +15,7 @@ class UnknownLanguageFocusedCodeFinderTests: XCTestCase { XCTAssertEqual(context, .init( scope: .top, contextRange: .init(startPair: (40, 0), endPair: (60, 3)), + smallestContextRange: .init(startPair: (45, 0), endPair: (55, 3)), focusedRange: .init(startPair: (45, 0), endPair: (55, 3)), focusedCode: stride(from: 45, through: 55, by: 1).map { "\($0)\n" }.joined(), imports: [], @@ -32,6 +33,7 @@ class UnknownLanguageFocusedCodeFinderTests: XCTestCase { XCTAssertEqual(context, .init( scope: .top, contextRange: .init(startPair: (0, 0), endPair: (15, 3)), + smallestContextRange: .init(startPair: (0, 0), endPair: (10, 3)), focusedRange: .init(startPair: (0, 0), endPair: (10, 3)), focusedCode: stride(from: 0, through: 10, by: 1).map { "\($0)\n" }.joined(), imports: [], @@ -49,6 +51,7 @@ class UnknownLanguageFocusedCodeFinderTests: XCTestCase { XCTAssertEqual(context, .init( scope: .top, contextRange: .init(startPair: (89, 0), endPair: (101, 1)), + smallestContextRange: .init(startPair: (94, 0), endPair: (101, 1)), focusedRange: .init(startPair: (94, 0), endPair: (101, 1)), focusedCode: stride(from: 94, through: 100, by: 1).map { "\($0)\n" }.joined() + "\n", imports: [], @@ -66,6 +69,7 @@ class UnknownLanguageFocusedCodeFinderTests: XCTestCase { XCTAssertEqual(context, .init( scope: .top, contextRange: .init(startPair: (0, 0), endPair: (11, 1)), + smallestContextRange: .init(startPair: (0, 0), endPair: (10, 3)), focusedRange: .init(startPair: (0, 0), endPair: (10, 3)), focusedCode: code, imports: [], @@ -83,6 +87,7 @@ class UnknownLanguageFocusedCodeFinderTests: XCTestCase { XCTAssertEqual(context, .init( scope: .top, contextRange: .init(startPair: (0, 0), endPair: (5, 1)), + smallestContextRange: .init(startPair: (0, 0), endPair: (5, 1)), focusedRange: .init(startPair: (0, 0), endPair: (5, 1)), focusedCode: code + "\n", imports: [], From 6c129d7b16296a58a3d90477ec14a02a19430236 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 2 Dec 2023 18:55:29 +0800 Subject: [PATCH 42/74] Fix that multiple function result was added to history --- Tool/Package.swift | 12 +- .../OpenAIService/ChatGPTService.swift | 47 +-- .../Memory/AutoManagedChatGPTMemory.swift | 2 +- .../OpenAIService/Memory/ChatGPTMemory.swift | 6 +- .../ChatGPTStreamTests.swift | 364 +++++++++--------- .../LimitMessagesTests.swift | 18 +- 6 files changed, 219 insertions(+), 230 deletions(-) diff --git a/Tool/Package.swift b/Tool/Package.swift index 6772dd0c..05134a03 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -299,11 +299,21 @@ let package = Package( "Keychain", .product(name: "JSONRPC", package: "JSONRPC"), .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), + .product( + name: "ComposableArchitecture", + package: "swift-composable-architecture" + ) ] ), .testTarget( name: "OpenAIServiceTests", - dependencies: ["OpenAIService"] + dependencies: [ + "OpenAIService", + .product( + name: "ComposableArchitecture", + package: "swift-composable-architecture" + ) + ] ), // MARK: - UI diff --git a/Tool/Sources/OpenAIService/ChatGPTService.swift b/Tool/Sources/OpenAIService/ChatGPTService.swift index d35af95a..37f230dc 100644 --- a/Tool/Sources/OpenAIService/ChatGPTService.swift +++ b/Tool/Sources/OpenAIService/ChatGPTService.swift @@ -1,4 +1,5 @@ import AsyncAlgorithms +import Dependencies import Foundation import Preferences @@ -62,7 +63,6 @@ public class ChatGPTService: ChatGPTServiceType { public var configuration: ChatGPTConfiguration public var functionProvider: ChatGPTFunctionProvider - var uuidGenerator: () -> String = { UUID().uuidString } var runningTask: Task? var buildCompletionStreamAPI: CompletionStreamAPIBuilder = OpenAICompletionStreamAPI.init var buildCompletionAPI: CompletionAPIBuilder = OpenAICompletionAPI.init @@ -80,6 +80,9 @@ public class ChatGPTService: ChatGPTServiceType { self.configuration = configuration self.functionProvider = functionProvider } + + @Dependency(\.uuid) var uuid + @Dependency(\.date) var date /// Send a message and stream the reply. public func send( @@ -88,7 +91,7 @@ public class ChatGPTService: ChatGPTServiceType { ) async throws -> AsyncThrowingStream { if !content.isEmpty || summary != nil { let newMessage = ChatMessage( - id: uuidGenerator(), + id: uuid().uuidString, role: .user, content: content, name: nil, @@ -132,7 +135,7 @@ public class ChatGPTService: ChatGPTServiceType { #endif case let .functionCall(call): if functionCall == nil { - functionCallMessageID = uuidGenerator() + functionCallMessageID = uuid().uuidString functionCall = call } else { functionCall?.name.append(call.name) @@ -171,7 +174,7 @@ public class ChatGPTService: ChatGPTServiceType { ) async throws -> String? { if !content.isEmpty || summary != nil { let newMessage = ChatMessage( - id: uuidGenerator(), + id: uuid().uuidString, role: .user, content: content, summary: summary @@ -276,11 +279,12 @@ extension ChatGPTService { #if DEBUG Debugger.didSendRequestBody(body: requestBody) #endif + + let proposedId = uuid().uuidString + String(date().timeIntervalSince1970) return AsyncThrowingStream { continuation in let task = Task { do { - let proposedId = UUID().uuidString + String(Date().timeIntervalSince1970) await memory.streamMessage( id: proposedId, references: prompt.references @@ -335,9 +339,9 @@ extension ChatGPTService { continuation.finish(throwing: error) } } - + runningTask = task - + continuation.onTermination = { _ in task.cancel() } @@ -346,7 +350,7 @@ extension ChatGPTService { /// Send the memory as prompt to ChatGPT, with stream disabled. func sendMemoryAndWait() async throws -> ChatMessage? { - let proposedId = UUID().uuidString + String(Date().timeIntervalSince1970) + let proposedId = uuid().uuidString + String(date().timeIntervalSince1970) let prompt = await memory.generatePrompt() guard let model = configuration.model else { @@ -425,13 +429,7 @@ extension ChatGPTService { /// to insert a message placeholder in memory. func prepareFunctionCall(_ call: ChatMessage.FunctionCall, messageId: String) async { guard let function = functionProvider.function(named: call.name) else { return } - let responseMessage = ChatMessage( - id: messageId, - role: .function, - content: nil, - name: call.name - ) - await memory.appendMessage(responseMessage) + await memory.streamMessage(id: messageId, role: .function, name: call.name) await function.prepare { [weak self] summary in await self?.memory.updateMessage(id: messageId) { message in message.summary = summary @@ -449,21 +447,13 @@ extension ChatGPTService { Debugger.didReceiveFunction(name: call.name, arguments: call.arguments) #endif - let messageId = messageId ?? uuidGenerator() + let messageId = messageId ?? uuid().uuidString guard let function = functionProvider.function(named: call.name) else { return await fallbackFunctionCall(call, messageId: messageId) } - // Insert the chat message into memory to indicate the start of the function. - let responseMessage = ChatMessage( - id: messageId, - role: .function, - content: nil, - name: call.name - ) - - await memory.appendMessage(responseMessage) + await memory.streamMessage(id: messageId, role: .function, name: call.name) do { // Run the function @@ -537,14 +527,13 @@ extension ChatGPTService { return "No result." } }() - let responseMessage = ChatMessage( + await memory.streamMessage( id: messageId, role: .function, content: content, name: call.name, summary: "Finished running function." ) - await memory.appendMessage(responseMessage) return content } } @@ -553,10 +542,6 @@ extension ChatGPTService { func changeBuildCompletionStreamAPI(_ builder: @escaping CompletionStreamAPIBuilder) { buildCompletionStreamAPI = builder } - - func changeUUIDGenerator(_ generator: @escaping () -> String) { - uuidGenerator = generator - } } func maxTokenForReply(maxToken: Int, remainingTokens: Int?) -> Int? { diff --git a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift index 4a78aae5..43e09d90 100644 --- a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift +++ b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift @@ -108,7 +108,7 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { availableTokenCountForRetrievedContent, messageUsage ) = generateMessageHistory( - maxNumberOfMessages: maxNumberOfMessages, + maxNumberOfMessages: maxNumberOfMessages - 1, // for the new message maxTokenCount: availableTokenCountForMessages, encoder: encoder ) diff --git a/Tool/Sources/OpenAIService/Memory/ChatGPTMemory.swift b/Tool/Sources/OpenAIService/Memory/ChatGPTMemory.swift index b25d5ce4..558b6cac 100644 --- a/Tool/Sources/OpenAIService/Memory/ChatGPTMemory.swift +++ b/Tool/Sources/OpenAIService/Memory/ChatGPTMemory.swift @@ -63,6 +63,7 @@ public extension ChatGPTMemory { id: String, role: ChatMessage.Role? = nil, content: String? = nil, + name: String? = nil, functionCall: ChatMessage.FunctionCall? = nil, summary: String? = nil, references: [ChatMessage.Reference]? = nil @@ -93,12 +94,15 @@ public extension ChatGPTMemory { if let references { history[index].references.append(contentsOf: references) } + if let name { + history[index].name = name + } } else { history.append(.init( id: id, role: role ?? .system, content: content, - name: nil, + name: name, functionCall: functionCall, summary: summary, references: references ?? [] diff --git a/Tool/Tests/OpenAIServiceTests/ChatGPTStreamTests.swift b/Tool/Tests/OpenAIServiceTests/ChatGPTStreamTests.swift index e40442cc..26fa600a 100644 --- a/Tool/Tests/OpenAIServiceTests/ChatGPTStreamTests.swift +++ b/Tool/Tests/OpenAIServiceTests/ChatGPTStreamTests.swift @@ -1,3 +1,4 @@ +import Dependencies import XCTest @testable import OpenAIService @@ -12,49 +13,53 @@ final class ChatGPTStreamTests: XCTestCase { functionProvider: functionProvider ) var requestBody: CompletionRequestBody? - var idCounter = 0 - service.changeUUIDGenerator { - defer { idCounter += 1 } - return "\(idCounter)" - } service.changeBuildCompletionStreamAPI { _, _, _, _requestBody in requestBody = _requestBody - return MockCompletionStreamAPI_Message(genId: { - defer { idCounter += 1 } - return "\(idCounter)" - }) + return MockCompletionStreamAPI_Message() } - let stream = try await service.send(content: "Hello") - var all = [String]() - for try await text in stream { - all.append(text) - let history = await memory.messages - XCTAssertEqual(history.last?.id, "1") - XCTAssertTrue( - history.last?.content?.hasPrefix(all.joined()) ?? false, - "History is not updated" - ) - } + try await withDependencies { values in + values.uuid = .incrementing + values.date = .constant(.init(timeIntervalSince1970: 0)) + } operation: { + let stream = try await service.send(content: "Hello") + var all = [String]() + for try await text in stream { + all.append(text) + let history = await memory.history + XCTAssertTrue( + history.last?.content?.hasPrefix(all.joined()) ?? false, + "History is not updated" + ) + } - XCTAssertEqual(requestBody?.messages, [ - .init(role: .system, content: "system"), - .init(role: .user, content: "Hello"), - ], "System prompt is not included") + XCTAssertEqual(requestBody?.messages, [ + .init(role: .system, content: "system"), + .init(role: .user, content: "Hello"), + ], "System prompt is not included") - XCTAssertEqual(all, ["hello", "my", "friends"], "Text stream is not correct") + XCTAssertEqual(all, ["hello", "my", "friends"], "Text stream is not correct") - var history = await memory.messages - for (i, _) in history.enumerated() { - history[i].tokensCount = nil - } - XCTAssertEqual(history, [ - .init(id: "s", role: .system, content: "system"), - .init(id: "0", role: .user, content: "Hello"), - .init(id: "1", role: .assistant, content: "hellomyfriends"), - ], "History is not updated") + var history = await memory.history + for (i, _) in history.enumerated() { + history[i].tokensCount = nil + } + XCTAssertEqual(history, [ + .init( + id: "s", + role: .system, + content: "system" + ), + .init(id: "00000000-0000-0000-0000-000000000000", role: .user, content: "Hello"), + .init( + id: "00000000-0000-0000-0000-0000000000010.0", + role: .assistant, + content: "hellomyfriends" + ), + ], "History is not updated") - XCTAssertEqual(requestBody?.functions, nil, "Function schema is not submitted") + XCTAssertEqual(requestBody?.functions, nil, "Function schema is not submitted") + } } func test_handling_function_call() async throws { @@ -67,77 +72,71 @@ final class ChatGPTStreamTests: XCTestCase { functionProvider: functionProvider ) var requestBody: CompletionRequestBody? - var idCounter = 0 - service.changeUUIDGenerator { - defer { idCounter += 1 } - return "\(idCounter)" - } service.changeBuildCompletionStreamAPI { _, _, _, _requestBody in requestBody = _requestBody if _requestBody.messages.count <= 2 { - return MockCompletionStreamAPI_Function(genId: { - defer { idCounter += 1 } - return "\(idCounter)" - }) + return MockCompletionStreamAPI_Function() } - return MockCompletionStreamAPI_Message(genId: { - defer { idCounter += 1 } - return "\(idCounter)" - }) - } - - let stream = try await service.send(content: "Hello") - var all = [String]() - for try await text in stream { - all.append(text) - let history = await memory.messages - XCTAssertEqual(history.last?.id, "3") - XCTAssertTrue( - history.last?.content?.hasPrefix(all.joined()) ?? false, - "History is not updated" - ) + return MockCompletionStreamAPI_Message() } - XCTAssertEqual(requestBody?.messages, [ - .init(role: .system, content: "system"), - .init(role: .user, content: "Hello"), - .init( - role: .assistant, content: "", - function_call: .init(name: "function", arguments: "{\n\"foo\": 1\n}") - ), - .init(role: .function, content: "Function is called.", name: "function"), - ], "System prompt is not included") - - XCTAssertEqual(all, ["hello", "my", "friends"], "Text stream is not correct") - - var history = await memory.messages - for (i, _) in history.enumerated() { - history[i].tokensCount = nil + try await withDependencies { values in + values.uuid = .incrementing + values.date = .constant(.init(timeIntervalSince1970: 0)) + } operation: { + let stream = try await service.send(content: "Hello") + var all = [String]() + for try await text in stream { + all.append(text) + let history = await memory.history + XCTAssertEqual(history.last?.id, "00000000-0000-0000-0000-0000000000040.0") + XCTAssertTrue( + history.last?.content?.hasPrefix(all.joined()) ?? false, + "History is not updated" + ) + } + + XCTAssertEqual(requestBody?.messages, [ + .init(role: .system, content: "system"), + .init(role: .user, content: "Hello"), + .init( + role: .assistant, content: "", + function_call: .init(name: "function", arguments: "{\n\"foo\": 1\n}") + ), + .init(role: .function, content: "Function is called.", name: "function"), + ], "System prompt is not included") + + XCTAssertEqual(all, ["hello", "my", "friends"], "Text stream is not correct") + + var history = await memory.history + for (i, _) in history.enumerated() { + history[i].tokensCount = nil + } + XCTAssertEqual(history, [ + .init(id: "s", role: .system, content: "system"), + .init(id: "00000000-0000-0000-0000-000000000000", role: .user, content: "Hello"), + .init( + id: "00000000-0000-0000-0000-0000000000010.0", + role: .assistant, + content: nil, + functionCall: .init(name: "function", arguments: "{\n\"foo\": 1\n}") + ), + .init( + id: "00000000-0000-0000-0000-000000000003", + role: .function, + content: "Function is called.", + name: "function", + summary: nil + ), + .init(id: "00000000-0000-0000-0000-0000000000040.0", role: .assistant, content: "hellomyfriends"), + ], "History is not updated") + + XCTAssertEqual(requestBody?.functions, [ + EmptyFunction(), + ].map { + .init(name: $0.name, description: $0.description, parameters: $0.argumentSchema) + }, "Function schema is not submitted") } - XCTAssertEqual(history, [ - .init(id: "s", role: .system, content: "system"), - .init(id: "0", role: .user, content: "Hello"), - .init( - id: "1", - role: .assistant, - content: nil, - functionCall: .init(name: "function", arguments: "{\n\"foo\": 1\n}") - ), - .init( - id: "2", - role: .function, - content: "Function is called.", - name: "function", - summary: nil - ), - .init(id: "3", role: .assistant, content: "hellomyfriends"), - ], "History is not updated") - - XCTAssertEqual(requestBody?.functions, [ - EmptyFunction(), - ].map { - .init(name: $0.name, description: $0.description, parameters: $0.argumentSchema) - }, "Function schema is not submitted") } func test_handling_multiple_function_call() async throws { @@ -150,106 +149,101 @@ final class ChatGPTStreamTests: XCTestCase { functionProvider: functionProvider ) var requestBody: CompletionRequestBody? - var idCounter = 0 - service.changeUUIDGenerator { - defer { idCounter += 1 } - return "\(idCounter)" - } + service.changeBuildCompletionStreamAPI { _, _, _, _requestBody in requestBody = _requestBody if _requestBody.messages.count <= 4 { - return MockCompletionStreamAPI_Function(genId: { - defer { idCounter += 1 } - return "\(idCounter)" - }) + return MockCompletionStreamAPI_Function() } - return MockCompletionStreamAPI_Message(genId: { - defer { idCounter += 1 } - return "\(idCounter)" - }) + return MockCompletionStreamAPI_Message() } - let stream = try await service.send(content: "Hello") - var all = [String]() - for try await text in stream { - all.append(text) - let history = await memory.messages - XCTAssertEqual(history.last?.id, "5") - XCTAssertTrue( - history.last?.content?.hasPrefix(all.joined()) ?? false, - "History is not updated" - ) - } - - XCTAssertEqual(requestBody?.messages, [ - .init(role: .system, content: "system"), - .init(role: .user, content: "Hello"), - .init( - role: .assistant, content: "", - function_call: .init(name: "function", arguments: "{\n\"foo\": 1\n}") - ), - .init(role: .function, content: "Function is called.", name: "function"), - .init( - role: .assistant, content: "", - function_call: .init(name: "function", arguments: "{\n\"foo\": 1\n}") - ), - .init(role: .function, content: "Function is called.", name: "function"), - ], "System prompt is not included") - - XCTAssertEqual(all, ["hello", "my", "friends"], "Text stream is not correct") - - var history = await memory.messages - for (i, _) in history.enumerated() { - history[i].tokensCount = nil + try await withDependencies { values in + values.uuid = .incrementing + values.date = .constant(.init(timeIntervalSince1970: 0)) + } operation: { + let stream = try await service.send(content: "Hello") + var all = [String]() + for try await text in stream { + all.append(text) + let history = await memory.history + XCTAssertEqual(history.last?.id, "00000000-0000-0000-0000-0000000000070.0") + XCTAssertTrue( + history.last?.content?.hasPrefix(all.joined()) ?? false, + "History is not updated" + ) + } + + XCTAssertEqual(requestBody?.messages, [ + .init(role: .system, content: "system"), + .init(role: .user, content: "Hello"), + .init( + role: .assistant, content: "", + function_call: .init(name: "function", arguments: "{\n\"foo\": 1\n}") + ), + .init(role: .function, content: "Function is called.", name: "function"), + .init( + role: .assistant, content: "", + function_call: .init(name: "function", arguments: "{\n\"foo\": 1\n}") + ), + .init(role: .function, content: "Function is called.", name: "function"), + ], "System prompt is not included") + + XCTAssertEqual(all, ["hello", "my", "friends"], "Text stream is not correct") + + var history = await memory.history + for (i, _) in history.enumerated() { + history[i].tokensCount = nil + } + XCTAssertEqual(history, [ + .init(id: "s", role: .system, content: "system"), + .init(id: "00000000-0000-0000-0000-000000000000", role: .user, content: "Hello"), + .init( + id: "00000000-0000-0000-0000-0000000000010.0", + role: .assistant, + content: nil, + functionCall: .init(name: "function", arguments: "{\n\"foo\": 1\n}") + ), + .init( + id: "00000000-0000-0000-0000-000000000003", + role: .function, + content: "Function is called.", + name: "function", + summary: nil + ), + .init( + id: "00000000-0000-0000-0000-0000000000040.0", + role: .assistant, + content: nil, + functionCall: .init(name: "function", arguments: "{\n\"foo\": 1\n}") + ), + .init( + id: "00000000-0000-0000-0000-000000000006", + role: .function, + content: "Function is called.", + name: "function", + summary: nil + ), + .init(id: "00000000-0000-0000-0000-0000000000070.0", role: .assistant, content: "hellomyfriends"), + ], "History is not updated") + + XCTAssertEqual(requestBody?.functions, [ + EmptyFunction(), + ].map { + .init(name: $0.name, description: $0.description, parameters: $0.argumentSchema) + }, "Function schema is not submitted") } - XCTAssertEqual(history, [ - .init(id: "s", role: .system, content: "system"), - .init(id: "0", role: .user, content: "Hello"), - .init( - id: "1", - role: .assistant, - content: nil, - functionCall: .init(name: "function", arguments: "{\n\"foo\": 1\n}") - ), - .init( - id: "2", - role: .function, - content: "Function is called.", - name: "function", - summary: nil - ), - .init( - id: "3", - role: .assistant, - content: nil, - functionCall: .init(name: "function", arguments: "{\n\"foo\": 1\n}") - ), - .init( - id: "4", - role: .function, - content: "Function is called.", - name: "function", - summary: nil - ), - .init(id: "5", role: .assistant, content: "hellomyfriends"), - ], "History is not updated") - - XCTAssertEqual(requestBody?.functions, [ - EmptyFunction(), - ].map { - .init(name: $0.name, description: $0.description, parameters: $0.argumentSchema) - }, "Function schema is not submitted") } } extension ChatGPTStreamTests { struct MockCompletionStreamAPI_Message: CompletionStreamAPI { - var genId: () -> String + @Dependency(\.uuid) var uuid func callAsFunction() async throws -> ( trunkStream: AsyncThrowingStream, cancel: OpenAIService.Cancellable ) { - let id = genId() + let id = uuid().uuidString return ( AsyncThrowingStream { continuation in let trunks: [CompletionStreamDataTrunk] = [ @@ -277,12 +271,12 @@ extension ChatGPTStreamTests { } struct MockCompletionStreamAPI_Function: CompletionStreamAPI { - var genId: () -> String + @Dependency(\.uuid) var uuid func callAsFunction() async throws -> ( trunkStream: AsyncThrowingStream, cancel: OpenAIService.Cancellable ) { - let id = genId() + let id = uuid().uuidString return ( AsyncThrowingStream { continuation in let trunks: [CompletionStreamDataTrunk] = [ diff --git a/Tool/Tests/OpenAIServiceTests/LimitMessagesTests.swift b/Tool/Tests/OpenAIServiceTests/LimitMessagesTests.swift index 24ddb64c..30e4f3a4 100644 --- a/Tool/Tests/OpenAIServiceTests/LimitMessagesTests.swift +++ b/Tool/Tests/OpenAIServiceTests/LimitMessagesTests.swift @@ -6,7 +6,7 @@ import XCTest final class AutoManagedChatGPTMemoryTests: XCTestCase { func test_send_all_messages_if_not_reached_token_limit() async { - let (messages, _, memory) = await runService( + let (messages, memory) = await runService( systemPrompt: "system", messages: [ "hi", "hello", @@ -31,7 +31,7 @@ final class AutoManagedChatGPTMemoryTests: XCTestCase { } func test_send_max_message_if_not_reached_token_limit() async { - let (messages, _, _) = await runService( + let (messages, _) = await runService( systemPrompt: "system", messages: [ "hi", "hello", @@ -49,7 +49,7 @@ final class AutoManagedChatGPTMemoryTests: XCTestCase { } func test_reached_token_limit() async { - let (messages, _, _) = await runService( + let (messages, _) = await runService( systemPrompt: "system", messages: [ "hi", "hello", @@ -65,7 +65,7 @@ final class AutoManagedChatGPTMemoryTests: XCTestCase { } func test_minimum_reply_tokens_count() async { - let (messages, _, _) = await runService( + let (messages, _) = await runService( systemPrompt: "system", messages: [ "hi", "hello", @@ -94,7 +94,7 @@ private func runService( maxTokens: Int, minimumReplyTokens: Int, maxNumberOfMessages: Int -) async -> (messages: [String], remainingTokens: Int?, memory: AutoManagedChatGPTMemory) { +) async -> (messages: [String], memory: AutoManagedChatGPTMemory) { let configuration = UserPreferenceChatGPTConfiguration().overriding(.init( maxTokens: maxTokens, minimumReplyTokens: minimumReplyTokens @@ -113,12 +113,8 @@ private func runService( maxNumberOfMessages: maxNumberOfMessages, encoder: MockEncoder() ) - let remainingTokens = await memory.generateRemainingTokens( - maxNumberOfMessages: maxNumberOfMessages, - encoder: MockEncoder() - ) - let contents = messages.map { $0.content ?? "" } - return (contents, remainingTokens, memory) + let contents = messages.history.map { $0.content ?? "" } + return (contents, memory) } From d117fb1d191c2f581bc266a7a6500a4302d68a5a Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 2 Dec 2023 19:04:44 +0800 Subject: [PATCH 43/74] Fix unit tests --- Pro | 2 +- TestPlan.xctestplan | 7 ++ .../ObjectiveCFocusedCodeFinderTests.swift | 78 ++++++++++--------- .../SwiftFocusedCodeFinderTests.swift | 18 ++--- ...nknownLanguageFocusedCodeFinderTests.swift | 8 +- 5 files changed, 61 insertions(+), 52 deletions(-) diff --git a/Pro b/Pro index d1282a45..eb67d4b7 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit d1282a456a11956942aabdd78cffb59809e3e2a2 +Subproject commit eb67d4b726a8280cb2efdfec7a5b0258c926f51a diff --git a/TestPlan.xctestplan b/TestPlan.xctestplan index 15a5c4cf..293a3428 100644 --- a/TestPlan.xctestplan +++ b/TestPlan.xctestplan @@ -126,6 +126,13 @@ "identifier" : "GitHubCopilotServiceTests", "name" : "GitHubCopilotServiceTests" } + }, + { + "target" : { + "containerPath" : "container:Tool", + "identifier" : "FocusedCodeFinderTests", + "name" : "FocusedCodeFinderTests" + } } ], "version" : 1 diff --git a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift index 0c27085f..c9b55764 100644 --- a/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/ObjectiveCFocusedCodeFinderTests.swift @@ -19,7 +19,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { start: CursorPosition(line: 2, character: 0), end: CursorPosition(line: 2, character: 4) ) - let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( in: document(code: code), containingRange: range ) @@ -36,7 +36,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { range: .init(startPair: (1, 0), endPair: (5, 1)) ), ]), - contextRange: .init(startPair: (0, 0), endPair: (6, 4)), + contextRange: .init(startPair: (0, 0), endPair: (6, 4)), smallestContextRange: range, focusedRange: range, focusedCode: """ @@ -57,7 +57,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { } """ let range = CursorRange(startPair: (2, 0), endPair: (2, 4)) - let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( in: document(code: code), containingRange: range ) @@ -80,7 +80,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { includes: [] )) } - + func test_selecting_a_method_inside_an_implementation_the_scope_should_be_the_implementation() { let code = """ __attribute__((objc_nonlazy_class)) @@ -96,7 +96,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { start: CursorPosition(line: 2, character: 0), end: CursorPosition(line: 6, character: 1) ) - let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( in: document(code: code), containingRange: range ) @@ -117,13 +117,13 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { NSLog(@"Hello"); NSLog(@"World"); } - + """, imports: [], includes: [] )) } - + func test_selecting_a_line_inside_an_interface_the_scope_should_be_the_interface() { let code = """ @interface ViewController >: NSObject @@ -136,7 +136,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { start: CursorPosition(line: 1, character: 0), end: CursorPosition(line: 3, character: 31) ) - let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( in: document(code: code), containingRange: range ) @@ -155,13 +155,13 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { - (void)fooWith:(NSInteger)foo; - (void)fooWith:(NSInteger)foo; - (void)fooWith:(NSInteger)foo; - + """, imports: [], includes: [] )) } - + func test_selecting_a_line_inside_an_interface_category_the_scope_should_be_the_interface() { let code = """ @interface __GENERICS(NSArray, ObjectType) (BlocksKit) @@ -174,7 +174,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { start: CursorPosition(line: 1, character: 0), end: CursorPosition(line: 3, character: 31) ) - let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( in: document(code: code), containingRange: range ) @@ -193,13 +193,13 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { - (void)fooWith:(NSInteger)foo; - (void)fooWith:(NSInteger)foo; - (void)fooWith:(NSInteger)foo; - + """, imports: [], includes: [] )) } - + func test_selecting_a_line_inside_a_protocol_the_scope_should_be_the_protocol() { let code = """ @protocol Foo @@ -212,7 +212,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { start: CursorPosition(line: 1, character: 0), end: CursorPosition(line: 3, character: 31) ) - let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( in: document(code: code), containingRange: range ) @@ -231,13 +231,13 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { - (void)fooWith:(NSInteger)foo; - (void)fooWith:(NSInteger)foo; - (void)fooWith:(NSInteger)foo; - + """, imports: [], includes: [] )) } - + func test_selecting_a_line_inside_a_struct_the_scope_should_be_the_struct() { let code = """ struct Foo { @@ -250,7 +250,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { start: CursorPosition(line: 1, character: 0), end: CursorPosition(line: 3, character: 31) ) - let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( in: document(code: code), containingRange: range ) @@ -269,13 +269,13 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { NSInteger foo; NSInteger bar; NSInteger baz; - + """, imports: [], includes: [] )) } - + func test_selecting_a_line_inside_a_enum_the_scope_should_be_the_enum() { let code = """ enum Foo { @@ -288,7 +288,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { start: CursorPosition(line: 1, character: 0), end: CursorPosition(line: 3, character: 31) ) - let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( in: document(code: code), containingRange: range ) @@ -307,13 +307,13 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { foo, bar, baz - + """, imports: [], includes: [] )) } - + func test_selecting_a_line_inside_an_NSEnum_the_scope_should_be_the_enum() { let code = """ typedef NS_ENUM(NSInteger, Foo) { @@ -326,7 +326,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { start: CursorPosition(line: 1, character: 0), end: CursorPosition(line: 3, character: 31) ) - let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( in: document(code: code), containingRange: range ) @@ -345,7 +345,7 @@ final class ObjectiveCFocusedCodeFinder_Selection_Tests: XCTestCase { foo, bar, baz - + """, imports: [], includes: [] @@ -368,7 +368,7 @@ final class ObjectiveCFocusedCodeFinder_Focus_Tests: XCTestCase { start: CursorPosition(line: 2, character: 0), end: CursorPosition(line: 2, character: 0) ) - let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( in: document(code: code), containingRange: range ) @@ -381,7 +381,7 @@ final class ObjectiveCFocusedCodeFinder_Focus_Tests: XCTestCase { ), ]), contextRange: .init(startPair: (0, 0), endPair: (6, 4)), - smallestContextRange: range, + smallestContextRange: .init(startPair: (1, 0), endPair: (5, 1)), focusedRange: .init(startPair: (1, 0), endPair: (5, 1)), focusedCode: """ - (void)fooWith:(NSInteger)foo { @@ -389,21 +389,22 @@ final class ObjectiveCFocusedCodeFinder_Focus_Tests: XCTestCase { NSLog(@"Hello"); NSLog(@"World"); } - + """, imports: [], includes: [] )) } - - func test_get_focused_code_inside_an_interface_category_the_focused_code_should_be_the_interface() { + + func test_get_focused_code_inside_an_interface_category_the_focused_code_should_be_the_interface( + ) { let code = """ @interface __GENERICS(NSArray, ObjectType) (BlocksKit) - (void)fooWith:(NSInteger)foo; - (void)fooWith:(NSInteger)foo; - (void)fooWith:(NSInteger)foo; @end - + @implementation Foo @end """ @@ -411,14 +412,14 @@ final class ObjectiveCFocusedCodeFinder_Focus_Tests: XCTestCase { start: CursorPosition(line: 1, character: 0), end: CursorPosition(line: 1, character: 0) ) - let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( in: document(code: code), containingRange: range ) XCTAssertEqual(context, .init( scope: .file, contextRange: .init(startPair: (0, 0), endPair: (0, 0)), - smallestContextRange: range, + smallestContextRange: .init(startPair: (0, 0), endPair: (4, 4)), focusedRange: .init(startPair: (0, 0), endPair: (4, 4)), focusedCode: """ @interface __GENERICS(NSArray, ObjectType) (BlocksKit) @@ -426,7 +427,7 @@ final class ObjectiveCFocusedCodeFinder_Focus_Tests: XCTestCase { - (void)fooWith:(NSInteger)foo; - (void)fooWith:(NSInteger)foo; @end - + """, imports: [], includes: [] @@ -442,19 +443,20 @@ final class ObjectiveCFocusedCodeFinder_Imports_Tests: XCTestCase { #import "Foo.h" #include "Bar.h" """ - - let context = ObjectiveCFocusedCodeFinder().findFocusedCode( + + let context = ObjectiveCFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( in: document(code: code), containingRange: .zero ) - + XCTAssertEqual(context.imports, [ "", "UIKit", - "\"Foo.h\"" + "\"Foo.h\"", ]) XCTAssertEqual(context.includes, [ - "\"Bar.h\"" + "\"Bar.h\"", ]) } } + diff --git a/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift index 00f56fbe..5cebe53b 100644 --- a/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/SwiftFocusedCodeFinderTests.swift @@ -29,7 +29,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { start: CursorPosition(line: 4, character: 0), end: CursorPosition(line: 4, character: 13) ) - let context = SwiftFocusedCodeFinder().findFocusedCode( + let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( in: document(code: code), containingRange: range ) @@ -75,7 +75,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { start: CursorPosition(line: 2, character: 0), end: CursorPosition(line: 7, character: 5) ) - let context = SwiftFocusedCodeFinder().findFocusedCode( + let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( in: document(code: code), containingRange: range ) @@ -118,7 +118,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { start: CursorPosition(line: 1, character: 0), end: CursorPosition(line: 1, character: 9) ) - let context = SwiftFocusedCodeFinder().findFocusedCode( + let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( in: document(code: code), containingRange: range ) @@ -156,7 +156,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { start: CursorPosition(line: 1, character: 0), end: CursorPosition(line: 1, character: 9) ) - let context = SwiftFocusedCodeFinder().findFocusedCode( + let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( in: document(code: code), containingRange: range ) @@ -194,7 +194,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { start: CursorPosition(line: 1, character: 0), end: CursorPosition(line: 1, character: 9) ) - let context = SwiftFocusedCodeFinder().findFocusedCode( + let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( in: document(code: code), containingRange: range ) @@ -233,7 +233,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { start: CursorPosition(line: 2, character: 0), end: CursorPosition(line: 2, character: 9) ) - let context = SwiftFocusedCodeFinder().findFocusedCode( + let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( in: document(code: code), containingRange: range ) @@ -273,7 +273,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { start: CursorPosition(line: 3, character: 0), end: CursorPosition(line: 3, character: 9) ) - let context = SwiftFocusedCodeFinder().findFocusedCode( + let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( in: document(code: code), containingRange: range ) @@ -313,7 +313,7 @@ final class SwiftFocusedCodeFinder_Selection_Tests: XCTestCase { start: CursorPosition(line: 2, character: 0), end: CursorPosition(line: 2, character: 9) ) - let context = SwiftFocusedCodeFinder().findFocusedCode( + let context = SwiftFocusedCodeFinder(maxFocusedCodeLineCount: .max).findFocusedCode( in: document(code: code), containingRange: range ) @@ -456,7 +456,7 @@ final class SwiftFocusedCodeFinder_FocusedCode_Tests: XCTestCase { XCTAssertEqual(context, .init( scope: .file, contextRange: .init(startPair: (0, 0), endPair: (0, 0)), - smallestContextRange: .init(startPair: (2, 0), endPair: (4, 11)), + smallestContextRange: .init(startPair: (0, 0), endPair: (8, 1)), focusedRange: .init(startPair: (2, 0), endPair: (4, 11)), focusedCode: """ indirect enum A { diff --git a/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift b/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift index 80febfc1..bdc00be9 100644 --- a/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift +++ b/Tool/Tests/FocusedCodeFinderTests/UnknownLanguageFocusedCodeFinderTests.swift @@ -15,7 +15,7 @@ class UnknownLanguageFocusedCodeFinderTests: XCTestCase { XCTAssertEqual(context, .init( scope: .top, contextRange: .init(startPair: (40, 0), endPair: (60, 3)), - smallestContextRange: .init(startPair: (45, 0), endPair: (55, 3)), + smallestContextRange: .init(startPair: (40, 0), endPair: (60, 3)), focusedRange: .init(startPair: (45, 0), endPair: (55, 3)), focusedCode: stride(from: 45, through: 55, by: 1).map { "\($0)\n" }.joined(), imports: [], @@ -33,7 +33,7 @@ class UnknownLanguageFocusedCodeFinderTests: XCTestCase { XCTAssertEqual(context, .init( scope: .top, contextRange: .init(startPair: (0, 0), endPair: (15, 3)), - smallestContextRange: .init(startPair: (0, 0), endPair: (10, 3)), + smallestContextRange: .init(startPair: (0, 0), endPair: (15, 3)), focusedRange: .init(startPair: (0, 0), endPair: (10, 3)), focusedCode: stride(from: 0, through: 10, by: 1).map { "\($0)\n" }.joined(), imports: [], @@ -51,7 +51,7 @@ class UnknownLanguageFocusedCodeFinderTests: XCTestCase { XCTAssertEqual(context, .init( scope: .top, contextRange: .init(startPair: (89, 0), endPair: (101, 1)), - smallestContextRange: .init(startPair: (94, 0), endPair: (101, 1)), + smallestContextRange: .init(startPair: (89, 0), endPair: (101, 1)), focusedRange: .init(startPair: (94, 0), endPair: (101, 1)), focusedCode: stride(from: 94, through: 100, by: 1).map { "\($0)\n" }.joined() + "\n", imports: [], @@ -69,7 +69,7 @@ class UnknownLanguageFocusedCodeFinderTests: XCTestCase { XCTAssertEqual(context, .init( scope: .top, contextRange: .init(startPair: (0, 0), endPair: (11, 1)), - smallestContextRange: .init(startPair: (0, 0), endPair: (10, 3)), + smallestContextRange: .init(startPair: (0, 0), endPair: (11, 1)), focusedRange: .init(startPair: (0, 0), endPair: (10, 3)), focusedCode: code, imports: [], From 966f654bcaa60e1973447d732b3414ef8d045da8 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 2 Dec 2023 23:47:02 +0800 Subject: [PATCH 44/74] Add SubSection --- .../FeatureSettings/ChatSettingsView.swift | 48 +++----- .../HostApp/SharedComponents/SubSection.swift | 109 ++++++++++++++++++ 2 files changed, 123 insertions(+), 34 deletions(-) create mode 100644 Core/Sources/HostApp/SharedComponents/SubSection.swift diff --git a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift index ab2879a2..5f1fda4a 100644 --- a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift @@ -244,25 +244,27 @@ struct ChatSettingsView: View { @StateObject var settings = Settings() var body: some View { + SettingsDivider("Scopes") + VStack { - Scope( + SubSection( title: Text("File Scope"), description: "Enable the bot to read the metadata of the editing file." ) { Form { Toggle(isOn: $settings.enableFileScopeByDefaultInChatContext) { - Text("Enable @file scope by default in chat context.") + Text("Enable by default") } } } - Scope( + SubSection( title: Text("Code Scope"), description: "Enable the bot to read the code and metadata in the editing file." ) { Form { Toggle(isOn: $settings.enableCodeScopeByDefaultInChatContext) { - Text("Enable @code scope by default in chat context.") + Text("Enable by default") } HStack { @@ -282,8 +284,8 @@ struct ChatSettingsView: View { #if canImport(ProHostApp) - Scope( - title: WithFeatureEnabled(\.projectScopeInChat) { + SubSection( + title: WithFeatureEnabled(\.projectScopeInChat, alignment: .trailing) { Text("Sense Scope (Experimental)") }, description: "Experimental. Enable the bot to read the relevant code of the editing file in the project, third party packages and the SDK." @@ -291,7 +293,7 @@ struct ChatSettingsView: View { WithFeatureEnabled(\.projectScopeInChat, alignment: .hidden) { Form { Toggle(isOn: $settings.enableSenseScopeByDefaultInChatContext) { - Text("Enable @sense scope by default in chat context.") + Text("Enable by default") } Picker( @@ -321,16 +323,16 @@ struct ChatSettingsView: View { } } - Scope( - title: WithFeatureEnabled(\.projectScopeInChat) { + SubSection( + title: WithFeatureEnabled(\.projectScopeInChat, alignment: .trailing) { Text("Project Scope (Experimental)") }, - description: "Experimental. Enable the bot to search code symbols in the project, third party packages and the SDK." + description: "Experimental. Enable the bot to search code symbols in the project, third party packages and the SDK." ) { WithFeatureEnabled(\.projectScopeInChat, alignment: .hidden) { Form { Toggle(isOn: $settings.enableProjectScopeByDefaultInChatContext) { - Text("Enable @project scope by default in chat context.") + Text("Enable by default") } Picker( @@ -362,7 +364,7 @@ struct ChatSettingsView: View { #endif - Scope( + SubSection( title: Text("Web Scope"), description: "Allow the bot to search on Bing or read a web page." ) { @@ -398,28 +400,6 @@ struct ChatSettingsView: View { } } } - - struct Scope: View { - let title: Title - let description: String - let content: () -> Content - - var body: some View { - SettingsDivider(title) - VStack { - Text(description) - .multilineTextAlignment(.center) - .foregroundStyle(.secondary) - .frame(maxWidth: .infinity) - .padding(8) - .background { - RoundedRectangle(cornerRadius: 8) - .fill(Color.secondary.opacity(0.1)) - } - content() - } - } - } } } diff --git a/Core/Sources/HostApp/SharedComponents/SubSection.swift b/Core/Sources/HostApp/SharedComponents/SubSection.swift new file mode 100644 index 00000000..9bb06f3e --- /dev/null +++ b/Core/Sources/HostApp/SharedComponents/SubSection.swift @@ -0,0 +1,109 @@ +import SwiftUI + +struct SubSection: View { + let title: Title + let description: String + @ViewBuilder let content: () -> Content + + init(title: Title, description: String = "", @ViewBuilder content: @escaping () -> Content) { + self.title = title + self.description = description + self.content = content + } + + var body: some View { + VStack(alignment: .leading) { + if !(title is EmptyView && description.isEmpty) { + VStack(alignment: .leading, spacing: 8) { + title + + if !description.isEmpty { + Text(description) + .multilineTextAlignment(.leading) + .foregroundStyle(.secondary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + if !(title is EmptyView && description.isEmpty) { + Divider() + } + + content() + } + .padding() + .background { + RoundedRectangle(cornerRadius: 8) + .fill(Color.secondary.opacity(0.1)) + } + .overlay { + RoundedRectangle(cornerRadius: 8) + .strokeBorder(Color.secondary.opacity(0.2)) + } + } +} + +extension SubSection where Title == EmptyView { + init(description: String = "", @ViewBuilder content: @escaping () -> Content) { + self.init(title: EmptyView(), description: description, content: content) + } +} + +#Preview("Sub Section Default Style") { + SubSection(title: Text("Title"), description: "Description") { + Toggle(isOn: .constant(true), label: { + Text("Label") + }) + + Toggle(isOn: .constant(true), label: { + Text("Label") + }) + + Picker("Label", selection: .constant(0)) { + Text("Label").tag(0) + Text("Label").tag(1) + Text("Label").tag(2) + } + } + .padding() +} + +#Preview("Sub Section No Title") { + SubSection(description: "Description") { + Toggle(isOn: .constant(true), label: { + Text("Label") + }) + + Toggle(isOn: .constant(true), label: { + Text("Label") + }) + + Picker("Label", selection: .constant(0)) { + Text("Label").tag(0) + Text("Label").tag(1) + Text("Label").tag(2) + } + } + .padding() +} + +#Preview("Sub Section No Title or Description") { + SubSection { + Toggle(isOn: .constant(true), label: { + Text("Label") + }) + + Toggle(isOn: .constant(true), label: { + Text("Label") + }) + + Picker("Label", selection: .constant(0)) { + Text("Label").tag(0) + Text("Label").tag(1) + Text("Label").tag(2) + } + } + .padding() +} + From ed339b6d29ea38b69cf1772d0c2151dc852662b1 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 3 Dec 2023 00:05:51 +0800 Subject: [PATCH 45/74] Replace wrappers with SubSection --- .../HostApp/AccountSettings/CodeiumView.swift | 76 ++++++------ .../AccountSettings/GitHubCopilotView.swift | 115 ++++++++---------- .../CustomCommandView.swift | 46 +++---- .../FeatureSettings/ChatSettingsView.swift | 2 +- 4 files changed, 111 insertions(+), 128 deletions(-) diff --git a/Core/Sources/HostApp/AccountSettings/CodeiumView.swift b/Core/Sources/HostApp/AccountSettings/CodeiumView.swift index 37a1fd16..dc483fb5 100644 --- a/Core/Sources/HostApp/AccountSettings/CodeiumView.swift +++ b/Core/Sources/HostApp/AccountSettings/CodeiumView.swift @@ -135,7 +135,7 @@ struct CodeiumView: View { var body: some View { VStack(alignment: .leading) { - VStack(alignment: .leading) { + SubSection(title: Text("Codeium Language Server")) { switch viewModel.installationStatus { case .notInstalled: HStack { @@ -185,12 +185,6 @@ struct CodeiumView: View { } } } - .padding(8) - .frame(maxWidth: .infinity, alignment: .leading) - .overlay { - RoundedRectangle(cornerRadius: 8) - .stroke(Color(nsColor: .separatorColor), style: .init(lineWidth: 1)) - } .sheet(isPresented: $isSignInPanelPresented) { CodeiumSignInView(viewModel: viewModel, isPresented: $isSignInPanelPresented) } @@ -209,16 +203,12 @@ struct CodeiumView: View { } } - Form { - Toggle("Codeium Enterprise Mode", isOn: $viewModel.codeiumEnterpriseMode) - TextField("Codeium Portal URL", text: $viewModel.codeiumPortalUrl) - TextField("Codeium API URL", text: $viewModel.codeiumApiUrl) - } - .padding(8) - .frame(maxWidth: .infinity, alignment: .leading) - .overlay { - RoundedRectangle(cornerRadius: 8) - .stroke(Color(nsColor: .separatorColor), style: .init(lineWidth: 1)) + SubSection(title: Text("Enterprise")) { + Form { + Toggle("Codeium Enterprise Mode", isOn: $viewModel.codeiumEnterpriseMode) + TextField("Codeium Portal URL", text: $viewModel.codeiumPortalUrl) + TextField("Codeium API URL", text: $viewModel.codeiumApiUrl) + } } SettingsDivider("Advanced") @@ -305,32 +295,34 @@ struct CodeiumView_Previews: PreviewProvider { } static var previews: some View { - VStack(alignment: .leading, spacing: 8) { - CodeiumView(viewModel: TestViewModel( - isSignedIn: false, - installationStatus: .notInstalled, - installationStep: nil - )) - - CodeiumView(viewModel: TestViewModel( - isSignedIn: true, - installationStatus: .installed("1.2.9"), - installationStep: nil - )) - - CodeiumView(viewModel: TestViewModel( - isSignedIn: true, - installationStatus: .outdated(current: "1.2.9", latest: "1.3.0"), - installationStep: .downloading - )) - - CodeiumView(viewModel: TestViewModel( - isSignedIn: true, - installationStatus: .unsupported(current: "1.5.9", latest: "1.3.0"), - installationStep: .downloading - )) + ScrollView { + VStack(alignment: .leading, spacing: 8) { + CodeiumView(viewModel: TestViewModel( + isSignedIn: false, + installationStatus: .notInstalled, + installationStep: nil + )) + + CodeiumView(viewModel: TestViewModel( + isSignedIn: true, + installationStatus: .installed("1.2.9"), + installationStep: nil + )) + + CodeiumView(viewModel: TestViewModel( + isSignedIn: true, + installationStatus: .outdated(current: "1.2.9", latest: "1.3.0"), + installationStep: .downloading + )) + + CodeiumView(viewModel: TestViewModel( + isSignedIn: true, + installationStatus: .unsupported(current: "1.5.9", latest: "1.3.0"), + installationStep: .downloading + )) + } + .padding(8) } - .padding(8) } } diff --git a/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift index 1874ddd7..d3292e2b 100644 --- a/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift +++ b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift @@ -142,72 +142,67 @@ struct GitHubCopilotView: View { var body: some View { HStack { VStack(alignment: .leading, spacing: 8) { - Form { - TextField( - text: $settings.nodePath, - prompt: Text( - "node" + SubSection( + title: Text("Node Settings"), + description: """ + You may have to restart the extension app to apply the changes. To do so, simply close the helper app by clicking on the menu bar icon that looks like a tentacle, it will automatically restart as needed. + """ + ) { + Form { + TextField( + text: $settings.nodePath, + prompt: Text( + "node" + ) + ) { + Text("Path to Node (v18+)") + } + + Text( + "Provide the path to the executable if it can't be found by the app, shim executable is not supported" ) - ) { - Text("Path to Node (v18+)") - } - - Text( - "Provide the path to the executable if it can't be found by the app, shim executable is not supported" - ) - .lineLimit(10) - .foregroundColor(.secondary) - .font(.callout) - .dynamicHeightTextInFormWorkaround() - - Picker(selection: $settings.runNodeWith) { - ForEach(NodeRunner.allCases, id: \.rawValue) { runner in - switch runner { + .lineLimit(10) + .foregroundColor(.secondary) + .font(.callout) + .dynamicHeightTextInFormWorkaround() + + Picker(selection: $settings.runNodeWith) { + ForEach(NodeRunner.allCases, id: \.rawValue) { runner in + switch runner { + case .env: + Text("/usr/bin/env").tag(runner) + case .bash: + Text("/bin/bash -i -l").tag(runner) + case .shell: + Text("$SHELL -i -l").tag(runner) + } + } + } label: { + Text("Run Node with") + } + + Group { + switch settings.runNodeWith { case .env: - Text("/usr/bin/env").tag(runner) + Text( + "PATH: `/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`" + ) case .bash: - Text("/bin/bash -i -l").tag(runner) + Text("PATH inherited from bash configurations.") case .shell: - Text("$SHELL -i -l").tag(runner) + Text("PATH inherited from $SHELL configurations.") } } - } label: { - Text("Run Node with") - } - - Group { - switch settings.runNodeWith { - case .env: - Text( - "PATH: `/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`" - ) - case .bash: - Text("PATH inherited from bash configurations.") - case .shell: - Text("PATH inherited from $SHELL configurations.") - } + .lineLimit(10) + .foregroundColor(.secondary) + .font(.callout) + .dynamicHeightTextInFormWorkaround() } - .lineLimit(10) - .foregroundColor(.secondary) - .font(.callout) - .dynamicHeightTextInFormWorkaround() - - Spacer() - - Text(""" - You may have to restart the helper app to apply the changes. To do so, simply close the helper app by clicking on the menu bar icon that looks like a tentacle, it will automatically restart as needed. - """) - .lineLimit(6) - .dynamicHeightTextInFormWorkaround() - .foregroundColor(.secondary) - } - .padding(8) - .overlay { - RoundedRectangle(cornerRadius: 8) - .stroke(Color(nsColor: .separatorColor), style: .init(lineWidth: 1)) } - VStack(alignment: .leading) { + SubSection( + title: Text("GitHub Copilot Language Server") + ) { HStack { switch viewModel.installationStatus { case .none: @@ -263,12 +258,6 @@ struct GitHubCopilotView: View { .opacity(isRunningAction ? 0.8 : 1) .disabled(isRunningAction) } - .padding(8) - .frame(maxWidth: .infinity, alignment: .leading) - .overlay { - RoundedRectangle(cornerRadius: 8) - .stroke(Color(nsColor: .separatorColor), style: .init(lineWidth: 1)) - } SettingsDivider("Advanced") diff --git a/Core/Sources/HostApp/CustomCommandSettings/CustomCommandView.swift b/Core/Sources/HostApp/CustomCommandSettings/CustomCommandView.swift index c7aaea39..66547d6e 100644 --- a/Core/Sources/HostApp/CustomCommandSettings/CustomCommandView.swift +++ b/Core/Sources/HostApp/CustomCommandSettings/CustomCommandView.swift @@ -2,8 +2,8 @@ import ComposableArchitecture import MarkdownUI import PlusFeatureFlag import Preferences -import SwiftUI import SharedUIComponents +import SwiftUI extension List { @ViewBuilder @@ -155,27 +155,29 @@ struct CustomCommandView: View { )) { store in EditCustomCommandView(store: store) } else: { - CustomCommandTypeDescription(text: """ - # Send Message - - This command sends a message to the active chat tab. You can provide additional context through the "Extra System Prompt" as well. - - # Prompt to Code - - This command opens the prompt-to-code panel and executes the provided requirements on the selected code. You can provide additional context through the "Extra Context" as well. - - # Custom Chat - - This command will overwrite the system prompt to let the bot behave differently. - - # Single Round Dialog - - This command allows you to send a message to a temporary chat without opening the chat panel. - - It is particularly useful for one-time commands, such as running a terminal command with `/run`. - - For example, you can set the prompt to `/run open .` to open the project in Finder. - """) + VStack { + SubSection(title: Text("Send Message")) { + Text( + "This command sends a message to the active chat tab. You can provide additional context through the \"Extra System Prompt\" as well." + ) + } + SubSection(title: Text("Prompt to Code")) { + Text( + "This command opens the prompt-to-code panel and executes the provided requirements on the selected code. You can provide additional context through the \"Extra Context\" as well." + ) + } + SubSection(title: Text("Custom Chat")) { + Text( + "This command will overwrite the system prompt to let the bot behave differently." + ) + } + SubSection(title: Text("Single Round Dialog")) { + Text( + "This command allows you to send a message to a temporary chat without opening the chat panel. It is particularly useful for one-time commands, such as running a terminal command with `/run`. For example, you can set the prompt to `/run open .` to open the project in Finder." + ) + } + } + .padding() } } } diff --git a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift index 5f1fda4a..0dde8655 100644 --- a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift @@ -260,7 +260,7 @@ struct ChatSettingsView: View { SubSection( title: Text("Code Scope"), - description: "Enable the bot to read the code and metadata in the editing file." + description: "Enable the bot to read the code and metadata of the editing file." ) { Form { Toggle(isOn: $settings.enableCodeScopeByDefaultInChatContext) { From 3aba060ce6deab6717b79ebf4d8eb1e09edb84d6 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 3 Dec 2023 00:10:02 +0800 Subject: [PATCH 46/74] Update style --- Core/Sources/HostApp/SharedComponents/SubSection.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Core/Sources/HostApp/SharedComponents/SubSection.swift b/Core/Sources/HostApp/SharedComponents/SubSection.swift index 9bb06f3e..cfb58569 100644 --- a/Core/Sources/HostApp/SharedComponents/SubSection.swift +++ b/Core/Sources/HostApp/SharedComponents/SubSection.swift @@ -16,6 +16,7 @@ struct SubSection: View { if !(title is EmptyView && description.isEmpty) { VStack(alignment: .leading, spacing: 8) { title + .font(.system(size: 14).weight(.semibold)) if !description.isEmpty { Text(description) @@ -27,7 +28,7 @@ struct SubSection: View { } if !(title is EmptyView && description.isEmpty) { - Divider() + Divider().padding(.bottom, 4) } content() From b11f49ee70576defa82f2cc7207d4abd0df43299 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 3 Dec 2023 00:15:25 +0800 Subject: [PATCH 47/74] Add scope settings to prompt to code --- .../FeatureSettings/ChatSettingsView.swift | 2 +- .../PromptToCodeSettingsView.swift | 49 +++++++++++++++++-- Tool/Sources/Preferences/Keys.swift | 4 ++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift index 0dde8655..b33770d0 100644 --- a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift @@ -290,7 +290,7 @@ struct ChatSettingsView: View { }, description: "Experimental. Enable the bot to read the relevant code of the editing file in the project, third party packages and the SDK." ) { - WithFeatureEnabled(\.projectScopeInChat, alignment: .hidden) { + WithFeatureEnabled(\.senseScopeInChat, alignment: .hidden) { Form { Toggle(isOn: $settings.enableSenseScopeByDefaultInChatContext) { Text("Enable by default") diff --git a/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift index d06fdf7e..b5ca5e9a 100644 --- a/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift @@ -1,6 +1,11 @@ +import Preferences import SharedUIComponents import SwiftUI +#if canImport(ProHostApp) +import ProHostApp +#endif + struct PromptToCodeSettingsView: View { final class Settings: ObservableObject { @AppStorage(\.hideCommonPrecedingSpacesInSuggestion) @@ -99,13 +104,49 @@ struct PromptToCodeSettingsView: View { Text("pt") }.disabled(true) } + + ScopeForm() } } -} + + struct ScopeForm: View { + class Settings: ObservableObject { + @AppStorage(\.enableSenseScopeByDefaultInPromptToCode) + var enableSenseScopeByDefaultInPromptToCode: Bool + init() {} + } + + @StateObject var settings = Settings() + + var body: some View { + SettingsDivider("Scopes") + + VStack { + #if canImport(ProHostApp) + + SubSection( + title: WithFeatureEnabled(\.senseScopeInChat, alignment: .trailing) { + Text("Sense Scope (Experimental)") + }, + description: "Experimental. Enable the bot to read the relevant code of the editing file in the project, third party packages and the SDK." + ) { + WithFeatureEnabled(\.projectScopeInChat, alignment: .hidden) { + Form { + Toggle(isOn: $settings.enableSenseScopeByDefaultInPromptToCode) { + Text("Enable by default") + } + } + } + } -struct PromptToCodeSettingsView_Previews: PreviewProvider { - static var previews: some View { - PromptToCodeSettingsView() + #endif + } + } } } +#Preview { + PromptToCodeSettingsView() + .padding() +} + diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index c6b276d4..f45946de 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -277,6 +277,10 @@ public extension UserDefaultPreferenceKeys { var promptToCodeEmbeddingModelId: PreferenceKey { .init(defaultValue: "", key: "PromptToCodeEmbeddingModelId") } + + var enableSenseScopeByDefaultInPromptToCode: PreferenceKey { + .init(defaultValue: false, key: "EnableSenseScopeByDefaultInPromptToCode") + } } // MARK: - Suggestion From bc0eb57fcfdf923b60a4406990257ff792aa86a6 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 3 Dec 2023 00:17:57 +0800 Subject: [PATCH 48/74] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index eb67d4b7..071ca53e 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit eb67d4b726a8280cb2efdfec7a5b0258c926f51a +Subproject commit 071ca53ec361e473ce6184744f0664e54d84af91 From 4f1a60b75de4a9501977c7b55402a18cd305adc1 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 3 Dec 2023 00:37:38 +0800 Subject: [PATCH 49/74] Add instructions to chat model settings --- .../ChatModelEditView.swift | 68 +++++++++++++------ 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift index 4a815566..4dd8fb92 100644 --- a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift +++ b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift @@ -63,6 +63,7 @@ struct ChatModelEditView: View { .onAppear { store.send(.appear) } + .fixedSize(horizontal: false, vertical: true) } var nameTextField: some View { @@ -224,6 +225,17 @@ struct ChatModelEditView: View { maxTokensTextField supportsFunctionCallingToggle + + VStack(alignment: .leading, spacing: 8) { + Text(Image(systemName: "exclamationmark.triangle.fill")) + Text( + " To get an API key, please visit [https://platform.openai.com/api-keys](https://platform.openai.com/api-keys)" + ) + + Text(Image(systemName: "exclamationmark.triangle.fill")) + Text( + " If you don't have access to GPT-4, you may need to visit [https://platform.openai.com/account/billing/overview](https://platform.openai.com/account/billing/overview) to buy some credits. A ChatGPT Plus subscription is not enough to access GPT-4 through API." + ) + } + .padding(.vertical) } @ViewBuilder @@ -259,25 +271,43 @@ struct ChatModelEditView: View { } } -class ChatModelManagementView_Editing_Previews: PreviewProvider { - static var previews: some View { - ChatModelEditView( - store: .init( - initialState: .init(model: ChatModel( - id: "3", - name: "Test Model 3", - format: .openAICompatible, - info: .init( - apiKeyName: "key", - baseURL: "apple.com", - maxTokens: 3000, - supportsFunctionCalling: false, - modelName: "gpt-3.5-turbo" - ) - )), - reducer: ChatModelEdit() - ) +#Preview("OpenAI") { + ChatModelEditView( + store: .init( + initialState: .init(model: ChatModel( + id: "3", + name: "Test Model 3", + format: .openAI, + info: .init( + apiKeyName: "key", + baseURL: "apple.com", + maxTokens: 3000, + supportsFunctionCalling: false, + modelName: "gpt-3.5-turbo" + ) + )), + reducer: ChatModelEdit() ) - } + ) +} + +#Preview("OpenAI Compatible") { + ChatModelEditView( + store: .init( + initialState: .init(model: ChatModel( + id: "3", + name: "Test Model 3", + format: .openAICompatible, + info: .init( + apiKeyName: "key", + baseURL: "apple.com", + maxTokens: 3000, + supportsFunctionCalling: false, + modelName: "gpt-3.5-turbo" + ) + )), + reducer: ChatModelEdit() + ) + ) } From c0e8aa0544fcf84c6d3f7fdeef26ccd247cab15f Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 3 Dec 2023 01:06:27 +0800 Subject: [PATCH 50/74] Handle errors from azure openai --- Tool/Sources/OpenAIService/ChatGPTService.swift | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Tool/Sources/OpenAIService/ChatGPTService.swift b/Tool/Sources/OpenAIService/ChatGPTService.swift index 37f230dc..029901e8 100644 --- a/Tool/Sources/OpenAIService/ChatGPTService.swift +++ b/Tool/Sources/OpenAIService/ChatGPTService.swift @@ -41,11 +41,16 @@ public struct ChatGPTError: Error, Codable, LocalizedError { public struct ErrorContent: Codable { public var message: String - public var type: String + public var type: String? public var param: String? public var code: String? - public init(message: String, type: String, param: String? = nil, code: String? = nil) { + public init( + message: String, + type: String? = nil, + param: String? = nil, + code: String? = nil + ) { self.message = message self.type = type self.param = param @@ -80,7 +85,7 @@ public class ChatGPTService: ChatGPTServiceType { self.configuration = configuration self.functionProvider = functionProvider } - + @Dependency(\.uuid) var uuid @Dependency(\.date) var date @@ -279,7 +284,7 @@ extension ChatGPTService { #if DEBUG Debugger.didSendRequestBody(body: requestBody) #endif - + let proposedId = uuid().uuidString + String(date().timeIntervalSince1970) return AsyncThrowingStream { continuation in From 9f528c6015dccf4595cd8775435b5bc4c36e0e65 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 3 Dec 2023 01:13:59 +0800 Subject: [PATCH 51/74] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 071ca53e..9eee25c3 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 071ca53ec361e473ce6184744f0664e54d84af91 +Subproject commit 9eee25c3123b7343e8250245a6c1b6a47eba7e3c From 81051ab298efd6dfa571d1ed9d7722ad48365a6a Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 3 Dec 2023 17:21:55 +0800 Subject: [PATCH 52/74] Add git ignore checker --- Tool/Package.swift | 19 +++++- .../GitIgnoreCheck/GitIgnoreCheck.swift | 60 +++++++++++++++++++ Tool/Sources/Preferences/Keys.swift | 4 ++ 3 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift diff --git a/Tool/Package.swift b/Tool/Package.swift index 05134a03..3c4bbe37 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -42,6 +42,7 @@ let package = Package( "AppActivator", ] ), + .library(name: "GitIgnoreCheck", targets: ["GitIgnoreCheck"]), ], dependencies: [ // A fork of https://github.com/aespinilla/Tiktoken to allow loading from local files. @@ -202,6 +203,7 @@ let package = Package( .target( name: "Workspace", dependencies: [ + "GitIgnoreCheck", "UserDefaultsObserver", "SuggestionModel", "Environment", @@ -235,6 +237,18 @@ let package = Package( dependencies: ["FocusedCodeFinder"] ), + .target( + name: "GitIgnoreCheck", + dependencies: [ + "Terminal", + "Preferences", + .product( + name: "ComposableArchitecture", + package: "swift-composable-architecture" + ), + ] + ), + // MARK: - Services .target( @@ -302,7 +316,7 @@ let package = Package( .product( name: "ComposableArchitecture", package: "swift-composable-architecture" - ) + ), ] ), .testTarget( @@ -312,7 +326,7 @@ let package = Package( .product( name: "ComposableArchitecture", package: "swift-composable-architecture" - ) + ), ] ), @@ -344,6 +358,7 @@ let package = Package( "Preferences", "FocusedCodeFinder", "XcodeInspector", + "GitIgnoreCheck", ], path: "Sources/ChatContextCollectors/ActiveDocumentChatContextCollector" ), diff --git a/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift b/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift new file mode 100644 index 00000000..70eb0ebe --- /dev/null +++ b/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift @@ -0,0 +1,60 @@ +import Dependencies +import Foundation +import Terminal +import Preferences + +public struct CheckIfGitIgnoredDependencyKey: DependencyKey { + public static var liveValue: GitIgnoredChecker = DefaultGitIgnoredChecker() +} + +public extension DependencyValues { + var gitIgnoredChecker: GitIgnoredChecker { + get { self[CheckIfGitIgnoredDependencyKey.self] } + set { self[CheckIfGitIgnoredDependencyKey.self] = newValue } + } +} + +public protocol GitIgnoredChecker { + func checkIfGitIgnored(fileURL: URL) async -> Bool +} + +extension GitIgnoredChecker { + func checkIfGitIgnored(filePath: String) async -> Bool { + await checkIfGitIgnored(fileURL: URL(fileURLWithPath: filePath)) + } +} + +struct DefaultGitIgnoredChecker: GitIgnoredChecker { + func checkIfGitIgnored(fileURL: URL) async -> Bool { + if UserDefaults.shared.value(for: \.disableGitIgnoreCheck) { return false } + let terminal = Terminal() + guard let gitFolderURL = gitFolderURL(forFileURL: fileURL) else { + return false + } + do { + _ = try await terminal.runCommand( + "/bin/bash", + arguments: ["-c", "check-ignore \"filePath\""], + currentDirectoryPath: gitFolderURL.path, + environment: [:] + ) + return true + } catch { + return false + } + } +} + +func gitFolderURL(forFileURL fileURL: URL) -> URL? { + var currentURL = fileURL + let fileManager = FileManager.default + while currentURL.path != "/" { + let gitFolderURL = currentURL.appendingPathComponent(".git") + if fileManager.fileExists(atPath: gitFolderURL.path) { + return gitFolderURL + } + currentURL = currentURL.deletingLastPathComponent() + } + return nil +} + diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index f45946de..cae7c756 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -546,6 +546,10 @@ public extension UserDefaultPreferenceKeys { key: "FeatureFlag-DisableGitHubCopilotSettingsAutoRefreshOnAppear" ) } + + var disableGitIgnoreCheck: FeatureFlag { + .init(defaultValue: false, key: "FeatureFlag-DisableGitIgnoreCheck") + } var disableEnhancedWorkspace: FeatureFlag { .init( From a916e142e653bcf21266a321b8d6a1892ec31d6f Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 3 Dec 2023 17:22:32 +0800 Subject: [PATCH 53/74] Update to not generate suggestion if a file is git ignored --- Core/Sources/HostApp/DebugView.swift | 5 +++ Tool/Sources/Workspace/Filespace.swift | 39 ++++++++++++++++++- .../Workspace+SuggestionService.swift | 2 + 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/Core/Sources/HostApp/DebugView.swift b/Core/Sources/HostApp/DebugView.swift index b94d8ae1..c7e0ad39 100644 --- a/Core/Sources/HostApp/DebugView.swift +++ b/Core/Sources/HostApp/DebugView.swift @@ -16,6 +16,7 @@ final class DebugSettings: ObservableObject { var disableGitHubCopilotSettingsAutoRefreshOnAppear @AppStorage(\.useUserDefaultsBaseAPIKeychain) var useUserDefaultsBaseAPIKeychain @AppStorage(\.disableEnhancedWorkspace) var disableEnhancedWorkspace + @AppStorage(\.disableGitIgnoreCheck) var disableGitIgnoreCheck init() {} } @@ -64,6 +65,10 @@ struct DebugSettingsView: View { Text("Disable Enhanced Workspace") } + Toggle(isOn: $settings.disableGitIgnoreCheck) { + Text("Disable Git Ignore Check") + } + Button("Reset Migration Version to 0") { UserDefaults.shared.set(nil, forKey: "OldMigrationVersion") } diff --git a/Tool/Sources/Workspace/Filespace.swift b/Tool/Sources/Workspace/Filespace.swift index 2683ce13..30553332 100644 --- a/Tool/Sources/Workspace/Filespace.swift +++ b/Tool/Sources/Workspace/Filespace.swift @@ -1,5 +1,7 @@ +import Dependencies import Environment import Foundation +import GitIgnoreCheck import SuggestionModel public protocol FilespacePropertyKey { @@ -47,20 +49,34 @@ public struct FilespaceCodeMetadata: Equatable { @dynamicMemberLookup public final class Filespace { + struct GitIgnoreStatus { + var isIgnored: Bool + var checkTime: Date + var isExpired: Bool { + Environment.now().timeIntervalSince(checkTime) > 60 * 3 + } + } + + // MARK: Metadata + public let fileURL: URL public private(set) lazy var language: CodeLanguage = languageIdentifierFromFileURL(fileURL) public var codeMetadata: FilespaceCodeMetadata = .init() + + // MARK: Suggestions + + public private(set) var suggestionIndex: Int = 0 public internal(set) var suggestions: [CodeSuggestion] = [] { didSet { refreshUpdateTime() } } - public private(set) var suggestionIndex: Int = 0 - public var presentingSuggestion: CodeSuggestion? { guard suggestions.endIndex > suggestionIndex, suggestionIndex >= 0 else { return nil } return suggestions[suggestionIndex] } + // MARK: Life Cycle + public var isExpired: Bool { Environment.now().timeIntervalSince(lastSuggestionUpdateTime) > 60 * 3 } @@ -70,6 +86,25 @@ public final class Filespace { let fileSaveWatcher: FileSaveWatcher let onClose: (URL) -> Void + // MARK: Git Ignore + + private var gitIgnoreStatus: GitIgnoreStatus? + public var isGitIgnored: Bool { + get async { + @Dependency(\.gitIgnoredChecker) var gitIgnoredChecker + @Dependency(\.date) var date + + if let gitIgnoreStatus = gitIgnoreStatus, !gitIgnoreStatus.isExpired { + return gitIgnoreStatus.isIgnored + } + let isIgnored = await gitIgnoredChecker.checkIfGitIgnored(fileURL: fileURL) + gitIgnoreStatus = .init(isIgnored: isIgnored, checkTime: date()) + return isIgnored + } + } + + // MARK: Methods + deinit { onClose(fileURL) } diff --git a/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift b/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift index 2aa15bf8..d7f5deba 100644 --- a/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift +++ b/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift @@ -34,6 +34,8 @@ public extension Workspace { refreshUpdateTime() let filespace = createFilespaceIfNeeded(fileURL: fileURL) + + guard await filespace.isGitIgnored else { return [] } if !editor.uti.isEmpty { filespace.codeMetadata.uti = editor.uti From 6c4fc823a167ffbc55f2293a009d039ed712b5b2 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 3 Dec 2023 17:23:23 +0800 Subject: [PATCH 54/74] Update ActiveDocumentChatContextCollector to only include selected code when the file is gitignored --- .../ActiveDocumentChatContextCollector.swift | 82 +++++++++++-------- 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift index 2495ceee..40073154 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift @@ -1,7 +1,9 @@ import ASTParser import ChatContextCollector +import Dependencies import FocusedCodeFinder import Foundation +import GitIgnoreCheck import OpenAIService import Preferences import SuggestionModel @@ -12,22 +14,27 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { public var activeDocumentContext: ActiveDocumentContext? + @Dependency(\.gitIgnoredChecker) var gitIgnoredChecker + public func generateContext( history: [ChatMessage], scopes: Set, content: String, configuration: ChatGPTConfiguration - ) -> ChatContext { + ) async -> ChatContext { guard let info = getEditorInformation() else { return .empty } let context = getActiveDocumentContext(info) activeDocumentContext = context - guard scopes.contains(.code) else { + let isSensitive = await gitIgnoredChecker.checkIfGitIgnored(fileURL: info.documentURL) + + guard scopes.contains(.code) + else { if scopes.contains(.file) { var removedCode = context removedCode.focusedContext = nil return .init( - systemPrompt: extractSystemPrompt(removedCode), + systemPrompt: extractSystemPrompt(removedCode, isSensitive: isSensitive), retrievedContent: [], functions: [] ) @@ -37,36 +44,39 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { var functions = [any ChatGPTFunction]() - // When the bot is already focusing on a piece of code, it can expand the range. + if !isSensitive { + // When the bot is already focusing on a piece of code, it can expand the range. - if context.focusedContext != nil { - functions.append(ExpandFocusRangeFunction(contextCollector: self)) - } + if context.focusedContext != nil { + functions.append(ExpandFocusRangeFunction(contextCollector: self)) + } - // When the bot is not focusing on any code, or the focusing area is not the user's - // selection, it can move the focus back to the user's selection. + // When the bot is not focusing on any code, or the focusing area is not the user's + // selection, it can move the focus back to the user's selection. - if context.focusedContext == nil || - !(context.focusedContext?.codeRange.contains(context.selectionRange) ?? false) - { - functions.append(MoveToFocusedCodeFunction(contextCollector: self)) - } + if context.focusedContext == nil || + !(context.focusedContext?.codeRange.contains(context.selectionRange) ?? false) + { + functions.append(MoveToFocusedCodeFunction(contextCollector: self)) + } - // When there is a line annotation not in the focused area, the bot can move the focus area - // to the code covering the line of the annotation. + // When there is a line annotation not in the focused area, the bot can move the focus + // area + // to the code covering the line of the annotation. - if let focusedContext = context.focusedContext, - !focusedContext.otherLineAnnotations.isEmpty - { - functions.append(MoveToCodeAroundLineFunction(contextCollector: self)) - } + if let focusedContext = context.focusedContext, + !focusedContext.otherLineAnnotations.isEmpty + { + functions.append(MoveToCodeAroundLineFunction(contextCollector: self)) + } - if context.focusedContext == nil, !context.lineAnnotations.isEmpty { - functions.append(MoveToCodeAroundLineFunction(contextCollector: self)) + if context.focusedContext == nil, !context.lineAnnotations.isEmpty { + functions.append(MoveToCodeAroundLineFunction(contextCollector: self)) + } } return .init( - systemPrompt: extractSystemPrompt(context), + systemPrompt: extractSystemPrompt(context, isSensitive: isSensitive), retrievedContent: [], functions: functions ) @@ -90,7 +100,7 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { return activeDocumentContext } - func extractSystemPrompt(_ context: ActiveDocumentContext) -> String { + func extractSystemPrompt(_ context: ActiveDocumentContext, isSensitive: Bool) -> String { let start = """ ## File and Code Scope @@ -108,7 +118,7 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { let language = "Language: \(context.language.rawValue)" if let focusedContext = context.focusedContext { - let codeContext = focusedContext.context.isEmpty + let codeContext = focusedContext.context.isEmpty || isSensitive ? "" : """ Focused Context: @@ -119,14 +129,16 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { let codeRange = "Focused Range [line, character]: \(focusedContext.codeRange)" - let code = """ - Focused Code (start from line \(focusedContext.codeRange.start.line + 1)): - ```\(context.language.rawValue) - \(focusedContext.code) - ``` - """ + let code = context.selectionRange.isEmpty && isSensitive + ? "" + : """ + Focused Code (start from line \(focusedContext.codeRange.start.line + 1)): + ```\(context.language.rawValue) + \(focusedContext.code) + ``` + """ - let fileAnnotations = focusedContext.otherLineAnnotations.isEmpty + let fileAnnotations = focusedContext.otherLineAnnotations.isEmpty || isSensitive ? "" : """ Other Annotations:\""" @@ -139,7 +151,7 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { \""" """ - let codeAnnotations = focusedContext.lineAnnotations.isEmpty + let codeAnnotations = focusedContext.lineAnnotations.isEmpty || isSensitive ? "" : """ Annotations Inside Focused Range:\""" @@ -165,7 +177,7 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { .joined(separator: "\n\n") } else { let selectionRange = "Selection Range [line, character]: \(context.selectionRange)" - let lineAnnotations = context.lineAnnotations.isEmpty + let lineAnnotations = context.lineAnnotations.isEmpty || isSensitive ? "" : """ Line Annotations:\""" From f6ee5b70a9d6447d9cb1a431a4025fe9d366edfd Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 3 Dec 2023 21:39:04 +0800 Subject: [PATCH 55/74] Update --- .../GitIgnoreCheck/GitIgnoreCheck.swift | 56 +++++++++++++++++-- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift b/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift index 70eb0ebe..d5bab231 100644 --- a/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift +++ b/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift @@ -5,6 +5,7 @@ import Preferences public struct CheckIfGitIgnoredDependencyKey: DependencyKey { public static var liveValue: GitIgnoredChecker = DefaultGitIgnoredChecker() + public static var testValue: GitIgnoredChecker = DefaultGitIgnoredChecker(isTest: true) } public extension DependencyValues { @@ -16,17 +17,36 @@ public extension DependencyValues { public protocol GitIgnoredChecker { func checkIfGitIgnored(fileURL: URL) async -> Bool + func checkIfGitIgnored(fileURLs: [URL]) async -> [URL] } -extension GitIgnoredChecker { +public extension GitIgnoredChecker { func checkIfGitIgnored(filePath: String) async -> Bool { await checkIfGitIgnored(fileURL: URL(fileURLWithPath: filePath)) } + + func checkIfGitIgnored(filePaths: [String]) async -> [String] { + await checkIfGitIgnored(fileURLs: filePaths.map { URL(fileURLWithPath: $0) }) + .map(\.path) + } } -struct DefaultGitIgnoredChecker: GitIgnoredChecker { - func checkIfGitIgnored(fileURL: URL) async -> Bool { - if UserDefaults.shared.value(for: \.disableGitIgnoreCheck) { return false } +public struct DefaultGitIgnoredChecker: GitIgnoredChecker { + var isTest = false + + var noCheck: Bool { + if isTest { return false } + return UserDefaults.shared.value(for: \.disableGitIgnoreCheck) + } + + public init() {} + + init(isTest: Bool) { + self.isTest = isTest + } + + public func checkIfGitIgnored(fileURL: URL) async -> Bool { + if noCheck { return false } let terminal = Terminal() guard let gitFolderURL = gitFolderURL(forFileURL: fileURL) else { return false @@ -34,7 +54,7 @@ struct DefaultGitIgnoredChecker: GitIgnoredChecker { do { _ = try await terminal.runCommand( "/bin/bash", - arguments: ["-c", "check-ignore \"filePath\""], + arguments: ["-c", "check-ignore \"\(fileURL.path)\""], currentDirectoryPath: gitFolderURL.path, environment: [:] ) @@ -43,6 +63,30 @@ struct DefaultGitIgnoredChecker: GitIgnoredChecker { return false } } + + public func checkIfGitIgnored(fileURLs: [URL]) async -> [URL] { + if noCheck { return [] } + let filePaths = fileURLs.map { "\"\($0.path)\"" }.joined(separator: " ") + guard let firstFileURL = fileURLs.first else { return [] } + let terminal = Terminal() + guard let gitFolderURL = gitFolderURL(forFileURL: firstFileURL) else { + return [] + } + do { + let result = try await terminal.runCommand( + "/bin/bash", + arguments: ["-c", "check-ignore \(filePaths)"], + currentDirectoryPath: gitFolderURL.path, + environment: [:] + ) + return result + .split(separator: "\n") + .map(String.init) + .compactMap(URL.init(fileURLWithPath:)) + } catch { + return [] + } + } } func gitFolderURL(forFileURL fileURL: URL) -> URL? { @@ -51,7 +95,7 @@ func gitFolderURL(forFileURL fileURL: URL) -> URL? { while currentURL.path != "/" { let gitFolderURL = currentURL.appendingPathComponent(".git") if fileManager.fileExists(atPath: gitFolderURL.path) { - return gitFolderURL + return currentURL } currentURL = currentURL.deletingLastPathComponent() } From e9dfeb17341a6d9e148765565388cbd233450805 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 3 Dec 2023 21:39:26 +0800 Subject: [PATCH 56/74] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 9eee25c3..33771b25 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 9eee25c3123b7343e8250245a6c1b6a47eba7e3c +Subproject commit 33771b25accde2eac7fa5c7229ffc57523c9c7de From a00c478a609f66a327e589ae283629d6ff7f992e Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 3 Dec 2023 22:33:59 +0800 Subject: [PATCH 57/74] Fix gitignore check --- Pro | 2 +- .../ActiveDocumentChatContextCollector.swift | 5 ++++- Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift | 4 ++-- .../Workspace+SuggestionService.swift | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Pro b/Pro index 33771b25..38003883 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 33771b25accde2eac7fa5c7229ffc57523c9c7de +Subproject commit 3800388381465dfff20dab3d8c4365faf7aaa545 diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift index 40073154..9ac109bf 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift @@ -130,7 +130,10 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { let codeRange = "Focused Range [line, character]: \(focusedContext.codeRange)" let code = context.selectionRange.isEmpty && isSensitive - ? "" + ? """ + The file is in gitignore, you can't read the file. + Ask the user to select the code in the editor to get help. Also tell them the file is in gitignore. + """ : """ Focused Code (start from line \(focusedContext.codeRange.start.line + 1)): ```\(context.language.rawValue) diff --git a/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift b/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift index d5bab231..f573044d 100644 --- a/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift +++ b/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift @@ -54,7 +54,7 @@ public struct DefaultGitIgnoredChecker: GitIgnoredChecker { do { _ = try await terminal.runCommand( "/bin/bash", - arguments: ["-c", "check-ignore \"\(fileURL.path)\""], + arguments: ["-i", "-c", "git check-ignore \"\(fileURL.path)\""], currentDirectoryPath: gitFolderURL.path, environment: [:] ) @@ -75,7 +75,7 @@ public struct DefaultGitIgnoredChecker: GitIgnoredChecker { do { let result = try await terminal.runCommand( "/bin/bash", - arguments: ["-c", "check-ignore \(filePaths)"], + arguments: ["-i", "-c", "git check-ignore \(filePaths)"], currentDirectoryPath: gitFolderURL.path, environment: [:] ) diff --git a/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift b/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift index d7f5deba..54898736 100644 --- a/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift +++ b/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift @@ -35,7 +35,7 @@ public extension Workspace { let filespace = createFilespaceIfNeeded(fileURL: fileURL) - guard await filespace.isGitIgnored else { return [] } + guard !(await filespace.isGitIgnored) else { throw SuggestionFeatureDisabledError() } if !editor.uti.isEmpty { filespace.codeMetadata.uti = editor.uti From e43902a2573ce9a6f93a42172713a8163906bea5 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 3 Dec 2023 22:45:45 +0800 Subject: [PATCH 58/74] Limit to 15 references --- Core/Sources/ChatService/DynamicContextController.swift | 1 + Pro | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Core/Sources/ChatService/DynamicContextController.swift b/Core/Sources/ChatService/DynamicContextController.swift index 45c0fe19..4668649a 100644 --- a/Core/Sources/ChatService/DynamicContextController.swift +++ b/Core/Sources/ChatService/DynamicContextController.swift @@ -101,6 +101,7 @@ final class DynamicContextController { .flatMap(\.retrievedContent) .filter { !$0.document.content.isEmpty } .sorted { $0.priority > $1.priority } + .prefix(15) let contextualSystemPrompt = """ \(language.isEmpty ? "" : "You must always reply in \(language)") diff --git a/Pro b/Pro index 38003883..991a9216 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 3800388381465dfff20dab3d8c4365faf7aaa545 +Subproject commit 991a9216de55642622ff9ab0f26c36b0053118f1 From 28ac89cec4ae31be9699966cc1abccbe2e7337d4 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 3 Dec 2023 22:45:54 +0800 Subject: [PATCH 59/74] Limit references token usage --- .../OpenAIService/Memory/AutoManagedChatGPTMemory.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift index 43e09d90..61e516e7 100644 --- a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift +++ b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift @@ -30,7 +30,7 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { static let encoder: TokenEncoder = TiktokenCl100kBaseTokenEncoder() var onHistoryChange: () -> Void = {} - + let composeHistory: HistoryComposer public init( @@ -251,6 +251,9 @@ extension AutoManagedChatGPTMemory { usage: Int, references: [ChatMessage.Reference] ) { + /// the available tokens count for retrieved content + let thresholdMaxTokenCount = min(maxTokenCount, configuration.maxTokens / 2) + var retrievedContentTokenCount = 0 let separator = String(repeating: "=", count: 32) // only 1 token var message = "" @@ -258,7 +261,7 @@ extension AutoManagedChatGPTMemory { func appendToMessage(_ text: String) -> Bool { let tokensCount = encoder.countToken(text: text) - if tokensCount + retrievedContentTokenCount > maxTokenCount { return false } + if tokensCount + retrievedContentTokenCount > thresholdMaxTokenCount { return false } retrievedContentTokenCount += tokensCount message += text return true From de6fa08eae18c8f6dedfa4d8c10a118c391b4c3a Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 4 Dec 2023 13:53:58 +0800 Subject: [PATCH 60/74] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 991a9216..9408c503 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 991a9216de55642622ff9ab0f26c36b0053118f1 +Subproject commit 9408c503c88d07258430a7f876eff00644c15a40 From c1404c42dbe1d51c19fd2b71b7dc14deda66c041 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 4 Dec 2023 13:54:38 +0800 Subject: [PATCH 61/74] Bump version to 0.28.0 --- Version.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Version.xcconfig b/Version.xcconfig index 0a2b0d10..22c9a5ae 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,3 +1,3 @@ -APP_VERSION = 0.27.1 -APP_BUILD = 281 +APP_VERSION = 0.28.0 +APP_BUILD = 290 From 0b54d7c044cfb3a039be3f8c19606c5d3e7546ff Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 4 Dec 2023 14:04:44 +0800 Subject: [PATCH 62/74] Update README.md --- Pro | 2 +- README.md | 27 +++++++++++++++++++-------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Pro b/Pro index 9408c503..b78bc3f3 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 9408c503c88d07258430a7f876eff00644c15a40 +Subproject commit b78bc3f36dd1db0f3f746a2d3ebf6ba970068ed8 diff --git a/README.md b/README.md index c7a32022..afc080b2 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ For chat and prompt to code features: > The installation process is a bit complicated. Here is a quick overview: > -> 1. Install the app into the Applications folder. +> 1. Install the app into the Applications folder, open it once. > 2. Enable the source editor extension. > 3. Grant Accessibility API permission to the extension app. > 4. Setup accounts and models in the host app. @@ -180,13 +180,7 @@ You can also set it to quit automatically when the above 2 apps are closed. ## Update -If the app was installed via Homebrew, you can update it by running: - -```bash -brew upgrade --cask copilot-for-xcode -``` - -Alternatively, You can use the in-app updater or download the latest version manually from the latest [release](https://github.com/intitni/CopilotForXcode/releases). +You can use the in-app updater or download the latest version manually from the latest [release](https://github.com/intitni/CopilotForXcode/releases). After updating, please open Copilot for Xcode.app once and restart Xcode to allow the extension to reload. @@ -194,6 +188,8 @@ If you find that some of the features are no longer working, please first try re ## Feature +> Files in gitignore will not receive suggestion. Both chat and prompt to code feature will not have access to them unless you manually select code from them. + ### Suggestion The app can provide real-time code suggestions based on the files you have opened. It's powered by GitHub Copilot and Codeium. @@ -310,6 +306,18 @@ This feature is recommended when you need to update a specific piece of code. So - Polishing and correcting grammar and spelling errors in the documentation. - Translating a localizable strings file. +#### Prompt to Code Scope + +The chat panel allows for chat scope to temporarily control the context of the conversation for the latest message. To use a scope, simply prefix the message with `@scope`. + +| Scope | Description | +| :--------: | ---------------------------------------------------------------------------------------- | +| `@sense` | Experimental. Read the relevant information of the focused code | + +To use scopes, you can prefix a message with `@sense`. + +You can use shorthand to represent a scope, such as `@sense`, and enable multiple scopes with `@c+web`. + #### Commands - Prompt to Code: Open a prompt to code window, where you can use natural language to write or edit selected code. @@ -342,6 +350,9 @@ These features are included in another repo, and are not open sourced. The currently available Plus features include: +- `@project` scope in chat. +- Suggestion Cheatsheet that provides relevant content to the suggestion service. +- `@sense` scope in chat and prompt to code. - Terminal tab in chat panel. - Unlimited chat/embedding models. - Tab to accept suggestions. From 66a349421e73e307bb4935b30c01d3264710160c Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 4 Dec 2023 14:31:06 +0800 Subject: [PATCH 63/74] Use fallback solution when it can't find xcode --- Core/Sources/HostApp/DebugView.swift | 8 ++++---- Tool/Sources/Environment/Environment.swift | 20 +++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Core/Sources/HostApp/DebugView.swift b/Core/Sources/HostApp/DebugView.swift index c7e0ad39..5d93f5e5 100644 --- a/Core/Sources/HostApp/DebugView.swift +++ b/Core/Sources/HostApp/DebugView.swift @@ -42,7 +42,7 @@ struct DebugSettingsView: View { Text("Use custom scroll view workaround for smooth scrolling") } Toggle(isOn: $settings.triggerActionWithAccessibilityAPI) { - Text("Trigger command with AccessibilityAPI") + Text("Trigger command with Accessibility API") } Group { Toggle(isOn: $settings.alwaysAcceptSuggestionWithAccessibilityAPI) { @@ -62,14 +62,14 @@ struct DebugSettingsView: View { } Toggle(isOn: $settings.disableEnhancedWorkspace) { - Text("Disable Enhanced Workspace") + Text("Disable enhanced workspace") } Toggle(isOn: $settings.disableGitIgnoreCheck) { - Text("Disable Git Ignore Check") + Text("Disable git ignore check") } - Button("Reset Migration Version to 0") { + Button("Reset migration version to 0") { UserDefaults.shared.set(nil, forKey: "OldMigrationVersion") } diff --git a/Tool/Sources/Environment/Environment.swift b/Tool/Sources/Environment/Environment.swift index 5beae16d..445159de 100644 --- a/Tool/Sources/Environment/Environment.swift +++ b/Tool/Sources/Environment/Environment.swift @@ -4,6 +4,7 @@ import AXExtension import Foundation import Logger import Preferences +import XcodeInspector public struct NoAccessToAccessibilityAPIError: Error, LocalizedError { public var errorDescription: String? { @@ -149,9 +150,16 @@ public enum Environment { } public static var triggerAction: (_ name: String) async throws -> Void = { name in - guard let activeXcode = ActiveApplicationMonitor.shared.activeXcode - ?? ActiveApplicationMonitor.shared.latestXcode - else { return } + struct CantRunCommand: Error, LocalizedError { + let name: String + var errorDescription: String? { + "Can't run command \(name)." + } + } + + guard let activeXcode = XcodeInspector.shared.latestActiveXcode?.runningApplication + else { throw CantRunCommand(name: name) } + let bundleName = Bundle.main .object(forInfoDictionaryKey: "EXTENSION_BUNDLE_NAME") as! String @@ -186,12 +194,6 @@ public enum Environment { return } } - struct CantRunCommand: Error, LocalizedError { - let name: String - var errorDescription: String? { - "Can't run command \(name)." - } - } throw CantRunCommand(name: name) } else { From ed2a203a9bb5b09e1476bf6927ea820ac04ef558 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 4 Dec 2023 15:30:08 +0800 Subject: [PATCH 64/74] Remove a fatalError --- Tool/Sources/OpenAIService/CompletionAPI.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tool/Sources/OpenAIService/CompletionAPI.swift b/Tool/Sources/OpenAIService/CompletionAPI.swift index 479c57bb..116fb623 100644 --- a/Tool/Sources/OpenAIService/CompletionAPI.swift +++ b/Tool/Sources/OpenAIService/CompletionAPI.swift @@ -112,7 +112,7 @@ struct OpenAICompletionAPI: CompletionAPI { return try JSONDecoder().decode(CompletionResponseBody.self, from: result) } catch { dump(error) - fatalError() + throw error } } } From c452faec260b33b18dce33fc11f099ed0fefdf54 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 4 Dec 2023 15:30:37 +0800 Subject: [PATCH 65/74] Update --- Pro | 2 +- Tool/Package.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Pro b/Pro index b78bc3f3..af0519d1 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit b78bc3f36dd1db0f3f746a2d3ebf6ba970068ed8 +Subproject commit af0519d184db95510a55fdcee90a6c3c656d12f0 diff --git a/Tool/Package.swift b/Tool/Package.swift index 3c4bbe37..13177622 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -105,6 +105,7 @@ let package = Package( name: "Environment", dependencies: [ "ActiveApplicationMonitor", + "XcodeInspector", "AXExtension", "Preferences", ] @@ -173,7 +174,6 @@ let package = Package( dependencies: [ "AXExtension", "SuggestionModel", - "Environment", "AXNotificationStream", "Logger", .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), From 2312248f3fb9c639dd2f5ad70276077ae785c1a6 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 4 Dec 2023 16:07:52 +0800 Subject: [PATCH 66/74] Adjust UI --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index af0519d1..465473e7 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit af0519d184db95510a55fdcee90a6c3c656d12f0 +Subproject commit 465473e79a243e8a2b76c0c953d89ada888b5abe From 8ccb85f1c851bb13ad896ac0864d8498848b690c Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 4 Dec 2023 16:08:02 +0800 Subject: [PATCH 67/74] Update appcast.xml --- appcast.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/appcast.xml b/appcast.xml index b9eea600..1ddf6f97 100644 --- a/appcast.xml +++ b/appcast.xml @@ -3,6 +3,18 @@ Copilot for Xcode + + 0.28.0 + Mon, 04 Dec 2023 16:04:23 +0800 + 290 + 0.28.0 + 12.0 + + https://github.com/intitni/CopilotForXcode/releases/tag/0.28.0 + + + + 0.27.1 Sat, 18 Nov 2023 12:46:36 +0800 From 143c966ea5097b9908adc6b67e9d756f40e357e0 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 4 Dec 2023 22:32:06 +0800 Subject: [PATCH 68/74] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 465473e7..a33512ff 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 465473e79a243e8a2b76c0c953d89ada888b5abe +Subproject commit a33512ffb46a72b4fee6d89d397560c28f536e79 From ea61843da6665062cf34e66c144ae8ebbc8eac26 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 4 Dec 2023 22:32:53 +0800 Subject: [PATCH 69/74] Disable git ignore check by default --- Pro | 2 +- Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift | 2 +- Tool/Sources/Preferences/Keys.swift | 2 +- .../Workspace+SuggestionService.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Pro b/Pro index 465473e7..a33512ff 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 465473e79a243e8a2b76c0c953d89ada888b5abe +Subproject commit a33512ffb46a72b4fee6d89d397560c28f536e79 diff --git a/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift b/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift index f573044d..9bfc7ef2 100644 --- a/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift +++ b/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift @@ -35,7 +35,7 @@ public struct DefaultGitIgnoredChecker: GitIgnoredChecker { var isTest = false var noCheck: Bool { - if isTest { return false } + if isTest { return true } return UserDefaults.shared.value(for: \.disableGitIgnoreCheck) } diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index cae7c756..91a3dbd1 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -548,7 +548,7 @@ public extension UserDefaultPreferenceKeys { } var disableGitIgnoreCheck: FeatureFlag { - .init(defaultValue: false, key: "FeatureFlag-DisableGitIgnoreCheck") + .init(defaultValue: true, key: "FeatureFlag-DisableGitIgnoreCheck") } var disableEnhancedWorkspace: FeatureFlag { diff --git a/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift b/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift index 54898736..a37ef931 100644 --- a/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift +++ b/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift @@ -35,7 +35,7 @@ public extension Workspace { let filespace = createFilespaceIfNeeded(fileURL: fileURL) - guard !(await filespace.isGitIgnored) else { throw SuggestionFeatureDisabledError() } + guard !(await filespace.isGitIgnored) else { return [] } if !editor.uti.isEmpty { filespace.codeMetadata.uti = editor.uti From c5dd90e32687966f54e0188513de803c37aad73d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 4 Dec 2023 22:33:04 +0800 Subject: [PATCH 70/74] Bump version to 0.28.1 --- Version.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Version.xcconfig b/Version.xcconfig index 22c9a5ae..b17aaf5f 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,3 +1,3 @@ -APP_VERSION = 0.28.0 -APP_BUILD = 290 +APP_VERSION = 0.28.1 +APP_BUILD = 291 From de2b38241b7da315f53e8cf14fdef7b346a48c89 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 4 Dec 2023 22:38:11 +0800 Subject: [PATCH 71/74] Update appcast.xml --- appcast.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/appcast.xml b/appcast.xml index 1ddf6f97..d885107c 100644 --- a/appcast.xml +++ b/appcast.xml @@ -3,6 +3,18 @@ Copilot for Xcode + + 0.28.1 + Mon, 04 Dec 2023 22:36:27 +0800 + 291 + 0.28.1 + 12.0 + + https://github.com/intitni/CopilotForXcode/releases/tag/0.28.1 + + + + 0.28.0 Mon, 04 Dec 2023 16:04:23 +0800 From b5b6019c4d6aff95051f9f821127d59105647934 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 4 Dec 2023 23:19:36 +0800 Subject: [PATCH 72/74] Fix git ignore check --- Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift | 7 ++++--- Tool/Sources/Preferences/Keys.swift | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift b/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift index 9bfc7ef2..f44acca0 100644 --- a/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift +++ b/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift @@ -52,12 +52,13 @@ public struct DefaultGitIgnoredChecker: GitIgnoredChecker { return false } do { - _ = try await terminal.runCommand( + let result = try await terminal.runCommand( "/bin/bash", - arguments: ["-i", "-c", "git check-ignore \"\(fileURL.path)\""], + arguments: ["-c", "git check-ignore \"\(fileURL.path)\""], currentDirectoryPath: gitFolderURL.path, environment: [:] ) + if result.isEmpty { return false } return true } catch { return false @@ -75,7 +76,7 @@ public struct DefaultGitIgnoredChecker: GitIgnoredChecker { do { let result = try await terminal.runCommand( "/bin/bash", - arguments: ["-i", "-c", "git check-ignore \(filePaths)"], + arguments: ["-c", "git check-ignore \(filePaths)"], currentDirectoryPath: gitFolderURL.path, environment: [:] ) diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 91a3dbd1..cae7c756 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -548,7 +548,7 @@ public extension UserDefaultPreferenceKeys { } var disableGitIgnoreCheck: FeatureFlag { - .init(defaultValue: true, key: "FeatureFlag-DisableGitIgnoreCheck") + .init(defaultValue: false, key: "FeatureFlag-DisableGitIgnoreCheck") } var disableEnhancedWorkspace: FeatureFlag { From 95ba0c36eade73c130f24e7cc89d64a836cf0728 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 4 Dec 2023 23:22:24 +0800 Subject: [PATCH 73/74] Bump version 0.28.2 --- Version.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Version.xcconfig b/Version.xcconfig index b17aaf5f..fc9e5829 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,3 +1,3 @@ -APP_VERSION = 0.28.1 -APP_BUILD = 291 +APP_VERSION = 0.28.2 +APP_BUILD = 292 From d0e41d2ff8b73328b6635e7348365fffbcc5cd19 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 4 Dec 2023 23:22:31 +0800 Subject: [PATCH 74/74] Update appcast.xml --- appcast.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/appcast.xml b/appcast.xml index d885107c..b2b9b2f7 100644 --- a/appcast.xml +++ b/appcast.xml @@ -4,15 +4,15 @@ Copilot for Xcode - 0.28.1 - Mon, 04 Dec 2023 22:36:27 +0800 - 291 - 0.28.1 + 0.28.2 + Mon, 04 Dec 2023 23:11:26 +0800 + 292 + 0.28.2 12.0 - https://github.com/intitni/CopilotForXcode/releases/tag/0.28.1 + https://github.com/intitni/CopilotForXcode/releases/tag/0.28.2 - +