Skip to content

Commit da44c76

Browse files
committed
Reimplement suffix recovering
1 parent 6a7cab7 commit da44c76

1 file changed

Lines changed: 73 additions & 79 deletions

File tree

Core/Sources/SuggestionInjector/SuggestionInjector.swift

Lines changed: 73 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -174,97 +174,77 @@ public struct SuggestionInjector {
174174
)
175175
}
176176

177-
#warning(
178-
"TODO: I feel like the implementation is doing a lot of receptive work to recover suffix."
177+
let recoveredSuffixLength = recoverSuffixIfNeeded(
178+
endOfReplacedContent: end,
179+
toBeInserted: &toBeInserted,
180+
lastRemovedLine: lastRemovedLine
179181
)
180182

181-
/// The number of character that is in the last modified line but not included.
182-
let leftoverCount: Int = {
183-
let maxCount = lastRemovedLine?.count ?? 0
184-
guard let first = toBeInserted.first?
185-
.dropLast((toBeInserted.first?.hasSuffix("\n") ?? false) ? 1 : 0),
186-
!first.isEmpty else { return maxCount }
187-
guard let last = toBeInserted.last?
188-
.dropLast((toBeInserted.last?.hasSuffix("\n") ?? false) ? 1 : 0),
189-
!last.isEmpty else { return maxCount }
190-
let droppedLast = lastRemovedLine?
191-
.dropLast((lastRemovedLine?.hasSuffix("\n") ?? false) ? 1 : 0)
192-
guard let droppedLast, !droppedLast.isEmpty else { return maxCount }
193-
// case 1: user keeps typing as the suggestion suggests.
194-
if first.hasPrefix(droppedLast) {
195-
return 0
196-
}
197-
// case 2: user also typed the suffix of the suggestion (or auto-completed by Xcode)
198-
if end.character < droppedLast.count {
199-
// locate the split index, the prefix of which matches the suggestion prefix.
200-
var splitIndex: String.Index?
201-
for offset in end.character..<droppedLast.count {
202-
let proposedIndex = droppedLast.index(
203-
droppedLast.startIndex,
204-
offsetBy: offset
205-
)
206-
let prefix = droppedLast[..<proposedIndex]
207-
if first.hasPrefix(prefix) {
208-
splitIndex = proposedIndex
209-
}
210-
}
183+
let cursorCol = toBeInserted[toBeInserted.endIndex - 1].count - 1 - recoveredSuffixLength
184+
let insertingIndex = min(start.line, content.endIndex)
185+
content.insert(contentsOf: toBeInserted, at: insertingIndex)
186+
extraInfo.modifications.append(.inserted(insertingIndex, toBeInserted))
187+
cursorPosition = .init(
188+
line: startLine + toBeInserted.count - 1,
189+
character: max(0, cursorCol)
190+
)
191+
}
211192

212-
// then check how many characters are not in the suffix of the suggestion.
213-
if let splitIndex {
214-
let suffix = droppedLast[splitIndex...]
215-
if last.hasSuffix(suffix) {
216-
return 0
217-
}
218-
return suffix.count
219-
}
220-
}
193+
func recoverSuffixIfNeeded(
194+
endOfReplacedContent end: CursorPosition,
195+
toBeInserted: inout [String],
196+
lastRemovedLine: String?
197+
) -> Int {
198+
// If there is no line removed, there is no need to recover anything.
199+
guard let lastRemovedLine, !lastRemovedLine.isEmptyOrNewLine else { return 0 }
221200

222-
return maxCount
223-
}()
201+
let lastRemovedLineCleaned = lastRemovedLine.droppedLineBreak()
224202

225-
/// The number of characters appended to the last line.
226-
var appenderCount = 0
203+
// If the replaced range covers the whole line, return immediately.
204+
guard end.character >= 0, end.character - 1 < lastRemovedLineCleaned.count else { return 0 }
227205

228-
// appending suffix text not in range if needed.
229-
if let lastRemovedLine,
230-
leftoverCount > 0,
231-
!lastRemovedLine.isEmptyOrNewLine,
232-
end.character >= 0,
233-
end.character - 1 < lastRemovedLine.count,
234-
!toBeInserted.isEmpty
235-
{
236-
let leftoverRange = (lastRemovedLine.index(
237-
lastRemovedLine.startIndex,
238-
offsetBy: end.character,
239-
limitedBy: lastRemovedLine.endIndex
240-
) ?? lastRemovedLine.endIndex)..<lastRemovedLine.endIndex
241-
if toBeInserted[toBeInserted.endIndex - 1].hasSuffix("\n") {
242-
toBeInserted[toBeInserted.endIndex - 1].removeLast(1)
243-
}
244-
let leftover = {
245-
if lastRemovedLine.hasSuffix("\n") {
246-
return lastRemovedLine[leftoverRange].dropLast(1).suffix(leftoverCount)
247-
}
248-
return lastRemovedLine[leftoverRange].suffix(leftoverCount)
249-
}()
206+
// if we are not inserting anything, return immediately.
207+
guard !toBeInserted.isEmpty,
208+
let first = toBeInserted.first?.droppedLineBreak(), !first.isEmpty,
209+
let last = toBeInserted.last?.droppedLineBreak(), !last.isEmpty
210+
else { return 0 }
250211

251-
toBeInserted[toBeInserted.endIndex - 1].append(contentsOf: leftover)
212+
// case 1: user keeps typing as the suggestion suggests.
252213

253-
appenderCount = leftover.count
214+
if first.hasPrefix(lastRemovedLineCleaned) {
215+
return 0
216+
}
254217

255-
if !toBeInserted[toBeInserted.endIndex - 1].hasSuffix("\n") {
256-
toBeInserted[toBeInserted.endIndex - 1].append("\n")
218+
// case 2: user also typed the suffix of the suggestion (or auto-completed by Xcode)
219+
220+
// locate the split index, the prefix of which matches the suggestion prefix.
221+
var splitIndex: String.Index?
222+
223+
for offset in end.character..<lastRemovedLineCleaned.count {
224+
let proposedIndex = lastRemovedLineCleaned.index(
225+
lastRemovedLineCleaned.startIndex,
226+
offsetBy: offset
227+
)
228+
let prefix = lastRemovedLineCleaned[..<proposedIndex]
229+
if first.hasPrefix(prefix) {
230+
splitIndex = proposedIndex
257231
}
258232
}
259233

260-
let cursorCol = toBeInserted[toBeInserted.endIndex - 1].count - 1 - appenderCount
261-
let insertingIndex = min(start.line, content.endIndex)
262-
content.insert(contentsOf: toBeInserted, at: insertingIndex)
263-
extraInfo.modifications.append(.inserted(insertingIndex, toBeInserted))
264-
cursorPosition = .init(
265-
line: startLine + toBeInserted.count - 1,
266-
character: max(0, cursorCol)
267-
)
234+
// then check how many characters are not in the suffix of the suggestion.
235+
guard let splitIndex else { return 0 }
236+
237+
let suffix = lastRemovedLineCleaned[splitIndex...]
238+
if last.hasSuffix(suffix) { return 0 }
239+
240+
let lastInsertingLine = toBeInserted[toBeInserted.endIndex - 1]
241+
.droppedLineBreak()
242+
.appending(suffix)
243+
.recoveredLineBreak()
244+
245+
toBeInserted[toBeInserted.endIndex - 1] = lastInsertingLine
246+
247+
return suffix.count
268248
}
269249
}
270250

@@ -302,6 +282,20 @@ extension String {
302282
var isEmptyOrNewLine: Bool {
303283
isEmpty || self == "\n"
304284
}
285+
286+
func droppedLineBreak() -> String {
287+
if hasSuffix("\n") {
288+
return String(dropLast(1))
289+
}
290+
return self
291+
}
292+
293+
func recoveredLineBreak() -> String {
294+
if hasSuffix("\n") {
295+
return self
296+
}
297+
return self + "\n"
298+
}
305299
}
306300

307301
func longestCommonPrefix(of a: String, and b: String) -> String {

0 commit comments

Comments
 (0)