Skip to content

Commit 0e4f7f8

Browse files
committed
Reuse suggestion if user is typing according to it
1 parent 5f80ae9 commit 0e4f7f8

File tree

5 files changed

+80
-14
lines changed

5 files changed

+80
-14
lines changed

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public class RealtimeSuggestionController {
2626
private var activeApplicationMonitorTask: Task<Void, Error>?
2727
private var editorObservationTask: Task<Void, Error>?
2828
private var focusedUIElement: AXUIElement?
29-
private var sourceEditor: AXUIElement?
29+
private var sourceEditor: SourceEditor?
3030

3131
var isCommentMode: Bool {
3232
UserDefaults.shared.value(for: \.suggestionPresentationMode) == .comment
@@ -116,7 +116,7 @@ public class RealtimeSuggestionController {
116116
}
117117

118118
guard focusElementType == "Source Editor" else { return }
119-
sourceEditor = focusElement
119+
sourceEditor = SourceEditor(runningApplication: activeXcode, element: focusElement)
120120

121121
editorObservationTask?.cancel()
122122
editorObservationTask = nil
@@ -138,11 +138,7 @@ public class RealtimeSuggestionController {
138138
self.triggerPrefetchDebounced()
139139
await self.notifyEditingFileChange(editor: focusElement)
140140
case kAXSelectedTextChangedNotification:
141-
guard let editor = sourceEditor else { continue }
142-
let sourceEditor = SourceEditor(
143-
runningApplication: activeXcode,
144-
element: editor
145-
)
141+
guard let sourceEditor else { continue }
146142
await PseudoCommandHandler()
147143
.invalidateRealtimeSuggestionsIfNeeded(sourceEditor: sourceEditor)
148144
default:

Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,21 @@ struct PseudoCommandHandler {
3939
))
4040
}
4141

42-
func generateRealtimeSuggestions(sourceEditor: AXUIElement?) async {
42+
func generateRealtimeSuggestions(sourceEditor: SourceEditor?) async {
4343
// Can't use handler if content is not available.
44-
guard let editor = await getEditorContent(sourceEditor: sourceEditor) else { return }
44+
guard
45+
let editor = await getEditorContent(sourceEditor: sourceEditor),
46+
let filespace = await getFilespace()
47+
else { return }
48+
49+
if await filespace.validateSuggestions(
50+
lines: editor.lines,
51+
cursorPosition: editor.cursorPosition
52+
) {
53+
return
54+
} else {
55+
PresentInWindowSuggestionPresenter().discardSuggestion(fileURL: filespace.fileURL)
56+
}
4557

4658
// Otherwise, get it from pseudo handler directly.
4759
let mode = UserDefaults.shared.value(for: \.suggestionPresentationMode)
@@ -246,11 +258,9 @@ extension PseudoCommandHandler {
246258
}
247259

248260
@ServiceActor
249-
func getEditorContent(sourceEditor: AXUIElement?) async -> EditorContent? {
250-
guard
251-
let filespace = await getFilespace(),
252-
let content = await getFileContent(sourceEditor: sourceEditor)
253-
else { return nil }
261+
func getEditorContent(sourceEditor: SourceEditor?) async -> EditorContent? {
262+
guard let filespace = await getFilespace(), let sourceEditor else { return nil }
263+
let content = sourceEditor.content
254264
let uti = filespace.uti ?? ""
255265
let tabSize = filespace.tabSize ?? 4
256266
let indentSize = filespace.indentSize ?? 4

Core/Sources/Service/Workspace.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ final class Filespace {
7979
return false
8080
}
8181

82+
let editingLine = lines[cursorPosition.line].dropLast(1) // dropping \n
83+
let suggestionFirstLine = presentingSuggestion?.text.split(separator: "\n").first ?? ""
84+
if !suggestionFirstLine.hasPrefix(editingLine) {
85+
reset()
86+
return false
87+
}
88+
8289
return true
8390
}
8491
}

Core/Sources/SuggestionInjector/SuggestionInjector.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ public struct SuggestionInjector {
154154

155155
var toBeInserted = suggestionContent.breakLines(appendLineBreakToLastLine: true)
156156

157+
// prepending prefix text not in range if needed.
157158
if let firstRemovedLine,
158159
!firstRemovedLine.isEmptyOrNewLine,
159160
start.character > 0,
@@ -175,8 +176,16 @@ public struct SuggestionInjector {
175176
)
176177
}
177178

179+
// appending suffix text not in range if needed.
178180
let cursorCol = toBeInserted[toBeInserted.endIndex - 1].count - 1
181+
let skipAppendingDueToContinueTyping = {
182+
guard let first = toBeInserted.first?.dropLast(1), !first.isEmpty else { return false }
183+
let droppedLast = lastRemovedLine?.dropLast(1)
184+
guard let droppedLast, !droppedLast.isEmpty else { return false }
185+
return first.hasPrefix(droppedLast)
186+
}()
179187
if let lastRemovedLine,
188+
!skipAppendingDueToContinueTyping,
180189
!lastRemovedLine.isEmptyOrNewLine,
181190
end.character >= 0,
182191
end.character - 1 < lastRemovedLine.count,
@@ -191,6 +200,7 @@ public struct SuggestionInjector {
191200
toBeInserted[toBeInserted.endIndex - 1].removeLast(1)
192201
}
193202
let leftover = lastRemovedLine[leftoverRange]
203+
194204
toBeInserted[toBeInserted.endIndex - 1]
195205
.append(contentsOf: leftover)
196206
}

Core/Tests/SuggestionInjectorTests/AcceptSuggestionTests.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,49 @@ final class AcceptSuggestionTests: XCTestCase {
131131
}
132132
""")
133133
}
134+
135+
func test_accept_suggestion_overlap_continue_typing() async throws {
136+
let content = """
137+
struct Cat {
138+
var name: Str
139+
}
140+
"""
141+
let text = """
142+
var name: String
143+
var age: String
144+
"""
145+
let suggestion = CodeSuggestion(
146+
text: text,
147+
position: .init(line: 1, character: 12),
148+
uuid: "",
149+
range: .init(
150+
start: .init(line: 1, character: 0),
151+
end: .init(line: 1, character: 12)
152+
),
153+
displayText: ""
154+
)
155+
156+
var extraInfo = SuggestionInjector.ExtraInfo()
157+
var lines = content.breakLines()
158+
var cursor = CursorPosition(line: 0, character: 0)
159+
SuggestionInjector().acceptSuggestion(
160+
intoContentWithoutSuggestion: &lines,
161+
cursorPosition: &cursor,
162+
completion: suggestion,
163+
extraInfo: &extraInfo
164+
)
165+
XCTAssertTrue(extraInfo.didChangeContent)
166+
XCTAssertTrue(extraInfo.didChangeCursorPosition)
167+
XCTAssertNil(extraInfo.suggestionRange)
168+
XCTAssertEqual(lines, content.breakLines().applying(extraInfo.modifications))
169+
XCTAssertEqual(cursor, .init(line: 2, character: 19))
170+
XCTAssertEqual(lines.joined(separator: ""), """
171+
struct Cat {
172+
var name: String
173+
var age: String
174+
}
175+
""")
176+
}
134177

135178
func test_propose_suggestion_partial_overlap() async throws {
136179
let content = "func quickSort() {}}\n"

0 commit comments

Comments
 (0)