Skip to content

Commit 8630be4

Browse files
committed
Fix that suggestion was not invalidated when it doesn't match the typed content
1 parent a0ece87 commit 8630be4

File tree

4 files changed

+130
-27
lines changed

4 files changed

+130
-27
lines changed

Core/Tests/ServiceTests/FilespaceSuggestionInvalidationTests.swift

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase {
3838
)
3939
let isValid = await filespace.validateSuggestions(
4040
lines: lines,
41-
cursorPosition: .init(line: 1, character: 4)
41+
cursorPosition: .init(line: 1, character: 4),
42+
alwaysTrueIfCursorNotMoved: false // TODO: What
4243
)
4344
XCTAssertTrue(isValid)
4445
let suggestion = filespace.presentingSuggestion
@@ -55,7 +56,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase {
5556
)
5657
let isValid = await filespace.validateSuggestions(
5758
lines: lines,
58-
cursorPosition: .init(line: 1, character: 4)
59+
cursorPosition: .init(line: 1, character: 4),
60+
alwaysTrueIfCursorNotMoved: false
5961
)
6062
XCTAssertTrue(isValid)
6163
let suggestion = filespace.presentingSuggestion
@@ -72,7 +74,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase {
7274
)
7375
let isValid = await filespace.validateSuggestions(
7476
lines: lines,
75-
cursorPosition: .init(line: 1, character: 4)
77+
cursorPosition: .init(line: 1, character: 4),
78+
alwaysTrueIfCursorNotMoved: false
7679
)
7780
XCTAssertTrue(isValid)
7881
let suggestion = filespace.presentingSuggestion
@@ -89,7 +92,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase {
8992
)
9093
let isValid = await filespace.validateSuggestions(
9194
lines: lines,
92-
cursorPosition: .init(line: 1, character: 2)
95+
cursorPosition: .init(line: 1, character: 2),
96+
alwaysTrueIfCursorNotMoved: false
9397
)
9498
XCTAssertTrue(isValid)
9599
let suggestion = filespace.presentingSuggestion
@@ -109,12 +113,37 @@ class FilespaceSuggestionInvalidationTests: XCTestCase {
109113
)
110114
let isValid = await filespace.validateSuggestions(
111115
lines: lines,
112-
cursorPosition: .init(line: 1, character: 3)
116+
cursorPosition: .init(line: 1, character: 3),
117+
alwaysTrueIfCursorNotMoved: false
113118
)
114119
XCTAssertTrue(isValid)
115120
let suggestion = filespace.presentingSuggestion
116121
XCTAssertNotNil(suggestion)
117122
}
123+
124+
func test_typing_not_according_to_suggestion_should_invalidate() async throws {
125+
let lines = ["\n", "hello ma\n", "\n"]
126+
let filespace = try await prepare(
127+
lines: lines,
128+
suggestionText: "hello man",
129+
cursorPosition: .init(line: 1, character: 8),
130+
range: .init(startPair: (1, 0), endPair: (1, 8))
131+
)
132+
let wasValid = await filespace.validateSuggestions(
133+
lines: lines,
134+
cursorPosition: .init(line: 1, character: 8),
135+
alwaysTrueIfCursorNotMoved: false
136+
)
137+
let isValid = await filespace.validateSuggestions(
138+
lines: ["\n", "hello mat\n", "\n"],
139+
cursorPosition: .init(line: 1, character: 9),
140+
alwaysTrueIfCursorNotMoved: false
141+
)
142+
XCTAssertTrue(wasValid)
143+
XCTAssertFalse(isValid)
144+
let suggestion = filespace.presentingSuggestion
145+
XCTAssertNil(suggestion)
146+
}
118147

119148
func test_text_cursor_moved_to_another_line_should_invalidate() async throws {
120149
let lines = ["\n", "hell\n", "\n"]
@@ -126,7 +155,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase {
126155
)
127156
let isValid = await filespace.validateSuggestions(
128157
lines: lines,
129-
cursorPosition: .init(line: 2, character: 0)
158+
cursorPosition: .init(line: 2, character: 0),
159+
alwaysTrueIfCursorNotMoved: false
130160
)
131161
XCTAssertFalse(isValid)
132162
let suggestion = filespace.presentingSuggestion
@@ -143,7 +173,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase {
143173
)
144174
let isValid = await filespace.validateSuggestions(
145175
lines: lines,
146-
cursorPosition: .init(line: 100, character: 4)
176+
cursorPosition: .init(line: 100, character: 4),
177+
alwaysTrueIfCursorNotMoved: false
147178
)
148179
XCTAssertFalse(isValid)
149180
let suggestion = filespace.presentingSuggestion
@@ -159,7 +190,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase {
159190
)
160191
let isValid = await filespace.validateSuggestions(
161192
lines: ["\n", "helo\n", "\n"],
162-
cursorPosition: .init(line: 1, character: 4)
193+
cursorPosition: .init(line: 1, character: 4),
194+
alwaysTrueIfCursorNotMoved: false
163195
)
164196
XCTAssertFalse(isValid)
165197
let suggestion = filespace.presentingSuggestion
@@ -175,7 +207,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase {
175207
)
176208
let isValid = await filespace.validateSuggestions(
177209
lines: ["\n", "helo\n", "\n"],
178-
cursorPosition: .init(line: 1, character: 100)
210+
cursorPosition: .init(line: 1, character: 100),
211+
alwaysTrueIfCursorNotMoved: false
179212
)
180213
XCTAssertFalse(isValid)
181214
let suggestion = filespace.presentingSuggestion
@@ -192,11 +225,13 @@ class FilespaceSuggestionInvalidationTests: XCTestCase {
192225
)
193226
let wasValid = await filespace.validateSuggestions(
194227
lines: lines,
195-
cursorPosition: .init(line: 1, character: 8)
228+
cursorPosition: .init(line: 1, character: 8),
229+
alwaysTrueIfCursorNotMoved: false
196230
)
197231
let isValid = await filespace.validateSuggestions(
198232
lines: ["\n", "hello man\n", "\n"],
199-
cursorPosition: .init(line: 1, character: 9)
233+
cursorPosition: .init(line: 1, character: 9),
234+
alwaysTrueIfCursorNotMoved: false
200235
)
201236
XCTAssertTrue(wasValid)
202237
XCTAssertFalse(isValid)
@@ -215,11 +250,13 @@ class FilespaceSuggestionInvalidationTests: XCTestCase {
215250
)
216251
let wasValid = await filespace.validateSuggestions(
217252
lines: lines,
218-
cursorPosition: .init(line: 1, character: 12)
253+
cursorPosition: .init(line: 1, character: 12),
254+
alwaysTrueIfCursorNotMoved: false
219255
)
220256
let isValid = await filespace.validateSuggestions(
221257
lines: ["\n", "hello m🎆🎆an\n", "\n"],
222-
cursorPosition: .init(line: 1, character: 13)
258+
cursorPosition: .init(line: 1, character: 13),
259+
alwaysTrueIfCursorNotMoved: false
223260
)
224261
XCTAssertTrue(wasValid)
225262
XCTAssertFalse(isValid)
@@ -238,11 +275,13 @@ class FilespaceSuggestionInvalidationTests: XCTestCase {
238275
)
239276
let wasValid = await filespace.validateSuggestions(
240277
lines: lines,
241-
cursorPosition: .init(line: 1, character: 8)
278+
cursorPosition: .init(line: 1, character: 8),
279+
alwaysTrueIfCursorNotMoved: false
242280
)
243281
let isValid = await filespace.validateSuggestions(
244282
lines: ["\n", "hello man!!!!!\n", "\n"],
245-
cursorPosition: .init(line: 1, character: 9)
283+
cursorPosition: .init(line: 1, character: 9),
284+
alwaysTrueIfCursorNotMoved: false
246285
)
247286
XCTAssertTrue(wasValid)
248287
XCTAssertFalse(isValid)
@@ -260,7 +299,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase {
260299
)
261300
let isValid = await filespace.validateSuggestions(
262301
lines: lines,
263-
cursorPosition: .init(line: 1, character: 9)
302+
cursorPosition: .init(line: 1, character: 9),
303+
alwaysTrueIfCursorNotMoved: false
264304
)
265305
XCTAssertTrue(isValid)
266306
let suggestion = filespace.presentingSuggestion
@@ -278,7 +318,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase {
278318
)
279319
let isValid = await filespace.validateSuggestions(
280320
lines: lines,
281-
cursorPosition: .init(line: 1, character: 13)
321+
cursorPosition: .init(line: 1, character: 13),
322+
alwaysTrueIfCursorNotMoved: false
282323
)
283324
XCTAssertTrue(isValid)
284325
let suggestion = filespace.presentingSuggestion
@@ -296,7 +337,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase {
296337
)
297338
let isValid = await filespace.validateSuggestions(
298339
lines: lines,
299-
cursorPosition: .init(line: 1, character: 4)
340+
cursorPosition: .init(line: 1, character: 4),
341+
alwaysTrueIfCursorNotMoved: false
300342
)
301343
XCTAssertFalse(isValid)
302344
let suggestion = filespace.presentingSuggestion
@@ -313,7 +355,8 @@ class FilespaceSuggestionInvalidationTests: XCTestCase {
313355
)
314356
let isValid = await filespace.validateSuggestions(
315357
lines: lines,
316-
cursorPosition: .init(line: 0, character: 15)
358+
cursorPosition: .init(line: 0, character: 15),
359+
alwaysTrueIfCursorNotMoved: false
317360
)
318361
XCTAssertTrue(isValid)
319362
let suggestion = filespace.presentingSuggestion
@@ -325,12 +368,13 @@ class FilespaceSuggestionInvalidationTests: XCTestCase {
325368
let filespace = try await prepare(
326369
lines: lines,
327370
suggestionText: "hello world !!!",
328-
cursorPosition: .init(line: 0, character: 15),
329-
range: .init(startPair: (0, 0), endPair: (0, 15))
371+
cursorPosition: .init(line: 0, character: 18),
372+
range: .init(startPair: (0, 0), endPair: (0, 18))
330373
)
331374
let isValid = await filespace.validateSuggestions(
332375
lines: lines,
333-
cursorPosition: .init(line: 0, character: 18)
376+
cursorPosition: .init(line: 0, character: 18),
377+
alwaysTrueIfCursorNotMoved: false
334378
)
335379
XCTAssertTrue(isValid)
336380
let suggestion = filespace.presentingSuggestion

ExtensionService/AppDelegate.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ let serviceIdentifier = bundleIdentifierBase + ".ExtensionService"
1818

1919
@main
2020
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
21+
@MainActor
2122
let service = Service.shared
2223
var statusBarItem: NSStatusItem!
2324
var xpcController: XPCController?

Tool/Sources/SharedUIComponents/AsyncCodeBlock.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,9 @@ extension AsyncCodeBlock {
269269
location: max(0, offset),
270270
length: max(0, change.element.count + (offset < 0 ? offset : 0))
271271
)
272+
if range.location + range.length > mutableString.length {
273+
continue
274+
}
272275
mutableString.addAttributes([
273276
.backgroundColor: NSColor.systemGreen.withAlphaComponent(0.2),
274277
], range: range)

Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,14 @@ public extension Filespace {
6565
/// - Parameters:
6666
/// - lines: lines of the file
6767
/// - cursorPosition: cursor position
68+
/// - alwaysTrueIfCursorNotMoved: for unit tests
6869
/// - Returns: `true` if the suggestion is still valid
6970
@WorkspaceActor
70-
func validateSuggestions(lines: [String], cursorPosition: CursorPosition) -> Bool {
71+
func validateSuggestions(
72+
lines: [String],
73+
cursorPosition: CursorPosition,
74+
alwaysTrueIfCursorNotMoved: Bool = true
75+
) -> Bool {
7176
guard let presentingSuggestion else { return false }
7277
let snapshot = self[keyPath: \.suggestionSourceSnapshot]
7378
if snapshot.cursorPosition == .outOfScope { return false }
@@ -76,7 +81,8 @@ public extension Filespace {
7681
presentingSuggestion,
7782
snapshot: snapshot,
7883
lines: lines,
79-
cursorPosition: cursorPosition
84+
cursorPosition: cursorPosition,
85+
alwaysTrueIfCursorNotMoved: alwaysTrueIfCursorNotMoved
8086
) else {
8187
reset()
8288
resetSnapshot()
@@ -92,16 +98,31 @@ extension Filespace {
9298
_ suggestion: CodeSuggestion,
9399
snapshot: FilespaceSuggestionSnapshot,
94100
lines: [String],
95-
cursorPosition: CursorPosition
101+
cursorPosition: CursorPosition,
102+
// For test
103+
alwaysTrueIfCursorNotMoved: Bool = true
96104
) -> Bool {
105+
// cursor is not even moved during the generation.
106+
if alwaysTrueIfCursorNotMoved, cursorPosition == suggestion.position { return true }
107+
97108
// cursor has moved to another line
98109
if cursorPosition.line != suggestion.position.line { return false }
99110

100111
// the cursor position is valid
101112
guard cursorPosition.line >= 0, cursorPosition.line < lines.count else { return false }
102113

103114
let editingLine = lines[cursorPosition.line].dropLast(1) // dropping line ending
104-
let suggestionLines = suggestion.text.split(whereSeparator: \.isNewline)
115+
let suggestionLines = suggestion.text.breakLines(appendLineBreakToLastLine: true)
116+
117+
if Self.validateThatIsNotTypingSuggestion(
118+
suggestion,
119+
snapshot: snapshot,
120+
lines: lines,
121+
suggestionLines: suggestionLines,
122+
cursorPosition: cursorPosition
123+
) {
124+
return false
125+
}
105126

106127
// if the line will not change after accepting the suggestion
107128
if Self.validateThatSuggestionMakeNoDifferent(
@@ -120,10 +141,44 @@ extension Filespace {
120141
return true
121142
}
122143

144+
static func validateThatIsNotTypingSuggestion(
145+
_ suggestion: CodeSuggestion,
146+
snapshot: FilespaceSuggestionSnapshot,
147+
lines: [String],
148+
suggestionLines: [String],
149+
cursorPosition: CursorPosition
150+
) -> Bool {
151+
let lineIndex = suggestion.range.start.line
152+
let typeStart = suggestion.position.character
153+
let cursorColumn = cursorPosition.character
154+
let suggestionStart = max(
155+
0,
156+
suggestion.position.character - suggestion.range.start.character
157+
)
158+
func contentBeforeCursor(
159+
_ string: String,
160+
start: Int
161+
) -> ArraySlice<String.UTF16View.Element> {
162+
if start >= cursorColumn { return [] }
163+
let elements = Array(string.utf16)
164+
guard start >= 0, start < elements.endIndex else { return [] }
165+
let endIndex = min(elements.endIndex, cursorColumn)
166+
return elements[start..<endIndex]
167+
}
168+
169+
guard lineIndex >= 0, lineIndex < lines.endIndex else { return false }
170+
let editingLine = lines[lineIndex]
171+
let suggestionFirstLine = suggestionLines.first ?? ""
172+
173+
let typed = contentBeforeCursor(editingLine, start: typeStart)
174+
let expectedTyped = contentBeforeCursor(suggestionFirstLine, start: suggestionStart)
175+
return typed != expectedTyped
176+
}
177+
123178
static func validateThatSuggestionMakeNoDifferent(
124179
_ suggestion: CodeSuggestion,
125180
lines: [String],
126-
suggestionLines: [Substring]
181+
suggestionLines: [String]
127182
) -> Bool {
128183
var editingRange = suggestion.range
129184
let startLine = max(0, editingRange.start.line)

0 commit comments

Comments
 (0)