Skip to content

Commit e726e32

Browse files
committed
Fix range conversion when there are characters with more than 1 unicode scalar in the code
1 parent 59ba332 commit e726e32

File tree

3 files changed

+234
-17
lines changed

3 files changed

+234
-17
lines changed

Tool/Sources/XcodeInspector/SourceEditor.swift

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public class SourceEditor {
1212
public struct AXNotification: Hashable {
1313
public var kind: AXNotificationKind
1414
public var element: AXUIElement
15-
15+
1616
public func hash(into hasher: inout Hasher) {
1717
kind.hash(into: &hasher)
1818
}
@@ -198,8 +198,8 @@ public extension SourceEditor {
198198
range.length = max(countE - range.location, 0)
199199
break
200200
}
201-
countS += line.count
202-
countE += line.count
201+
countS += line.utf16.count
202+
countE += line.utf16.count
203203
}
204204
return range
205205
}
@@ -221,24 +221,25 @@ public extension SourceEditor {
221221
var countE = 0
222222
var cursorRange = CursorRange(start: .zero, end: .outOfScope)
223223
for (i, line) in lines.enumerated() {
224-
// The range is counted in UTF8, which causes line endings like \r\n to be of length 2.
225-
let lineEndingAddition = line.lineEnding.utf8.count - 1
226224
if countS <= range.lowerBound,
227-
range.lowerBound < countS + line.count + lineEndingAddition
225+
range.lowerBound < countS + line.utf16.count
228226
{
229227
cursorRange.start = .init(line: i, character: range.lowerBound - countS)
230228
}
231229
if countE <= range.upperBound,
232-
range.upperBound < countE + line.count + lineEndingAddition
230+
range.upperBound < countE + line.utf16.count
233231
{
234232
cursorRange.end = .init(line: i, character: range.upperBound - countE)
235233
break
236234
}
237-
countS += line.count + lineEndingAddition
238-
countE += line.count + lineEndingAddition
235+
countS += line.utf16.count
236+
countE += line.utf16.count
239237
}
240238
if cursorRange.end == .outOfScope {
241-
cursorRange.end = .init(line: lines.endIndex - 1, character: lines.last?.count ?? 0)
239+
cursorRange.end = .init(
240+
line: lines.endIndex - 1,
241+
character: lines.last?.utf16.count ?? 0
242+
)
242243
}
243244
return cursorRange
244245
}

Tool/Tests/SharedUIComponentsTests/ConvertToCodeLinesTests.swift

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ final class ConvertToCodeLinesTests: XCTestCase {
1111
let (result, spaceCount) = highlighted(
1212
code: code,
1313
language: "swift",
14+
scenario: "a",
1415
brightMode: true,
1516
droppingLeadingSpaces: false,
16-
fontSize: 14
17+
font: .systemFont(ofSize: 14)
1718
)
1819

1920
XCTAssertEqual(spaceCount, 0)
@@ -32,9 +33,10 @@ final class ConvertToCodeLinesTests: XCTestCase {
3233
let (result, spaceCount) = highlighted(
3334
code: code,
3435
language: "md",
36+
scenario: "a",
3537
brightMode: true,
3638
droppingLeadingSpaces: true,
37-
fontSize: 14
39+
font: .systemFont(ofSize: 14)
3840
)
3941

4042
XCTAssertEqual(spaceCount, 0)
@@ -52,9 +54,10 @@ final class ConvertToCodeLinesTests: XCTestCase {
5254
let (result, spaceCount) = highlighted(
5355
code: code,
5456
language: "md",
57+
scenario: "a",
5558
brightMode: true,
5659
droppingLeadingSpaces: true,
57-
fontSize: 14
60+
font: .systemFont(ofSize: 14)
5861
)
5962

6063
XCTAssertEqual(spaceCount, 4)
@@ -72,9 +75,10 @@ final class ConvertToCodeLinesTests: XCTestCase {
7275
let (result, spaceCount) = highlighted(
7376
code: code,
7477
language: "md",
78+
scenario: "a",
7579
brightMode: true,
7680
droppingLeadingSpaces: true,
77-
fontSize: 14
81+
font: .systemFont(ofSize: 14)
7882
)
7983

8084
XCTAssertEqual(spaceCount, 8)
@@ -93,9 +97,10 @@ final class ConvertToCodeLinesTests: XCTestCase {
9397
let (result, spaceCount) = highlighted(
9498
code: code,
9599
language: "md",
100+
scenario: "a",
96101
brightMode: true,
97102
droppingLeadingSpaces: true,
98-
fontSize: 14
103+
font: .systemFont(ofSize: 14)
99104
)
100105

101106
XCTAssertEqual(spaceCount, 4)
@@ -115,9 +120,10 @@ final class ConvertToCodeLinesTests: XCTestCase {
115120
let (result, spaceCount) = highlighted(
116121
code: code,
117122
language: "md",
123+
scenario: "a",
118124
brightMode: true,
119125
droppingLeadingSpaces: true,
120-
fontSize: 14
126+
font: .systemFont(ofSize: 14)
121127
)
122128

123129
XCTAssertEqual(spaceCount, 0)
@@ -137,9 +143,10 @@ final class ConvertToCodeLinesTests: XCTestCase {
137143
let (result, spaceCount) = highlighted(
138144
code: code,
139145
language: "md",
146+
scenario: "a",
140147
brightMode: true,
141148
droppingLeadingSpaces: true,
142-
fontSize: 14
149+
font: .systemFont(ofSize: 14)
143150
)
144151

145152
XCTAssertEqual(spaceCount, 4)
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import Foundation
2+
import SuggestionModel
3+
import XCTest
4+
5+
@testable import XcodeInspector
6+
7+
class SourceEditorRangeConversionTests: XCTestCase {
8+
// MARK: - Convert to CursorRange
9+
10+
func test_convert_multiline_range() {
11+
let code = """
12+
import Foundation
13+
import XCTest
14+
15+
class SourceEditorRangeConversionTests {
16+
func testSomething() {
17+
// test
18+
}
19+
}
20+
21+
"""
22+
23+
let range = 21...39
24+
let cursorRange = SourceEditor.convertRangeToCursorRange(range, in: code)
25+
26+
XCTAssertEqual(cursorRange.start, .init(line: 1, character: 3))
27+
XCTAssertEqual(cursorRange.end, .init(line: 3, character: 6))
28+
}
29+
30+
func test_convert_multiline_range_with_special_line_endings() {
31+
let code = """
32+
import Foundation
33+
import XCTest
34+
35+
class SourceEditorRangeConversionTests {
36+
func testSomething() {
37+
// test
38+
}
39+
}
40+
41+
""".replacingOccurrences(of: "\n", with: "\r\n")
42+
43+
let range = 21...39
44+
let cursorRange = SourceEditor.convertRangeToCursorRange(range, in: code)
45+
46+
XCTAssertEqual(cursorRange.start, .init(line: 1, character: 2))
47+
XCTAssertEqual(cursorRange.end, .init(line: 3, character: 3))
48+
}
49+
50+
func test_convert_multiline_range_with_emoji() {
51+
let code = """
52+
import Foundation
53+
import 🎆🎆🎆🎆🎆🎆
54+
55+
class SourceEditorRangeConversionTests {
56+
func testSomething() {
57+
// test
58+
}
59+
}
60+
61+
"""
62+
63+
let range = 21...42
64+
let cursorRange = SourceEditor.convertRangeToCursorRange(range, in: code)
65+
66+
XCTAssertEqual(cursorRange.start, .init(line: 1, character: 3))
67+
XCTAssertEqual(cursorRange.end, .init(line: 3, character: 3))
68+
}
69+
70+
func test_convert_range_with_no_code() {
71+
let code = ""
72+
let range = 21...39
73+
let cursorRange = SourceEditor.convertRangeToCursorRange(range, in: code)
74+
75+
XCTAssertEqual(cursorRange.start, .zero)
76+
XCTAssertEqual(cursorRange.end, .zero)
77+
}
78+
79+
func test_convert_multiline_range_with_out_of_range_cursor() {
80+
let code = """
81+
import Foundation
82+
import XCTest
83+
84+
class SourceEditorRangeConversionTests {
85+
func testSomething() {
86+
// test
87+
}
88+
}
89+
90+
"""
91+
92+
let range = 999...1000
93+
let cursorRange = SourceEditor.convertRangeToCursorRange(range, in: code)
94+
95+
// undefined behavior
96+
97+
XCTAssertEqual(cursorRange.start, .zero)
98+
XCTAssertEqual(cursorRange.end, .init(line: 8, character: 0))
99+
}
100+
101+
// MARK: - Convert to CFRange
102+
103+
func test_back_convert_multiline_cursor_range() {
104+
let code = """
105+
import Foundation
106+
import XCTest
107+
108+
class SourceEditorRangeConversionTests {
109+
func testSomething() {
110+
// test
111+
}
112+
}
113+
114+
"""
115+
116+
let cursorRange = CursorRange(
117+
start: .init(line: 1, character: 3),
118+
end: .init(line: 3, character: 6)
119+
)
120+
let range = SourceEditor.convertCursorRangeToRange(cursorRange, in: code)
121+
122+
XCTAssertEqual(range.range, 21...39)
123+
}
124+
125+
func test_back_convert_multiline_range_with_out_of_range_cursor() {
126+
let code = """
127+
import Foundation
128+
import XCTest
129+
130+
class SourceEditorRangeConversionTests {
131+
func testSomething() {
132+
// test
133+
}
134+
}
135+
136+
"""
137+
138+
let cursorRange = CursorRange(
139+
start: .init(line: 999, character: 0),
140+
end: .init(line: 1000, character: 0)
141+
)
142+
let range = SourceEditor.convertCursorRangeToRange(cursorRange, in: code)
143+
144+
// undefined behavior
145+
146+
XCTAssertEqual(range.range, 0...0)
147+
}
148+
149+
func test_back_convert_multiline_range_with_special_line_endings() {
150+
let code = """
151+
import Foundation
152+
import XCTest
153+
154+
class SourceEditorRangeConversionTests {
155+
func testSomething() {
156+
// test
157+
}
158+
}
159+
160+
""".replacingOccurrences(of: "\n", with: "\r\n")
161+
162+
let cursorRange = CursorRange(
163+
start: .init(line: 1, character: 2),
164+
end: .init(line: 3, character: 3)
165+
)
166+
let range = SourceEditor.convertCursorRangeToRange(cursorRange, in: code)
167+
168+
XCTAssertEqual(range.range, 21...39)
169+
}
170+
171+
func test_back_convert_multiline_range_with_emoji() {
172+
let code = """
173+
import Foundation
174+
import 🎆🎆🎆🎆🎆🎆
175+
176+
class SourceEditorRangeConversionTests {
177+
func testSomething() {
178+
// test
179+
}
180+
}
181+
182+
"""
183+
184+
let cursorRange = CursorRange(
185+
start: .init(line: 1, character: 3),
186+
end: .init(line: 3, character: 3)
187+
)
188+
let range = SourceEditor.convertCursorRangeToRange(cursorRange, in: code)
189+
XCTAssertEqual(range.range, 21...42)
190+
}
191+
192+
func test_back_convert_range_with_no_code() {
193+
let code = ""
194+
let range = 21...39
195+
let cursorRange = SourceEditor.convertCursorRangeToRange(
196+
SourceEditor.convertRangeToCursorRange(range, in: code),
197+
in: code
198+
)
199+
200+
XCTAssertEqual(cursorRange.range, 0...0)
201+
}
202+
}
203+
204+
private extension CFRange {
205+
var range: ClosedRange<Int> {
206+
return location...(location + length)
207+
}
208+
}
209+

0 commit comments

Comments
 (0)