Skip to content

Commit 17e9b5a

Browse files
committed
Merge tag 'accepting-suggestion-keep-typing-has-suffix' into develop
2 parents 0575d41 + 509273c commit 17e9b5a

File tree

11 files changed

+107
-45
lines changed

11 files changed

+107
-45
lines changed

Core/Sources/GitHubCopilotService/GitHubCopilotService.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ public protocol GitHubCopilotSuggestionServiceType {
2323
tabSize: Int,
2424
indentSize: Int,
2525
usesTabsForIndentation: Bool,
26-
ignoreSpaceOnlySuggestions: Bool
26+
ignoreSpaceOnlySuggestions: Bool,
27+
ignoreTrailingNewLinesAndSpaces: Bool
2728
) async throws -> [CodeSuggestion]
2829
func notifyAccepted(_ completion: CodeSuggestion) async
2930
func notifyRejected(_ completions: [CodeSuggestion]) async
@@ -269,7 +270,8 @@ public final class GitHubCopilotSuggestionService: GitHubCopilotBaseService,
269270
tabSize: Int,
270271
indentSize: Int,
271272
usesTabsForIndentation: Bool,
272-
ignoreSpaceOnlySuggestions: Bool
273+
ignoreSpaceOnlySuggestions: Bool,
274+
ignoreTrailingNewLinesAndSpaces: Bool
273275
) async throws -> [CodeSuggestion] {
274276
let languageId = languageIdentifierFromFileURL(fileURL)
275277

@@ -313,7 +315,7 @@ public final class GitHubCopilotSuggestionService: GitHubCopilotBaseService,
313315
return true
314316
}
315317
.map {
316-
if UserDefaults.shared.value(for: \.gitHubCopilotIgnoreTrailingNewLines) {
318+
if ignoreTrailingNewLinesAndSpaces {
317319
var updated = $0
318320
var text = updated.text[...]
319321
while let last = text.last, last.isNewline || last.isWhitespace {

Core/Sources/PromptToCodeService/CopilotPromptToCodeAPI.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ final class CopilotPromptToCodeAPI: PromptToCodeAPI {
7070
tabSize: indentSize,
7171
indentSize: indentSize,
7272
usesTabsForIndentation: usesTabsForIndentation,
73-
ignoreSpaceOnlySuggestions: true
73+
ignoreSpaceOnlySuggestions: true,
74+
ignoreTrailingNewLinesAndSpaces: false
7475
)
7576
try Task.checkCancellation()
7677
guard let first = result.first else { throw CancellationError() }

Core/Sources/SuggestionInjector/SuggestionInjector.swift

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import SuggestionModel
21
import Foundation
2+
import SuggestionModel
33

44
let suggestionStart = "/*========== Copilot Suggestion"
55
let suggestionEnd = "*///======== End of Copilot Suggestion"
@@ -141,8 +141,6 @@ public struct SuggestionInjector {
141141
let end = completion.range.end
142142
let suggestionContent = completion.text
143143

144-
let _ = start.line < content.endIndex ? content[start.line] : nil
145-
146144
let firstRemovedLine = content[safe: start.line]
147145
let lastRemovedLine = content[safe: end.line]
148146
let startLine = max(0, start.line)
@@ -177,12 +175,24 @@ public struct SuggestionInjector {
177175
}
178176

179177
// appending suffix text not in range if needed.
180-
let cursorCol = toBeInserted[toBeInserted.endIndex - 1].count - 1
181178
let skipAppendingDueToContinueTyping = {
182179
guard let first = toBeInserted.first?.dropLast(1), !first.isEmpty else { return false }
183180
let droppedLast = lastRemovedLine?.dropLast(1)
184181
guard let droppedLast, !droppedLast.isEmpty else { return false }
185-
return first.hasPrefix(droppedLast)
182+
// case 1: user keeps typing as the suggestion suggests.
183+
if first.hasPrefix(droppedLast) {
184+
return true
185+
}
186+
// case 2: user also typed the suffix of the suggestion (or auto-completed by Xcode)
187+
if end.character < droppedLast.count - 1 {
188+
let splitIndex = droppedLast.index(droppedLast.startIndex, offsetBy: end.character)
189+
let prefix = droppedLast[..<splitIndex]
190+
let suffix = droppedLast[splitIndex...]
191+
if first.hasPrefix(prefix), first.hasSuffix(suffix) {
192+
return true
193+
}
194+
}
195+
return false
186196
}()
187197
if let lastRemovedLine,
188198
!skipAppendingDueToContinueTyping,
@@ -200,11 +210,12 @@ public struct SuggestionInjector {
200210
toBeInserted[toBeInserted.endIndex - 1].removeLast(1)
201211
}
202212
let leftover = lastRemovedLine[leftoverRange]
203-
213+
204214
toBeInserted[toBeInserted.endIndex - 1]
205215
.append(contentsOf: leftover)
206216
}
207217

218+
let cursorCol = toBeInserted[toBeInserted.endIndex - 1].count - 1
208219
let insertingIndex = min(start.line, content.endIndex)
209220
content.insert(contentsOf: toBeInserted, at: insertingIndex)
210221
extraInfo.modifications.append(.inserted(insertingIndex, toBeInserted))
@@ -270,3 +281,4 @@ extension Array {
270281
indices.contains(index) ? self[index] : nil
271282
}
272283
}
284+

Core/Sources/SuggestionService/GitHubCopilotSuggestionProvider.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ extension GitHubCopilotSuggestionProvider {
4242
tabSize: tabSize,
4343
indentSize: indentSize,
4444
usesTabsForIndentation: usesTabsForIndentation,
45-
ignoreSpaceOnlySuggestions: ignoreSpaceOnlySuggestions
45+
ignoreSpaceOnlySuggestions: ignoreSpaceOnlySuggestions,
46+
ignoreTrailingNewLinesAndSpaces: UserDefaults.shared
47+
.value(for: \.gitHubCopilotIgnoreTrailingNewLines)
4648
)
4749
}
4850

@@ -73,12 +75,12 @@ extension GitHubCopilotSuggestionProvider {
7375
try await (try? createGitHubCopilotServiceIfNeeded())?
7476
.notifySaveTextDocument(fileURL: fileURL)
7577
}
76-
78+
7779
func cancelRequest() async {
7880
await (try? createGitHubCopilotServiceIfNeeded())?
7981
.cancelRequest()
8082
}
81-
83+
8284
func terminate() async {
8385
await (try? createGitHubCopilotServiceIfNeeded())?.terminate()
8486
}

Core/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import XCTest
44
@testable import GitHubCopilotService
55

66
final class FetchSuggestionTests: XCTestCase {
7-
func test_process_sugestions_from_server() async throws {
7+
func test_process_suggestions_from_server() async throws {
88
struct TestServer: GitHubCopilotLSP {
99
func sendNotification(_ notif: LanguageServerProtocol.ClientNotification) async throws {
1010
fatalError()
@@ -44,7 +44,8 @@ final class FetchSuggestionTests: XCTestCase {
4444
tabSize: 4,
4545
indentSize: 4,
4646
usesTabsForIndentation: false,
47-
ignoreSpaceOnlySuggestions: false
47+
ignoreSpaceOnlySuggestions: false,
48+
ignoreTrailingNewLinesAndSpaces: false
4849
)
4950
XCTAssertEqual(completions.count, 3)
5051
}
@@ -89,7 +90,8 @@ final class FetchSuggestionTests: XCTestCase {
8990
tabSize: 4,
9091
indentSize: 4,
9192
usesTabsForIndentation: false,
92-
ignoreSpaceOnlySuggestions: true
93+
ignoreSpaceOnlySuggestions: true,
94+
ignoreTrailingNewLinesAndSpaces: false
9395
)
9496
XCTAssertEqual(completions.count, 1)
9597
XCTAssertEqual(completions.first?.text, "Hello World\n")
@@ -128,9 +130,10 @@ final class FetchSuggestionTests: XCTestCase {
128130
tabSize: 4,
129131
indentSize: 4,
130132
usesTabsForIndentation: false,
131-
ignoreSpaceOnlySuggestions: false
133+
ignoreSpaceOnlySuggestions: false,
134+
ignoreTrailingNewLinesAndSpaces: true
132135
)
133136
XCTAssertEqual(completions.count, 1)
134-
XCTAssertEqual(completions.first?.text, "Hello World\n")
137+
XCTAssertEqual(completions.first?.text, "Hello World")
135138
}
136139
}

Core/Tests/ServiceTests/Environment.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ class MockSuggestionService: GitHubCopilotSuggestionServiceType {
7171
tabSize: Int,
7272
indentSize: Int,
7373
usesTabsForIndentation: Bool,
74-
ignoreSpaceOnlySuggestions: Bool
74+
ignoreSpaceOnlySuggestions: Bool,
75+
ignoreTrailingNewLinesAndSpaces: Bool
7576
) async throws -> [SuggestionModel.CodeSuggestion] {
7677
completions
7778
}

Core/Tests/SuggestionInjectorTests/AcceptSuggestionTests.swift

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,47 @@ final class AcceptSuggestionTests: XCTestCase {
174174
}
175175
""")
176176
}
177+
178+
func test_accept_suggestion_overlap_continue_typing_has_suffix_typed() async throws {
179+
let content = """
180+
struct Cat {
181+
var n: String
182+
}
183+
"""
184+
let text = """
185+
var name: String
186+
"""
187+
let suggestion = CodeSuggestion(
188+
text: text,
189+
position: .init(line: 1, character: 9),
190+
uuid: "",
191+
range: .init(
192+
start: .init(line: 1, character: 0),
193+
end: .init(line: 1, character: 9)
194+
),
195+
displayText: ""
196+
)
197+
198+
var extraInfo = SuggestionInjector.ExtraInfo()
199+
var lines = content.breakLines()
200+
var cursor = CursorPosition(line: 0, character: 0)
201+
SuggestionInjector().acceptSuggestion(
202+
intoContentWithoutSuggestion: &lines,
203+
cursorPosition: &cursor,
204+
completion: suggestion,
205+
extraInfo: &extraInfo
206+
)
207+
XCTAssertTrue(extraInfo.didChangeContent)
208+
XCTAssertTrue(extraInfo.didChangeCursorPosition)
209+
XCTAssertNil(extraInfo.suggestionRange)
210+
XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications))
211+
XCTAssertEqual(cursor, .init(line: 1, character: 20))
212+
XCTAssertEqual(lines.joined(separator: ""), """
213+
struct Cat {
214+
var name: String
215+
}
216+
""")
217+
}
177218

178219
func test_propose_suggestion_partial_overlap() async throws {
179220
let content = "func quickSort() {}}\n"
@@ -308,7 +349,7 @@ final class AcceptSuggestionTests: XCTestCase {
308349
XCTAssertTrue(extraInfo.didChangeCursorPosition)
309350
XCTAssertNil(extraInfo.suggestionRange)
310351
XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications))
311-
XCTAssertEqual(cursor, .init(line: 4, character: 1))
352+
XCTAssertEqual(cursor, .init(line: 4, character: 0))
312353
XCTAssertEqual(lines.joined(separator: ""), text)
313354
}
314355

Tool/Tests/LangChainTests/TextSplitterTests/TextSplitterTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ final class TextSplitterTests: XCTestCase {
5858

5959
XCTAssertEqual(
6060
result,
61-
["Madam Speaker,", " Madam Vice", " President, our", " our First"]
61+
["Madam Speaker,", "Madam Vice", "President, our", "our First"]
6262
)
6363
XCTAssertTrue(result.allSatisfy { $0.count <= 15 })
6464
}

Tool/Tests/LangChainTests/VectorStoreTests/TemporaryUSearchTests.swift

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
11
import Foundation
2+
import USearchIndex
23
import XCTest
34

45
import USearch
56

67
@testable import LangChain
78

89
class TemporaryUSearchTests: XCTestCase {
9-
func test_usearch() {
10-
let index = USearchIndex.make(
10+
func test_usearch() async throws {
11+
let index = USearchIndex(
1112
metric: USearchMetric.l2sq,
1213
dimensions: 4,
1314
connectivity: 8,
1415
quantization: USearchScalar.F32
1516
)
16-
let vectorA: [Float32] = [0.3, 0.5, 1.2, 1.4]
17-
let vectorB: [Float32] = [0.4, 0.2, 1.2, 1.1]
18-
index.clear()
19-
index.add(label: 42, vector: vectorA[...])
20-
index.add(label: 43, vector: vectorB[...])
17+
let vectorA: [Float] = [0.3, 0.5, 1.2, 1.4]
18+
let vectorB: [Float] = [0.4, 0.2, 1.2, 1.1]
19+
try await index.clear()
20+
try await index.add(label: 42, vector: vectorA)
21+
try await index.add(label: 43, vector: vectorB)
2122

22-
let results = index.search(vector: vectorA[...], count: 10)
23-
assert(results.0[0] == 42)
23+
let results = try await index.search(vector: vectorA, count: 10)
24+
assert(results[0].label == 42)
2425
}
2526

2627
func test_setting_data() async throws {

Tool/Tests/OpenAIServiceTests/ChatGPTStreamTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ final class ChatGPTStreamTests: XCTestCase {
5454
.init(id: "1", role: .assistant, content: "hellomyfriends"),
5555
], "History is not updated")
5656

57-
XCTAssertEqual(requestBody?.functions, [], "Function schema is not submitted")
57+
XCTAssertEqual(requestBody?.functions, nil, "Function schema is not submitted")
5858
}
5959

6060
func test_handling_function_call() async throws {
@@ -128,7 +128,7 @@ final class ChatGPTStreamTests: XCTestCase {
128128
role: .function,
129129
content: "Function is called.",
130130
name: "function",
131-
summary: "running"
131+
summary: nil
132132
),
133133
.init(id: "3", role: .assistant, content: "hellomyfriends"),
134134
], "History is not updated")
@@ -216,7 +216,7 @@ final class ChatGPTStreamTests: XCTestCase {
216216
role: .function,
217217
content: "Function is called.",
218218
name: "function",
219-
summary: "running"
219+
summary: nil
220220
),
221221
.init(
222222
id: "3",
@@ -229,7 +229,7 @@ final class ChatGPTStreamTests: XCTestCase {
229229
role: .function,
230230
content: "Function is called.",
231231
name: "function",
232-
summary: "running"
232+
summary: nil
233233
),
234234
.init(id: "5", role: .assistant, content: "hellomyfriends"),
235235
], "History is not updated")

0 commit comments

Comments
 (0)