Skip to content

Commit 07b41a6

Browse files
committed
Update to use in-place mutation on buffer.lines to update content
So that the editor won't jump to top on undos / redos. Some tests are behaving weird, should find a better way to assert.
1 parent 0a67c60 commit 07b41a6

File tree

10 files changed

+240
-85
lines changed

10 files changed

+240
-85
lines changed

Core/Package.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ let package = Package(
3636
name: "CopilotModel",
3737
dependencies: ["LanguageClient"]
3838
),
39+
.testTarget(
40+
name: "CopilotModelTests",
41+
dependencies: ["CopilotModel"]
42+
),
3943
.target(
4044
name: "SuggestionInjector",
4145
dependencies: ["CopilotModel"]

Core/Sources/CopilotModel/Modification.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public extension Array where Element == String {
1414
let removingRange = range.lowerBound ..< (range.upperBound + 1)
1515
removeSubrange(removingRange.clamped(to: 0 ..< endIndex))
1616
case let .inserted(index, strings):
17-
insert(contentsOf: strings, at: index)
17+
insert(contentsOf: strings, at: Swift.min(endIndex, index))
1818
}
1919
}
2020
}
@@ -25,3 +25,20 @@ public extension Array where Element == String {
2525
return newArray
2626
}
2727
}
28+
29+
public extension NSMutableArray {
30+
func apply(_ modifications: [Modification]) {
31+
for modification in modifications {
32+
switch modification {
33+
case let .deleted(range):
34+
if count == 0 { break }
35+
let newRange = range.clamped(to: 0 ... (count - 1))
36+
removeObjects(in: NSRange(newRange))
37+
case let .inserted(index, strings):
38+
for string in strings.reversed() {
39+
insert(string, at: Swift.min(count, index))
40+
}
41+
}
42+
}
43+
}
44+
}

Core/Sources/SuggestionInjector/SuggestionInjector.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,19 +123,19 @@ public struct SuggestionInjector {
123123
let commonPrefix = longestCommonPrefix(of: suggestionContent, and: existedLine ?? "")
124124

125125
if let existedLine, existedLine.count > 1, !commonPrefix.isEmpty {
126-
content.remove(at: start.line)
127126
extraInfo.modifications.append(.deleted(start.line...start.line))
127+
content.remove(at: start.line)
128128
} else if content.count > start.line,
129129
content[start.line].isEmpty || content[start.line] == "\n"
130130
{
131-
content.remove(at: start.line)
132131
extraInfo.modifications.append(.deleted(start.line...start.line))
132+
content.remove(at: start.line)
133133
}
134134

135135
let toBeInserted = suggestionContent.breakLines(appendLineBreakToLastLine: true)
136136
if content.endIndex < start.line {
137-
content.append(contentsOf: toBeInserted)
138137
extraInfo.modifications.append(.inserted(content.endIndex, toBeInserted))
138+
content.append(contentsOf: toBeInserted)
139139
cursorPosition = .init(
140140
line: toBeInserted.endIndex,
141141
character: (toBeInserted.last?.count ?? 1) - 1
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import XCTest
2+
3+
@testable import CopilotModel
4+
5+
final class ModificationTests: XCTestCase {
6+
func test_nsmutablearray_deleting_an_element() {
7+
let a = NSMutableArray(array: ["a", "b", "c"])
8+
a.apply([.deleted(0 ... 0)])
9+
XCTAssertEqual(a as! [String], ["b", "c"])
10+
}
11+
12+
func test_nsmutablearray_deleting_all_element() {
13+
let a = NSMutableArray(array: ["a", "b", "c"])
14+
a.apply([.deleted(0 ... 2)])
15+
XCTAssertEqual(a as! [String], [])
16+
}
17+
18+
func test_nsmutablearray_deleting_too_much_element() {
19+
let a = NSMutableArray(array: ["a", "b", "c"])
20+
a.apply([.deleted(0 ... 100)])
21+
XCTAssertEqual(a as! [String], [])
22+
}
23+
24+
func test_nsmutablearray_inserting_elements() {
25+
let a = NSMutableArray(array: ["a", "b", "c"])
26+
a.apply([.inserted(0, ["y", "z"])])
27+
XCTAssertEqual(a as! [String], ["y", "z", "a", "b", "c"])
28+
a.apply([.inserted(1, ["0", "1"])])
29+
XCTAssertEqual(a as! [String], ["y", "0", "1", "z", "a", "b", "c"])
30+
}
31+
32+
func test_nsmutablearray_inserting_elements_at_index_out_of_range() {
33+
let a = NSMutableArray(array: ["a", "b", "c"])
34+
a.apply([.inserted(1000, ["z"])])
35+
XCTAssertEqual(a as! [String], ["a", "b", "c", "z"])
36+
}
37+
}

EditorExtension/Helpers.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,33 @@ import Foundation
33
import XcodeKit
44

55
extension XCSourceEditorCommandInvocation {
6-
func mutateCompleteBuffer(content: String, restoringSelections restore: Bool) {
6+
func mutateCompleteBuffer(modifications: [Modification], restoringSelections restore: Bool) {
77
if restore {
88
let selectionsRangesToRestore = buffer.selections.compactMap { $0 as? XCSourceTextRange }
99
buffer.selections.removeAllObjects()
10-
buffer.completeBuffer = content
10+
buffer.lines.apply(modifications)
1111
for range in selectionsRangesToRestore {
1212
buffer.selections.add(range)
1313
}
1414
} else {
15-
buffer.completeBuffer = content
15+
buffer.lines.apply(modifications)
1616
}
1717
}
1818

1919
func accept(_ updatedContent: UpdatedContent) {
2020
if let newCursor = updatedContent.newCursor {
21-
mutateCompleteBuffer(content: updatedContent.content, restoringSelections: false)
21+
mutateCompleteBuffer(
22+
modifications: updatedContent.modifications,
23+
restoringSelections: false
24+
)
2225
buffer.selections.removeAllObjects()
2326
buffer.selections.add(XCSourceTextRange(
2427
start: .init(line: newCursor.line, column: newCursor.character),
2528
end: .init(line: newCursor.line, column: newCursor.character)
2629
))
2730
} else {
2831
mutateCompleteBuffer(
29-
content: updatedContent.content,
32+
modifications: updatedContent.modifications,
3033
restoringSelections: true
3134
)
3235
}

XCPServiceTests/AcceptSuggestionTests.swift

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ final class AcceptSuggestionTests: XCTestCase {
2020
struct Dog {}
2121
""",
2222
range: .init(
23-
start: .init(line: 7, character: 0),
24-
end: .init(line: 7, character: 12)
23+
start: .init(line: 1, character: 0),
24+
end: .init(line: 1, character: 12)
2525
)
2626
),
2727
]
@@ -33,7 +33,7 @@ final class AcceptSuggestionTests: XCTestCase {
3333

3434
let result1 = try await service.getSuggestedCode(editorContent: .init(
3535
content: content,
36-
lines: content.breakLines(),
36+
lines: content.breakLines(appendLineBreakToLastLine: true),
3737
uti: "",
3838
cursorPosition: .init(line: 0, character: 0),
3939
tabSize: 1,
@@ -43,18 +43,23 @@ final class AcceptSuggestionTests: XCTestCase {
4343

4444
let result2 = try await service.getSuggestionAcceptedCode(editorContent: .init(
4545
content: result1.content,
46-
lines: result1.content.breakLines(),
46+
lines: result1.content.breakLines(appendLineBreakToLastLine: true),
4747
uti: "",
4848
cursorPosition: .init(line: 3, character: 5),
4949
tabSize: 1,
5050
indentSize: 1,
5151
usesTabsForIndentation: false
5252
))
5353

54+
XCTAssertEqual(
55+
Array(result2.content.breakLines(appendLineBreakToLastLine: true).dropLast(1)),
56+
result1.content.breakLines(appendLineBreakToLastLine: true).applying(result2.modifications)
57+
)
5458
XCTAssertEqual(result2.content, """
5559
struct Cat {}
5660
5761
struct Dog {}
62+
5863
5964
""", "Previous suggestions should be removed.")
6065

@@ -66,7 +71,7 @@ final class AcceptSuggestionTests: XCTestCase {
6671

6772
let result3 = try await service.getSuggestionAcceptedCode(editorContent: .init(
6873
content: content,
69-
lines: content.breakLines(),
74+
lines: content.breakLines(appendLineBreakToLastLine: true),
7075
uti: "",
7176
cursorPosition: .init(line: 0, character: 3),
7277
tabSize: 1,
@@ -75,7 +80,10 @@ final class AcceptSuggestionTests: XCTestCase {
7580
))
7681

7782
XCTAssertEqual(result3.content, content, "Deleting the code and accept again does nothing")
78-
83+
XCTAssertEqual(
84+
result3.content.breakLines(appendLineBreakToLastLine: true),
85+
content.breakLines(appendLineBreakToLastLine: true).applying(result3.modifications)
86+
)
7987
XCTAssertEqual(result3.newCursor, nil)
8088
}
8189
}

XCPServiceTests/GetSuggestionsTests.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ final class GetSuggestionsTests: XCTestCase {
4343
usesTabsForIndentation: false
4444
))
4545

46+
XCTAssertEqual(
47+
result.content.breakLines(appendLineBreakToLastLine: true),
48+
content.breakLines(appendLineBreakToLastLine: true).applying(result.modifications)
49+
)
4650
XCTAssertEqual(result.content, """
4751
struct Cat {
4852
@@ -92,7 +96,10 @@ final class GetSuggestionsTests: XCTestCase {
9296
indentSize: 1,
9397
usesTabsForIndentation: false
9498
))
95-
99+
XCTAssertEqual(
100+
result.content.breakLines(appendLineBreakToLastLine: true),
101+
content.breakLines(appendLineBreakToLastLine: true).applying(result.modifications)
102+
)
96103
XCTAssertEqual(result.content, """
97104
struct Cat {
98105

XPCService/Shared/Models.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ struct EditorContent: Codable {
1414
struct UpdatedContent: Codable {
1515
var content: String
1616
var newCursor: CursorPosition?
17+
var modifications: [Modification]
1718
}

0 commit comments

Comments
 (0)