Skip to content

Commit ed00e70

Browse files
committed
Update SuggestionInjector to work on utf-16 view
1 parent 61a0a78 commit ed00e70

File tree

2 files changed

+264
-14
lines changed

2 files changed

+264
-14
lines changed

Core/Sources/SuggestionInjector/SuggestionInjector.swift

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ public struct SuggestionInjector {
5454
start.character < firstRemovedLine.count,
5555
!toBeInserted.isEmpty
5656
{
57-
let leftoverRange = firstRemovedLine.startIndex..<(firstRemovedLine.index(
58-
firstRemovedLine.startIndex,
57+
let leftoverRange = firstRemovedLine.utf16.startIndex..<(firstRemovedLine.utf16.index(
58+
firstRemovedLine.utf16.startIndex,
5959
offsetBy: start.character,
60-
limitedBy: firstRemovedLine.endIndex
61-
) ?? firstRemovedLine.endIndex)
62-
var leftover = firstRemovedLine[leftoverRange]
60+
limitedBy: firstRemovedLine.utf16.endIndex
61+
) ?? firstRemovedLine.utf16.endIndex)
62+
var leftover = String(firstRemovedLine[leftoverRange])
6363
if leftover.last?.isNewline ?? false {
6464
leftover.removeLast(1)
6565
}
@@ -76,7 +76,8 @@ public struct SuggestionInjector {
7676
lineEnding: lineEnding
7777
)
7878

79-
let cursorCol = toBeInserted[toBeInserted.endIndex - 1].count - 1 - recoveredSuffixLength
79+
let cursorCol = toBeInserted[toBeInserted.endIndex - 1].utf16.count
80+
- 1 - recoveredSuffixLength
8081
let insertingIndex = min(start.line, content.endIndex)
8182
content.insert(contentsOf: toBeInserted, at: insertingIndex)
8283
extraInfo.modifications.append(.inserted(insertingIndex, toBeInserted))
@@ -98,7 +99,8 @@ public struct SuggestionInjector {
9899
let lastRemovedLineCleaned = lastRemovedLine.droppedLineBreak()
99100

100101
// If the replaced range covers the whole line, return immediately.
101-
guard end.character >= 0, end.character - 1 < lastRemovedLineCleaned.count else { return 0 }
102+
guard end.character >= 0, end.character - 1 < lastRemovedLineCleaned.utf16.count
103+
else { return 0 }
102104

103105
// if we are not inserting anything, return immediately.
104106
guard !toBeInserted.isEmpty,
@@ -117,12 +119,13 @@ public struct SuggestionInjector {
117119
// locate the split index, the prefix of which matches the suggestion prefix.
118120
var splitIndex: String.Index?
119121

120-
for offset in end.character..<lastRemovedLineCleaned.count {
121-
let proposedIndex = lastRemovedLineCleaned.index(
122-
lastRemovedLineCleaned.startIndex,
123-
offsetBy: offset
124-
)
125-
let prefix = lastRemovedLineCleaned[..<proposedIndex]
122+
for offset in end.character..<lastRemovedLineCleaned.utf16.count {
123+
let proposedIndex = lastRemovedLineCleaned.utf16.index(
124+
lastRemovedLineCleaned.utf16.startIndex,
125+
offsetBy: offset,
126+
limitedBy: lastRemovedLineCleaned.utf16.endIndex
127+
) ?? lastRemovedLineCleaned.utf16.endIndex
128+
let prefix = String(lastRemovedLineCleaned[..<proposedIndex])
126129
if first.hasPrefix(prefix) {
127130
splitIndex = proposedIndex
128131
}
@@ -156,7 +159,7 @@ public struct SuggestionInjector {
156159

157160
toBeInserted[toBeInserted.endIndex - 1] = lastInsertingLine
158161

159-
return suffix.count
162+
return suffix.utf16.count
160163
}
161164
}
162165

Core/Tests/SuggestionInjectorTests/AcceptSuggestionTests.swift

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,253 @@ final class AcceptSuggestionTests: XCTestCase {
562562
563563
""")
564564
}
565+
566+
func test_accept_suggestion_start_from_previous_line_has_emoji_inside() async throws {
567+
let content = """
568+
struct 😹😹 {
569+
}
570+
"""
571+
let text = """
572+
struct 😹😹 {
573+
var name: String
574+
var age: String
575+
"""
576+
let suggestion = CodeSuggestion(
577+
id: "",
578+
text: text,
579+
position: .init(line: 0, character: 13),
580+
range: .init(
581+
start: .init(line: 0, character: 0),
582+
end: .init(line: 0, character: 13)
583+
)
584+
)
585+
586+
var extraInfo = SuggestionInjector.ExtraInfo()
587+
var lines = content.breakIntoEditorStyleLines()
588+
var cursor = CursorPosition(line: 0, character: 13)
589+
SuggestionInjector().acceptSuggestion(
590+
intoContentWithoutSuggestion: &lines,
591+
cursorPosition: &cursor,
592+
completion: suggestion,
593+
extraInfo: &extraInfo
594+
)
595+
XCTAssertTrue(extraInfo.didChangeContent)
596+
XCTAssertTrue(extraInfo.didChangeCursorPosition)
597+
XCTAssertNil(extraInfo.suggestionRange)
598+
XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications))
599+
XCTAssertEqual(cursor, .init(line: 2, character: 19))
600+
XCTAssertEqual(lines.joined(separator: ""), """
601+
struct 😹😹 {
602+
var name: String
603+
var age: String
604+
}
605+
606+
""")
607+
}
608+
609+
func test_accept_suggestion_overlap_with_emoji_in_the_previous_code() async throws {
610+
let content = """
611+
struct 😹😹 {
612+
var name
613+
}
614+
"""
615+
let text = """
616+
var name: String
617+
var age: String
618+
"""
619+
let suggestion = CodeSuggestion(
620+
id: "",
621+
text: text,
622+
position: .init(line: 1, character: 13),
623+
range: .init(
624+
start: .init(line: 1, character: 0),
625+
end: .init(line: 1, character: 13)
626+
)
627+
)
628+
629+
var extraInfo = SuggestionInjector.ExtraInfo()
630+
var lines = content.breakIntoEditorStyleLines()
631+
var cursor = CursorPosition(line: 1, character: 13)
632+
SuggestionInjector().acceptSuggestion(
633+
intoContentWithoutSuggestion: &lines,
634+
cursorPosition: &cursor,
635+
completion: suggestion,
636+
extraInfo: &extraInfo
637+
)
638+
XCTAssertTrue(extraInfo.didChangeContent)
639+
XCTAssertTrue(extraInfo.didChangeCursorPosition)
640+
XCTAssertNil(extraInfo.suggestionRange)
641+
XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications))
642+
XCTAssertEqual(cursor, .init(line: 2, character: 19))
643+
XCTAssertEqual(lines.joined(separator: ""), """
644+
struct 😹😹 {
645+
var name: String
646+
var age: String
647+
}
648+
649+
""")
650+
}
651+
652+
func test_accept_suggestion_overlap_continue_typing_has_emoji_inside() async throws {
653+
let content = """
654+
struct 😹😹 {
655+
var name: Str
656+
}
657+
"""
658+
let text = """
659+
var name: String
660+
var age: String
661+
"""
662+
let suggestion = CodeSuggestion(
663+
id: "",
664+
text: text,
665+
position: .init(line: 1, character: 13),
666+
range: .init(
667+
start: .init(line: 1, character: 0),
668+
end: .init(line: 1, character: 13)
669+
)
670+
)
671+
672+
var extraInfo = SuggestionInjector.ExtraInfo()
673+
var lines = content.breakIntoEditorStyleLines()
674+
var cursor = CursorPosition(line: 1, character: 13)
675+
SuggestionInjector().acceptSuggestion(
676+
intoContentWithoutSuggestion: &lines,
677+
cursorPosition: &cursor,
678+
completion: suggestion,
679+
extraInfo: &extraInfo
680+
)
681+
XCTAssertTrue(extraInfo.didChangeContent)
682+
XCTAssertTrue(extraInfo.didChangeCursorPosition)
683+
XCTAssertNil(extraInfo.suggestionRange)
684+
XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications))
685+
XCTAssertEqual(cursor, .init(line: 2, character: 19))
686+
XCTAssertEqual(lines.joined(separator: ""), """
687+
struct 😹😹 {
688+
var name: String
689+
var age: String
690+
}
691+
692+
""")
693+
}
694+
695+
func test_replacing_multiple_lines_with_emoji() async throws {
696+
let content = """
697+
struct 😹😹 {
698+
func speak() { print("meow") }
699+
}
700+
"""
701+
let text = """
702+
struct 🐶🐶 {
703+
func speak() {
704+
print("woof")
705+
}
706+
}
707+
"""
708+
let suggestion = CodeSuggestion(
709+
id: "",
710+
text: text,
711+
position: .init(line: 0, character: 7),
712+
range: .init(
713+
start: .init(line: 0, character: 0),
714+
end: .init(line: 2, character: 1)
715+
)
716+
)
717+
718+
var extraInfo = SuggestionInjector.ExtraInfo()
719+
var lines = content.breakIntoEditorStyleLines()
720+
var cursor = CursorPosition(line: 0, character: 7)
721+
SuggestionInjector().acceptSuggestion(
722+
intoContentWithoutSuggestion: &lines,
723+
cursorPosition: &cursor,
724+
completion: suggestion,
725+
extraInfo: &extraInfo
726+
)
727+
728+
XCTAssertTrue(extraInfo.didChangeContent)
729+
XCTAssertTrue(extraInfo.didChangeCursorPosition)
730+
XCTAssertNil(extraInfo.suggestionRange)
731+
XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications))
732+
XCTAssertEqual(cursor, .init(line: 4, character: 1))
733+
XCTAssertEqual(lines.joined(separator: ""), """
734+
struct 🐶🐶 {
735+
func speak() {
736+
print("woof")
737+
}
738+
}
739+
740+
""")
741+
}
742+
743+
func test_accept_suggestion_overlap_continue_typing_suggestion_with_emoji_in_the_middle() async throws {
744+
let content = """
745+
print("🐶")
746+
"""
747+
let text = """
748+
print("🐶llo 🐶rld!
749+
"""
750+
let suggestion = CodeSuggestion(
751+
id: "",
752+
text: text,
753+
position: .init(line: 0, character: 6),
754+
range: .init(
755+
start: .init(line: 0, character: 0),
756+
end: .init(line: 0, character: 6)
757+
)
758+
)
759+
760+
var extraInfo = SuggestionInjector.ExtraInfo()
761+
var lines = content.breakIntoEditorStyleLines()
762+
var cursor = CursorPosition(line: 0, character: 7)
763+
SuggestionInjector().acceptSuggestion(
764+
intoContentWithoutSuggestion: &lines,
765+
cursorPosition: &cursor,
766+
completion: suggestion,
767+
extraInfo: &extraInfo
768+
)
769+
XCTAssertTrue(extraInfo.didChangeContent)
770+
XCTAssertTrue(extraInfo.didChangeCursorPosition)
771+
XCTAssertNil(extraInfo.suggestionRange)
772+
XCTAssertEqual(lines, content.breakIntoEditorStyleLines().applying(extraInfo.modifications))
773+
XCTAssertEqual(cursor, .init(line: 0, character: 19))
774+
XCTAssertEqual(lines.joined(separator: ""), """
775+
print("🐶llo 🐶rld!")
776+
777+
""")
778+
}
779+
780+
func test_replacing_single_line_in_the_middle_should_not_remove_the_next_character_with_emoji(
781+
) async throws {
782+
let content = """
783+
🐶KeyName: ,,
784+
"""
785+
786+
let suggestion = CodeSuggestion(
787+
id: "",
788+
text: "🐶KeyName: azure👩‍❤️‍👨AIAPIKeyName",
789+
position: .init(line: 0, character: 11),
790+
range: .init(
791+
start: .init(line: 0, character: 0),
792+
end: .init(line: 0, character: 11)
793+
)
794+
)
795+
796+
var lines = content.breakIntoEditorStyleLines()
797+
var extraInfo = SuggestionInjector.ExtraInfo()
798+
var cursor = CursorPosition(line: 5, character: 34)
799+
SuggestionInjector().acceptSuggestion(
800+
intoContentWithoutSuggestion: &lines,
801+
cursorPosition: &cursor,
802+
completion: suggestion,
803+
extraInfo: &extraInfo
804+
)
805+
806+
XCTAssertEqual(cursor, .init(line: 0, character: 36))
807+
XCTAssertEqual(lines.joined(separator: ""), """
808+
🐶KeyName: azure👩‍❤️‍👨AIAPIKeyName,,
809+
810+
""")
811+
}
565812
}
566813

567814
extension String {

0 commit comments

Comments
 (0)