Skip to content

Commit 189194b

Browse files
committed
fix: prevent surrogate pairs from being split by the selected range
This is already handled in functions like `expandSelectionInDirection` which won't select a half of emoji etc. but always the whole surrogate pair. The solution here is the same, if the cursor is placed in the middle of a surrogate pair, it will be moved to the end of the pair. Similarly a selection will either select both or neither.
1 parent 7bf3e5a commit 189194b

File tree

3 files changed

+206
-1
lines changed

3 files changed

+206
-1
lines changed

src/test/system.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ import "test/system/level_2_input_test"
1717
import "test/system/list_formatting_test"
1818
import "test/system/mutation_input_test"
1919
import "test/system/pasting_test"
20+
import "test/system/set_selected_range_test"
2021
import "test/system/text_formatting_test"
2122
import "test/system/undo_test"
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { assert, insertString, test, testGroup } from "test/test_helper"
2+
3+
testGroup(
4+
"Set selected range at the start of the text",
5+
{ template: "editor_empty" },
6+
() => {
7+
test("selection of the surrogate pair", () => {
8+
insertString("🙂foo")
9+
getComposition().setSelectedRange([ 0, 2 ])
10+
assert.selectedRange([ 0, 2 ])
11+
})
12+
13+
test("selection of the first surrogate pair character", () => {
14+
insertString("🙂foo")
15+
getComposition().setSelectedRange([ 0, 1 ])
16+
assert.selectedRange([ 0, 2 ])
17+
})
18+
19+
test("selection of the second surrogate pair character", () => {
20+
insertString("🙂foo")
21+
getComposition().setSelectedRange([ 1, 2 ])
22+
assert.selectedRange([ 2, 2 ])
23+
})
24+
25+
test("collapssed selection in the middle of the surrogate pair", () => {
26+
insertString("🙂foo")
27+
getComposition().setSelectedRange([ 1, 1 ])
28+
assert.selectedRange([ 2, 2 ])
29+
})
30+
31+
test("collapsed selection after surrogate pair", () => {
32+
insertString("🙂foo")
33+
getComposition().setSelectedRange([ 2, 2 ])
34+
assert.selectedRange([ 2, 2 ])
35+
})
36+
37+
test("selection after surrogate pair", () => {
38+
insertString("🙂foo")
39+
getComposition().setSelectedRange([ 2, 4 ])
40+
assert.selectedRange([ 2, 4 ])
41+
})
42+
43+
test("collapsed selection far after surrogate pair", () => {
44+
insertString("🙂foo")
45+
getComposition().setSelectedRange([ 3, 3 ])
46+
assert.selectedRange([ 3, 3 ])
47+
})
48+
49+
test("selection far after surrogate pair", () => {
50+
insertString("🙂foo")
51+
getComposition().setSelectedRange([ 3, 5 ])
52+
assert.selectedRange([ 3, 5 ])
53+
})
54+
},
55+
)
56+
57+
testGroup(
58+
"Set selected range in the middle of the text",
59+
{ template: "editor_empty" },
60+
() => {
61+
test("collapsed selection far before surrogate pair", () => {
62+
insertString("foo🙂bar")
63+
getComposition().setSelectedRange([ 2, 2 ])
64+
assert.selectedRange([ 2, 2 ])
65+
})
66+
67+
test("selection far before surrogate pair", () => {
68+
insertString("foo🙂bar")
69+
getComposition().setSelectedRange([ 0, 2 ])
70+
assert.selectedRange([ 0, 2 ])
71+
})
72+
73+
test("collapsed selection before surrogate pair", () => {
74+
insertString("foo🙂bar")
75+
getComposition().setSelectedRange([ 3, 3 ])
76+
assert.selectedRange([ 3, 3 ])
77+
})
78+
79+
test("selection before surrogate pair", () => {
80+
insertString("foo🙂bar")
81+
getComposition().setSelectedRange([ 1, 3 ])
82+
assert.selectedRange([ 1, 3 ])
83+
})
84+
85+
test("selection of the surrogate pair", () => {
86+
insertString("foo🙂bar")
87+
getComposition().setSelectedRange([ 3, 5 ])
88+
assert.selectedRange([ 3, 5 ])
89+
})
90+
91+
test("selection of the first surrogate pair character", () => {
92+
insertString("foo🙂bar")
93+
getComposition().setSelectedRange([ 3, 4 ])
94+
assert.selectedRange([ 3, 5 ])
95+
})
96+
97+
test("selection of the second surrogate pair character", () => {
98+
insertString("foo🙂bar")
99+
getComposition().setSelectedRange([ 4, 5 ])
100+
assert.selectedRange([ 5, 5 ])
101+
})
102+
103+
test("collapssed selection in the middle of the surrogate pair", () => {
104+
insertString("foo🙂bar")
105+
getComposition().setSelectedRange([ 4, 4 ])
106+
assert.selectedRange([ 5, 5 ])
107+
})
108+
109+
test("selection after surrogate pair", () => {
110+
insertString("foo🙂bar")
111+
getComposition().setSelectedRange([ 5, 7 ])
112+
assert.selectedRange([ 5, 7 ])
113+
})
114+
115+
test("collapsed selection after surrogate pair", () => {
116+
insertString("foo🙂bar")
117+
getComposition().setSelectedRange([ 5, 5 ])
118+
assert.selectedRange([ 5, 5 ])
119+
})
120+
121+
test("selection far after surrogate pair", () => {
122+
insertString("foo🙂bar")
123+
getComposition().setSelectedRange([ 6, 8 ])
124+
assert.selectedRange([ 6, 8 ])
125+
})
126+
127+
test("collapsed selection far after surrogate pair", () => {
128+
insertString("foo🙂bar")
129+
getComposition().setSelectedRange([ 6, 6 ])
130+
assert.selectedRange([ 6, 6 ])
131+
})
132+
},
133+
)
134+
135+
testGroup(
136+
"Set selected range at the end of the text",
137+
{ template: "editor_empty" },
138+
() => {
139+
test("collapsed selection far before surrogate pair", () => {
140+
insertString("foo🙂")
141+
getComposition().setSelectedRange([ 2, 2 ])
142+
assert.selectedRange([ 2, 2 ])
143+
})
144+
145+
test("selection far before surrogate pair", () => {
146+
insertString("foo🙂")
147+
getComposition().setSelectedRange([ 0, 2 ])
148+
assert.selectedRange([ 0, 2 ])
149+
})
150+
151+
test("collapsed selection before surrogate pair", () => {
152+
insertString("foo🙂")
153+
getComposition().setSelectedRange([ 3, 3 ])
154+
assert.selectedRange([ 3, 3 ])
155+
})
156+
157+
test("selection before surrogate pair", () => {
158+
insertString("foo🙂")
159+
getComposition().setSelectedRange([ 1, 3 ])
160+
assert.selectedRange([ 1, 3 ])
161+
})
162+
163+
test("selection of the surrogate pair", () => {
164+
insertString("foo🙂")
165+
getComposition().setSelectedRange([ 3, 5 ])
166+
assert.selectedRange([ 3, 5 ])
167+
})
168+
169+
test("selection of the first surrogate pair character", () => {
170+
insertString("foo🙂")
171+
getComposition().setSelectedRange([ 3, 4 ])
172+
assert.selectedRange([ 3, 5 ])
173+
})
174+
175+
test("selection of the second surrogate pair character", () => {
176+
insertString("foo🙂")
177+
getComposition().setSelectedRange([ 4, 5 ])
178+
assert.selectedRange([ 5, 5 ])
179+
})
180+
181+
test("collapssed selection in the middle of the surrogate pair", () => {
182+
insertString("foo🙂")
183+
getComposition().setSelectedRange([ 4, 4 ])
184+
assert.selectedRange([ 5, 5 ])
185+
})
186+
187+
test("collapsed selection after surrogate pair", () => {
188+
insertString("foo🙂")
189+
getComposition().setSelectedRange([ 5, 5 ])
190+
assert.selectedRange([ 5, 5 ])
191+
})
192+
},
193+
)

src/trix/models/composition.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,18 @@ export default class Composition extends BasicObject {
521521
}
522522

523523
setSelectedRange(selectedRange) {
524-
const locationRange = this.document.locationRangeFromRange(selectedRange)
524+
const [ start, end ] = normalizeRange(selectedRange)
525+
526+
// Make sure that surrogate pairs are always selected both or neither.
527+
const correctedSelectedRange = [
528+
this.translateUTF16PositionFromOffset(start, 0),
529+
this.translateUTF16PositionFromOffset(end, 0),
530+
]
531+
532+
const locationRange = this.document.locationRangeFromRange(
533+
correctedSelectedRange,
534+
)
535+
525536
return this.getSelectionManager().setLocationRange(locationRange)
526537
}
527538

0 commit comments

Comments
 (0)