From ffc3da7b1b83859d7c0b76ca00bf0221f89dd52b Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 5 Dec 2023 23:38:29 +0800 Subject: [PATCH 01/71] Add extension point --- Copilot for Xcode.xcodeproj/project.pbxproj | 58 ++++++--------------- ExtensionPoint.appextensionpoint | 11 ++++ 2 files changed, 26 insertions(+), 43 deletions(-) create mode 100644 ExtensionPoint.appextensionpoint diff --git a/Copilot for Xcode.xcodeproj/project.pbxproj b/Copilot for Xcode.xcodeproj/project.pbxproj index ca8e5bfe..13ab8477 100644 --- a/Copilot for Xcode.xcodeproj/project.pbxproj +++ b/Copilot for Xcode.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ C8216B782980370100AD38C7 /* ReloadLaunchAgent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8216B772980370100AD38C7 /* ReloadLaunchAgent.swift */; }; C8216B7D2980374300AD38C7 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = C8216B7C2980374300AD38C7 /* ArgumentParser */; }; C8216B802980378300AD38C7 /* Helper in Embed XPCService */ = {isa = PBXBuildFile; fileRef = C8216B70298036EC00AD38C7 /* Helper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + C828B27F2B1F7B4F00E7612A /* ExtensionPoint.appextensionpoint in Copy Extension Point */ = {isa = PBXBuildFile; fileRef = C828B27D2B1F241500E7612A /* ExtensionPoint.appextensionpoint */; }; C8520301293C4D9000460097 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8520300293C4D9000460097 /* Helpers.swift */; }; C861A6A329E5503F005C41A3 /* PromptToCodeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C861A6A229E5503F005C41A3 /* PromptToCodeCommand.swift */; }; C861E6112994F6070056CB02 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C861E6102994F6070056CB02 /* AppDelegate.swift */; }; @@ -90,6 +91,17 @@ ); runOnlyForDeploymentPostprocessing = 1; }; + C828B27E2B1F7B3C00E7612A /* Copy Extension Point */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(EXTENSIONS_FOLDER_PATH)"; + dstSubfolderSpec = 16; + files = ( + C828B27F2B1F7B4F00E7612A /* ExtensionPoint.appextensionpoint in Copy Extension Point */, + ); + name = "Copy Extension Point"; + runOnlyForDeploymentPostprocessing = 0; + }; C8520306293CF0EF00460097 /* Embed XPCService */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -163,6 +175,7 @@ C8216B70298036EC00AD38C7 /* Helper */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = Helper; sourceTree = BUILT_PRODUCTS_DIR; }; C8216B72298036EC00AD38C7 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; C8216B772980370100AD38C7 /* ReloadLaunchAgent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReloadLaunchAgent.swift; sourceTree = ""; }; + C828B27D2B1F241500E7612A /* ExtensionPoint.appextensionpoint */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = ExtensionPoint.appextensionpoint; sourceTree = ""; }; C82E38492A1F025F00D4EADF /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; C83E5DED2A38CD8C0071506D /* Makefile */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = ""; }; C8520300293C4D9000460097 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; @@ -270,6 +283,7 @@ C81458AD293A009600135263 /* Config.xcconfig */, C81458AE293A009800135263 /* Config.debug.xcconfig */, C8CD828229B88006008D044D /* TestPlan.xctestplan */, + C828B27D2B1F241500E7612A /* ExtensionPoint.appextensionpoint */, C81D181E2A1B509B006C1B70 /* Tool */, C8189B282938979000C9DCDA /* Core */, C8189B182938972F00C9DCDA /* Copilot for Xcode */, @@ -413,8 +427,7 @@ C861E60A2994F6070056CB02 /* Sources */, C861E60B2994F6070056CB02 /* Frameworks */, C861E60C2994F6070056CB02 /* Resources */, - C8A3AE572A28852D0046E809 /* Sign Python STD */, - C8A3B1782A2894E10046E809 /* Sign Python Site Packages */, + C828B27E2B1F7B3C00E7612A /* Copy Extension Point */, ); buildRules = ( ); @@ -505,47 +518,6 @@ }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - C8A3AE572A28852D0046E809 /* Sign Python STD */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 8; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Sign Python STD"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 1; - shellPath = /bin/sh; - shellScript = "#set -e\n#echo \"Signing as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)\"\n#find \"$CODESIGNING_FOLDER_PATH/Contents/Resources/python-stdlib/lib-dynload\" -name \"*.so\" -exec /usr/bin/codesign --force --sign \"$EXPANDED_CODE_SIGN_IDENTITY\" -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der {} \\;\n"; - }; - C8A3B1782A2894E10046E809 /* Sign Python Site Packages */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 8; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Sign Python Site Packages"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 1; - shellPath = /bin/sh; - shellScript = "#set -e\n#echo \"Signing as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)\"\n#find \"$CODESIGNING_FOLDER_PATH/Contents/Resources/site-packages\" -type f \\( -name \"*.so\" -o -name \"*.dylib\" \\) -exec /usr/bin/codesign --force --sign \"$EXPANDED_CODE_SIGN_IDENTITY\" -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der {} \\;\n"; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ C81458882939EFDC00135263 /* Sources */ = { isa = PBXSourcesBuildPhase; diff --git a/ExtensionPoint.appextensionpoint b/ExtensionPoint.appextensionpoint new file mode 100644 index 00000000..507800f5 --- /dev/null +++ b/ExtensionPoint.appextensionpoint @@ -0,0 +1,11 @@ + + + + + com.intii.CopilotForXcode.ExtensionService.Extension + + ExtensionServiceExtension + + + + From 14f4463d660c2fb50c8f0130083a0e52eb765186 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 10 Dec 2023 14:45:18 +0800 Subject: [PATCH 02/71] Update --- ExtensionPoint.appextensionpoint | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ExtensionPoint.appextensionpoint b/ExtensionPoint.appextensionpoint index 507800f5..0a3d3f89 100644 --- a/ExtensionPoint.appextensionpoint +++ b/ExtensionPoint.appextensionpoint @@ -4,8 +4,8 @@ com.intii.CopilotForXcode.ExtensionService.Extension - ExtensionServiceExtension - + EXPresentsUserInterface + From 1b5776710d57b34eef2477e03c26a97aa3154b9e Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 12 Dec 2023 16:07:45 +0800 Subject: [PATCH 03/71] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index a9e9bebd..bc2c0564 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit a9e9bebd3af1af4fe5b20d2c3df4e436938c2bb6 +Subproject commit bc2c0564504510356b6a6fcbcba57f66c9a38688 From f9b673b560e56a387a58fb4458a2bb6b2465d234 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 18 Dec 2023 00:10:26 +0800 Subject: [PATCH 04/71] Update the interface of CodeSuggestion --- .../WindowBaseCommandHandler.swift | 5 +- Core/Tests/ServiceTests/Environment.swift | 6 +- ...FilespaceSuggestionInvalidationTests.swift | 5 +- .../AcceptSuggestionTests.swift | 65 ++++++++----------- .../ProposeSuggestionTests.swift | 35 ++++------ .../CodeiumService/CodeiumService.swift | 7 +- .../GitHubCopilotRequest.swift | 32 ++++++++- .../GitHubCopilotService.swift | 4 +- .../SuggestionModel/CodeSuggestion.swift | 12 ++-- .../FetchSuggestionsTests.swift | 35 ++++------ 10 files changed, 100 insertions(+), 106 deletions(-) diff --git a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift index e2b75d88..bb7d8629 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift @@ -214,11 +214,10 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler { }() let suggestion = CodeSuggestion( + id: UUID().uuidString, text: promptToCode.code, position: range.start, - uuid: UUID().uuidString, - range: range, - displayText: promptToCode.code + range: range ) injector.acceptSuggestion( diff --git a/Core/Tests/ServiceTests/Environment.swift b/Core/Tests/ServiceTests/Environment.swift index 3bf19a99..5084d111 100644 --- a/Core/Tests/ServiceTests/Environment.swift +++ b/Core/Tests/ServiceTests/Environment.swift @@ -11,7 +11,7 @@ import XPCShared @testable import Service func completion(text: String, range: CursorRange, uuid: String = "") -> CodeSuggestion { - .init(text: text, position: range.start, uuid: uuid, range: range, displayText: text) + .init(id: uuid, text: text, position: range.start, range: range) } class MockSuggestionService: GitHubCopilotSuggestionServiceType { @@ -61,11 +61,11 @@ class MockSuggestionService: GitHubCopilotSuggestionServiceType { } func notifyAccepted(_ completion: CodeSuggestion) async { - accepted = completion.uuid + accepted = completion.id } func notifyRejected(_ completions: [CodeSuggestion]) async { - rejected = completions.map(\.uuid) + rejected = completions.map(\.id) } } diff --git a/Core/Tests/ServiceTests/FilespaceSuggestionInvalidationTests.swift b/Core/Tests/ServiceTests/FilespaceSuggestionInvalidationTests.swift index dbb08595..59507e59 100644 --- a/Core/Tests/ServiceTests/FilespaceSuggestionInvalidationTests.swift +++ b/Core/Tests/ServiceTests/FilespaceSuggestionInvalidationTests.swift @@ -13,11 +13,10 @@ class FilespaceSuggestionInvalidationTests: XCTestCase { .fetchOrCreateWorkspaceAndFilespace(fileURL: URL(fileURLWithPath: "file/path/to.swift")) filespace.suggestions = [ .init( + id: "", text: suggestionText, position: cursorPosition, - uuid: "", - range: .outOfScope, - displayText: "" + range: .outOfScope ), ] return filespace diff --git a/Core/Tests/SuggestionInjectorTests/AcceptSuggestionTests.swift b/Core/Tests/SuggestionInjectorTests/AcceptSuggestionTests.swift index 5fc2af8f..c7faef1d 100644 --- a/Core/Tests/SuggestionInjectorTests/AcceptSuggestionTests.swift +++ b/Core/Tests/SuggestionInjectorTests/AcceptSuggestionTests.swift @@ -15,14 +15,13 @@ final class AcceptSuggestionTests: XCTestCase { var age: String """ let suggestion = CodeSuggestion( + id: "", text: text, position: .init(line: 0, character: 1), - uuid: "", range: .init( start: .init(line: 1, character: 0), end: .init(line: 1, character: 0) - ), - displayText: "" + ) ) var extraInfo = SuggestionInjector.ExtraInfo() var lines = content.breakIntoEditorStyleLines() @@ -62,14 +61,13 @@ final class AcceptSuggestionTests: XCTestCase { var age: String """ let suggestion = CodeSuggestion( + id: "", text: text, position: .init(line: 0, character: 12), - uuid: "", range: .init( start: .init(line: 0, character: 0), end: .init(line: 0, character: 12) - ), - displayText: "" + ) ) var extraInfo = SuggestionInjector.ExtraInfo() @@ -106,14 +104,13 @@ final class AcceptSuggestionTests: XCTestCase { var age: String """ let suggestion = CodeSuggestion( + id: "", text: text, position: .init(line: 1, character: 12), - uuid: "", range: .init( start: .init(line: 1, character: 0), end: .init(line: 1, character: 12) - ), - displayText: "" + ) ) var extraInfo = SuggestionInjector.ExtraInfo() @@ -150,14 +147,13 @@ final class AcceptSuggestionTests: XCTestCase { var age: String """ let suggestion = CodeSuggestion( + id: "", text: text, position: .init(line: 1, character: 12), - uuid: "", range: .init( start: .init(line: 1, character: 0), end: .init(line: 1, character: 12) - ), - displayText: "" + ) ) var extraInfo = SuggestionInjector.ExtraInfo() @@ -191,14 +187,13 @@ final class AcceptSuggestionTests: XCTestCase { print("Hello World!") """ let suggestion = CodeSuggestion( + id: "", text: text, position: .init(line: 0, character: 6), - uuid: "", range: .init( start: .init(line: 0, character: 0), end: .init(line: 0, character: 6) - ), - displayText: "" + ) ) var extraInfo = SuggestionInjector.ExtraInfo() @@ -229,14 +224,13 @@ final class AcceptSuggestionTests: XCTestCase { print("Hello World! """ let suggestion = CodeSuggestion( + id: "", text: text, position: .init(line: 0, character: 6), - uuid: "", range: .init( start: .init(line: 0, character: 0), end: .init(line: 0, character: 6) - ), - displayText: "" + ) ) var extraInfo = SuggestionInjector.ExtraInfo() @@ -271,14 +265,13 @@ final class AcceptSuggestionTests: XCTestCase { } """ let suggestion = CodeSuggestion( + id: "", text: text, position: .init(line: 0, character: 6), - uuid: "", range: .init( start: .init(line: 0, character: 0), end: .init(line: 0, character: 6) - ), - displayText: "" + ) ) var extraInfo = SuggestionInjector.ExtraInfo() @@ -316,14 +309,13 @@ final class AcceptSuggestionTests: XCTestCase { } """ let suggestion = CodeSuggestion( + id: "", text: text, position: .init(line: 0, character: 18), - uuid: "", range: .init( start: .init(line: 0, character: 0), end: .init(line: 0, character: 20) - ), - displayText: "" + ) ) var extraInfo = SuggestionInjector.ExtraInfo() @@ -363,14 +355,13 @@ final class AcceptSuggestionTests: XCTestCase { } """ let suggestion = CodeSuggestion( + id: "", text: text, position: .init(line: 0, character: 18), - uuid: "", range: .init( start: .init(line: 1, character: 0), end: .init(line: 1, character: 0) - ), - displayText: "" + ) ) var extraInfo = SuggestionInjector.ExtraInfo() @@ -413,14 +404,13 @@ final class AcceptSuggestionTests: XCTestCase { } """ let suggestion = CodeSuggestion( + id: "", text: text, position: .init(line: 0, character: 7), - uuid: "", range: .init( start: .init(line: 0, character: 0), end: .init(line: 2, character: 1) - ), - displayText: "" + ) ) var extraInfo = SuggestionInjector.ExtraInfo() @@ -467,14 +457,13 @@ final class AcceptSuggestionTests: XCTestCase { } """ let suggestion = CodeSuggestion( + id: "", text: text, position: .init(line: 5, character: 34), - uuid: "", range: .init( start: .init(line: 4, character: 7), end: .init(line: 5, character: 34) - ), - displayText: "" + ) ) var extraInfo = SuggestionInjector.ExtraInfo() @@ -515,14 +504,13 @@ final class AcceptSuggestionTests: XCTestCase { """ let suggestion = CodeSuggestion( + id: "", text: "apiKeyName: azureOpenAIAPIKeyName", position: .init(line: 0, character: 12), - uuid: "", range: .init( start: .init(line: 0, character: 0), end: .init(line: 0, character: 12) - ), - displayText: "" + ) ) var lines = content.breakIntoEditorStyleLines() @@ -549,14 +537,13 @@ final class AcceptSuggestionTests: XCTestCase { """ let suggestion = CodeSuggestion( + id: "", text: "apiKeyName: azureOpenAIAPIKeyName", position: .init(line: 0, character: 12), - uuid: "", range: .init( start: .init(line: 0, character: 0), end: .init(line: 0, character: 12) - ), - displayText: "" + ) ) var lines = content.breakIntoEditorStyleLines() diff --git a/Core/Tests/SuggestionInjectorTests/ProposeSuggestionTests.swift b/Core/Tests/SuggestionInjectorTests/ProposeSuggestionTests.swift index ae7bdcdf..78f90f63 100644 --- a/Core/Tests/SuggestionInjectorTests/ProposeSuggestionTests.swift +++ b/Core/Tests/SuggestionInjectorTests/ProposeSuggestionTests.swift @@ -15,14 +15,13 @@ final class ProposeSuggestionTests: XCTestCase { var age: String """ let suggestion = CodeSuggestion( + id: "", text: text, position: .init(line: 2, character: 19), - uuid: "", range: .init( start: .init(line: 1, character: 0), end: .init(line: 2, character: 18) - ), - displayText: "" + ) ) var extraInfo = SuggestionInjector.ExtraInfo() var lines = content.breakLines() @@ -62,14 +61,13 @@ final class ProposeSuggestionTests: XCTestCase { var age: String """ let suggestion = CodeSuggestion( + id: "", text: text, position: .init(line: 1, character: 0), - uuid: "", range: .init( start: .init(line: 1, character: 0), end: .init(line: 2, character: 18) - ), - displayText: "" + ) ) var extraInfo = SuggestionInjector.ExtraInfo() var lines = content.breakLines() @@ -105,14 +103,13 @@ final class ProposeSuggestionTests: XCTestCase { var age: String """ let suggestion = CodeSuggestion( + id: "", text: text, position: .init(line: 1, character: 0), - uuid: "", range: .init( start: .init(line: 1, character: 0), end: .init(line: 2, character: 18) - ), - displayText: "" + ) ) var extraInfo = SuggestionInjector.ExtraInfo() var lines = content.breakLines() @@ -149,14 +146,13 @@ final class ProposeSuggestionTests: XCTestCase { var age: String """ let suggestion = CodeSuggestion( + id: "", text: text, position: .init(line: 1, character: 0), - uuid: "", range: .init( start: .init(line: 1, character: 0), end: .init(line: 2, character: 18) - ), - displayText: "" + ) ) var extraInfo = SuggestionInjector.ExtraInfo() var lines = content.breakLines() @@ -197,14 +193,13 @@ final class ProposeSuggestionTests: XCTestCase { print(array) """ let suggestion = CodeSuggestion( + id: "", text: text, position: .init(line: 1, character: 0), - uuid: "", range: .init( start: .init(line: 1, character: 0), end: .init(line: 2, character: 18) - ), - displayText: "" + ) ) var extraInfo = SuggestionInjector.ExtraInfo() var lines = content.breakLines() @@ -247,14 +242,13 @@ final class ProposeSuggestionTests: XCTestCase { } """ let suggestion = CodeSuggestion( + id: "", text: text, position: .init(line: 0, character: 0), - uuid: "", range: .init( start: .init(line: 0, character: 0), end: .init(line: 5, character: 15) - ), - displayText: "" + ) ) var extraInfo = SuggestionInjector.ExtraInfo() var lines = content.breakLines() @@ -294,14 +288,13 @@ final class ProposeSuggestionTests: XCTestCase { """ let text = "} else {\n" let suggestion = CodeSuggestion( + id: "", text: text, position: .init(line: 2, character: 0), - uuid: "", range: .init( start: .init(line: 2, character: 0), end: .init(line: 2, character: 8) - ), - displayText: "" + ) ) var extraInfo = SuggestionInjector.ExtraInfo() var lines = content.breakLines() diff --git a/Tool/Sources/CodeiumService/CodeiumService.swift b/Tool/Sources/CodeiumService/CodeiumService.swift index f5f783da..a62b6021 100644 --- a/Tool/Sources/CodeiumService/CodeiumService.swift +++ b/Tool/Sources/CodeiumService/CodeiumService.swift @@ -278,9 +278,9 @@ extension CodeiumSuggestionService: CodeiumSuggestionServiceType { return true }.map { item in CodeSuggestion( + id: item.completion.completionId, text: item.completion.text, position: cursorPosition, - uuid: item.completion.completionId, range: CursorRange( start: .init( line: item.range.startPosition?.row.flatMap(Int.init) ?? 0, @@ -290,8 +290,7 @@ extension CodeiumSuggestionService: CodeiumSuggestionServiceType { line: item.range.endPosition?.row.flatMap(Int.init) ?? 0, character: item.range.endPosition?.col.flatMap(Int.init) ?? 0 ) - ), - displayText: item.completion.text + ) ) } ?? [] } @@ -314,7 +313,7 @@ extension CodeiumSuggestionService: CodeiumSuggestionServiceType { _ = try? await (try setupServerIfNeeded()) .sendRequest(CodeiumRequest.AcceptCompletion(requestBody: .init( metadata: getMetadata(), - completion_id: suggestion.uuid + completion_id: suggestion.id ))) } diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift index f7ccefb1..f036d100 100644 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift +++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift @@ -22,6 +22,34 @@ protocol GitHubCopilotRequestType { var request: ClientRequest { get } } +public struct GitHubCopilotCodeSuggestion: Codable, Equatable { + public init( + text: String, + position: CursorPosition, + uuid: String, + range: CursorRange, + displayText: String + ) { + self.text = text + self.position = position + self.uuid = uuid + self.range = range + self.displayText = displayText + } + + /// The new code to be inserted and the original code on the first line. + public var text: String + /// The position of the cursor before generating the completion. + public var position: CursorPosition + /// An id. + public var uuid: String + /// The range of the original code that should be replaced. + public var range: CursorRange + /// The new code to be inserted. + public var displayText: String +} + + enum GitHubCopilotRequest { struct SetEditorInfo: GitHubCopilotRequestType { struct Response: Codable {} @@ -142,7 +170,7 @@ enum GitHubCopilotRequest { struct GetCompletions: GitHubCopilotRequestType { struct Response: Codable { - var completions: [CodeSuggestion] + var completions: [GitHubCopilotCodeSuggestion] } var doc: GitHubCopilotDoc @@ -174,7 +202,7 @@ enum GitHubCopilotRequest { struct GetPanelCompletions: GitHubCopilotRequestType { struct Response: Codable { - var completions: [CodeSuggestion] + var completions: [GitHubCopilotCodeSuggestion] } var doc: GitHubCopilotDoc diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift index d3ac0034..9f6eeff3 100644 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift @@ -339,13 +339,13 @@ public final class GitHubCopilotSuggestionService: GitHubCopilotBaseService, public func notifyAccepted(_ completion: CodeSuggestion) async { _ = try? await server.sendRequest( - GitHubCopilotRequest.NotifyAccepted(completionUUID: completion.uuid) + GitHubCopilotRequest.NotifyAccepted(completionUUID: completion.id) ) } public func notifyRejected(_ completions: [CodeSuggestion]) async { _ = try? await server.sendRequest( - GitHubCopilotRequest.NotifyRejected(completionUUIDs: completions.map(\.uuid)) + GitHubCopilotRequest.NotifyRejected(completionUUIDs: completions.map(\.id)) ) } diff --git a/Tool/Sources/SuggestionModel/CodeSuggestion.swift b/Tool/Sources/SuggestionModel/CodeSuggestion.swift index 0d7b1ec4..e6b2d85c 100644 --- a/Tool/Sources/SuggestionModel/CodeSuggestion.swift +++ b/Tool/Sources/SuggestionModel/CodeSuggestion.swift @@ -2,17 +2,15 @@ import Foundation public struct CodeSuggestion: Codable, Equatable { public init( + id: String, text: String, position: CursorPosition, - uuid: String, - range: CursorRange, - displayText: String + range: CursorRange ) { self.text = text self.position = position - self.uuid = uuid + self.id = id self.range = range - self.displayText = displayText } /// The new code to be inserted and the original code on the first line. @@ -20,9 +18,7 @@ public struct CodeSuggestion: Codable, Equatable { /// The position of the cursor before generating the completion. public var position: CursorPosition /// An id. - public var uuid: String + public var id: String /// The range of the original code that should be replaced. public var range: CursorRange - /// The new code to be inserted. - public var displayText: String } diff --git a/Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift b/Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift index 16421888..159f36f2 100644 --- a/Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift +++ b/Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift @@ -13,25 +13,22 @@ final class FetchSuggestionTests: XCTestCase { func sendRequest(_: E) async throws -> E.Response where E: GitHubCopilotRequestType { return GitHubCopilotRequest.GetCompletionsCycling.Response(completions: [ .init( + id: "uuid", text: "Hello World\n", position: .init((0, 0)), - uuid: "uuid", - range: .init(start: .init((0, 0)), end: .init((0, 4))), - displayText: "Hello" + range: .init(start: .init((0, 0)), end: .init((0, 4))) ), .init( + id: "uuid", text: " ", position: .init((0, 0)), - uuid: "uuid", - range: .init(start: .init((0, 0)), end: .init((0, 1))), - displayText: " " + range: .init(start: .init((0, 0)), end: .init((0, 1))) ), .init( + id: "uuid", text: " \n", position: .init((0, 0)), - uuid: "uuid", - range: .init(start: .init((0, 0)), end: .init((0, 2))), - displayText: " \n" + range: .init(start: .init((0, 0)), end: .init((0, 2))) ), ]) as! E.Response } @@ -59,25 +56,22 @@ final class FetchSuggestionTests: XCTestCase { func sendRequest(_: E) async throws -> E.Response where E: GitHubCopilotRequestType { return GitHubCopilotRequest.GetCompletionsCycling.Response(completions: [ .init( + id: "uuid", text: "Hello World\n", position: .init((0, 0)), - uuid: "uuid", - range: .init(start: .init((0, 0)), end: .init((0, 4))), - displayText: "Hello" + range: .init(start: .init((0, 0)), end: .init((0, 4))) ), .init( + id: "uuid", text: " ", position: .init((0, 0)), - uuid: "uuid", - range: .init(start: .init((0, 0)), end: .init((0, 1))), - displayText: " " + range: .init(start: .init((0, 0)), end: .init((0, 1))) ), .init( + id: "uuid", text: " \n", position: .init((0, 0)), - uuid: "uuid", - range: .init(start: .init((0, 0)), end: .init((0, 2))), - displayText: " \n" + range: .init(start: .init((0, 0)), end: .init((0, 2))) ), ]) as! E.Response } @@ -112,11 +106,10 @@ final class FetchSuggestionTests: XCTestCase { func sendRequest(_ r: E) async throws -> E.Response where E: GitHubCopilotRequestType { return GitHubCopilotRequest.GetCompletionsCycling.Response(completions: [ .init( + id: "uuid", text: "Hello World\n", position: .init((0, 0)), - uuid: "uuid", - range: .init(start: .init((0, 0)), end: .init((0, 4))), - displayText: "Hello" + range: .init(start: .init((0, 0)), end: .init((0, 4))) ), ]) as! E.Response } From 147ee41027f05618ffc60ee5d60966f94f109185 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 18 Dec 2023 16:11:28 +0800 Subject: [PATCH 05/71] Rename SuggestionFeatureProvider to BuiltInXXX --- .../HostApp/FeatureSettings/SuggestionSettingsView.swift | 2 +- Tool/Sources/Preferences/Keys.swift | 2 +- .../Preferences/Types/SuggestionFeatureProvider.swift | 8 +++++++- Tool/Sources/SuggestionService/SuggestionService.swift | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift index 7094ac3f..e2196d2b 100644 --- a/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift @@ -53,7 +53,7 @@ struct SuggestionSettingsView: View { } Picker(selection: $settings.suggestionFeatureProvider) { - ForEach(SuggestionFeatureProvider.allCases, id: \.rawValue) { + ForEach(BuiltInSuggestionFeatureProvider.allCases, id: \.rawValue) { switch $0 { case .gitHubCopilot: Text("GitHub Copilot").tag($0) diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index d1808434..0508ce40 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -286,7 +286,7 @@ public extension UserDefaultPreferenceKeys { // MARK: - Suggestion public extension UserDefaultPreferenceKeys { - var suggestionFeatureProvider: PreferenceKey { + var suggestionFeatureProvider: PreferenceKey { .init(defaultValue: .gitHubCopilot, key: "SuggestionFeatureProvider") } diff --git a/Tool/Sources/Preferences/Types/SuggestionFeatureProvider.swift b/Tool/Sources/Preferences/Types/SuggestionFeatureProvider.swift index b08b84b9..48644bb4 100644 --- a/Tool/Sources/Preferences/Types/SuggestionFeatureProvider.swift +++ b/Tool/Sources/Preferences/Types/SuggestionFeatureProvider.swift @@ -1,6 +1,12 @@ import Foundation -public enum SuggestionFeatureProvider: Int, CaseIterable { +public enum BuiltInSuggestionFeatureProvider: Int, CaseIterable, Codable { case gitHubCopilot case codeium } + +public enum SuggestionFeatureProvider: Codable { + case builtIn(BuiltInSuggestionFeatureProvider) + case extended(name: String, bundleIdentifier: String) +} + diff --git a/Tool/Sources/SuggestionService/SuggestionService.swift b/Tool/Sources/SuggestionService/SuggestionService.swift index 2cc194e3..540a66f1 100644 --- a/Tool/Sources/SuggestionService/SuggestionService.swift +++ b/Tool/Sources/SuggestionService/SuggestionService.swift @@ -94,7 +94,7 @@ public actor SuggestionService: SuggestionServiceType { lazy var suggestionProvider: SuggestionServiceProvider = buildService() - var serviceType: SuggestionFeatureProvider { + var serviceType: BuiltInSuggestionFeatureProvider { UserDefaults.shared.value(for: \.suggestionFeatureProvider) } From bf2ae6eec418f93767c0510cf0048406df290a57 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 18 Dec 2023 16:12:13 +0800 Subject: [PATCH 06/71] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index bc2c0564..203d4e53 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit bc2c0564504510356b6a6fcbcba57f66c9a38688 +Subproject commit 203d4e53aed5b51b6f66bd9058072c4a6c026a4c From 99993a2b0edb7a074d281dc62a837017c957db64 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 20 Dec 2023 17:21:21 +0800 Subject: [PATCH 07/71] Add logger for app extension --- Tool/Sources/Logger/Logger.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tool/Sources/Logger/Logger.swift b/Tool/Sources/Logger/Logger.swift index 00b1d6eb..840244fd 100644 --- a/Tool/Sources/Logger/Logger.swift +++ b/Tool/Sources/Logger/Logger.swift @@ -21,6 +21,7 @@ public final class Logger { public static let langchain = Logger(category: "LangChain") public static let retrieval = Logger(category: "Retrieval") public static let license = Logger(category: "License") + public static let `extension` = Logger(category: "Extension") #if DEBUG /// Use a temp logger to log something temporary. I won't be available in release builds. public static let temp = Logger(category: "Temp") From 683d0b6ee533b88b791c6a603cc3eb9b5bc7aba9 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 20 Dec 2023 17:21:27 +0800 Subject: [PATCH 08/71] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 203d4e53..a6769f7b 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 203d4e53aed5b51b6f66bd9058072c4a6c026a4c +Subproject commit a6769f7b0244b4f193ada0be1b9a911a1be3b5c6 From 870dc5261145215e89e5a3a0fb5766fc940d1e4c Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 23 Dec 2023 18:06:07 +0800 Subject: [PATCH 09/71] Fix GitHubCopilotService parsing response --- .../GitHubCopilotRequest.swift | 2 +- .../GitHubCopilotService.swift | 41 ++++++++++--------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift index f036d100..0b090450 100644 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift +++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift @@ -186,7 +186,7 @@ enum GitHubCopilotRequest { struct GetCompletionsCycling: GitHubCopilotRequestType { struct Response: Codable { - var completions: [CodeSuggestion] + var completions: [GitHubCopilotCodeSuggestion] } var doc: GitHubCopilotDoc diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift index 9f6eeff3..9a030198 100644 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift @@ -81,14 +81,12 @@ public class GitHubCopilotBaseService { "\"\(agentJSURL.path)\"", "--stdio", ].joined(separator: " ") - executionParams = { - Process.ExecutionParameters( - path: "/bin/bash", - arguments: ["-i", "-l", "-c", command], - environment: [:], - currentDirectoryURL: urls.supportURL - ) - }() + executionParams = Process.ExecutionParameters( + path: "/bin/bash", + arguments: ["-i", "-l", "-c", command], + environment: [:], + currentDirectoryURL: urls.supportURL + ) case .shell: let shell = ProcessInfo.processInfo.shellExecutablePath let nodePath = UserDefaults.shared.value(for: \.nodePath) @@ -97,16 +95,15 @@ public class GitHubCopilotBaseService { "\"\(agentJSURL.path)\"", "--stdio", ].joined(separator: " ") - executionParams = { - Process.ExecutionParameters( - path: shell, - arguments: ["-i", "-l", "-c", command], - environment: [:], - currentDirectoryURL: urls.supportURL - ) - }() + executionParams = Process.ExecutionParameters( + path: shell, + arguments: ["-i", "-l", "-c", command], + environment: [:], + currentDirectoryURL: urls.supportURL + ) case .env: - let userEnvPath = "/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" + let userEnvPath = + "/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" executionParams = { let nodePath = UserDefaults.shared.value(for: \.nodePath) return Process.ExecutionParameters( @@ -313,8 +310,14 @@ public final class GitHubCopilotSuggestionService: GitHubCopilotBaseService, return true } .map { + var suggestion = CodeSuggestion( + id: $0.uuid, + text: $0.text, + position: $0.position, + range: $0.range + ) if ignoreTrailingNewLinesAndSpaces { - var updated = $0 + var updated = suggestion var text = updated.text[...] while let last = text.last, last.isNewline || last.isWhitespace { text = text.dropLast(1) @@ -322,7 +325,7 @@ public final class GitHubCopilotSuggestionService: GitHubCopilotBaseService, updated.text = String(text) return updated } - return $0 + return suggestion } try Task.checkCancellation() return completions From 9a34b8943f569cac24a8dfe0099cf4782edfe4a7 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 23 Dec 2023 18:07:25 +0800 Subject: [PATCH 10/71] Break SuggestionService into SuggestionProvider and SuggestionService in Core --- Core/Package.swift | 18 +++- Core/Sources/Service/Service.swift | 7 +- .../Workspace+Cleanup.swift | 2 +- .../SuggestionService/SuggestionService.swift | 94 +++---------------- Tool/Package.swift | 8 +- .../CodeiumSuggestionProvider.swift | 11 ++- .../GitHubCopilotSuggestionProvider.swift | 8 +- .../SuggestionProvider.swift | 47 ++++++++++ .../SuggestionServiceMiddleware.swift | 41 ++++++-- .../SuggestionWorkspacePlugin.swift | 39 +++++--- .../Workspace+SuggestionService.swift | 22 +++-- 11 files changed, 164 insertions(+), 133 deletions(-) rename {Tool => Core}/Sources/SuggestionService/SuggestionService.swift (51%) rename Tool/Sources/{SuggestionService => SuggestionProvider}/CodeiumSuggestionProvider.swift (88%) rename Tool/Sources/{SuggestionService => SuggestionProvider}/GitHubCopilotSuggestionProvider.swift (90%) create mode 100644 Tool/Sources/SuggestionProvider/SuggestionProvider.swift rename Tool/Sources/{SuggestionService => SuggestionProvider}/SuggestionServiceMiddleware.swift (56%) diff --git a/Core/Package.swift b/Core/Package.swift index d2f3bd54..474f91e5 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -109,7 +109,7 @@ let package = Package( name: "Client", dependencies: [ .product(name: "XPCShared", package: "Tool"), - .product(name: "SuggestionService", package: "Tool"), + .product(name: "SuggestionProvider", package: "Tool"), .product(name: "SuggestionModel", package: "Tool"), .product(name: "Logger", package: "Tool"), .product(name: "Preferences", package: "Tool"), @@ -119,12 +119,13 @@ let package = Package( name: "Service", dependencies: [ "SuggestionWidget", + "SuggestionService", "ChatService", "PromptToCodeService", "ServiceUpdateMigration", "ChatGPTChatTab", .product(name: "XPCShared", package: "Tool"), - .product(name: "SuggestionService", package: "Tool"), + .product(name: "SuggestionProvider", package: "Tool"), .product(name: "Workspace", package: "Tool"), .product(name: "UserDefaultsObserver", package: "Tool"), .product(name: "AppMonitoring", package: "Tool"), @@ -149,7 +150,7 @@ let package = Package( "Client", "SuggestionInjector", .product(name: "XPCShared", package: "Tool"), - .product(name: "SuggestionService", package: "Tool"), + .product(name: "SuggestionProvider", package: "Tool"), .product(name: "SuggestionModel", package: "Tool"), .product(name: "Environment", package: "Tool"), .product(name: "Preferences", package: "Tool"), @@ -164,7 +165,7 @@ let package = Package( "Client", "LaunchAgentManager", "PlusFeatureFlag", - .product(name: "SuggestionService", package: "Tool"), + .product(name: "SuggestionProvider", package: "Tool"), .product(name: "Toast", package: "Tool"), .product(name: "SharedUIComponents", package: "Tool"), .product(name: "SuggestionModel", package: "Tool"), @@ -180,6 +181,13 @@ let package = Package( // MARK: - Suggestion Service + .target( + name: "SuggestionService", + dependencies: [ + .product(name: "SuggestionModel", package: "Tool"), + .product(name: "SuggestionProvider", package: "Tool") + ] + ), .target( name: "SuggestionInjector", dependencies: [.product(name: "SuggestionModel", package: "Tool")] @@ -290,7 +298,7 @@ let package = Package( .target( name: "ServiceUpdateMigration", dependencies: [ - .product(name: "SuggestionService", package: "Tool"), + .product(name: "SuggestionProvider", package: "Tool"), .product(name: "Preferences", package: "Tool"), .product(name: "Keychain", package: "Tool"), ] diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift index 4702ee68..0cec486c 100644 --- a/Core/Sources/Service/Service.swift +++ b/Core/Sources/Service/Service.swift @@ -1,5 +1,6 @@ import Dependencies import Foundation +import SuggestionService import Workspace import WorkspaceSuggestionService import XcodeInspector @@ -33,7 +34,11 @@ public final class Service { @Dependency(\.workspacePool) var workspacePool scheduledCleaner = .init() - workspacePool.registerPlugin { SuggestionServiceWorkspacePlugin(workspace: $0) } + workspacePool.registerPlugin { + SuggestionServiceWorkspacePlugin(workspace: $0) { projectRootURL, onLaunched in + SuggestionService(projectRootURL: projectRootURL, onServiceLaunched: onLaunched) + } + } self.workspacePool = workspacePool globalShortcutManager = .init(guiController: guiController) diff --git a/Core/Sources/Service/WorkspaceExtension/Workspace+Cleanup.swift b/Core/Sources/Service/WorkspaceExtension/Workspace+Cleanup.swift index a2f439d1..6841a24c 100644 --- a/Core/Sources/Service/WorkspaceExtension/Workspace+Cleanup.swift +++ b/Core/Sources/Service/WorkspaceExtension/Workspace+Cleanup.swift @@ -1,6 +1,6 @@ import Foundation import Workspace -import SuggestionService +import SuggestionProvider import WorkspaceSuggestionService extension Workspace { diff --git a/Tool/Sources/SuggestionService/SuggestionService.swift b/Core/Sources/SuggestionService/SuggestionService.swift similarity index 51% rename from Tool/Sources/SuggestionService/SuggestionService.swift rename to Core/Sources/SuggestionService/SuggestionService.swift index 540a66f1..fe20c9e3 100644 --- a/Tool/Sources/SuggestionService/SuggestionService.swift +++ b/Core/Sources/SuggestionService/SuggestionService.swift @@ -1,91 +1,18 @@ -import AppKit import Foundation import Preferences import SuggestionModel +import SuggestionProvider import UserDefaultsObserver -public struct SuggestionRequest { - public var fileURL: URL - public var content: String - public var cursorPosition: CursorPosition - public var tabSize: Int - public var indentSize: Int - public var usesTabsForIndentation: Bool - public var ignoreSpaceOnlySuggestions: Bool - - public init( - fileURL: URL, - content: String, - cursorPosition: CursorPosition, - tabSize: Int, - indentSize: Int, - usesTabsForIndentation: Bool, - ignoreSpaceOnlySuggestions: Bool - ) { - self.fileURL = fileURL - self.content = content - self.cursorPosition = cursorPosition - self.tabSize = tabSize - self.indentSize = indentSize - self.usesTabsForIndentation = usesTabsForIndentation - self.ignoreSpaceOnlySuggestions = ignoreSpaceOnlySuggestions - } -} - -public protocol SuggestionServiceType { - func getSuggestions(_ request: SuggestionRequest) async throws -> [CodeSuggestion] - - func notifyAccepted(_ suggestion: CodeSuggestion) async - func notifyRejected(_ suggestions: [CodeSuggestion]) async - func notifyOpenTextDocument(fileURL: URL, content: String) async throws - func notifyChangeTextDocument(fileURL: URL, content: String) async throws - func notifyCloseTextDocument(fileURL: URL) async throws - func notifySaveTextDocument(fileURL: URL) async throws - func cancelRequest() async - func terminate() async -} - -public extension SuggestionServiceType { - func getSuggestions( - fileURL: URL, - content: String, - cursorPosition: CursorPosition, - tabSize: Int, - indentSize: Int, - usesTabsForIndentation: Bool, - ignoreSpaceOnlySuggestions: Bool - ) async throws -> [CodeSuggestion] { - return try await getSuggestions(.init( - fileURL: fileURL, - content: content, - cursorPosition: cursorPosition, - tabSize: tabSize, - indentSize: indentSize, - usesTabsForIndentation: usesTabsForIndentation, - ignoreSpaceOnlySuggestions: ignoreSpaceOnlySuggestions - )) - } -} - -protocol SuggestionServiceProvider: SuggestionServiceType {} +public protocol SuggestionServiceType: SuggestionServiceProvider {} public actor SuggestionService: SuggestionServiceType { - static var builtInMiddlewares: [SuggestionServiceMiddleware] = [ - DisabledLanguageSuggestionServiceMiddleware(), - ] - - static var customMiddlewares: [SuggestionServiceMiddleware] = [] - - static var middlewares: [SuggestionServiceMiddleware] { - builtInMiddlewares + customMiddlewares - } - - public static func addMiddleware(_ middleware: SuggestionServiceMiddleware) { - customMiddlewares.append(middleware) + var middlewares: [SuggestionServiceMiddleware] { + SuggestionServiceMiddlewareContainer.middlewares } let projectRootURL: URL - let onServiceLaunched: (SuggestionServiceType) -> Void + let onServiceLaunched: (SuggestionServiceProvider) -> Void let providerChangeObserver = UserDefaultsObserver( object: UserDefaults.shared, forKeyPaths: [UserDefaultPreferenceKeys().suggestionFeatureProvider.key], @@ -98,7 +25,10 @@ public actor SuggestionService: SuggestionServiceType { UserDefaults.shared.value(for: \.suggestionFeatureProvider) } - public init(projectRootURL: URL, onServiceLaunched: @escaping (SuggestionServiceType) -> Void) { + public init( + projectRootURL: URL, + onServiceLaunched: @escaping (SuggestionServiceProvider) -> Void + ) { self.projectRootURL = projectRootURL self.onServiceLaunched = onServiceLaunched @@ -131,12 +61,12 @@ public actor SuggestionService: SuggestionServiceType { } public extension SuggestionService { - func getSuggestions( + func getSuggestions( _ request: SuggestionRequest ) async throws -> [SuggestionModel.CodeSuggestion] { var getSuggestion = suggestionProvider.getSuggestions - - for middleware in Self.middlewares.reversed() { + + for middleware in middlewares.reversed() { getSuggestion = { [getSuggestion] request in try await middleware.getSuggestion(request, next: getSuggestion) } diff --git a/Tool/Package.swift b/Tool/Package.swift index 13177622..6145f4e9 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -29,8 +29,8 @@ let package = Package( .library(name: "UserDefaultsObserver", targets: ["UserDefaultsObserver"]), .library(name: "Workspace", targets: ["Workspace", "WorkspaceSuggestionService"]), .library( - name: "SuggestionService", - targets: ["SuggestionService", "GitHubCopilotService", "CodeiumService"] + name: "SuggestionProvider", + targets: ["SuggestionProvider", "GitHubCopilotService", "CodeiumService"] ), .library( name: "AppMonitoring", @@ -217,7 +217,7 @@ let package = Package( name: "WorkspaceSuggestionService", dependencies: [ "Workspace", - "SuggestionService", + "SuggestionProvider", "XPCShared", ] ), @@ -264,7 +264,7 @@ let package = Package( .target(name: "BingSearchService"), - .target(name: "SuggestionService", dependencies: [ + .target(name: "SuggestionProvider", dependencies: [ "GitHubCopilotService", "CodeiumService", "UserDefaultsObserver", diff --git a/Tool/Sources/SuggestionService/CodeiumSuggestionProvider.swift b/Tool/Sources/SuggestionProvider/CodeiumSuggestionProvider.swift similarity index 88% rename from Tool/Sources/SuggestionService/CodeiumSuggestionProvider.swift rename to Tool/Sources/SuggestionProvider/CodeiumSuggestionProvider.swift index a990db46..e2f7d164 100644 --- a/Tool/Sources/SuggestionService/CodeiumSuggestionProvider.swift +++ b/Tool/Sources/SuggestionProvider/CodeiumSuggestionProvider.swift @@ -3,12 +3,15 @@ import Foundation import Preferences import SuggestionModel -actor CodeiumSuggestionProvider: SuggestionServiceProvider { +public actor CodeiumSuggestionProvider: SuggestionServiceProvider { let projectRootURL: URL - let onServiceLaunched: (SuggestionServiceType) -> Void + let onServiceLaunched: (SuggestionServiceProvider) -> Void var codeiumService: CodeiumSuggestionServiceType? - init(projectRootURL: URL, onServiceLaunched: @escaping (SuggestionServiceType) -> Void) { + public init( + projectRootURL: URL, + onServiceLaunched: @escaping (SuggestionServiceProvider) -> Void + ) { self.projectRootURL = projectRootURL self.onServiceLaunched = onServiceLaunched } @@ -27,7 +30,7 @@ actor CodeiumSuggestionProvider: SuggestionServiceProvider { } } -extension CodeiumSuggestionProvider { +public extension CodeiumSuggestionProvider { func getSuggestions(_ request: SuggestionRequest) async throws -> [SuggestionModel.CodeSuggestion] { diff --git a/Tool/Sources/SuggestionService/GitHubCopilotSuggestionProvider.swift b/Tool/Sources/SuggestionProvider/GitHubCopilotSuggestionProvider.swift similarity index 90% rename from Tool/Sources/SuggestionService/GitHubCopilotSuggestionProvider.swift rename to Tool/Sources/SuggestionProvider/GitHubCopilotSuggestionProvider.swift index 3632ba77..98f4ba46 100644 --- a/Tool/Sources/SuggestionService/GitHubCopilotSuggestionProvider.swift +++ b/Tool/Sources/SuggestionProvider/GitHubCopilotSuggestionProvider.swift @@ -3,12 +3,12 @@ import GitHubCopilotService import Preferences import SuggestionModel -actor GitHubCopilotSuggestionProvider: SuggestionServiceProvider { +public actor GitHubCopilotSuggestionProvider: SuggestionServiceProvider { let projectRootURL: URL - let onServiceLaunched: (SuggestionServiceType) -> Void + let onServiceLaunched: (SuggestionServiceProvider) -> Void var gitHubCopilotService: GitHubCopilotSuggestionServiceType? - init(projectRootURL: URL, onServiceLaunched: @escaping (SuggestionServiceType) -> Void) { + public init(projectRootURL: URL, onServiceLaunched: @escaping (SuggestionServiceProvider) -> Void) { self.projectRootURL = projectRootURL self.onServiceLaunched = onServiceLaunched } @@ -25,7 +25,7 @@ actor GitHubCopilotSuggestionProvider: SuggestionServiceProvider { } } -extension GitHubCopilotSuggestionProvider { +public extension GitHubCopilotSuggestionProvider { func getSuggestions(_ request: SuggestionRequest) async throws -> [SuggestionModel.CodeSuggestion] { diff --git a/Tool/Sources/SuggestionProvider/SuggestionProvider.swift b/Tool/Sources/SuggestionProvider/SuggestionProvider.swift new file mode 100644 index 00000000..7c592555 --- /dev/null +++ b/Tool/Sources/SuggestionProvider/SuggestionProvider.swift @@ -0,0 +1,47 @@ +import AppKit +import Foundation +import Preferences +import SuggestionModel +import UserDefaultsObserver + +public struct SuggestionRequest { + public var fileURL: URL + public var content: String + public var cursorPosition: CursorPosition + public var tabSize: Int + public var indentSize: Int + public var usesTabsForIndentation: Bool + public var ignoreSpaceOnlySuggestions: Bool + + public init( + fileURL: URL, + content: String, + cursorPosition: CursorPosition, + tabSize: Int, + indentSize: Int, + usesTabsForIndentation: Bool, + ignoreSpaceOnlySuggestions: Bool + ) { + self.fileURL = fileURL + self.content = content + self.cursorPosition = cursorPosition + self.tabSize = tabSize + self.indentSize = indentSize + self.usesTabsForIndentation = usesTabsForIndentation + self.ignoreSpaceOnlySuggestions = ignoreSpaceOnlySuggestions + } +} + +public protocol SuggestionServiceProvider { + func getSuggestions(_ request: SuggestionRequest) async throws -> [CodeSuggestion] + + func notifyAccepted(_ suggestion: CodeSuggestion) async + func notifyRejected(_ suggestions: [CodeSuggestion]) async + func notifyOpenTextDocument(fileURL: URL, content: String) async throws + func notifyChangeTextDocument(fileURL: URL, content: String) async throws + func notifyCloseTextDocument(fileURL: URL) async throws + func notifySaveTextDocument(fileURL: URL) async throws + func cancelRequest() async + func terminate() async +} + diff --git a/Tool/Sources/SuggestionService/SuggestionServiceMiddleware.swift b/Tool/Sources/SuggestionProvider/SuggestionServiceMiddleware.swift similarity index 56% rename from Tool/Sources/SuggestionService/SuggestionServiceMiddleware.swift rename to Tool/Sources/SuggestionProvider/SuggestionServiceMiddleware.swift index 29575a40..429ec403 100644 --- a/Tool/Sources/SuggestionService/SuggestionServiceMiddleware.swift +++ b/Tool/Sources/SuggestionProvider/SuggestionServiceMiddleware.swift @@ -1,15 +1,36 @@ import Foundation -import SuggestionModel import Logger +import SuggestionModel public protocol SuggestionServiceMiddleware { typealias Next = (SuggestionRequest) async throws -> [CodeSuggestion] - + func getSuggestion(_ request: SuggestionRequest, next: Next) async throws -> [CodeSuggestion] } -struct DisabledLanguageSuggestionServiceMiddleware: SuggestionServiceMiddleware { - func getSuggestion(_ request: SuggestionRequest, next: Next) async throws -> [CodeSuggestion] { +public enum SuggestionServiceMiddlewareContainer { + static var builtInMiddlewares: [SuggestionServiceMiddleware] = [ + DisabledLanguageSuggestionServiceMiddleware(), + ] + + static var customMiddlewares: [SuggestionServiceMiddleware] = [] + + public static var middlewares: [SuggestionServiceMiddleware] { + builtInMiddlewares + customMiddlewares + } + + public static func addMiddleware(_ middleware: SuggestionServiceMiddleware) { + customMiddlewares.append(middleware) + } +} + +public struct DisabledLanguageSuggestionServiceMiddleware: SuggestionServiceMiddleware { + public init() {} + + public func getSuggestion( + _ request: SuggestionRequest, + next: Next + ) async throws -> [CodeSuggestion] { let language = languageIdentifierFromFileURL(request.fileURL) if UserDefaults.shared.value(for: \.suggestionFeatureDisabledLanguageList) .contains(where: { $0 == language.rawValue }) @@ -19,15 +40,18 @@ struct DisabledLanguageSuggestionServiceMiddleware: SuggestionServiceMiddleware #endif return [] } - + return try await next(request) } } public struct DebugSuggestionServiceMiddleware: SuggestionServiceMiddleware { public init() {} - - public func getSuggestion(_ request: SuggestionRequest, next: Next) async throws -> [CodeSuggestion] { + + public func getSuggestion( + _ request: SuggestionRequest, + next: Next + ) async throws -> [CodeSuggestion] { Logger.service.debug(""" Get suggestion for \(request.fileURL) at \(request.cursorPosition) """) @@ -35,7 +59,8 @@ public struct DebugSuggestionServiceMiddleware: SuggestionServiceMiddleware { Logger.service.debug(""" Receive \(suggestions.count) suggestions for \(request.fileURL) at \(request.cursorPosition) """) - + return suggestions } } + diff --git a/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift b/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift index e9147a79..67f460bb 100644 --- a/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift +++ b/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift @@ -1,12 +1,17 @@ import Environment import Foundation +import Preferences +import SuggestionModel +import SuggestionProvider import UserDefaultsObserver import Workspace -import SuggestionService -import SuggestionModel -import Preferences public final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { + public typealias SuggestionServiceFactory = ( + _ projectRootURL: URL, + _ onServiceLaunched: @escaping (any SuggestionServiceProvider) -> Void + ) -> any SuggestionServiceProvider + let userDefaultsObserver = UserDefaultsObserver( object: UserDefaults.shared, forKeyPaths: [ UserDefaultPreferenceKeys().suggestionFeatureEnabledProjectList.key, @@ -18,9 +23,11 @@ public final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { UserDefaults.shared.value(for: \.realtimeSuggestionToggle) } - private var _suggestionService: SuggestionServiceType? + let suggestionServiceFactory: SuggestionServiceFactory - public var suggestionService: SuggestionServiceType? { + private var _suggestionService: SuggestionServiceProvider? + + public var suggestionService: SuggestionServiceProvider? { // Check if the workspace is disabled. let isSuggestionDisabledGlobally = UserDefaults.shared .value(for: \.disableSuggestionFeatureGlobally) @@ -34,7 +41,7 @@ public final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { } if _suggestionService == nil { - _suggestionService = SuggestionService(projectRootURL: projectRootURL) { + _suggestionService = suggestionServiceFactory(projectRootURL) { [weak self] _ in guard let self else { return } for (_, filespace) in filespaces { @@ -56,8 +63,12 @@ public final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { } return true } - - public override init(workspace: Workspace) { + + public init( + workspace: Workspace, + suggestionProviderFactory: @escaping SuggestionServiceFactory + ) { + self.suggestionServiceFactory = suggestionProviderFactory super.init(workspace: workspace) userDefaultsObserver.onChange = { [weak self] in @@ -66,19 +77,19 @@ public final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { } } - public override func didOpenFilespace(_ filespace: Filespace) { + override public func didOpenFilespace(_ filespace: Filespace) { notifyOpenFile(filespace: filespace) } - public override func didSaveFilespace(_ filespace: Filespace) { + override public func didSaveFilespace(_ filespace: Filespace) { notifySaveFile(filespace: filespace) } - - public override func didUpdateFilespace(_ filespace: Filespace, content: String) { + + override public func didUpdateFilespace(_ filespace: Filespace, content: String) { notifyUpdateFile(filespace: filespace, content: content) } - public override func didCloseFilespace(_ fileURL: URL) { + override public func didCloseFilespace(_ fileURL: URL) { Task { try await suggestionService?.notifyCloseTextDocument(fileURL: fileURL) } @@ -98,7 +109,7 @@ public final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { try await suggestionService?.notifyOpenTextDocument( fileURL: filespace.fileURL, - content: try String(contentsOf: filespace.fileURL, encoding: .utf8) + content: String(contentsOf: filespace.fileURL, encoding: .utf8) ) } } diff --git a/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift b/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift index a37ef931..37f06d0c 100644 --- a/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift +++ b/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift @@ -1,6 +1,6 @@ import Foundation import SuggestionModel -import SuggestionService +import SuggestionProvider import Workspace import XPCShared @@ -9,7 +9,7 @@ public extension Workspace { plugin(for: SuggestionServiceWorkspacePlugin.self) } - var suggestionService: SuggestionServiceType? { + var suggestionService: SuggestionServiceProvider? { suggestionPlugin?.suggestionService } @@ -34,7 +34,7 @@ public extension Workspace { refreshUpdateTime() let filespace = createFilespaceIfNeeded(fileURL: fileURL) - + guard !(await filespace.isGitIgnored) else { return [] } if !editor.uti.isEmpty { @@ -53,13 +53,15 @@ public extension Workspace { guard let suggestionService else { throw SuggestionFeatureDisabledError() } let completions = try await suggestionService.getSuggestions( - fileURL: fileURL, - content: editor.lines.joined(separator: ""), - cursorPosition: editor.cursorPosition, - tabSize: editor.tabSize, - indentSize: editor.indentSize, - usesTabsForIndentation: editor.usesTabsForIndentation, - ignoreSpaceOnlySuggestions: true + .init( + fileURL: fileURL, + content: editor.lines.joined(separator: ""), + cursorPosition: editor.cursorPosition, + tabSize: editor.tabSize, + indentSize: editor.indentSize, + usesTabsForIndentation: editor.usesTabsForIndentation, + ignoreSpaceOnlySuggestions: true + ) ) filespace.setSuggestions(completions) From bdf562c5d441b3f86269401b689025fe09546f20 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 24 Dec 2023 21:46:51 +0800 Subject: [PATCH 11/71] Make the task more structured --- .../XcodeInspector/XcodeWindowInspector.swift | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift b/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift index 8c7b2b2d..95c9fb64 100644 --- a/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeWindowInspector.swift @@ -26,36 +26,43 @@ public final class WorkspaceXcodeWindowInspector: XcodeWindowInspector { } public func refresh() { - updateURLs() + Task { @MainActor in updateURLs() } } public init(app: NSRunningApplication, uiElement: AXUIElement) { self.app = app super.init(uiElement: uiElement) - focusedElementChangedTask = Task { @MainActor in - updateURLs() + let notifications = AXNotificationStream( + app: app, + notificationNames: kAXFocusedUIElementChangedNotification + ) - Task { @MainActor in - // prevent that documentURL may not be available yet - try await Task.sleep(nanoseconds: 500_000_000) - if documentURL == .init(fileURLWithPath: "/") { - updateURLs() + #warning("Test Me") + focusedElementChangedTask = Task { [weak self] in + await self?.updateURLs() + + await withThrowingTaskGroup(of: Void.self) { [weak self] group in + group.addTask { [weak self] in + // prevent that documentURL may not be available yet + try await Task.sleep(nanoseconds: 500_000_000) + if self?.documentURL == .init(fileURLWithPath: "/") { + await self?.updateURLs() + } } - } - let notifications = AXNotificationStream( - app: app, - notificationNames: kAXFocusedUIElementChangedNotification - ) - - for await _ in notifications { - try Task.checkCancellation() - updateURLs() + group.addTask { [weak self] in + for await _ in notifications { + guard let self else { return } + try Task.checkCancellation() + await self.updateURLs() + } + } } } } + @MainActor func updateURLs() { let documentURL = Self.extractDocumentURL(windowElement: uiElement) if let documentURL { From 8cfe9cb8d20947ad2649c918591a1d0b22b6c7e9 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 24 Dec 2023 21:47:16 +0800 Subject: [PATCH 12/71] Remove warnings --- Core/Sources/SuggestionWidget/SharedPanelView.swift | 4 ++-- Pro | 2 +- Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Core/Sources/SuggestionWidget/SharedPanelView.swift b/Core/Sources/SuggestionWidget/SharedPanelView.swift index c16f6cc1..5abeb81f 100644 --- a/Core/Sources/SuggestionWidget/SharedPanelView.swift +++ b/Core/Sources/SuggestionWidget/SharedPanelView.swift @@ -56,7 +56,7 @@ struct SharedPanelView: View { animation: .easeInOut(duration: 0.2) ) } - } else if let promptToCode = viewStore.state.promptToCode { + } else if let _ = viewStore.state.promptToCode { IfLetStore(store.scope( state: { $0.content.promptToCodeGroup.activePromptToCode }, action: { @@ -66,7 +66,7 @@ struct SharedPanelView: View { )) { PromptToCodePanel(store: $0) } - + } else if let suggestion = viewStore.state.suggestion { switch suggestionPresentationMode { case .nearbyTextCursor: diff --git a/Pro b/Pro index a6769f7b..fd9fe0c2 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit a6769f7b0244b4f193ada0be1b9a911a1be3b5c6 +Subproject commit fd9fe0c2c2096c66bf45fafe3a4a19bba819701e diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift index 9a030198..77cff0fe 100644 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift @@ -310,7 +310,7 @@ public final class GitHubCopilotSuggestionService: GitHubCopilotBaseService, return true } .map { - var suggestion = CodeSuggestion( + let suggestion = CodeSuggestion( id: $0.uuid, text: $0.text, position: $0.position, From bc458cba1bc34b8f919dcdb56910c4653530655d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 24 Dec 2023 22:14:12 +0800 Subject: [PATCH 13/71] Remove some warnings --- .../XcodeInspector/XcodeInspector.swift | 58 +++++++++++-------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/Tool/Sources/XcodeInspector/XcodeInspector.swift b/Tool/Sources/XcodeInspector/XcodeInspector.swift index 748af74d..8622ea76 100644 --- a/Tool/Sources/XcodeInspector/XcodeInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeInspector.swift @@ -88,9 +88,11 @@ public final class XcodeInspector: ObservableObject { .first(where: \.isActive) .map(AppInstanceInspector.init(runningApplication:)) - Task { @MainActor in // Did activate app + #warning("Test Me") + + Task { // Did activate app if let activeXcode { - setActiveXcode(activeXcode) + await setActiveXcode(activeXcode) } let sequence = NSWorkspace.shared.notificationCenter @@ -101,23 +103,30 @@ public final class XcodeInspector: ObservableObject { .userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { continue } if app.isXcode { - if let existed = xcodes.first( - where: { $0.runningApplication.processIdentifier == app.processIdentifier } - ) { - setActiveXcode(existed) + if let existed = xcodes.first(where: { + $0.runningApplication.processIdentifier == app.processIdentifier + }) { + await MainActor.run { + setActiveXcode(existed) + } } else { let new = XcodeAppInstanceInspector(runningApplication: app) - xcodes.append(new) - setActiveXcode(new) + await MainActor.run { + xcodes.append(new) + setActiveXcode(new) + } } } else { - previousActiveApplication = activeApplication - activeApplication = AppInstanceInspector(runningApplication: app) + let appInspector = AppInstanceInspector(runningApplication: app) + await MainActor.run { + previousActiveApplication = activeApplication + activeApplication = appInspector + } } } } - Task { @MainActor in // Did terminate app + Task { // Did terminate app let sequence = NSWorkspace.shared.notificationCenter .notifications(named: NSWorkspace.didTerminateApplicationNotification) for await notification in sequence { @@ -126,25 +135,26 @@ public final class XcodeInspector: ObservableObject { .userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { continue } if app.isXcode { - xcodes.removeAll { - $0.runningApplication.processIdentifier == app.processIdentifier - } - if latestActiveXcode?.runningApplication.processIdentifier - == app.processIdentifier - { - latestActiveXcode = nil - } - - if let activeXcode = xcodes.first(where: \.isActive) { - setActiveXcode(activeXcode) + let processIdentifier = app.processIdentifier + await MainActor.run { + xcodes.removeAll { + $0.runningApplication.processIdentifier == processIdentifier + } + if latestActiveXcode?.runningApplication + .processIdentifier == processIdentifier + { + latestActiveXcode = nil + } + + if let activeXcode = xcodes.first(where: \.isActive) { + setActiveXcode(activeXcode) + } } } } } } - #warning("TODO: Double check before releasing 0.27.0") - @MainActor func setActiveXcode(_ xcode: XcodeAppInstanceInspector) { previousActiveApplication = activeApplication From ad1816c55930f14335cc64c2bd66681d7316df80 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 25 Dec 2023 15:03:54 +0800 Subject: [PATCH 14/71] Add new suggestionFeatureProvider key --- Core/Package.swift | 4 +++- .../SuggestionSettingsView.swift | 4 ++-- .../SuggestionService/SuggestionService.swift | 18 ++++++++++++++---- Tool/Sources/Preferences/Keys.swift | 6 +++++- .../Types/SuggestionFeatureProvider.swift | 19 +++++++++++++++++-- Tool/Sources/Preferences/UserDefaults.swift | 4 ++++ 6 files changed, 45 insertions(+), 10 deletions(-) diff --git a/Core/Package.swift b/Core/Package.swift index 474f91e5..20ef1b15 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -186,7 +186,9 @@ let package = Package( dependencies: [ .product(name: "SuggestionModel", package: "Tool"), .product(name: "SuggestionProvider", package: "Tool") - ] + ].pro([ + "ProExtension", + ]) ), .target( name: "SuggestionInjector", diff --git a/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift index e2196d2b..9d74851b 100644 --- a/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift @@ -56,9 +56,9 @@ struct SuggestionSettingsView: View { ForEach(BuiltInSuggestionFeatureProvider.allCases, id: \.rawValue) { switch $0 { case .gitHubCopilot: - Text("GitHub Copilot").tag($0) + Text("GitHub Copilot").tag(SuggestionFeatureProvider.builtIn($0)) case .codeium: - Text("Codeium").tag($0) + Text("Codeium").tag(SuggestionFeatureProvider.builtIn($0)) } } } label: { diff --git a/Core/Sources/SuggestionService/SuggestionService.swift b/Core/Sources/SuggestionService/SuggestionService.swift index fe20c9e3..0de9970a 100644 --- a/Core/Sources/SuggestionService/SuggestionService.swift +++ b/Core/Sources/SuggestionService/SuggestionService.swift @@ -4,6 +4,10 @@ import SuggestionModel import SuggestionProvider import UserDefaultsObserver +#if canImport(ProExtension) +import ProExtension +#endif + public protocol SuggestionServiceType: SuggestionServiceProvider {} public actor SuggestionService: SuggestionServiceType { @@ -15,13 +19,13 @@ public actor SuggestionService: SuggestionServiceType { let onServiceLaunched: (SuggestionServiceProvider) -> Void let providerChangeObserver = UserDefaultsObserver( object: UserDefaults.shared, - forKeyPaths: [UserDefaultPreferenceKeys().suggestionFeatureProvider.key], + forKeyPaths: [UserDefaultPreferenceKeys().oldSuggestionFeatureProvider.key], context: nil ) lazy var suggestionProvider: SuggestionServiceProvider = buildService() - var serviceType: BuiltInSuggestionFeatureProvider { + var serviceType: SuggestionFeatureProvider { UserDefaults.shared.value(for: \.suggestionFeatureProvider) } @@ -41,13 +45,19 @@ public actor SuggestionService: SuggestionServiceType { } func buildService() -> SuggestionServiceProvider { + #if canImport(ProExtension) + if let provider = ProExtension.suggestionProviderFactory(serviceType) { + return provider + } + #endif + switch serviceType { - case .codeium: + case .builtIn(.codeium): return CodeiumSuggestionProvider( projectRootURL: projectRootURL, onServiceLaunched: onServiceLaunched ) - case .gitHubCopilot: + case .builtIn(.gitHubCopilot), .extension: return GitHubCopilotSuggestionProvider( projectRootURL: projectRootURL, onServiceLaunched: onServiceLaunched diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 0508ce40..200c2fba 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -286,9 +286,13 @@ public extension UserDefaultPreferenceKeys { // MARK: - Suggestion public extension UserDefaultPreferenceKeys { - var suggestionFeatureProvider: PreferenceKey { + var oldSuggestionFeatureProvider: DeprecatedPreferenceKey { .init(defaultValue: .gitHubCopilot, key: "SuggestionFeatureProvider") } + + var suggestionFeatureProvider: PreferenceKey { + .init(defaultValue: .builtIn(.gitHubCopilot), key: "NewSuggestionFeatureProvider") + } var realtimeSuggestionToggle: PreferenceKey { .init(defaultValue: true, key: "RealtimeSuggestionToggle") diff --git a/Tool/Sources/Preferences/Types/SuggestionFeatureProvider.swift b/Tool/Sources/Preferences/Types/SuggestionFeatureProvider.swift index 48644bb4..de321450 100644 --- a/Tool/Sources/Preferences/Types/SuggestionFeatureProvider.swift +++ b/Tool/Sources/Preferences/Types/SuggestionFeatureProvider.swift @@ -5,8 +5,23 @@ public enum BuiltInSuggestionFeatureProvider: Int, CaseIterable, Codable { case codeium } -public enum SuggestionFeatureProvider: Codable { +public enum SuggestionFeatureProvider: Codable, RawRepresentable, Hashable { case builtIn(BuiltInSuggestionFeatureProvider) - case extended(name: String, bundleIdentifier: String) + case `extension`(name: String, bundleIdentifier: String) + + public init?(rawValue: String) { + guard let data = rawValue.data(using: .utf8), + let value = try? JSONDecoder().decode(Self.self, from: data) + else { return nil } + + self = value + } + + public var rawValue: String { + if let data = try? JSONEncoder().encode(self) { + return String(data: data, encoding: .utf8) ?? "" + } + return "" + } } diff --git a/Tool/Sources/Preferences/UserDefaults.swift b/Tool/Sources/Preferences/UserDefaults.swift index 9370bf6c..63b88eb1 100644 --- a/Tool/Sources/Preferences/UserDefaults.swift +++ b/Tool/Sources/Preferences/UserDefaults.swift @@ -21,6 +21,10 @@ public extension UserDefaults { shared.setupDefaultValue(for: \.runNodeWith, defaultValue: .env) shared.setupDefaultValue(for: \.chatModels) shared.setupDefaultValue(for: \.embeddingModels) + shared.setupDefaultValue( + for: \.suggestionFeatureProvider, + defaultValue: .builtIn(shared.value(for: \.oldSuggestionFeatureProvider)) + ) } } From 5f80e5c5c6068a8c52809483723e7c2fb87612bc Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 25 Dec 2023 15:12:47 +0800 Subject: [PATCH 15/71] Remove warning --- Tool/Sources/Preferences/UserDefaults.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tool/Sources/Preferences/UserDefaults.swift b/Tool/Sources/Preferences/UserDefaults.swift index 63b88eb1..fb5244d8 100644 --- a/Tool/Sources/Preferences/UserDefaults.swift +++ b/Tool/Sources/Preferences/UserDefaults.swift @@ -23,7 +23,7 @@ public extension UserDefaults { shared.setupDefaultValue(for: \.embeddingModels) shared.setupDefaultValue( for: \.suggestionFeatureProvider, - defaultValue: .builtIn(shared.value(for: \.oldSuggestionFeatureProvider)) + defaultValue: .builtIn(shared.deprecatedValue(for: \.oldSuggestionFeatureProvider)) ) } } From 8593273f9495927c73e9ae4930aa9b9d7590e00a Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 25 Dec 2023 16:43:55 +0800 Subject: [PATCH 16/71] Fix a crash --- .../Types/SuggestionFeatureProvider.swift | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/Tool/Sources/Preferences/Types/SuggestionFeatureProvider.swift b/Tool/Sources/Preferences/Types/SuggestionFeatureProvider.swift index de321450..b6f2eeb5 100644 --- a/Tool/Sources/Preferences/Types/SuggestionFeatureProvider.swift +++ b/Tool/Sources/Preferences/Types/SuggestionFeatureProvider.swift @@ -5,20 +5,35 @@ public enum BuiltInSuggestionFeatureProvider: Int, CaseIterable, Codable { case codeium } -public enum SuggestionFeatureProvider: Codable, RawRepresentable, Hashable { +public enum SuggestionFeatureProvider: RawRepresentable, Hashable { case builtIn(BuiltInSuggestionFeatureProvider) case `extension`(name: String, bundleIdentifier: String) + enum Storage: Codable { + case builtIn(BuiltInSuggestionFeatureProvider) + case `extension`(name: String, bundleIdentifier: String) + } + public init?(rawValue: String) { guard let data = rawValue.data(using: .utf8), - let value = try? JSONDecoder().decode(Self.self, from: data) + let value = try? JSONDecoder().decode(Storage.self, from: data) else { return nil } - self = value + switch value { + case let .builtIn(provider): + self = .builtIn(provider) + case let .extension(name, bundleIdentifier): + self = .extension(name: name, bundleIdentifier: bundleIdentifier) + } } public var rawValue: String { - if let data = try? JSONEncoder().encode(self) { + let storage: Storage = switch self { + case let .builtIn(provider): .builtIn(provider) + case let .extension(name, bundleIdentifier): + .extension(name: name, bundleIdentifier: bundleIdentifier) + } + if let data = try? JSONEncoder().encode(storage) { return String(data: data, encoding: .utf8) ?? "" } return "" From af9c68822661cd3d29159832b90c98ada0fa10b6 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 25 Dec 2023 23:37:51 +0800 Subject: [PATCH 17/71] Add a Toast Reducer --- Tool/Sources/Toast/Toast.swift | 58 ++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/Tool/Sources/Toast/Toast.swift b/Tool/Sources/Toast/Toast.swift index 76c37eb0..1302a6b6 100644 --- a/Tool/Sources/Toast/Toast.swift +++ b/Tool/Sources/Toast/Toast.swift @@ -1,6 +1,7 @@ +import ComposableArchitecture +import Dependencies import Foundation import SwiftUI -import Dependencies public enum ToastType { case info @@ -28,14 +29,12 @@ public extension DependencyValues { get { self[ToastControllerDependencyKey.self] } set { self[ToastControllerDependencyKey.self] = newValue } } - - var toast: (String, ToastType) -> Void { - get { toastController.toast } - } + + var toast: (String, ToastType) -> Void { toastController.toast } } public class ToastController: ObservableObject { - public struct Message: Identifiable { + public struct Message: Identifiable, Equatable { public var id: UUID public var type: ToastType public var content: Text @@ -69,3 +68,50 @@ public class ToastController: ObservableObject { } } +public struct Toast: ReducerProtocol { + public typealias Message = ToastController.Message + public struct State: Equatable { + public var messages: [Message] = [] + } + + public enum Action: Equatable { + case appear + case updateMessages([Message]) + case toast(String, ToastType) + } + + @Dependency(\.toastController) var toastController + + struct CancelID: Hashable {} + + public init() {} + + public var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .appear: + return .run { send in + let stream = AsyncStream<[Message]> { continuation in + let cancellable = toastController.$messages.sink { newValue in + continuation.yield(newValue) + } + continuation.onTermination = { _ in + cancellable.cancel() + } + } + for await newValue in stream { + try Task.checkCancellation() + await send(.updateMessages(newValue)) + } + }.cancellable(id: CancelID(), cancelInFlight: true) + case let .updateMessages(messages): + state.messages = messages + return .none + case let .toast(content, type): + toastController.toast(content: content, type: type) + return .none + } + } + } +} + From f2e788876008e34fec970f8ecce96a8b2eee591d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 27 Dec 2023 14:58:18 +0800 Subject: [PATCH 18/71] Add Toast to WidgetFeature --- .../FeatureReducers/WidgetFeature.swift | 24 +++++++++++++++---- Tool/Sources/Toast/Toast.swift | 11 +++++++-- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift index 5409ed95..8709dd9d 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift @@ -7,6 +7,7 @@ import Environment import Foundation import Preferences import SwiftUI +import Toast import XcodeInspector public struct WidgetFeature: ReducerProtocol { @@ -32,6 +33,8 @@ public struct WidgetFeature: ReducerProtocol { var focusingDocumentURL: URL? public var colorScheme: ColorScheme = .light + public var toast = Toast.State() + // MARK: Panels public var panelState = PanelFeature.State() @@ -120,6 +123,7 @@ public struct WidgetFeature: ReducerProtocol { case updateWindowOpacityFinished case updateKeyWindow(WindowCanBecomeKey) + case toast(Toast.Action) case panel(PanelFeature.Action) case chatPanel(ChatPanelFeature.Action) case circularWidget(CircularWidgetFeature.Action) @@ -143,6 +147,10 @@ public struct WidgetFeature: ReducerProtocol { public init() {} public var body: some ReducerProtocol { + Scope(state: \.toast, action: /Action.toast) { + Toast() + } + Scope(state: \._circularWidgetState, action: /Action.circularWidget) { CircularWidgetFeature() } @@ -227,11 +235,14 @@ public struct WidgetFeature: ReducerProtocol { switch action { case .startup: return .merge( - .run { send in await send(.observeActiveApplicationChange) }, - .run { send in await send(.observeCompletionPanelChange) }, - .run { send in await send(.observeFullscreenChange) }, - .run { send in await send(.observeColorSchemeChange) }, - .run { send in await send(.observePresentationModeChange) } + .run { send in + await send(.toast(.start)) + await send(.observeActiveApplicationChange) + await send(.observeCompletionPanelChange) + await send(.observeFullscreenChange) + await send(.observeColorSchemeChange) + await send(.observePresentationModeChange) + } ) case .observeActiveApplicationChange: @@ -628,6 +639,9 @@ public struct WidgetFeature: ReducerProtocol { await windows.sharedPanelWindow.makeKeyAndOrderFront(nil) } } + + case .toast: + return .none case .circularWidget: return .none diff --git a/Tool/Sources/Toast/Toast.swift b/Tool/Sources/Toast/Toast.swift index 1302a6b6..4d4642e1 100644 --- a/Tool/Sources/Toast/Toast.swift +++ b/Tool/Sources/Toast/Toast.swift @@ -71,11 +71,16 @@ public class ToastController: ObservableObject { public struct Toast: ReducerProtocol { public typealias Message = ToastController.Message public struct State: Equatable { + var isObservingToastController = false public var messages: [Message] = [] + + public init(messages: [Message] = []) { + self.messages = messages + } } public enum Action: Equatable { - case appear + case start case updateMessages([Message]) case toast(String, ToastType) } @@ -89,7 +94,9 @@ public struct Toast: ReducerProtocol { public var body: some ReducerProtocol { Reduce { state, action in switch action { - case .appear: + case .start: + guard !state.isObservingToastController else { return .none } + state.isObservingToastController = true return .run { send in let stream = AsyncStream<[Message]> { continuation in let cancellable = toastController.$messages.sink { newValue in From c641ed09266e4dee843c5c3b5d907962c8834af7 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 27 Dec 2023 16:45:29 +0800 Subject: [PATCH 19/71] Support toast in widgets --- Core/Package.swift | 1 + Core/Sources/Service/Service.swift | 3 ++ .../FeatureReducers/ToastPanel.swift | 35 ++++++++++++ .../FeatureReducers/WidgetFeature.swift | 41 +++++++------- .../SuggestionWidget/ModuleDependency.swift | 4 +- .../ToastPanelView.swift | 54 +++++++++++++++++++ .../SuggestionWidgetController.swift | 53 +++++++++--------- Core/Sources/SuggestionWidget/TabView.swift | 49 ----------------- Tool/Sources/Toast/Toast.swift | 2 +- 9 files changed, 142 insertions(+), 100 deletions(-) create mode 100644 Core/Sources/SuggestionWidget/FeatureReducers/ToastPanel.swift create mode 100644 Core/Sources/SuggestionWidget/SuggestionPanelContent/ToastPanelView.swift delete mode 100644 Core/Sources/SuggestionWidget/TabView.swift diff --git a/Core/Package.swift b/Core/Package.swift index 20ef1b15..50a6e18e 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -273,6 +273,7 @@ let package = Package( dependencies: [ "PromptToCodeService", "ChatGPTChatTab", + .product(name: "Toast", package: "Tool"), .product(name: "UserDefaultsObserver", package: "Tool"), .product(name: "SharedUIComponents", package: "Tool"), .product(name: "AppMonitoring", package: "Tool"), diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift index 0cec486c..c486e6a4 100644 --- a/Core/Sources/Service/Service.swift +++ b/Core/Sources/Service/Service.swift @@ -1,6 +1,7 @@ import Dependencies import Foundation import SuggestionService +import Toast import Workspace import WorkspaceSuggestionService import XcodeInspector @@ -30,6 +31,8 @@ public final class Service { let proService: ProService #endif + @Dependency(\.toast) var toast + private init() { @Dependency(\.workspacePool) var workspacePool diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/ToastPanel.swift b/Core/Sources/SuggestionWidget/FeatureReducers/ToastPanel.swift new file mode 100644 index 00000000..c70c60a7 --- /dev/null +++ b/Core/Sources/SuggestionWidget/FeatureReducers/ToastPanel.swift @@ -0,0 +1,35 @@ +import ComposableArchitecture +import Environment +import Preferences +import SwiftUI +import Toast + +public struct ToastPanel: ReducerProtocol { + public struct State: Equatable { + var toast: Toast.State = .init() + var colorScheme: ColorScheme = .light + var alignTopToAnchor = false + } + + public enum Action: Equatable { + case start + case toast(Toast.Action) + } + + public var body: some ReducerProtocol { + Scope(state: \.toast, action: /Action.toast) { + Toast() + } + + Reduce { state, action in + switch action { + case .start: + return .run { send in + await send(.toast(.start)) + } + case .toast: + return .none + } + } + } +} diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift index 8709dd9d..8578ab67 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift @@ -16,14 +16,6 @@ public struct WidgetFeature: ReducerProtocol { var frame: CGRect = .zero } - public struct Windows: Equatable { - public var widgetWindowState = WindowState() - public var chatWindowState = WindowState() - public var suggestionPanelWindowState = WindowState() - public var sharedPanelWindowState = WindowState() - public var tabWindowState = WindowState() - } - public enum WindowCanBecomeKey: Equatable { case sharedPanel case chatPanel @@ -33,8 +25,8 @@ public struct WidgetFeature: ReducerProtocol { var focusingDocumentURL: URL? public var colorScheme: ColorScheme = .light - public var toast = Toast.State() - + var toastPanel = ToastPanel.State() + // MARK: Panels public var panelState = PanelFeature.State() @@ -123,7 +115,7 @@ public struct WidgetFeature: ReducerProtocol { case updateWindowOpacityFinished case updateKeyWindow(WindowCanBecomeKey) - case toast(Toast.Action) + case toastPanel(ToastPanel.Action) case panel(PanelFeature.Action) case chatPanel(ChatPanelFeature.Action) case circularWidget(CircularWidgetFeature.Action) @@ -147,10 +139,10 @@ public struct WidgetFeature: ReducerProtocol { public init() {} public var body: some ReducerProtocol { - Scope(state: \.toast, action: /Action.toast) { - Toast() + Scope(state: \.toastPanel, action: /Action.toastPanel) { + ToastPanel() } - + Scope(state: \._circularWidgetState, action: /Action.circularWidget) { CircularWidgetFeature() } @@ -236,7 +228,7 @@ public struct WidgetFeature: ReducerProtocol { case .startup: return .merge( .run { send in - await send(.toast(.start)) + await send(.toastPanel(.start)) await send(.observeActiveApplicationChange) await send(.observeCompletionPanelChange) await send(.observeFullscreenChange) @@ -490,6 +482,7 @@ public struct WidgetFeature: ReducerProtocol { }() state.colorScheme = scheme + state.toastPanel.colorScheme = scheme state.panelState.sharedPanelState.colorScheme = scheme state.panelState.suggestionPanelState.colorScheme = scheme state.chatPanelState.colorScheme = scheme @@ -514,6 +507,10 @@ public struct WidgetFeature: ReducerProtocol { state.panelState.suggestionPanelState.isPanelOutOfFrame = true } + state.toastPanel.alignTopToAnchor = widgetLocation + .defaultPanelLocation + .alignPanelTop + let isChatPanelDetached = state.chatPanelState.chatPanelInASeparateWindow return .run { _ in @@ -523,8 +520,8 @@ public struct WidgetFeature: ReducerProtocol { display: false, animate: animated ) - windows.tabWindow.setFrame( - widgetLocation.tabFrame, + windows.toastWindow.setFrame( + widgetLocation.defaultPanelLocation.frame, display: false, animate: animated ) @@ -582,7 +579,7 @@ public struct WidgetFeature: ReducerProtocol { windows.sharedPanelWindow.alphaValue = noFocus ? 0 : 1 windows.suggestionPanelWindow.alphaValue = noFocus ? 0 : 1 windows.widgetWindow.alphaValue = noFocus ? 0 : 1 - windows.tabWindow.alphaValue = 0 + windows.toastWindow.alphaValue = noFocus ? 0 : 1 if isChatPanelDetached { windows.chatPanelWindow.alphaValue = hasChat ? 1 : 0 @@ -604,7 +601,7 @@ public struct WidgetFeature: ReducerProtocol { windows.sharedPanelWindow.alphaValue = noFocus ? 0 : 1 windows.suggestionPanelWindow.alphaValue = noFocus ? 0 : 1 windows.widgetWindow.alphaValue = noFocus ? 0 : 1 - windows.tabWindow.alphaValue = 0 + windows.toastWindow.alphaValue = noFocus ? 0 : 1 if isChatPanelDetached { windows.chatPanelWindow.alphaValue = hasChat ? 1 : 0 } else { @@ -615,7 +612,7 @@ public struct WidgetFeature: ReducerProtocol { windows.sharedPanelWindow.alphaValue = 0 windows.suggestionPanelWindow.alphaValue = 0 windows.widgetWindow.alphaValue = 0 - windows.tabWindow.alphaValue = 0 + windows.toastWindow.alphaValue = 0 if !isChatPanelDetached { windows.chatPanelWindow.alphaValue = 0 } @@ -639,8 +636,8 @@ public struct WidgetFeature: ReducerProtocol { await windows.sharedPanelWindow.makeKeyAndOrderFront(nil) } } - - case .toast: + + case .toastPanel: return .none case .circularWidget: diff --git a/Core/Sources/SuggestionWidget/ModuleDependency.swift b/Core/Sources/SuggestionWidget/ModuleDependency.swift index 3fbebeab..c50e820a 100644 --- a/Core/Sources/SuggestionWidget/ModuleDependency.swift +++ b/Core/Sources/SuggestionWidget/ModuleDependency.swift @@ -22,17 +22,17 @@ public final class SuggestionWidgetControllerDependency { public final class WidgetWindows { var fullscreenDetector: NSWindow! var widgetWindow: NSWindow! - var tabWindow: NSWindow! var sharedPanelWindow: NSWindow! var suggestionPanelWindow: NSWindow! var chatPanelWindow: NSWindow! + var toastWindow: NSWindow! nonisolated init() {} func orderFront() { widgetWindow?.orderFrontRegardless() - tabWindow?.orderFrontRegardless() + toastWindow?.orderFrontRegardless() sharedPanelWindow?.orderFrontRegardless() suggestionPanelWindow?.orderFrontRegardless() chatPanelWindow?.orderFrontRegardless() diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ToastPanelView.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ToastPanelView.swift new file mode 100644 index 00000000..69c30e61 --- /dev/null +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ToastPanelView.swift @@ -0,0 +1,54 @@ +import ComposableArchitecture +import Dependencies +import Foundation +import SwiftUI +import Toast + +struct ToastPanelView: View { + let store: StoreOf + + struct ViewState: Equatable { + let colorScheme: ColorScheme + let alignTopToAnchor: Bool + } + + var body: some View { + WithViewStore(store, observe: { + ViewState( + colorScheme: $0.colorScheme, + alignTopToAnchor: $0.alignTopToAnchor + ) + }) { viewStore in + VStack(spacing: 4) { + if !viewStore.alignTopToAnchor { + Spacer() + } + + WithViewStore(store, observe: \.toast.messages) { viewStore in + ForEach(viewStore.state) { message in + message.content + .foregroundColor(.white) + .padding(8) + .frame(maxWidth: .infinity) + .background({ + switch message.type { + case .info: return Color(nsColor: .systemIndigo) + case .error: return Color(nsColor: .systemRed) + case .warning: return Color(nsColor: .systemOrange) + } + }() as Color, in: RoundedRectangle(cornerRadius: 8)) + .shadow(color: Color.black.opacity(0.2), radius: 4) + } + } + + if viewStore.alignTopToAnchor { + Spacer() + } + } + .colorScheme(viewStore.colorScheme) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .allowsHitTesting(false) + } +} + diff --git a/Core/Sources/SuggestionWidget/SuggestionWidgetController.swift b/Core/Sources/SuggestionWidget/SuggestionWidgetController.swift index cc252db4..15312c32 100644 --- a/Core/Sources/SuggestionWidget/SuggestionWidgetController.swift +++ b/Core/Sources/SuggestionWidget/SuggestionWidgetController.swift @@ -58,30 +58,6 @@ public final class SuggestionWidgetController: NSObject { return it }() - private lazy var tabWindow = { - let it = CanBecomeKeyWindow( - contentRect: .zero, - styleMask: .borderless, - backing: .buffered, - defer: false - ) - it.isReleasedWhenClosed = false - it.isOpaque = false - it.backgroundColor = .clear - it.level = .floating - it.collectionBehavior = [.fullScreenAuxiliary, .transient] - it.hasShadow = true - it.contentView = NSHostingView( - rootView: TabView(store: store.scope( - state: \.chatPanelState, - action: WidgetFeature.Action.chatPanel - )) - ) - it.setIsVisible(true) - it.canBecomeKeyChecker = { false } - return it - }() - private lazy var sharedPanelWindow = { let it = CanBecomeKeyWindow( contentRect: .init(x: 0, y: 0, width: Style.panelWidth, height: Style.panelHeight), @@ -171,6 +147,31 @@ public final class SuggestionWidgetController: NSObject { return it }() + private lazy var toastWindow = { + let it = CanBecomeKeyWindow( + contentRect: .zero, + styleMask: [.borderless], + backing: .buffered, + defer: false + ) + it.isReleasedWhenClosed = false + it.isOpaque = true + it.backgroundColor = .clear + it.level = .floating + it.collectionBehavior = [.fullScreenAuxiliary, .transient] + it.hasShadow = false + it.contentView = NSHostingView( + rootView: ToastPanelView(store: store.scope( + state: \.toastPanel, + action: WidgetFeature.Action.toastPanel + )) + ) + it.setIsVisible(true) + it.ignoresMouseEvents = true + it.canBecomeKeyChecker = { false } + return it + }() + let store: StoreOf let viewStore: ViewStoreOf let chatTabPool: ChatTabPool @@ -193,7 +194,7 @@ public final class SuggestionWidgetController: NSObject { if ProcessInfo.processInfo.environment["IS_UNIT_TEST"] == "YES" { return } dependency.windows.chatPanelWindow = chatPanelWindow - dependency.windows.tabWindow = tabWindow + dependency.windows.toastWindow = toastWindow dependency.windows.sharedPanelWindow = sharedPanelWindow dependency.windows.suggestionPanelWindow = suggestionPanelWindow dependency.windows.fullscreenDetector = fullscreenDetector @@ -223,7 +224,7 @@ public extension SuggestionWidgetController { } func presentError(_ errorDescription: String) { - store.send(.panel(.presentError(errorDescription))) + store.send(.toastPanel(.toast(.toast(errorDescription, .error)))) } func presentChatRoom() { diff --git a/Core/Sources/SuggestionWidget/TabView.swift b/Core/Sources/SuggestionWidget/TabView.swift deleted file mode 100644 index 82166169..00000000 --- a/Core/Sources/SuggestionWidget/TabView.swift +++ /dev/null @@ -1,49 +0,0 @@ -import ComposableArchitecture -import SwiftUI - -struct TabView: View { - let store: StoreOf - - struct State: Equatable { - var chatPanelInASeparateWindow: Bool - var colorScheme: ColorScheme - } - - var body: some View { - WithViewStore( - store, - observe: { - State( - chatPanelInASeparateWindow: $0.chatPanelInASeparateWindow, - colorScheme: $0.colorScheme - ) - } - ) { viewStore in - Button(action: { - viewStore.send(.toggleChatPanelDetachedButtonClicked) - }, label: { - Image(systemName: "ellipsis.bubble.fill") - .frame(width: Style.widgetWidth, height: Style.widgetHeight) - .background( - Color.userChatContentBackground, - in: Circle() - ) - }) - .buttonStyle(.plain) - .opacity(viewStore.chatPanelInASeparateWindow ? 1 : 0) - .preferredColorScheme(viewStore.colorScheme) - .frame(maxWidth: Style.widgetWidth, maxHeight: Style.widgetHeight) - } - } -} - -struct TabView_Preview: PreviewProvider { - static var previews: some View { - VStack { - TabView(store: .init(initialState: .init(), reducer: ChatPanelFeature())) - } - .frame(width: 30) - .background(Color.black) - } -} - diff --git a/Tool/Sources/Toast/Toast.swift b/Tool/Sources/Toast/Toast.swift index 4d4642e1..3aca842d 100644 --- a/Tool/Sources/Toast/Toast.swift +++ b/Tool/Sources/Toast/Toast.swift @@ -108,7 +108,7 @@ public struct Toast: ReducerProtocol { } for await newValue in stream { try Task.checkCancellation() - await send(.updateMessages(newValue)) + await send(.updateMessages(newValue), animation: .linear(duration: 0.2)) } }.cancellable(id: CancelID(), cancelInFlight: true) case let .updateMessages(messages): From 2e7caa600b688d630a31a66b729837fa0f48718d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 28 Dec 2023 03:12:31 +0800 Subject: [PATCH 20/71] Update XPCService --- Core/Sources/Client/AsyncXPCService.swift | 42 ++++++-- Core/Sources/Service/XPCService.swift | 36 +++++-- .../XPCShared/XPCServiceProtocol.swift | 95 ++++++++++++++++++- 3 files changed, 155 insertions(+), 18 deletions(-) diff --git a/Core/Sources/Client/AsyncXPCService.swift b/Core/Sources/Client/AsyncXPCService.swift index bd366d0f..36729d5f 100644 --- a/Core/Sources/Client/AsyncXPCService.swift +++ b/Core/Sources/Client/AsyncXPCService.swift @@ -20,7 +20,7 @@ public struct AsyncXPCService { } } } - + public func getXPCServiceAccessibilityPermission() async throws -> Bool { try await withXPCServiceConnected(connection: connection) { service, continuation in @@ -85,7 +85,7 @@ public struct AsyncXPCService { { $0.getRealtimeSuggestedCode } ) } - + public func getPromptToCodeAcceptedCode(editorContent: EditorContent) async throws -> UpdatedContent? { @@ -144,7 +144,7 @@ public struct AsyncXPCService { { service in { service.customCommand(id: id, editorContent: $0, withReply: $1) } } ) } - + public func postNotification(name: String) async throws { try await withXPCServiceConnected(connection: connection) { service, continuation in @@ -153,17 +153,41 @@ public struct AsyncXPCService { } } } - - public func performAction(name: String, arguments: String) async throws -> String { - try await withXPCServiceConnected(connection: connection) { - service, continuation in - service.performAction(name: name, arguments: arguments) { - continuation.resume($0) + + public func send( + requestBody: M + ) async throws -> M.ResponseBody { + try await withXPCServiceConnected(connection: connection) { service, continuation in + do { + let requestBodyData = try JSONEncoder().encode(requestBody) + service.send(endpoint: M.endpoint, requestBody: requestBodyData) { data, error in + if let error { + continuation.reject(error) + } else { + do { + guard let data = data else { + continuation.reject(NoDataError()) + return + } + let responseBody = try JSONDecoder().decode( + M.ResponseBody.self, + from: data + ) + continuation.resume(responseBody) + } catch { + continuation.reject(error) + } + } + } + } catch { + continuation.reject(error) } } } } +struct NoDataError: Error {} + struct AutoFinishContinuation { var continuation: AsyncThrowingStream.Continuation diff --git a/Core/Sources/Service/XPCService.swift b/Core/Sources/Service/XPCService.swift index 3e5d8d06..7871cc8f 100644 --- a/Core/Sources/Service/XPCService.swift +++ b/Core/Sources/Service/XPCService.swift @@ -45,7 +45,7 @@ public class XPCService: NSObject, XPCServiceProtocol { return } try Task.checkCancellation() - reply(try JSONEncoder().encode(updatedContent), nil) + try reply(JSONEncoder().encode(updatedContent), nil) } catch { Logger.service.error("\(file):\(line) \(error.localizedDescription)") reply(nil, NSError.from(error)) @@ -102,7 +102,7 @@ public class XPCService: NSObject, XPCServiceProtocol { try await handler.acceptSuggestion(editor: editor) } } - + public func getPromptToCodeAcceptedCode( editorContent: Data, withReply reply: @escaping (Data?, Error?) -> Void @@ -191,12 +191,34 @@ public class XPCService: NSObject, XPCServiceProtocol { NSWorkspace.shared.notificationCenter.post(name: .init(name), object: nil) } - public func performAction( - name: String, - arguments: String, - withReply reply: @escaping (String) -> Void + // MARK: - Requests + + public func send( + endpoint: String, + requestBody: Data, + reply: @escaping (Data?, Error?) -> Void ) { - reply("None") + do { + try ExtensionServiceRequests.OpenExtensionManager.handle( + endpoint: endpoint, + requestBody: requestBody, + reply: reply + ) { _ in + .none + } + + try ExtensionServiceRequests.GetExtensionSuggestionServices.handle( + endpoint: endpoint, + requestBody: requestBody, + reply: reply + ) { _ in + [] + } + } catch is XPCRequestHandlerHitError { + return + } catch { + reply(nil, error) + } } } diff --git a/Tool/Sources/XPCShared/XPCServiceProtocol.swift b/Tool/Sources/XPCShared/XPCServiceProtocol.swift index af74b11d..df45ac04 100644 --- a/Tool/Sources/XPCShared/XPCServiceProtocol.swift +++ b/Tool/Sources/XPCShared/XPCServiceProtocol.swift @@ -1,5 +1,5 @@ -import SuggestionModel import Foundation +import SuggestionModel @objc(XPCServiceProtocol) public protocol XPCServiceProtocol { @@ -55,5 +55,96 @@ public protocol XPCServiceProtocol { func getXPCServiceVersion(withReply reply: @escaping (String, String) -> Void) func getXPCServiceAccessibilityPermission(withReply reply: @escaping (Bool) -> Void) func postNotification(name: String, withReply reply: @escaping () -> Void) - func performAction(name: String, arguments: String, withReply reply: @escaping (String) -> Void) + func send(endpoint: String, requestBody: Data, reply: @escaping (Data?, Error?) -> Void) +} + +public struct NoResponse: Codable { + public static let none = NoResponse() +} + +public protocol ExtensionServiceRequestType: Codable { + associatedtype ResponseBody: Codable + static var endpoint: String { get } +} + +public enum ExtensionServiceRequests { + public struct OpenExtensionManager: ExtensionServiceRequestType { + public typealias ResponseBody = NoResponse + public static let endpoint = "OpenExtensionManager" + + public init() {} + } + + public struct GetExtensionSuggestionServices: ExtensionServiceRequestType { + public struct ServiceInfo: Codable { + public var bundleIdentifier: String + public var name: String + } + + public typealias ResponseBody = [ServiceInfo] + public static let endpoint = "GetExtensionSuggestionServices" + + public init() {} + } +} + +public struct XPCRequestHandlerHitError: Error, LocalizedError { + public var errorDescription: String? { + "This is not an actual error, it just indicates a request handler was hit, and no more check is needed." + } + + public init() {} } + +public struct XPCRequestNotHandledError: Error, LocalizedError { + public var errorDescription: String? { + "The request was not handled by the XPC server." + } + + public init() {} +} + +extension ExtensionServiceRequestType { + /// A helper method to handle requests. + static func _handle( + endpoint: String, + requestBody data: Data, + reply: @escaping (Data?, Error?) -> Void, + handler: @escaping (Request) async throws -> Response + ) throws { + guard endpoint == Self.endpoint else { + return + } + do { + let requestBody = try JSONDecoder().decode(Request.self, from: data) + Task { + do { + let responseBody = try await handler(requestBody) + let responseBodyData = try JSONEncoder().encode(responseBody) + reply(responseBodyData, nil) + } catch { + reply(nil, error) + } + } + } catch { + reply(nil, error) + } + throw XPCRequestHandlerHitError() + } + + public static func handle( + endpoint: String, + requestBody data: Data, + reply: @escaping (Data?, Error?) -> Void, + handler: @escaping (Self) async throws -> Self.ResponseBody + ) throws { + try _handle( + endpoint: endpoint, + requestBody: data, + reply: reply + ) { (request: Self) async throws -> Self.ResponseBody in + try await handler(request) + } + } +} + From b878b136c4e8f09c03918c515ef6b6cd7ab22c62 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 28 Dec 2023 17:00:14 +0800 Subject: [PATCH 21/71] Handle requests from XPCService --- Core/Sources/Service/Service.swift | 25 ++++++++++++++++- Core/Sources/Service/XPCService.swift | 28 ++++--------------- Pro | 2 +- .../XPCShared/XPCServiceProtocol.swift | 5 ++++ 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift index c486e6a4..a517c15c 100644 --- a/Core/Sources/Service/Service.swift +++ b/Core/Sources/Service/Service.swift @@ -5,6 +5,7 @@ import Toast import Workspace import WorkspaceSuggestionService import XcodeInspector +import XPCShared #if canImport(ProService) import ProService @@ -32,7 +33,7 @@ public final class Service { #endif @Dependency(\.toast) var toast - + private init() { @Dependency(\.workspacePool) var workspacePool @@ -71,3 +72,25 @@ public final class Service { } } +public extension Service { + func handleXPCServiceRequests( + endpoint: String, + requestBody: Data, + reply: @escaping (Data?, Error?) -> Void + ) { + do { + #if canImport(ProService) + try Service.shared.proService.handleXPCServiceRequests( + endpoint: endpoint, + requestBody: requestBody, + reply: reply + ) + #endif + } catch is XPCRequestHandlerHitError { + return + } catch { + reply(nil, error) + } + } +} + diff --git a/Core/Sources/Service/XPCService.swift b/Core/Sources/Service/XPCService.swift index 7871cc8f..3a8ed45f 100644 --- a/Core/Sources/Service/XPCService.swift +++ b/Core/Sources/Service/XPCService.swift @@ -192,33 +192,17 @@ public class XPCService: NSObject, XPCServiceProtocol { } // MARK: - Requests - + public func send( endpoint: String, requestBody: Data, reply: @escaping (Data?, Error?) -> Void ) { - do { - try ExtensionServiceRequests.OpenExtensionManager.handle( - endpoint: endpoint, - requestBody: requestBody, - reply: reply - ) { _ in - .none - } - - try ExtensionServiceRequests.GetExtensionSuggestionServices.handle( - endpoint: endpoint, - requestBody: requestBody, - reply: reply - ) { _ in - [] - } - } catch is XPCRequestHandlerHitError { - return - } catch { - reply(nil, error) - } + Service.shared.handleXPCServiceRequests( + endpoint: endpoint, + requestBody: requestBody, + reply: reply + ) } } diff --git a/Pro b/Pro index fd9fe0c2..f9b74993 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit fd9fe0c2c2096c66bf45fafe3a4a19bba819701e +Subproject commit f9b74993f480615e2ba299a39f1bd69be11a4da0 diff --git a/Tool/Sources/XPCShared/XPCServiceProtocol.swift b/Tool/Sources/XPCShared/XPCServiceProtocol.swift index df45ac04..6ecd7ae5 100644 --- a/Tool/Sources/XPCShared/XPCServiceProtocol.swift +++ b/Tool/Sources/XPCShared/XPCServiceProtocol.swift @@ -79,6 +79,11 @@ public enum ExtensionServiceRequests { public struct ServiceInfo: Codable { public var bundleIdentifier: String public var name: String + + public init(bundleIdentifier: String, name: String) { + self.bundleIdentifier = bundleIdentifier + self.name = name + } } public typealias ResponseBody = [ServiceInfo] From 1dd35c1e707e278e4cf5f7e18a1f254b18db949a Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 29 Dec 2023 02:03:12 +0800 Subject: [PATCH 22/71] Fix request handle --- Core/Sources/Service/Service.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift index a517c15c..7178055c 100644 --- a/Core/Sources/Service/Service.swift +++ b/Core/Sources/Service/Service.swift @@ -90,7 +90,10 @@ public extension Service { return } catch { reply(nil, error) + return } + + reply(nil, XPCRequestNotHandledError()) } } From 078c258a8d1ba2c297cadb3b6a0ee5f2cd3a5ac4 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 29 Dec 2023 02:04:15 +0800 Subject: [PATCH 23/71] Fix that continuations in XPC connection may leak --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index f9b74993..fca93397 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit f9b74993f480615e2ba299a39f1bd69be11a4da0 +Subproject commit fca933971e5d0841713e5ed30d03b7567366e5a6 From 4b5b99634788f7d7942f6fe89c845e9db0c77f47 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 29 Dec 2023 02:04:57 +0800 Subject: [PATCH 24/71] Support opening extension manager --- Core/Sources/HostApp/General.swift | 16 ++++++++++++++++ Core/Sources/HostApp/GeneralView.swift | 12 +++++++++++- Pro | 2 +- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/Core/Sources/HostApp/General.swift b/Core/Sources/HostApp/General.swift index 6e1aa17e..a0c1ea88 100644 --- a/Core/Sources/HostApp/General.swift +++ b/Core/Sources/HostApp/General.swift @@ -3,6 +3,7 @@ import ComposableArchitecture import Foundation import LaunchAgentManager import SwiftUI +import XPCShared struct General: ReducerProtocol { struct State: Equatable { @@ -14,6 +15,7 @@ struct General: ReducerProtocol { enum Action: Equatable { case appear case setupLaunchAgentIfNeeded + case openExtensionManager case reloadStatus case finishReloading(xpcServiceVersion: String, permissionGranted: Bool) case failedReloading @@ -28,6 +30,7 @@ struct General: ReducerProtocol { return .run { send in await send(.setupLaunchAgentIfNeeded) } + case .setupLaunchAgentIfNeeded: return .run { send in #if DEBUG @@ -44,6 +47,19 @@ struct General: ReducerProtocol { #endif await send(.reloadStatus) } + + case .openExtensionManager: + return .run { send in + let service = try getService() + do { + _ = try await service + .send(requestBody: ExtensionServiceRequests.OpenExtensionManager()) + } catch { + toast(error.localizedDescription, .error) + await send(.failedReloading) + } + } + case .reloadStatus: state.isReloading = true return .run { send in diff --git a/Core/Sources/HostApp/GeneralView.swift b/Core/Sources/HostApp/GeneralView.swift index 90e352ca..3c33d9a4 100644 --- a/Core/Sources/HostApp/GeneralView.swift +++ b/Core/Sources/HostApp/GeneralView.swift @@ -12,7 +12,7 @@ struct GeneralView: View { var body: some View { ScrollView { VStack(alignment: .leading, spacing: 0) { - AppInfoView() + AppInfoView(store: store) SettingsDivider() ExtensionServiceView(store: store) SettingsDivider() @@ -30,6 +30,7 @@ struct GeneralView: View { struct AppInfoView: View { @State var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String @Environment(\.updateChecker) var updateChecker + let store: StoreOf var body: some View { VStack(alignment: .leading) { @@ -45,6 +46,15 @@ struct AppInfoView: View { .foregroundColor(.secondary) Spacer() + + Button(action: { + store.send(.openExtensionManager) + }) { + HStack(spacing: 2) { + Image(systemName: "puzzlepiece.extension.fill") + Text("Extensions") + } + } Button(action: { updateChecker.checkForUpdates() diff --git a/Pro b/Pro index fca93397..d57e90f5 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit fca933971e5d0841713e5ed30d03b7567366e5a6 +Subproject commit d57e90f547761315d82ae5d3fd396a76b7367bb2 From f81b88fcc423dbddd821c84f14c6dcf7613000d8 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 29 Dec 2023 03:01:53 +0800 Subject: [PATCH 25/71] Print errors --- .../SuggestionServiceMiddleware.swift | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Tool/Sources/SuggestionProvider/SuggestionServiceMiddleware.swift b/Tool/Sources/SuggestionProvider/SuggestionServiceMiddleware.swift index 429ec403..c7e13e61 100644 --- a/Tool/Sources/SuggestionProvider/SuggestionServiceMiddleware.swift +++ b/Tool/Sources/SuggestionProvider/SuggestionServiceMiddleware.swift @@ -55,12 +55,19 @@ public struct DebugSuggestionServiceMiddleware: SuggestionServiceMiddleware { Logger.service.debug(""" Get suggestion for \(request.fileURL) at \(request.cursorPosition) """) - let suggestions = try await next(request) - Logger.service.debug(""" - Receive \(suggestions.count) suggestions for \(request.fileURL) at \(request.cursorPosition) - """) - - return suggestions + do { + let suggestions = try await next(request) + Logger.service.debug(""" + Receive \(suggestions.count) suggestions for \(request.fileURL) \ + at \(request.cursorPosition) + """) + return suggestions + } catch { + Logger.service.debug(""" + Error: \(error.localizedDescription) + """) + throw error + } } } From f0689e9438cddaaadbeab67fec73e1452e443342 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 29 Dec 2023 03:28:57 +0800 Subject: [PATCH 26/71] Fix tests --- .../FetchSuggestionsTests.swift | 50 +++++++++++-------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift b/Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift index 159f36f2..60aa89ce 100644 --- a/Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift +++ b/Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift @@ -6,29 +6,32 @@ import XCTest final class FetchSuggestionTests: XCTestCase { func test_process_suggestions_from_server() async throws { struct TestServer: GitHubCopilotLSP { - func sendNotification(_ notif: LanguageServerProtocol.ClientNotification) async throws { + func sendNotification(_: LanguageServerProtocol.ClientNotification) async throws { fatalError() } - + func sendRequest(_: E) async throws -> E.Response where E: GitHubCopilotRequestType { return GitHubCopilotRequest.GetCompletionsCycling.Response(completions: [ .init( - id: "uuid", text: "Hello World\n", position: .init((0, 0)), - range: .init(start: .init((0, 0)), end: .init((0, 4))) + uuid: "uuid", + range: .init(start: .init((0, 0)), end: .init((0, 4))), + displayText: "" ), .init( - id: "uuid", text: " ", position: .init((0, 0)), - range: .init(start: .init((0, 0)), end: .init((0, 1))) + uuid: "uuid", + range: .init(start: .init((0, 0)), end: .init((0, 1))), + displayText: "" ), .init( - id: "uuid", text: " \n", position: .init((0, 0)), - range: .init(start: .init((0, 0)), end: .init((0, 2))) + uuid: "uuid", + range: .init(start: .init((0, 0)), end: .init((0, 2))), + displayText: "" ), ]) as! E.Response } @@ -49,29 +52,32 @@ final class FetchSuggestionTests: XCTestCase { func test_ignore_empty_suggestions() async throws { struct TestServer: GitHubCopilotLSP { - func sendNotification(_ notif: LanguageServerProtocol.ClientNotification) async throws { + func sendNotification(_: LanguageServerProtocol.ClientNotification) async throws { fatalError() } - + func sendRequest(_: E) async throws -> E.Response where E: GitHubCopilotRequestType { return GitHubCopilotRequest.GetCompletionsCycling.Response(completions: [ .init( - id: "uuid", text: "Hello World\n", position: .init((0, 0)), - range: .init(start: .init((0, 0)), end: .init((0, 4))) + uuid: "uuid", + range: .init(start: .init((0, 0)), end: .init((0, 4))), + displayText: "" ), .init( - id: "uuid", text: " ", position: .init((0, 0)), - range: .init(start: .init((0, 0)), end: .init((0, 1))) + uuid: "uuid", + range: .init(start: .init((0, 0)), end: .init((0, 1))), + displayText: "" ), .init( - id: "uuid", text: " \n", position: .init((0, 0)), - range: .init(start: .init((0, 0)), end: .init((0, 2))) + uuid: "uuid", + range: .init(start: .init((0, 0)), end: .init((0, 2))), + displayText: "" ), ]) as! E.Response } @@ -99,17 +105,18 @@ final class FetchSuggestionTests: XCTestCase { } class TestServer: GitHubCopilotLSP { - func sendNotification(_ notif: LanguageServerProtocol.ClientNotification) async throws { + func sendNotification(_: LanguageServerProtocol.ClientNotification) async throws { // unimplemented } - - func sendRequest(_ r: E) async throws -> E.Response where E: GitHubCopilotRequestType { + + func sendRequest(_: E) async throws -> E.Response where E: GitHubCopilotRequestType { return GitHubCopilotRequest.GetCompletionsCycling.Response(completions: [ .init( - id: "uuid", text: "Hello World\n", position: .init((0, 0)), - range: .init(start: .init((0, 0)), end: .init((0, 4))) + uuid: "uuid", + range: .init(start: .init((0, 0)), end: .init((0, 4))), + displayText: "" ), ]) as! E.Response } @@ -130,3 +137,4 @@ final class FetchSuggestionTests: XCTestCase { XCTAssertEqual(completions.first?.text, "Hello World") } } + From 7f49657b90ec1c4ce51ff4a9dbb014a066ce59d0 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 29 Dec 2023 03:29:11 +0800 Subject: [PATCH 27/71] Fix suggestion validation --- .../Filespace+SuggestionService.swift | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift b/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift index ae773b96..992b2ba4 100644 --- a/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift +++ b/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift @@ -5,7 +5,7 @@ import Workspace public struct FilespaceSuggestionSnapshot: Equatable { public var linesHash: Int public var cursorPosition: CursorPosition - + public init(linesHash: Int, cursorPosition: CursorPosition) { self.linesHash = linesHash self.cursorPosition = cursorPosition @@ -59,14 +59,45 @@ public extension Filespace { let editingLine = lines[cursorPosition.line].dropLast(1) // dropping \n let suggestionLines = presentingSuggestion.text.split(separator: "\n") let suggestionFirstLine = suggestionLines.first ?? "" + + /// For example: + /// ``` + /// ABCD012 // typed text + /// ^ + /// 0123456 // suggestion range 4-11, generated after `ABCD` + /// ``` + /// The suggestion should contain `012`, aka, the suggestion that is typed. + /// + /// Another case is that the suggestion may contain the whole line. + /// /// ``` + /// ABCD012 // typed text + /// ----^ + /// ABCD0123456 // suggestion range 0-11, generated after `ABCD` + /// The suggestion should contain `ABCD012`, aka, the suggestion that is typed. + /// ``` + let typedSuggestion = { + let startIndex = editingLine.index( + editingLine.startIndex, + offsetBy: presentingSuggestion.position.character, + limitedBy: editingLine.endIndex + ) ?? editingLine.startIndex + + let endIndex = editingLine.index( + editingLine.startIndex, + offsetBy: cursorPosition.character, + limitedBy: editingLine.endIndex + ) ?? editingLine.endIndex + + if endIndex > startIndex { + return editingLine[startIndex.. 0, - !suggestionFirstLine.hasPrefix(editingLine[..<(editingLine.index( - editingLine.startIndex, - offsetBy: cursorPosition.character, - limitedBy: editingLine.endIndex - ) ?? editingLine.endIndex)]) + !suggestionFirstLine.hasPrefix(typedSuggestion) { reset() resetSnapshot() @@ -74,7 +105,7 @@ public extension Filespace { } // finished typing the whole suggestion when the suggestion has only one line - if editingLine.hasPrefix(suggestionFirstLine), suggestionLines.count <= 1 { + if typedSuggestion.hasPrefix(suggestionFirstLine), suggestionLines.count <= 1 { reset() resetSnapshot() return false From 9aece4399ba53fe0fb366ca244cf5c6da32a2e4d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 29 Dec 2023 17:19:25 +0800 Subject: [PATCH 28/71] Fix that extension configuration scene can't display --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index d57e90f5..1398fd75 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit d57e90f547761315d82ae5d3fd396a76b7367bb2 +Subproject commit 1398fd7506647fa78898aec5bf3e7e759fa5ead8 From 0de5b6a2934518c560de37adff406002c3fce642 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 29 Dec 2023 17:58:20 +0800 Subject: [PATCH 29/71] Support selecting suggestion provider from extensions --- .../SuggestionSettingsView.swift | 49 ++++++++++++++++++- Pro | 2 +- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift index 9d74851b..a8929de6 100644 --- a/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift @@ -1,6 +1,8 @@ +import Client import Preferences import SharedUIComponents import SwiftUI +import XPCShared #if canImport(ProHostApp) import ProHostApp @@ -30,7 +32,45 @@ struct SuggestionSettingsView: View { var acceptSuggestionWithTab @AppStorage(\.isSuggestionSenseEnabled) var isSuggestionSenseEnabled - init() {} + + var refreshExtensionSuggestionFeatureProvidersTask: Task? + + @MainActor + @Published var extensionSuggestionFeatureProviders = [ExtensionSuggestionFeatureProvider]() + + init() { + Task { @MainActor in + refreshExtensionSuggestionFeatureProviders() + } + refreshExtensionSuggestionFeatureProvidersTask = Task { [weak self] in + let sequence = await NotificationCenter.default + .notifications(named: NSApplication.didBecomeActiveNotification) + for await _ in sequence { + guard let self else { return } + await MainActor.run { + self.refreshExtensionSuggestionFeatureProviders() + } + } + } + } + + struct ExtensionSuggestionFeatureProvider: Identifiable { + var id: String { bundleIdentifier } + var name: String + var bundleIdentifier: String + } + + @MainActor + func refreshExtensionSuggestionFeatureProviders() { + guard let service = try? getService() else { return } + Task { @MainActor in + let services = try await service + .send(requestBody: ExtensionServiceRequests.GetExtensionSuggestionServices()) + extensionSuggestionFeatureProviders = services.map { + .init(name: $0.name, bundleIdentifier: $0.bundleIdentifier) + } + } + } } @StateObject var settings = Settings() @@ -61,6 +101,13 @@ struct SuggestionSettingsView: View { Text("Codeium").tag(SuggestionFeatureProvider.builtIn($0)) } } + + ForEach(settings.extensionSuggestionFeatureProviders, id: \.id) { + Text($0.name).tag(SuggestionFeatureProvider.extension( + name: $0.name, + bundleIdentifier: $0.bundleIdentifier + )) + } } label: { Text("Feature Provider") } diff --git a/Pro b/Pro index 1398fd75..359d59c7 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 1398fd7506647fa78898aec5bf3e7e759fa5ead8 +Subproject commit 359d59c7d4de46f27863c8dbc7721d4782b8c4e3 From 5210da8d76b9671c3879ae22f2014838a2f31c14 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 30 Dec 2023 19:30:59 +0800 Subject: [PATCH 30/71] Update UI --- .../SuggestionPanelContent/ToastPanelView.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ToastPanelView.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ToastPanelView.swift index 69c30e61..efd98429 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ToastPanelView.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ToastPanelView.swift @@ -37,7 +37,10 @@ struct ToastPanelView: View { case .warning: return Color(nsColor: .systemOrange) } }() as Color, in: RoundedRectangle(cornerRadius: 8)) - .shadow(color: Color.black.opacity(0.2), radius: 4) + .overlay { + RoundedRectangle(cornerRadius: 8) + .stroke(Color.black.opacity(0.3), lineWidth: 1) + } } } From 1adcffe20d7304c0f8babc2ddb4347fac24fa868 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 30 Dec 2023 19:31:11 +0800 Subject: [PATCH 31/71] Remove unused type --- .../GitHubCopilotServiceTests/FetchSuggestionsTests.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift b/Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift index 60aa89ce..c500e34b 100644 --- a/Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift +++ b/Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift @@ -98,12 +98,6 @@ final class FetchSuggestionTests: XCTestCase { } func test_if_language_identifier_is_unknown_returns_correctly() async throws { - struct Err: Error, LocalizedError { - var errorDescription: String? { - "sendRequest Should not be falled" - } - } - class TestServer: GitHubCopilotLSP { func sendNotification(_: LanguageServerProtocol.ClientNotification) async throws { // unimplemented From 8cfaca8d51fff3ffb7dba26777aae2b22742e356 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 30 Dec 2023 19:31:18 +0800 Subject: [PATCH 32/71] Fix suggestion validation --- .../Filespace+SuggestionService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift b/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift index 992b2ba4..0a45148e 100644 --- a/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift +++ b/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift @@ -78,7 +78,7 @@ public extension Filespace { let typedSuggestion = { let startIndex = editingLine.index( editingLine.startIndex, - offsetBy: presentingSuggestion.position.character, + offsetBy: presentingSuggestion.range.start.character, limitedBy: editingLine.endIndex ) ?? editingLine.startIndex From 4a69aacd8822fc6af3a6c147397c6cce2320aaa3 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 30 Dec 2023 19:31:31 +0800 Subject: [PATCH 33/71] Fix suggestion provider observation --- Core/Sources/SuggestionService/SuggestionService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/Sources/SuggestionService/SuggestionService.swift b/Core/Sources/SuggestionService/SuggestionService.swift index 0de9970a..fbfb4d1d 100644 --- a/Core/Sources/SuggestionService/SuggestionService.swift +++ b/Core/Sources/SuggestionService/SuggestionService.swift @@ -19,7 +19,7 @@ public actor SuggestionService: SuggestionServiceType { let onServiceLaunched: (SuggestionServiceProvider) -> Void let providerChangeObserver = UserDefaultsObserver( object: UserDefaults.shared, - forKeyPaths: [UserDefaultPreferenceKeys().oldSuggestionFeatureProvider.key], + forKeyPaths: [UserDefaultPreferenceKeys().suggestionFeatureProvider.key], context: nil ) From 128d0407e3254bbfd84eb5fbd8d8e97386ee7bef Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 30 Dec 2023 19:31:53 +0800 Subject: [PATCH 34/71] Fix suggestion provider picker --- .../SuggestionSettingsView.swift | 87 +++++++++++++++---- Pro | 2 +- 2 files changed, 72 insertions(+), 17 deletions(-) diff --git a/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift index a8929de6..66a1d91b 100644 --- a/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift @@ -9,6 +9,30 @@ import ProHostApp #endif struct SuggestionSettingsView: View { + struct SuggestionFeatureProviderOption: Identifiable, Hashable { + var id: String { + (builtInProvider?.rawValue).map(String.init) ?? bundleIdentifier ?? "n/A" + } + + var name: String + var builtInProvider: BuiltInSuggestionFeatureProvider? + var bundleIdentifier: String? + + func hash(into hasher: inout Hasher) { + id.hash(into: &hasher) + } + + init( + name: String, + builtInProvider: BuiltInSuggestionFeatureProvider? = nil, + bundleIdentifier: String? = nil + ) { + self.name = name + self.builtInProvider = builtInProvider + self.bundleIdentifier = bundleIdentifier + } + } + final class Settings: ObservableObject { @AppStorage(\.realtimeSuggestionToggle) var realtimeSuggestionToggle @@ -36,7 +60,8 @@ struct SuggestionSettingsView: View { var refreshExtensionSuggestionFeatureProvidersTask: Task? @MainActor - @Published var extensionSuggestionFeatureProviders = [ExtensionSuggestionFeatureProvider]() + @Published + var extensionSuggestionFeatureProviderOptions = [SuggestionFeatureProviderOption]() init() { Task { @MainActor in @@ -54,21 +79,17 @@ struct SuggestionSettingsView: View { } } - struct ExtensionSuggestionFeatureProvider: Identifiable { - var id: String { bundleIdentifier } - var name: String - var bundleIdentifier: String - } - @MainActor func refreshExtensionSuggestionFeatureProviders() { guard let service = try? getService() else { return } Task { @MainActor in let services = try await service .send(requestBody: ExtensionServiceRequests.GetExtensionSuggestionServices()) - extensionSuggestionFeatureProviders = services.map { + extensionSuggestionFeatureProviderOptions = services.map { .init(name: $0.name, bundleIdentifier: $0.bundleIdentifier) } + print(services.map(\.bundleIdentifier)) + print(suggestionFeatureProvider) } } } @@ -92,21 +113,55 @@ struct SuggestionSettingsView: View { Text("Presentation") } - Picker(selection: $settings.suggestionFeatureProvider) { + Picker(selection: Binding(get: { + switch settings.suggestionFeatureProvider { + case let .builtIn(provider): + return SuggestionFeatureProviderOption( + name: "", + builtInProvider: provider + ) + case let .extension(name, identifier): + return SuggestionFeatureProviderOption( + name: name, + bundleIdentifier: identifier + ) + } + }, set: { (option: SuggestionFeatureProviderOption) in + if let provider = option.builtInProvider { + settings.suggestionFeatureProvider = .builtIn(provider) + } else { + settings.suggestionFeatureProvider = .extension( + name: option.name, + bundleIdentifier: option.bundleIdentifier ?? "" + ) + } + })) { ForEach(BuiltInSuggestionFeatureProvider.allCases, id: \.rawValue) { switch $0 { case .gitHubCopilot: - Text("GitHub Copilot").tag(SuggestionFeatureProvider.builtIn($0)) + Text("GitHub Copilot") + .tag(SuggestionFeatureProviderOption(name: "", builtInProvider: $0)) case .codeium: - Text("Codeium").tag(SuggestionFeatureProvider.builtIn($0)) + Text("Codeium") + .tag(SuggestionFeatureProviderOption(name: "", builtInProvider: $0)) } } - ForEach(settings.extensionSuggestionFeatureProviders, id: \.id) { - Text($0.name).tag(SuggestionFeatureProvider.extension( - name: $0.name, - bundleIdentifier: $0.bundleIdentifier - )) + ForEach(settings.extensionSuggestionFeatureProviderOptions, id: \.self) { item in + Text(item.name).tag(item) + } + + if case let .extension(name, identifier) = settings.suggestionFeatureProvider { + if !settings.extensionSuggestionFeatureProviderOptions.contains(where: { + $0.bundleIdentifier == identifier + }) { + Text("\(name) (Not Found)").tag( + SuggestionFeatureProviderOption( + name: name, + bundleIdentifier: identifier + ) + ) + } } } label: { Text("Feature Provider") diff --git a/Pro b/Pro index 359d59c7..4bade8fc 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 359d59c7d4de46f27863c8dbc7721d4782b8c4e3 +Subproject commit 4bade8fce1a70957c775c1f18773b03744bc270f From 18b8344a15a31aab149ec5b6c151f05cba27b3d0 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 30 Dec 2023 21:45:10 +0800 Subject: [PATCH 35/71] Bump version to 0.28.4 --- Pro | 2 +- Version.xcconfig | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Pro b/Pro index 4bade8fc..09b4e63e 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 4bade8fce1a70957c775c1f18773b03744bc270f +Subproject commit 09b4e63e52b23755b628f291a3ae976cadbdb424 diff --git a/Version.xcconfig b/Version.xcconfig index e5fad3e5..f55f7f94 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,3 +1,3 @@ -APP_VERSION = 0.28.3 -APP_BUILD = 293 +APP_VERSION = 0.28.4 +APP_BUILD = 294 From 5b4d29c46494deea04a52c20b60c8968362b85a9 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 3 Jan 2024 14:47:39 +0800 Subject: [PATCH 36/71] Replace separator: "\n" with whereSeparator: \.isNewline --- .../PromptToCodeService/OpenAIPromptToCodeService.swift | 2 +- Pro | 2 +- Tool/Sources/CodeiumService/CodeiumService.swift | 2 +- .../FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift | 4 ++-- .../FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift | 2 +- Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift | 2 +- .../Filespace+SuggestionService.swift | 2 +- Tool/Sources/XcodeInspector/SourceEditor.swift | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift b/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift index d85e8c34..4f53bac4 100644 --- a/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift +++ b/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift @@ -266,7 +266,7 @@ extension OpenAIPromptToCodeService { } func getCommonLeadingSpaceCount(_ code: String) -> Int { - let lines = code.split(separator: "\n") + let lines = code.split(whereSeparator: \.isNewline) guard !lines.isEmpty else { return 0 } var commonCount = Int.max for line in lines { diff --git a/Pro b/Pro index 09b4e63e..2022283e 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 09b4e63e52b23755b628f291a3ae976cadbdb424 +Subproject commit 2022283eb04f1451fd6f88d7cfd9b09a8c85b7ef diff --git a/Tool/Sources/CodeiumService/CodeiumService.swift b/Tool/Sources/CodeiumService/CodeiumService.swift index a62b6021..0bc1116b 100644 --- a/Tool/Sources/CodeiumService/CodeiumService.swift +++ b/Tool/Sources/CodeiumService/CodeiumService.swift @@ -359,7 +359,7 @@ func getXcodeVersion() async throws -> String { if let data = try outpipe.fileHandleForReading.readToEnd(), let content = String(data: data, encoding: .utf8) { - let firstLine = content.split(separator: "\n").first ?? "" + let firstLine = content.split(whereSeparator: \.isNewline).first ?? "" var version = firstLine.replacingOccurrences(of: "Xcode ", with: "") if version.isEmpty { version = "14.0" diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift index 421e3cc7..c9c061a0 100644 --- a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift @@ -130,11 +130,11 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< } } - prefix = prefix.split(separator: "\n") + prefix = prefix.split(whereSeparator: \.isNewline) .joined(separator: " ") .trimmingCharacters(in: .whitespacesAndNewlines) - extra = extra.split(separator: "\n") + extra = extra.split(whereSeparator: \.isNewline) .joined(separator: " ") .trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift index b2b235cc..2bb2bf11 100644 --- a/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift @@ -156,7 +156,7 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< let type = node.funcKeyword.text let name = node.identifier.text let signature = node.signature.trimmedDescription - .split(separator: "\n") + .split(whereSeparator: \.isNewline) .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .joined(separator: " ") diff --git a/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift b/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift index f44acca0..ebebfc6d 100644 --- a/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift +++ b/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift @@ -81,7 +81,7 @@ public struct DefaultGitIgnoredChecker: GitIgnoredChecker { environment: [:] ) return result - .split(separator: "\n") + .split(whereSeparator: \.isNewline) .map(String.init) .compactMap(URL.init(fileURLWithPath:)) } catch { diff --git a/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift b/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift index 0a45148e..e59b94d1 100644 --- a/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift +++ b/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift @@ -57,7 +57,7 @@ public extension Filespace { } let editingLine = lines[cursorPosition.line].dropLast(1) // dropping \n - let suggestionLines = presentingSuggestion.text.split(separator: "\n") + let suggestionLines = presentingSuggestion.text.split(whereSeparator: \.isNewline) let suggestionFirstLine = suggestionLines.first ?? "" /// For example: diff --git a/Tool/Sources/XcodeInspector/SourceEditor.swift b/Tool/Sources/XcodeInspector/SourceEditor.swift index 7937582e..b096d682 100644 --- a/Tool/Sources/XcodeInspector/SourceEditor.swift +++ b/Tool/Sources/XcodeInspector/SourceEditor.swift @@ -129,7 +129,7 @@ public extension SourceEditor { } static func breakLines(_ string: String) -> [String] { - let lines = string.split(separator: "\n", omittingEmptySubsequences: false) + let lines = string.split(whereSeparator: \.isNewline, omittingEmptySubsequences: false) var all = [String]() for (index, line) in lines.enumerated() { if index == lines.endIndex - 1 { From 1c8ad4cb801a6c854a2d92ab0e68e113c4c803db Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 3 Jan 2024 15:37:59 +0800 Subject: [PATCH 37/71] Remove ExpandFocusRangeFunction --- .../ActiveDocumentChatContextCollector.swift | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift index 9ac109bf..0536c9f3 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift @@ -45,12 +45,6 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { var functions = [any ChatGPTFunction]() 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)) - } - // 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. @@ -104,7 +98,9 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { let start = """ ## File and Code Scope - You can use the following context to answer my questions about the editing document or code. The context shows only a part of the code in the editing document, and will change during the conversation, so it may not match our conversation. + You can use the following context to answer my questions about the editing document \ + or code. The context shows only a part of the code in the editing document, and will \ + change during the conversation, so it may not match our conversation. \( context.focusedContext == nil From ccd8ef6de45cf6dd663f60108ae70faca99649b5 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 3 Jan 2024 15:39:33 +0800 Subject: [PATCH 38/71] Remove MoveToFocusedCodeFunction --- .../ActiveDocumentChatContextCollector.swift | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift index 0536c9f3..4a374216 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift @@ -45,15 +45,6 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { var functions = [any ChatGPTFunction]() if !isSensitive { - // 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)) - } - // 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. From bfc748ac1bed6642d2caf373ff3362c2802c298b Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 4 Jan 2024 15:10:41 +0800 Subject: [PATCH 39/71] Update MoveToCodeAroundLineFunction to get the code directly instead --- .../ActiveDocumentChatContextCollector.swift | 27 ++---- .../Functions/ExpandFocusRangeFunction.swift | 60 ------------ .../GetCodeCodeAroundLineFunction.swift | 93 +++++++++++++++++++ .../MoveToCodeAroundLineFunction.swift | 68 -------------- .../Functions/MoveToFocusedCodeFunction.swift | 55 ----------- 5 files changed, 101 insertions(+), 202 deletions(-) delete mode 100644 Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/ExpandFocusRangeFunction.swift create mode 100644 Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/GetCodeCodeAroundLineFunction.swift delete mode 100644 Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToCodeAroundLineFunction.swift delete mode 100644 Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToFocusedCodeFunction.swift diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift index 4a374216..ad5de1b8 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift @@ -45,19 +45,7 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { var functions = [any ChatGPTFunction]() if !isSensitive { - // 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 context.focusedContext == nil, !context.lineAnnotations.isEmpty { - functions.append(MoveToCodeAroundLineFunction(contextCollector: self)) - } + functions.append(GetCodeCodeAroundLineFunction(contextCollector: self)) } return .init( @@ -89,9 +77,8 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { let start = """ ## File and Code Scope - You can use the following context to answer my questions about the editing document \ - or code. The context shows only a part of the code in the editing document, and will \ - change during the conversation, so it may not match our conversation. + You can use the following context to answer my questions about the editing document.\ + The context shows only a part of the code in the editing document. \( context.focusedContext == nil @@ -122,7 +109,9 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { 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)): + Focused Code (from line \( + focusedContext.codeRange.start.line + 1 + ) to line \(focusedContext.codeRange.end.line + 1)): ```\(context.language.rawValue) \(focusedContext.code) ``` @@ -131,8 +120,8 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { let fileAnnotations = focusedContext.otherLineAnnotations.isEmpty || isSensitive ? "" : """ - Other Annotations:\""" - (They are not inside the focused code. You don't known how to handle them until you get the code at the line) + Out-of-scope Annotations:\""" + (They are not inside the focused code. You can get the code at the line for details) \( focusedContext.otherLineAnnotations .map(convertAnnotationToText) diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/ExpandFocusRangeFunction.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/ExpandFocusRangeFunction.swift deleted file mode 100644 index a70efe36..00000000 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/ExpandFocusRangeFunction.swift +++ /dev/null @@ -1,60 +0,0 @@ -import ASTParser -import Foundation -import OpenAIService -import SuggestionModel - -struct ExpandFocusRangeFunction: ChatGPTFunction { - struct Arguments: Codable {} - - struct Result: ChatGPTFunctionResult { - var range: CursorRange - - var botReadableContent: String { - "Editing Document Context is updated to display code at \(range)." - } - } - - struct E: Error, LocalizedError { - var errorDescription: String? - } - - var name: String { - "expandFocusRange" - } - - var description: String { - "Call when Editing Document Context provides too little context to answer a question." - } - - var argumentSchema: JSONSchemaValue { [ - .type: "object", - .properties: [:], - ] } - - weak var contextCollector: ActiveDocumentChatContextCollector? - - init(contextCollector: ActiveDocumentChatContextCollector) { - self.contextCollector = contextCollector - } - - func prepare(reportProgress: @escaping (String) async -> Void) async { - await reportProgress("Finding the focused code..") - } - - func call( - arguments: Arguments, - reportProgress: @escaping (String) async -> Void - ) async throws -> Result { - await reportProgress("Finding the focused code..") - contextCollector?.activeDocumentContext?.expandFocusedRangeToContextRange() - guard let newContext = contextCollector?.activeDocumentContext?.focusedContext else { - let progress = "Failed to expand focused code." - await reportProgress(progress) - throw E(errorDescription: progress) - } - let progress = "Looking at \(newContext.codeRange)." - await reportProgress(progress) - return .init(range: newContext.codeRange) - } -} - diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/GetCodeCodeAroundLineFunction.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/GetCodeCodeAroundLineFunction.swift new file mode 100644 index 00000000..16f2bab3 --- /dev/null +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/GetCodeCodeAroundLineFunction.swift @@ -0,0 +1,93 @@ +import ASTParser +import Foundation +import OpenAIService +import SuggestionModel + +struct GetCodeCodeAroundLineFunction: ChatGPTFunction { + struct Arguments: Codable { + var line: Int + } + + struct Result: ChatGPTFunctionResult { + var range: CursorRange + var content: String + var language: CodeLanguage + + var botReadableContent: String { + """ + Code in range \(range) + ```\(language.rawValue) + \(content) + ``` + """ + } + } + + struct E: Error, LocalizedError { + var errorDescription: String? + } + + var name: String { + "getCodeAtLine" + } + + var description: String { + "Get the code at the given line. You must ONLY call it when the user give you a specific line or the user ask about an out of scope annotation." + } + + var argumentSchema: JSONSchemaValue { [ + .type: "object", + .properties: [ + "line": [ + .type: "number", + .description: "The line number in the file", + ], + ], + .required: ["line"], + ] } + + weak var contextCollector: ActiveDocumentChatContextCollector? + + init(contextCollector: ActiveDocumentChatContextCollector) { + self.contextCollector = contextCollector + } + + func prepare(reportProgress: @escaping (String) async -> Void) async { + await reportProgress("Finding code around..") + } + + func call( + arguments: Arguments, + reportProgress: @escaping (String) async -> Void + ) async throws -> Result { + guard var activeDocumentContext = contextCollector?.activeDocumentContext else { + throw E(errorDescription: "No active document found.") + } + await reportProgress("Reading code around line \(arguments.line)..") + activeDocumentContext.moveToCodeAroundLine(max(arguments.line - 1, 0)) + guard let newContext = activeDocumentContext.focusedContext else { + let progress = "Failed to read code around line \(arguments.line)..)" + await reportProgress(progress) + throw E(errorDescription: progress) + } + let progress = "Finish reading code at \(newContext.codeRange)" + await reportProgress(progress) + return .init( + range: newContext.codeRange, + content: newContext.code + .split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) + .enumerated() + .map { + let (index, content) = $0 + if index + newContext.codeRange.start.line == arguments.line - 1 { + return content + " // <--- line \(arguments.line)" + } else { + return content + } + } + .joined(separator: "\n"), + language: activeDocumentContext.language + ) + } +} + diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToCodeAroundLineFunction.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToCodeAroundLineFunction.swift deleted file mode 100644 index 42ee50a2..00000000 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToCodeAroundLineFunction.swift +++ /dev/null @@ -1,68 +0,0 @@ -import ASTParser -import Foundation -import OpenAIService -import SuggestionModel - -struct MoveToCodeAroundLineFunction: ChatGPTFunction { - struct Arguments: Codable { - var line: Int - } - - struct Result: ChatGPTFunctionResult { - var range: CursorRange - - var botReadableContent: String { - "Editing Document Context is updated to display code at \(range)." - } - } - - struct E: Error, LocalizedError { - var errorDescription: String? - } - - var name: String { - "getCodeAtLine" - } - - var description: String { - "Get the code at the given line, so you can answer the question about the code at that line." - } - - var argumentSchema: JSONSchemaValue { [ - .type: "object", - .properties: [ - "line": [ - .type: "number", - .description: "The line number in the file", - ], - ], - .required: ["line"], - ] } - - weak var contextCollector: ActiveDocumentChatContextCollector? - - init(contextCollector: ActiveDocumentChatContextCollector) { - self.contextCollector = contextCollector - } - - func prepare(reportProgress: @escaping (String) async -> Void) async { - await reportProgress("Finding code around..") - } - - func call( - arguments: Arguments, - reportProgress: @escaping (String) async -> Void - ) async throws -> Result { - await reportProgress("Finding code around line \(arguments.line)..") - contextCollector?.activeDocumentContext?.moveToCodeAroundLine(arguments.line) - guard let newContext = contextCollector?.activeDocumentContext?.focusedContext else { - let progress = "Failed to move to focused code." - await reportProgress(progress) - throw E(errorDescription: progress) - } - let progress = "Looking at \(newContext.codeRange)" - await reportProgress(progress) - return .init(range: newContext.codeRange) - } -} - diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToFocusedCodeFunction.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToFocusedCodeFunction.swift deleted file mode 100644 index 3b3096a2..00000000 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToFocusedCodeFunction.swift +++ /dev/null @@ -1,55 +0,0 @@ -import ASTParser -import Foundation -import OpenAIService -import SuggestionModel - -struct MoveToFocusedCodeFunction: ChatGPTFunction { - typealias Arguments = NoArguments - - struct Result: ChatGPTFunctionResult { - var range: CursorRange - - var botReadableContent: String { - "Editing Document Context is updated to display code at \(range)." - } - } - - struct E: Error, LocalizedError { - var errorDescription: String? - } - - var name: String { - "moveToFocusedCode" - } - - var description: String { - "Move editing document context to the selected or focused code" - } - - weak var contextCollector: ActiveDocumentChatContextCollector? - - init(contextCollector: ActiveDocumentChatContextCollector) { - self.contextCollector = contextCollector - } - - func prepare(reportProgress: @escaping (String) async -> Void) async { - await reportProgress("Finding the focused code..") - } - - func call( - arguments: Arguments, - reportProgress: @escaping (String) async -> Void - ) async throws -> Result { - await reportProgress("Finding the focused code..") - contextCollector?.activeDocumentContext?.moveToFocusedCode() - guard let newContext = contextCollector?.activeDocumentContext?.focusedContext else { - let progress = "Failed to move to focused code." - await reportProgress(progress) - throw E(errorDescription: progress) - } - let progress = "Looking at \(newContext.codeRange)." - await reportProgress(progress) - return .init(range: newContext.codeRange) - } -} - From fbc2144f871e817e6709529abcdf93fd7ed61252 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 4 Jan 2024 15:10:56 +0800 Subject: [PATCH 40/71] Recover line endings --- Tool/Sources/XcodeInspector/SourceEditor.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Tool/Sources/XcodeInspector/SourceEditor.swift b/Tool/Sources/XcodeInspector/SourceEditor.swift index b096d682..cf6d1afc 100644 --- a/Tool/Sources/XcodeInspector/SourceEditor.swift +++ b/Tool/Sources/XcodeInspector/SourceEditor.swift @@ -129,13 +129,14 @@ public extension SourceEditor { } static func breakLines(_ string: String) -> [String] { - let lines = string.split(whereSeparator: \.isNewline, omittingEmptySubsequences: false) + let lineEnding = string.first(where: \.isNewline) ?? "\n" + let lines = string.split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) var all = [String]() for (index, line) in lines.enumerated() { if index == lines.endIndex - 1 { all.append(String(line)) } else { - all.append(String(line) + "\n") + all.append(String(line) + String(lineEnding)) } } return all From 69b1b312885c50aea641b95099a395f7c7926483 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 4 Jan 2024 17:45:14 +0800 Subject: [PATCH 41/71] Recover line ending when possible --- .../PseudoCommandHandler.swift | 19 +- .../WindowBaseCommandHandler.swift | 1 + .../SuggestionInjector.swift | 157 +---- ...FilespaceSuggestionInvalidationTests.swift | 40 +- .../ProposeSuggestionTests.swift | 640 +++++++++--------- .../RejectSuggestionTests.swift | 166 ++--- Pro | 2 +- .../ObjectiveC/ObjectiveCCodeFinder.swift | 6 +- .../Swift/SwiftFocusedCodeFinder.swift | 50 +- .../RecursiveCharacterTextSplitter.swift | 2 +- .../TextSplitterSeparatorSet.swift | 26 + .../SyntaxHighlighting.swift | 3 +- .../SuggestionModel/String+LineEnding.swift | 27 + Tool/Sources/Workspace/Filespace.swift | 17 +- .../Filespace+SuggestionService.swift | 17 +- .../Workspace+SuggestionService.swift | 3 + Tool/Sources/XPCShared/Models.swift | 1 + .../Sources/XcodeInspector/SourceEditor.swift | 20 +- 18 files changed, 581 insertions(+), 616 deletions(-) create mode 100644 Tool/Sources/SuggestionModel/String+LineEnding.swift diff --git a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift index bdae5dea..f359ebff 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift @@ -316,7 +316,7 @@ extension PseudoCommandHandler { else { return nil } guard let selectionRange = focusElement.selectedTextRange else { return nil } let content = focusElement.value - let split = content.breakLines() + let split = content.breakLines(appendLineBreakToLastLine: false) let range = convertRangeToCursorRange(selectionRange, in: content) return (content, split, [range], range.start) } @@ -409,20 +409,3 @@ extension PseudoCommandHandler { return cursorRange } } - -public extension String { - /// Break a string into lines. - func breakLines() -> [String] { - let lines = split(separator: "\n", omittingEmptySubsequences: false) - var all = [String]() - for (index, line) in lines.enumerated() { - if index == lines.endIndex - 1 { - all.append(String(line)) - } else { - all.append(String(line) + "\n") - } - } - return all - } -} - diff --git a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift index bb7d8629..03fdfd25 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift @@ -267,6 +267,7 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler { filespace.codeMetadata.tabSize = editor.tabSize filespace.codeMetadata.indentSize = editor.indentSize filespace.codeMetadata.usesTabsForIndentation = editor.usesTabsForIndentation + filespace.codeMetadata.guessLineEnding(from: editor.lines.first) return nil } diff --git a/Core/Sources/SuggestionInjector/SuggestionInjector.swift b/Core/Sources/SuggestionInjector/SuggestionInjector.swift index a84a2c74..19a25a1c 100644 --- a/Core/Sources/SuggestionInjector/SuggestionInjector.swift +++ b/Core/Sources/SuggestionInjector/SuggestionInjector.swift @@ -1,9 +1,6 @@ import Foundation import SuggestionModel -let suggestionStart = "/*========== Copilot Suggestion" -let suggestionEnd = "*///======== End of Copilot Suggestion" - // NOTE: Every lines from Xcode Extension has a line break at its end, even the last line. // NOTE: Copilot's completion always start at character 0, no matter where the cursor is. @@ -18,116 +15,6 @@ public struct SuggestionInjector { public init() {} } - public func rejectCurrentSuggestions( - from content: inout [String], - cursorPosition: inout CursorPosition, - extraInfo: inout ExtraInfo - ) { - var ranges = [ClosedRange]() - var suggestionStartIndex = -1 - - // find ranges of suggestion comments - for (index, line) in content.enumerated() { - if line.hasPrefix(suggestionStart) { - suggestionStartIndex = index - } - if suggestionStartIndex >= 0, line.hasPrefix(suggestionEnd) { - ranges.append(.init(uncheckedBounds: (suggestionStartIndex, index))) - suggestionStartIndex = -1 - } - } - - let reversedRanges = ranges.reversed() - - extraInfo.modifications.append(contentsOf: reversedRanges.map(Modification.deleted)) - extraInfo.didChangeContent = !ranges.isEmpty - - // remove the lines from bottom to top - for range in reversedRanges { - for i in stride(from: range.upperBound, through: range.lowerBound, by: -1) { - if i <= cursorPosition.line, cursorPosition.line >= 0 { - cursorPosition = .init( - line: cursorPosition.line - 1, - character: i == cursorPosition.line ? 0 : cursorPosition.character - ) - extraInfo.didChangeCursorPosition = true - } - content.remove(at: i) - } - } - - extraInfo.suggestionRange = nil - } - - public func proposeSuggestion( - intoContentWithoutSuggestion content: inout [String], - completion: CodeSuggestion, - index: Int, - count: Int, - extraInfo: inout ExtraInfo - ) { - // assemble suggestion comment - let start = completion.range.start - let startText = "\(suggestionStart) \(index + 1)/\(count)" - var lines = [startText + "\n"] - lines.append(contentsOf: completion.text.breakLines(appendLineBreakToLastLine: true)) - lines.append(suggestionEnd + "\n") - - // if suggestion is empty, returns without modifying the code - guard lines.count > 2 else { return } - - // replace the common prefix of the first line with space and carrot - let existedLine = start.line < content.endIndex ? content[start.line] : nil - let commonPrefix = longestCommonPrefix(of: lines[1], and: existedLine ?? "") - - if !commonPrefix.isEmpty { - let replacingText = { - switch (commonPrefix.hasSuffix("\n"), commonPrefix.count) { - case (false, let count): - return String(repeating: " ", count: count - 1) + "^" - case (true, let count) where count > 1: - return String(repeating: " ", count: count - 2) + "^\n" - case (true, _): - return "\n" - } - }() - - lines[1].replaceSubrange( - lines[1].startIndex..<( - lines[1].index( - lines[1].startIndex, - offsetBy: commonPrefix.count, - limitedBy: lines[1].endIndex - ) ?? lines[1].endIndex - ), - with: replacingText - ) - } - - // if the suggestion is only appending new lines and spaces, return without modification - if completion.text.dropFirst(commonPrefix.count) - .allSatisfy({ $0.isWhitespace || $0.isNewline }) { return } - - // determine if it's inserted to the current line or the next line - let lineIndex = start.line + { - guard let existedLine else { return 0 } - if existedLine.isEmptyOrNewLine { return 1 } - if commonPrefix.isEmpty { return 0 } - return 1 - }() - if content.endIndex < lineIndex { - extraInfo.didChangeContent = true - extraInfo.suggestionRange = content.endIndex...content.endIndex + lines.count - 1 - extraInfo.modifications.append(.inserted(content.endIndex, lines)) - content.append(contentsOf: lines) - } else { - extraInfo.didChangeContent = true - extraInfo.suggestionRange = lineIndex...lineIndex + lines.count - 1 - extraInfo.modifications.append(.inserted(lineIndex, lines)) - content.insert(contentsOf: lines, at: lineIndex) - } - } - public func acceptSuggestion( intoContentWithoutSuggestion content: inout [String], cursorPosition: inout CursorPosition, @@ -140,6 +27,11 @@ public struct SuggestionInjector { let start = completion.range.start let end = completion.range.end let suggestionContent = completion.text + let lineEnding = if let ending = content.first?.last, ending.isNewline { + String(ending) + } else { + "\n" + } let firstRemovedLine = content[safe: start.line] let lastRemovedLine = content[safe: end.line] @@ -150,7 +42,10 @@ public struct SuggestionInjector { content.removeSubrange(startLine...endLine) } - var toBeInserted = suggestionContent.breakLines(appendLineBreakToLastLine: true) + var toBeInserted = suggestionContent.breakLines( + proposedLineEnding: lineEnding, + appendLineBreakToLastLine: true + ) // prepending prefix text not in range if needed. if let firstRemovedLine, @@ -165,7 +60,7 @@ public struct SuggestionInjector { limitedBy: firstRemovedLine.endIndex ) ?? firstRemovedLine.endIndex) var leftover = firstRemovedLine[leftoverRange] - if leftover.hasSuffix("\n") { + if leftover.last?.isNewline ?? false { leftover.removeLast(1) } toBeInserted[0].insert( @@ -177,7 +72,8 @@ public struct SuggestionInjector { let recoveredSuffixLength = recoverSuffixIfNeeded( endOfReplacedContent: end, toBeInserted: &toBeInserted, - lastRemovedLine: lastRemovedLine + lastRemovedLine: lastRemovedLine, + lineEnding: lineEnding ) let cursorCol = toBeInserted[toBeInserted.endIndex - 1].count - 1 - recoveredSuffixLength @@ -193,7 +89,8 @@ public struct SuggestionInjector { func recoverSuffixIfNeeded( endOfReplacedContent end: CursorPosition, toBeInserted: inout [String], - lastRemovedLine: String? + lastRemovedLine: String?, + lineEnding: String ) -> Int { // If there is no line removed, there is no need to recover anything. guard let lastRemovedLine, !lastRemovedLine.isEmptyOrNewLine else { return 0 } @@ -255,7 +152,7 @@ public struct SuggestionInjector { let lastInsertingLine = toBeInserted[toBeInserted.endIndex - 1] .droppedLineBreak() .appending(suffix) - .recoveredLineBreak() + .recoveredLineBreak(lineEnding: lineEnding) toBeInserted[toBeInserted.endIndex - 1] = lastInsertingLine @@ -280,36 +177,22 @@ public struct SuggestionAnalyzer { } extension String { - /// Break a string into lines. - func breakLines(appendLineBreakToLastLine: Bool = false) -> [String] { - let lines = split(separator: "\n", omittingEmptySubsequences: false) - var all = [String]() - for (index, line) in lines.enumerated() { - if !appendLineBreakToLastLine, index == lines.endIndex - 1 { - all.append(String(line)) - } else { - all.append(String(line) + "\n") - } - } - return all - } - var isEmptyOrNewLine: Bool { - isEmpty || self == "\n" + isEmpty || self == "\n" || self == "\r\n" || self == "\r" } func droppedLineBreak() -> String { - if hasSuffix("\n") { + if last?.isNewline ?? false { return String(dropLast(1)) } return self } - func recoveredLineBreak() -> String { - if hasSuffix("\n") { + func recoveredLineBreak(lineEnding: String) -> String { + if hasSuffix(lineEnding) { return self } - return self + "\n" + return self + lineEnding } } diff --git a/Core/Tests/ServiceTests/FilespaceSuggestionInvalidationTests.swift b/Core/Tests/ServiceTests/FilespaceSuggestionInvalidationTests.swift index 59507e59..1bf2ee73 100644 --- a/Core/Tests/ServiceTests/FilespaceSuggestionInvalidationTests.swift +++ b/Core/Tests/ServiceTests/FilespaceSuggestionInvalidationTests.swift @@ -2,12 +2,16 @@ import Foundation import SuggestionModel import XCTest -@testable import Workspace @testable import Service +@testable import Workspace class FilespaceSuggestionInvalidationTests: XCTestCase { @WorkspaceActor - func prepare(suggestionText: String, cursorPosition: CursorPosition) async throws -> Filespace { + func prepare( + suggestionText: String, + cursorPosition: CursorPosition, + range: CursorRange + ) async throws -> Filespace { let pool = WorkspacePool() let (_, filespace) = try await pool .fetchOrCreateWorkspaceAndFilespace(fileURL: URL(fileURLWithPath: "file/path/to.swift")) @@ -16,7 +20,7 @@ class FilespaceSuggestionInvalidationTests: XCTestCase { id: "", text: suggestionText, position: cursorPosition, - range: .outOfScope + range: range ), ] return filespace @@ -25,7 +29,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase { func test_text_typing_suggestion_should_be_valid() async throws { let filespace = try await prepare( suggestionText: "hello man", - cursorPosition: .init(line: 1, character: 0) + cursorPosition: .init(line: 1, character: 0), + range: .init(startPair: (1, 0), endPair: (1, 0)) ) let isValid = await filespace.validateSuggestions( lines: ["\n", "hell\n", "\n"], @@ -39,7 +44,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase { func test_text_typing_suggestion_in_the_middle_should_be_valid() async throws { let filespace = try await prepare( suggestionText: "hello man", - cursorPosition: .init(line: 1, character: 0) + cursorPosition: .init(line: 1, character: 0), + range: .init(startPair: (1, 0), endPair: (1, 0)) ) let isValid = await filespace.validateSuggestions( lines: ["\n", "hell man\n", "\n"], @@ -53,7 +59,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase { func test_text_cursor_moved_to_another_line_should_invalidate() async throws { let filespace = try await prepare( suggestionText: "hello man", - cursorPosition: .init(line: 1, character: 0) + cursorPosition: .init(line: 1, character: 0), + range: .init(startPair: (1, 0), endPair: (1, 0)) ) let isValid = await filespace.validateSuggestions( lines: ["\n", "hell\n", "\n"], @@ -67,7 +74,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase { func test_text_cursor_is_invalid_should_invalidate() async throws { let filespace = try await prepare( suggestionText: "hello man", - cursorPosition: .init(line: 100, character: 0) + cursorPosition: .init(line: 100, character: 0), + range: .init(startPair: (1, 0), endPair: (1, 0)) ) let isValid = await filespace.validateSuggestions( lines: ["\n", "hell\n", "\n"], @@ -81,7 +89,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase { func test_line_content_does_not_match_input_should_invalidate() async throws { let filespace = try await prepare( suggestionText: "hello man", - cursorPosition: .init(line: 1, character: 0) + cursorPosition: .init(line: 1, character: 0), + range: .init(startPair: (1, 0), endPair: (1, 0)) ) let isValid = await filespace.validateSuggestions( lines: ["\n", "helo\n", "\n"], @@ -95,7 +104,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase { func test_line_content_does_not_match_input_should_invalidate_index_out_of_scope() async throws { let filespace = try await prepare( suggestionText: "hello man", - cursorPosition: .init(line: 1, character: 0) + cursorPosition: .init(line: 1, character: 0), + range: .init(startPair: (1, 0), endPair: (1, 0)) ) let isValid = await filespace.validateSuggestions( lines: ["\n", "helo\n", "\n"], @@ -109,7 +119,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase { func test_finish_typing_the_whole_single_line_suggestion_should_invalidate() async throws { let filespace = try await prepare( suggestionText: "hello man", - cursorPosition: .init(line: 1, character: 0) + cursorPosition: .init(line: 1, character: 0), + range: .init(startPair: (1, 0), endPair: (1, 0)) ) let isValid = await filespace.validateSuggestions( lines: ["\n", "hello man\n", "\n"], @@ -124,7 +135,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase { ) async throws { let filespace = try await prepare( suggestionText: "hello man", - cursorPosition: .init(line: 1, character: 0) + cursorPosition: .init(line: 1, character: 0), + range: .init(startPair: (1, 0), endPair: (1, 0)) ) let isValid = await filespace.validateSuggestions( lines: ["\n", "hello man!!!!!\n", "\n"], @@ -138,7 +150,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase { func test_finish_typing_the_whole_multiple_line_suggestion_should_be_valid() async throws { let filespace = try await prepare( suggestionText: "hello man\nhow are you?", - cursorPosition: .init(line: 1, character: 0) + cursorPosition: .init(line: 1, character: 0), + range: .init(startPair: (1, 0), endPair: (1, 0)) ) let isValid = await filespace.validateSuggestions( lines: ["\n", "hello man\n", "\n"], @@ -153,7 +166,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase { ) async throws { let filespace = try await prepare( suggestionText: "hello man", - cursorPosition: .init(line: 1, character: 5) // generating man from hello + cursorPosition: .init(line: 1, character: 5), // generating man from hello + range: .init(startPair: (1, 0), endPair: (1, 5)) ) let isValid = await filespace.validateSuggestions( lines: ["\n", "hell\n", "\n"], diff --git a/Core/Tests/SuggestionInjectorTests/ProposeSuggestionTests.swift b/Core/Tests/SuggestionInjectorTests/ProposeSuggestionTests.swift index 78f90f63..754eb90f 100644 --- a/Core/Tests/SuggestionInjectorTests/ProposeSuggestionTests.swift +++ b/Core/Tests/SuggestionInjectorTests/ProposeSuggestionTests.swift @@ -1,320 +1,320 @@ -import SuggestionModel -import XCTest - -@testable import SuggestionInjector - -final class ProposeSuggestionTests: XCTestCase { - func test_propose_suggestion_no_overlap() async throws { - let content = """ - struct Cat { - - } - """ - let text = """ - var name: String - var age: String - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 2, character: 19), - range: .init( - start: .init(line: 1, character: 0), - end: .init(line: 2, character: 18) - ) - ) - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakLines() - SuggestionInjector().proposeSuggestion( - intoContentWithoutSuggestion: &lines, - completion: suggestion, - index: 0, - count: 10, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertFalse(extraInfo.didChangeCursorPosition) - XCTAssertEqual(extraInfo.suggestionRange, 2...5) - XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications)) - XCTAssertEqual( - lines.joined(separator: ""), - """ - struct Cat { - - /*========== Copilot Suggestion 1/10 - var name: String - var age: String - *///======== End of Copilot Suggestion - } - """, - "The user may want to keep typing on the empty line, so suggestion is addded to the next line" - ) - } - - func test_propose_suggestion_no_overlap_start_from_previous_line() async throws { - let content = """ - struct Cat { - } - """ - let text = """ - var name: String - var age: String - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 1, character: 0), - range: .init( - start: .init(line: 1, character: 0), - end: .init(line: 2, character: 18) - ) - ) - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakLines() - SuggestionInjector().proposeSuggestion( - intoContentWithoutSuggestion: &lines, - completion: suggestion, - index: 0, - count: 10, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertFalse(extraInfo.didChangeCursorPosition) - XCTAssertEqual(extraInfo.suggestionRange, 1...4) - XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications)) - XCTAssertEqual(lines.joined(separator: ""), """ - struct Cat { - /*========== Copilot Suggestion 1/10 - var name: String - var age: String - *///======== End of Copilot Suggestion - } - """) - } - - func test_propose_suggestion_overlap() async throws { - let content = """ - struct Cat { - var name - } - """ - let text = """ - var name: String - var age: String - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 1, character: 0), - range: .init( - start: .init(line: 1, character: 0), - end: .init(line: 2, character: 18) - ) - ) - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakLines() - SuggestionInjector().proposeSuggestion( - intoContentWithoutSuggestion: &lines, - completion: suggestion, - index: 0, - count: 10, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertFalse(extraInfo.didChangeCursorPosition) - XCTAssertEqual(extraInfo.suggestionRange, 2...5) - XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications)) - XCTAssertEqual(lines.joined(separator: ""), """ - struct Cat { - var name - /*========== Copilot Suggestion 1/10 - ^: String - var age: String - *///======== End of Copilot Suggestion - } - """) - } - - func test_propose_suggestion_overlap_first_line_is_empty() async throws { - let content = """ - struct Cat { - var name: String - } - """ - let text = """ - var name: String - var age: String - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 1, character: 0), - range: .init( - start: .init(line: 1, character: 0), - end: .init(line: 2, character: 18) - ) - ) - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakLines() - SuggestionInjector().proposeSuggestion( - intoContentWithoutSuggestion: &lines, - completion: suggestion, - index: 0, - count: 10, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertFalse(extraInfo.didChangeCursorPosition) - XCTAssertEqual(extraInfo.suggestionRange, 2...5) - XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications)) - XCTAssertEqual(lines.joined(separator: ""), """ - struct Cat { - var name: String - /*========== Copilot Suggestion 1/10 - ^ - var age: String - *///======== End of Copilot Suggestion - } - """) - } - - // swiftformat:disable indent trailingSpace - func test_propose_suggestion_overlap_pure_spaces() async throws { - let content = """ - func quickSort() { - - } - """ // Yes the second line has 4 spaces! - let text = """ - var array = [1, 3, 2, 4, 5, 6, 7, 8, 9, 10] - var left = 0 - var right = array.count - 1 - quickSort(&array, left, right) - print(array) - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 1, character: 0), - range: .init( - start: .init(line: 1, character: 0), - end: .init(line: 2, character: 18) - ) - ) - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakLines() - SuggestionInjector().proposeSuggestion( - intoContentWithoutSuggestion: &lines, - completion: suggestion, - index: 0, - count: 10, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertFalse(extraInfo.didChangeCursorPosition) - XCTAssertEqual(extraInfo.suggestionRange, 2...8) - XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications)) - XCTAssertEqual(lines.joined(separator: ""), """ - func quickSort() { - - /*========== Copilot Suggestion 1/10 - ^var array = [1, 3, 2, 4, 5, 6, 7, 8, 9, 10] - var left = 0 - var right = array.count - 1 - quickSort(&array, left, right) - print(array) - *///======== End of Copilot Suggestion - } - """) // Yes the second line still has 4 spaces! - } - - // swiftformat:enable all - - func test_propose_suggestion_partial_overlap() async throws { - let content = "func quickSort() {}}\n" - let text = """ - func quickSort() { - var array = [1, 3, 2, 4, 5, 6, 7, 8, 9, 10] - var left = 0 - var right = array.count - 1 - quickSort(&array, left, right) - print(array) - } - """ - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 0, character: 0), - range: .init( - start: .init(line: 0, character: 0), - end: .init(line: 5, character: 15) - ) - ) - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakLines() - SuggestionInjector().proposeSuggestion( - intoContentWithoutSuggestion: &lines, - completion: suggestion, - index: 0, - count: 10, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertFalse(extraInfo.didChangeCursorPosition) - XCTAssertEqual(extraInfo.suggestionRange, 1...9) - XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications)) - XCTAssertEqual(lines.joined(separator: ""), """ - func quickSort() {}} - /*========== Copilot Suggestion 1/10 - ^ - var array = [1, 3, 2, 4, 5, 6, 7, 8, 9, 10] - var left = 0 - var right = array.count - 1 - quickSort(&array, left, right) - print(array) - } - *///======== End of Copilot Suggestion - - """) - } - - func test_propose_suggestion_overlap_one_line_adding_only_spaces() async throws { - let content = """ - if true { - print("hello") - } else { - print("world") - } - """ - let text = "} else {\n" - let suggestion = CodeSuggestion( - id: "", - text: text, - position: .init(line: 2, character: 0), - range: .init( - start: .init(line: 2, character: 0), - end: .init(line: 2, character: 8) - ) - ) - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakLines() - SuggestionInjector().proposeSuggestion( - intoContentWithoutSuggestion: &lines, - completion: suggestion, - index: 0, - count: 10, - extraInfo: &extraInfo - ) - XCTAssertFalse(extraInfo.didChangeContent) - XCTAssertFalse(extraInfo.didChangeCursorPosition) - XCTAssertNil(extraInfo.suggestionRange) - XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications)) - XCTAssertEqual(lines.joined(separator: ""), """ - if true { - print("hello") - } else { - print("world") - } - """) - } -} +//import SuggestionModel +//import XCTest +// +//@testable import SuggestionInjector +// +//final class ProposeSuggestionTests: XCTestCase { +// func test_propose_suggestion_no_overlap() async throws { +// let content = """ +// struct Cat { +// +// } +// """ +// let text = """ +// var name: String +// var age: String +// """ +// let suggestion = CodeSuggestion( +// id: "", +// text: text, +// position: .init(line: 2, character: 19), +// range: .init( +// start: .init(line: 1, character: 0), +// end: .init(line: 2, character: 18) +// ) +// ) +// var extraInfo = SuggestionInjector.ExtraInfo() +// var lines = content.breakLines() +// SuggestionInjector().proposeSuggestion( +// intoContentWithoutSuggestion: &lines, +// completion: suggestion, +// index: 0, +// count: 10, +// extraInfo: &extraInfo +// ) +// XCTAssertTrue(extraInfo.didChangeContent) +// XCTAssertFalse(extraInfo.didChangeCursorPosition) +// XCTAssertEqual(extraInfo.suggestionRange, 2...5) +// XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications)) +// XCTAssertEqual( +// lines.joined(separator: ""), +// """ +// struct Cat { +// +// /*========== Copilot Suggestion 1/10 +// var name: String +// var age: String +// *///======== End of Copilot Suggestion +// } +// """, +// "The user may want to keep typing on the empty line, so suggestion is addded to the next line" +// ) +// } +// +// func test_propose_suggestion_no_overlap_start_from_previous_line() async throws { +// let content = """ +// struct Cat { +// } +// """ +// let text = """ +// var name: String +// var age: String +// """ +// let suggestion = CodeSuggestion( +// id: "", +// text: text, +// position: .init(line: 1, character: 0), +// range: .init( +// start: .init(line: 1, character: 0), +// end: .init(line: 2, character: 18) +// ) +// ) +// var extraInfo = SuggestionInjector.ExtraInfo() +// var lines = content.breakLines() +// SuggestionInjector().proposeSuggestion( +// intoContentWithoutSuggestion: &lines, +// completion: suggestion, +// index: 0, +// count: 10, +// extraInfo: &extraInfo +// ) +// XCTAssertTrue(extraInfo.didChangeContent) +// XCTAssertFalse(extraInfo.didChangeCursorPosition) +// XCTAssertEqual(extraInfo.suggestionRange, 1...4) +// XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications)) +// XCTAssertEqual(lines.joined(separator: ""), """ +// struct Cat { +// /*========== Copilot Suggestion 1/10 +// var name: String +// var age: String +// *///======== End of Copilot Suggestion +// } +// """) +// } +// +// func test_propose_suggestion_overlap() async throws { +// let content = """ +// struct Cat { +// var name +// } +// """ +// let text = """ +// var name: String +// var age: String +// """ +// let suggestion = CodeSuggestion( +// id: "", +// text: text, +// position: .init(line: 1, character: 0), +// range: .init( +// start: .init(line: 1, character: 0), +// end: .init(line: 2, character: 18) +// ) +// ) +// var extraInfo = SuggestionInjector.ExtraInfo() +// var lines = content.breakLines() +// SuggestionInjector().proposeSuggestion( +// intoContentWithoutSuggestion: &lines, +// completion: suggestion, +// index: 0, +// count: 10, +// extraInfo: &extraInfo +// ) +// XCTAssertTrue(extraInfo.didChangeContent) +// XCTAssertFalse(extraInfo.didChangeCursorPosition) +// XCTAssertEqual(extraInfo.suggestionRange, 2...5) +// XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications)) +// XCTAssertEqual(lines.joined(separator: ""), """ +// struct Cat { +// var name +// /*========== Copilot Suggestion 1/10 +// ^: String +// var age: String +// *///======== End of Copilot Suggestion +// } +// """) +// } +// +// func test_propose_suggestion_overlap_first_line_is_empty() async throws { +// let content = """ +// struct Cat { +// var name: String +// } +// """ +// let text = """ +// var name: String +// var age: String +// """ +// let suggestion = CodeSuggestion( +// id: "", +// text: text, +// position: .init(line: 1, character: 0), +// range: .init( +// start: .init(line: 1, character: 0), +// end: .init(line: 2, character: 18) +// ) +// ) +// var extraInfo = SuggestionInjector.ExtraInfo() +// var lines = content.breakLines() +// SuggestionInjector().proposeSuggestion( +// intoContentWithoutSuggestion: &lines, +// completion: suggestion, +// index: 0, +// count: 10, +// extraInfo: &extraInfo +// ) +// XCTAssertTrue(extraInfo.didChangeContent) +// XCTAssertFalse(extraInfo.didChangeCursorPosition) +// XCTAssertEqual(extraInfo.suggestionRange, 2...5) +// XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications)) +// XCTAssertEqual(lines.joined(separator: ""), """ +// struct Cat { +// var name: String +// /*========== Copilot Suggestion 1/10 +// ^ +// var age: String +// *///======== End of Copilot Suggestion +// } +// """) +// } +// +// // swiftformat:disable indent trailingSpace +// func test_propose_suggestion_overlap_pure_spaces() async throws { +// let content = """ +// func quickSort() { +// +// } +// """ // Yes the second line has 4 spaces! +// let text = """ +// var array = [1, 3, 2, 4, 5, 6, 7, 8, 9, 10] +// var left = 0 +// var right = array.count - 1 +// quickSort(&array, left, right) +// print(array) +// """ +// let suggestion = CodeSuggestion( +// id: "", +// text: text, +// position: .init(line: 1, character: 0), +// range: .init( +// start: .init(line: 1, character: 0), +// end: .init(line: 2, character: 18) +// ) +// ) +// var extraInfo = SuggestionInjector.ExtraInfo() +// var lines = content.breakLines() +// SuggestionInjector().proposeSuggestion( +// intoContentWithoutSuggestion: &lines, +// completion: suggestion, +// index: 0, +// count: 10, +// extraInfo: &extraInfo +// ) +// XCTAssertTrue(extraInfo.didChangeContent) +// XCTAssertFalse(extraInfo.didChangeCursorPosition) +// XCTAssertEqual(extraInfo.suggestionRange, 2...8) +// XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications)) +// XCTAssertEqual(lines.joined(separator: ""), """ +// func quickSort() { +// +// /*========== Copilot Suggestion 1/10 +// ^var array = [1, 3, 2, 4, 5, 6, 7, 8, 9, 10] +// var left = 0 +// var right = array.count - 1 +// quickSort(&array, left, right) +// print(array) +// *///======== End of Copilot Suggestion +// } +// """) // Yes the second line still has 4 spaces! +// } +// +// // swiftformat:enable all +// +// func test_propose_suggestion_partial_overlap() async throws { +// let content = "func quickSort() {}}\n" +// let text = """ +// func quickSort() { +// var array = [1, 3, 2, 4, 5, 6, 7, 8, 9, 10] +// var left = 0 +// var right = array.count - 1 +// quickSort(&array, left, right) +// print(array) +// } +// """ +// let suggestion = CodeSuggestion( +// id: "", +// text: text, +// position: .init(line: 0, character: 0), +// range: .init( +// start: .init(line: 0, character: 0), +// end: .init(line: 5, character: 15) +// ) +// ) +// var extraInfo = SuggestionInjector.ExtraInfo() +// var lines = content.breakLines() +// SuggestionInjector().proposeSuggestion( +// intoContentWithoutSuggestion: &lines, +// completion: suggestion, +// index: 0, +// count: 10, +// extraInfo: &extraInfo +// ) +// XCTAssertTrue(extraInfo.didChangeContent) +// XCTAssertFalse(extraInfo.didChangeCursorPosition) +// XCTAssertEqual(extraInfo.suggestionRange, 1...9) +// XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications)) +// XCTAssertEqual(lines.joined(separator: ""), """ +// func quickSort() {}} +// /*========== Copilot Suggestion 1/10 +// ^ +// var array = [1, 3, 2, 4, 5, 6, 7, 8, 9, 10] +// var left = 0 +// var right = array.count - 1 +// quickSort(&array, left, right) +// print(array) +// } +// *///======== End of Copilot Suggestion +// +// """) +// } +// +// func test_propose_suggestion_overlap_one_line_adding_only_spaces() async throws { +// let content = """ +// if true { +// print("hello") +// } else { +// print("world") +// } +// """ +// let text = "} else {\n" +// let suggestion = CodeSuggestion( +// id: "", +// text: text, +// position: .init(line: 2, character: 0), +// range: .init( +// start: .init(line: 2, character: 0), +// end: .init(line: 2, character: 8) +// ) +// ) +// var extraInfo = SuggestionInjector.ExtraInfo() +// var lines = content.breakLines() +// SuggestionInjector().proposeSuggestion( +// intoContentWithoutSuggestion: &lines, +// completion: suggestion, +// index: 0, +// count: 10, +// extraInfo: &extraInfo +// ) +// XCTAssertFalse(extraInfo.didChangeContent) +// XCTAssertFalse(extraInfo.didChangeCursorPosition) +// XCTAssertNil(extraInfo.suggestionRange) +// XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications)) +// XCTAssertEqual(lines.joined(separator: ""), """ +// if true { +// print("hello") +// } else { +// print("world") +// } +// """) +// } +//} diff --git a/Core/Tests/SuggestionInjectorTests/RejectSuggestionTests.swift b/Core/Tests/SuggestionInjectorTests/RejectSuggestionTests.swift index eeef6be6..54b70d3c 100644 --- a/Core/Tests/SuggestionInjectorTests/RejectSuggestionTests.swift +++ b/Core/Tests/SuggestionInjectorTests/RejectSuggestionTests.swift @@ -1,83 +1,83 @@ -import SuggestionModel -import XCTest - -@testable import SuggestionInjector - -final class RejectSuggestionTests: XCTestCase { - func test_rejecting_suggestion() async throws { - let content = """ - struct Cat { - var name - /*========== Copilot Suggestion 1/10 - ^: String - var age: String - *///======== End of Copilot Suggestion - } - """ - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakLines() - var cursor = CursorPosition(line: 1, character: 12) - SuggestionInjector().rejectCurrentSuggestions( - from: &lines, - cursorPosition: &cursor, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertFalse(extraInfo.didChangeCursorPosition) - XCTAssertNil(extraInfo.suggestionRange) - XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications)) - XCTAssertEqual(lines.joined(separator: ""), """ - struct Cat { - var name - } - """) - XCTAssertEqual( - cursor, - .init(line: 1, character: 12), - "If cursor is above deletion, don't move it." - ) - } - - func test_broken_suggestion() async throws { - let content = """ - struct Cat { - var name - /*========== Copilot Suggestion 1/10 - ^: String - var age: String - *///======== End of Copilot Suggestion - - /*========== Copilot Suggestion 2/10 - - /*========== Copilot Suggestion 1/10 - ^: String - var age: String - *///======== End of Copilot Suggestion - """ - var extraInfo = SuggestionInjector.ExtraInfo() - var lines = content.breakLines() - var cursor = CursorPosition(line: 6, character: 0) - SuggestionInjector().rejectCurrentSuggestions( - from: &lines, - cursorPosition: &cursor, - extraInfo: &extraInfo - ) - XCTAssertTrue(extraInfo.didChangeContent) - XCTAssertTrue(extraInfo.didChangeCursorPosition) - XCTAssertNil(extraInfo.suggestionRange) - XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications)) - XCTAssertEqual(lines.joined(separator: ""), """ - struct Cat { - var name - - /*========== Copilot Suggestion 2/10 - - - """) - XCTAssertEqual( - cursor, - .init(line: 2, character: 0), - "If cursor is below deletion, move it up." - ) - } -} +//import SuggestionModel +//import XCTest +// +//@testable import SuggestionInjector +// +//final class RejectSuggestionTests: XCTestCase { +// func test_rejecting_suggestion() async throws { +// let content = """ +// struct Cat { +// var name +// /*========== Copilot Suggestion 1/10 +// ^: String +// var age: String +// *///======== End of Copilot Suggestion +// } +// """ +// var extraInfo = SuggestionInjector.ExtraInfo() +// var lines = content.breakLines() +// var cursor = CursorPosition(line: 1, character: 12) +// SuggestionInjector().rejectCurrentSuggestions( +// from: &lines, +// cursorPosition: &cursor, +// extraInfo: &extraInfo +// ) +// XCTAssertTrue(extraInfo.didChangeContent) +// XCTAssertFalse(extraInfo.didChangeCursorPosition) +// XCTAssertNil(extraInfo.suggestionRange) +// XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications)) +// XCTAssertEqual(lines.joined(separator: ""), """ +// struct Cat { +// var name +// } +// """) +// XCTAssertEqual( +// cursor, +// .init(line: 1, character: 12), +// "If cursor is above deletion, don't move it." +// ) +// } +// +// func test_broken_suggestion() async throws { +// let content = """ +// struct Cat { +// var name +// /*========== Copilot Suggestion 1/10 +// ^: String +// var age: String +// *///======== End of Copilot Suggestion +// +// /*========== Copilot Suggestion 2/10 +// +// /*========== Copilot Suggestion 1/10 +// ^: String +// var age: String +// *///======== End of Copilot Suggestion +// """ +// var extraInfo = SuggestionInjector.ExtraInfo() +// var lines = content.breakLines() +// var cursor = CursorPosition(line: 6, character: 0) +// SuggestionInjector().rejectCurrentSuggestions( +// from: &lines, +// cursorPosition: &cursor, +// extraInfo: &extraInfo +// ) +// XCTAssertTrue(extraInfo.didChangeContent) +// XCTAssertTrue(extraInfo.didChangeCursorPosition) +// XCTAssertNil(extraInfo.suggestionRange) +// XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications)) +// XCTAssertEqual(lines.joined(separator: ""), """ +// struct Cat { +// var name +// +// /*========== Copilot Suggestion 2/10 +// +// +// """) +// XCTAssertEqual( +// cursor, +// .init(line: 2, character: 0), +// "If cursor is below deletion, move it up." +// ) +// } +//} diff --git a/Pro b/Pro index 2022283e..d0b8da6f 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 2022283eb04f1451fd6f88d7cfd9b09a8c85b7ef +Subproject commit d0b8da6fd46da7736d9cc930aad5b01c3b307c67 diff --git a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift index c9c061a0..cb3103c4 100644 --- a/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/ObjectiveC/ObjectiveCCodeFinder.swift @@ -192,7 +192,8 @@ public class ObjectiveCFocusedCodeFinder: KnownLanguageFocusedCodeFinder< signaturePointRange ) = node.extractInformationBeforeNode(withFieldName: "body") let signature = textProvider(.range(range: signatureRange, pointRange: signaturePointRange)) - .replacingOccurrences(of: "\n", with: " ") + .breakLines(proposedLineEnding: " ", appendLineBreakToLastLine: false) + .joined() .trimmingCharacters(in: .whitespacesAndNewlines) if signature.isEmpty { return nil } return .init( @@ -219,7 +220,8 @@ extension ObjectiveCFocusedCodeFinder { signaturePointRange ) = node.extractInformationBeforeNode(withFieldName: "body") let signature = textProvider(.range(range: signatureRange, pointRange: signaturePointRange)) - .replacingOccurrences(of: "\n", with: "") + .breakLines(proposedLineEnding: " ", appendLineBreakToLastLine: false) + .joined() .trimmingCharacters(in: .whitespacesAndNewlines) if signature.isEmpty { return nil } return .init( diff --git a/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift index 2bb2bf11..3eb7a91f 100644 --- a/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/Swift/SwiftFocusedCodeFinder.swift @@ -77,7 +77,8 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) - .replacingOccurrences(of: "\n", with: " "), + .split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) + .joined(separator: " "), name: name ) @@ -89,7 +90,8 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) - .replacingOccurrences(of: "\n", with: " "), + .split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) + .joined(separator: " "), name: name ) @@ -101,7 +103,8 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) - .replacingOccurrences(of: "\n", with: " "), + .split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) + .joined(separator: " "), name: name ) @@ -113,7 +116,8 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) - .replacingOccurrences(of: "\n", with: ""), + .split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) + .joined(separator: " "), name: name ) @@ -124,7 +128,8 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< node: node, signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) - .replacingOccurrences(of: "\n", with: " "), + .split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) + .joined(separator: " "), name: name ) @@ -136,7 +141,8 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) - .replacingOccurrences(of: "\n", with: " "), + .split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) + .joined(separator: " "), name: name ) @@ -148,7 +154,8 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) - .replacingOccurrences(of: "\n", with: " "), + .split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) + .joined(separator: " "), name: name ) @@ -156,7 +163,7 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< let type = node.funcKeyword.text let name = node.identifier.text let signature = node.signature.trimmedDescription - .split(whereSeparator: \.isNewline) + .split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .joined(separator: " ") @@ -176,7 +183,8 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< node: node, signature: "\(type) \(name)\(signature.isEmpty ? "" : "\(signature)")" .prefixedModifiers(node.modifierAndAttributeText(extractText)) - .replacingOccurrences(of: "\n", with: " "), + .split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) + .joined(separator: " "), name: name, canBeUsedAsCodeRange: false ) @@ -189,7 +197,8 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< node: node, signature: signature .prefixedModifiers(node.modifierAndAttributeText(extractText)) - .replacingOccurrences(of: "\n", with: " "), + .split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) + .joined(separator: " "), name: keyword ) @@ -203,7 +212,8 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< node: node, signature: signature .prefixedModifiers(node.modifierAndAttributeText(extractText)) - .replacingOccurrences(of: "\n", with: " "), + .split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) + .joined(separator: " "), name: "subscript" ) @@ -214,7 +224,8 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< node: node, signature: "\(signature)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) - .replacingOccurrences(of: "\n", with: " "), + .split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) + .joined(separator: " "), name: "init" ) @@ -225,7 +236,8 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< node: node, signature: signature .prefixedModifiers(node.modifierAndAttributeText(extractText)) - .replacingOccurrences(of: "\n", with: " "), + .split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) + .joined(separator: " "), name: "deinit" ) @@ -234,7 +246,9 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< return .init( node: node, - signature: signature.replacingOccurrences(of: "\n", with: " "), + signature: signature + .split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) + .joined(separator: " "), name: "closure" ) @@ -243,7 +257,9 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< return .init( node: node, - signature: signature.replacingOccurrences(of: "\n", with: " "), + signature: signature + .split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) + .joined(separator: " "), name: "function call", canBeUsedAsCodeRange: false ) @@ -251,7 +267,9 @@ public class SwiftFocusedCodeFinder: KnownLanguageFocusedCodeFinder< case let node as SwitchCaseSyntax: return .init( node: node, - signature: node.trimmedDescription.replacingOccurrences(of: "\n", with: " "), + signature: node.trimmedDescription + .split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) + .joined(separator: " "), name: "switch" ) diff --git a/Tool/Sources/LangChain/DocumentTransformer/RecursiveCharacterTextSplitter.swift b/Tool/Sources/LangChain/DocumentTransformer/RecursiveCharacterTextSplitter.swift index da19b80e..d71e147b 100644 --- a/Tool/Sources/LangChain/DocumentTransformer/RecursiveCharacterTextSplitter.swift +++ b/Tool/Sources/LangChain/DocumentTransformer/RecursiveCharacterTextSplitter.swift @@ -19,7 +19,7 @@ public class RecursiveCharacterTextSplitter: TextSplitter { /// - chunkOverlap: The maximum overlap between chunks. /// - lengthFunction: A function to compute the length of text. public init( - separators: [String] = ["\n\n", "\n", " ", ""], + separators: [String] = ["\n\n", "\r\n", "\n", "\r", " ", ""], chunkSize: Int = 4000, chunkOverlap: Int = 200, lengthFunction: @escaping (String) -> Int = { $0.count } diff --git a/Tool/Sources/LangChain/DocumentTransformer/TextSplitterSeparatorSet.swift b/Tool/Sources/LangChain/DocumentTransformer/TextSplitterSeparatorSet.swift index a60e57e2..1acdf6d9 100644 --- a/Tool/Sources/LangChain/DocumentTransformer/TextSplitterSeparatorSet.swift +++ b/Tool/Sources/LangChain/DocumentTransformer/TextSplitterSeparatorSet.swift @@ -37,7 +37,9 @@ public struct TextSplitterSeparatorSet: ExpressibleByArrayLiteral { "\ncase ", // Split by the normal type of lines "\n\n", + "\r\n", "\n", + "\r", " ", "", ] @@ -60,7 +62,9 @@ public struct TextSplitterSeparatorSet: ExpressibleByArrayLiteral { "\ncase ", // Split by the normal type of lines "\n\n", + "\r\n", "\n", + "\r", " ", "", ] @@ -87,7 +91,9 @@ public struct TextSplitterSeparatorSet: ExpressibleByArrayLiteral { "\ncase ", // Split by the normal type of lines "\n\n", + "\r\n", "\n", + "\r", " ", "", ] @@ -107,7 +113,9 @@ public struct TextSplitterSeparatorSet: ExpressibleByArrayLiteral { "\ncase ", // Split by the normal type of lines "\n\n", + "\r\n", "\n", + "\r", " ", "", ] @@ -130,7 +138,9 @@ public struct TextSplitterSeparatorSet: ExpressibleByArrayLiteral { "\ncase ", // Split by the normal type of lines "\n\n", + "\r\n", "\n", + "\r", " ", "", ] @@ -151,7 +161,9 @@ public struct TextSplitterSeparatorSet: ExpressibleByArrayLiteral { "\ncase ", // Split by the normal type of lines "\n\n", + "\r\n", "\n", + "\r", " ", "", ] @@ -174,7 +186,9 @@ public struct TextSplitterSeparatorSet: ExpressibleByArrayLiteral { "\ndefault ", // Split by the normal type of lines "\n\n", + "\r\n", "\n", + "\r", " ", "", ] @@ -188,7 +202,9 @@ public struct TextSplitterSeparatorSet: ExpressibleByArrayLiteral { "\n\tdef ", // Now split by the normal type of lines "\n\n", + "\r\n", "\n", + "\r", " ", "", ] @@ -209,7 +225,9 @@ public struct TextSplitterSeparatorSet: ExpressibleByArrayLiteral { "\nconst ", // Split by the normal type of lines "\n\n", + "\r\n", "\n", + "\r", " ", "", ] @@ -230,7 +248,9 @@ public struct TextSplitterSeparatorSet: ExpressibleByArrayLiteral { "\nrescue ", // Split by the normal type of lines "\n\n", + "\r\n", "\n", + "\r", " ", "", ] @@ -256,7 +276,9 @@ public struct TextSplitterSeparatorSet: ExpressibleByArrayLiteral { // Note that this splitter doesn't handle horizontal lines defined // by *three or more* of ***, ---, or ___, but this is not handled "\n\n", + "\r\n", "\n", + "\r", " ", "", ] @@ -318,6 +340,10 @@ public struct TextSplitterSeparatorSet: ExpressibleByArrayLiteral { "