Skip to content

Commit f77986f

Browse files
Merge branch 'main' into fix/undo-not-highlighted
2 parents 979b35a + 63bae5a commit f77986f

File tree

5 files changed

+108
-28
lines changed

5 files changed

+108
-28
lines changed

Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample.xcodeproj/xcshareddata/xcschemes/CodeEditSourceEditorExample.xcscheme

+12
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@
2929
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
3030
shouldUseLaunchSchemeArgsEnv = "YES"
3131
shouldAutocreateTestPlan = "YES">
32+
<Testables>
33+
<TestableReference
34+
skipped = "NO">
35+
<BuildableReference
36+
BuildableIdentifier = "primary"
37+
BlueprintIdentifier = "CodeEditSourceEditorTests"
38+
BuildableName = "CodeEditSourceEditorTests"
39+
BlueprintName = "CodeEditSourceEditorTests"
40+
ReferencedContainer = "container:../..">
41+
</BuildableReference>
42+
</TestableReference>
43+
</Testables>
3244
</TestAction>
3345
<LaunchAction
3446
buildConfiguration = "Debug"
+1-10
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,6 @@
77

88
import _RopeModule
99

10-
extension StyledRangeStore {
11-
/// Finds a Rope index, given a string offset.
12-
/// - Parameter offset: The offset to query for.
13-
/// - Returns: The index of the containing element in the rope.
14-
func findIndex(at offset: Int) -> (index: Index, remaining: Int) {
15-
_guts.find(at: offset, in: OffsetMetric(), preferEnd: false)
16-
}
17-
}
18-
1910
extension StyledRangeStore {
2011
/// Coalesce items before and after the given range.
2112
///
@@ -32,7 +23,7 @@ extension StyledRangeStore {
3223
}
3324

3425
index = findIndex(at: range.lowerBound).index
35-
if index > _guts.startIndex && _guts.count > 1 {
26+
if index > _guts.startIndex && index < _guts.endIndex && _guts.count > 1 {
3627
index = _guts.index(before: index)
3728
coalesceRunAfter(index: &index)
3829
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// StyledRangeStore+FindIndex.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 1/6/25.
6+
//
7+
8+
extension StyledRangeStore {
9+
/// Finds a Rope index, given a string offset.
10+
/// - Parameter offset: The offset to query for.
11+
/// - Returns: The index of the containing element in the rope.
12+
func findIndex(at offset: Int) -> (index: Index, remaining: Int) {
13+
_guts.find(at: offset, in: OffsetMetric(), preferEnd: false)
14+
}
15+
}

Tests/CodeEditSourceEditorTests/Highlighting/HighlighterTests.swift

+66-18
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,30 @@ final class HighlighterTests: XCTestCase {
3838
func attributesFor(_ capture: CaptureName?) -> [NSAttributedString.Key: Any] { [:] }
3939
}
4040

41+
class SentryStorageDelegate: NSObject, NSTextStorageDelegate {
42+
var editedIndices: IndexSet = IndexSet()
43+
44+
func textStorage(
45+
_ textStorage: NSTextStorage,
46+
didProcessEditing editedMask: NSTextStorageEditActions,
47+
range editedRange: NSRange,
48+
changeInLength delta: Int) {
49+
editedIndices.insert(integersIn: editedRange)
50+
}
51+
}
52+
53+
var attributeProvider: MockAttributeProvider!
54+
var textView: TextView!
55+
56+
override func setUp() {
57+
attributeProvider = MockAttributeProvider()
58+
textView = Mock.textView()
59+
textView.frame = NSRect(x: 0, y: 0, width: 1000, height: 1000)
60+
}
61+
4162
@MainActor
4263
func test_canceledHighlightsAreInvalidated() {
4364
let highlightProvider = MockHighlightProvider()
44-
let attributeProvider = MockAttributeProvider()
45-
let textView = Mock.textView()
46-
textView.frame = NSRect(x: 0, y: 0, width: 1000, height: 1000)
4765
textView.setText("Hello World!")
4866
let highlighter = Mock.highlighter(
4967
textView: textView,
@@ -62,23 +80,8 @@ final class HighlighterTests: XCTestCase {
6280

6381
@MainActor
6482
func test_highlightsDoNotInvalidateEntireTextView() {
65-
class SentryStorageDelegate: NSObject, NSTextStorageDelegate {
66-
var editedIndices: IndexSet = IndexSet()
67-
68-
func textStorage(
69-
_ textStorage: NSTextStorage,
70-
didProcessEditing editedMask: NSTextStorageEditActions,
71-
range editedRange: NSRange,
72-
changeInLength delta: Int) {
73-
editedIndices.insert(integersIn: editedRange)
74-
}
75-
}
76-
7783
let highlightProvider = TreeSitterClient()
7884
highlightProvider.forceSyncOperation = true
79-
let attributeProvider = MockAttributeProvider()
80-
let textView = Mock.textView()
81-
textView.frame = NSRect(x: 0, y: 0, width: 1000, height: 1000)
8285
textView.setText("func helloWorld() {\n\tprint(\"Hello World!\")\n}")
8386

8487
let highlighter = Mock.highlighter(
@@ -97,4 +100,49 @@ final class HighlighterTests: XCTestCase {
97100

98101
XCTAssertEqual(sentryStorage.editedIndices, invalidSet) // Should only cause highlights on the first line
99102
}
103+
104+
// This test isn't testing much highlighter functionality. However, we've seen crashes and other errors after normal
105+
// editing that were caused by the highlighter and would only have been caught by an integration test like this.
106+
@MainActor
107+
func test_editFile() {
108+
let highlightProvider = TreeSitterClient()
109+
highlightProvider.forceSyncOperation = true
110+
textView.setText("func helloWorld() {\n\tprint(\"Hello World!\")\n}") // 44 chars
111+
112+
let highlighter = Mock.highlighter(
113+
textView: textView,
114+
highlightProvider: highlightProvider,
115+
attributeProvider: attributeProvider
116+
)
117+
textView.addStorageDelegate(highlighter)
118+
highlighter.setLanguage(language: .swift)
119+
highlighter.invalidate()
120+
121+
// Delete Characters
122+
textView.replaceCharacters(in: [NSRange(location: 43, length: 1)], with: "")
123+
textView.replaceCharacters(in: [NSRange(location: 0, length: 4)], with: "")
124+
textView.replaceCharacters(in: [NSRange(location: 6, length: 5)], with: "")
125+
textView.replaceCharacters(in: [NSRange(location: 25, length: 5)], with: "")
126+
127+
XCTAssertEqual(textView.string, " hello() {\n\tprint(\"Hello !\")\n")
128+
129+
// Insert Characters
130+
textView.replaceCharacters(in: [NSRange(location: 29, length: 0)], with: "}")
131+
textView.replaceCharacters(
132+
in: [NSRange(location: 25, length: 0), NSRange(location: 6, length: 0)],
133+
with: "World"
134+
)
135+
// emulate typing with a cursor
136+
textView.selectionManager.setSelectedRange(NSRange(location: 0, length: 0))
137+
textView.insertText("f")
138+
textView.insertText("u")
139+
textView.insertText("n")
140+
textView.insertText("c")
141+
XCTAssertEqual(textView.string, "func helloWorld() {\n\tprint(\"Hello World!\")\n}")
142+
143+
// Replace contents
144+
textView.replaceCharacters(in: textView.documentRange, with: "")
145+
textView.insertText("func helloWorld() {\n\tprint(\"Hello World!\")\n}")
146+
XCTAssertEqual(textView.string, "func helloWorld() {\n\tprint(\"Hello World!\")\n}")
147+
}
100148
}

Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeStoreTests.swift

+14
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ final class StyledRangeStoreTests: XCTestCase {
3636
XCTAssertEqual(store.count, 1, "Failed to coalesce")
3737
}
3838

39+
func test_storageRemoveSingleCharacterFromEnd() {
40+
let store = StyledRangeStore(documentLength: 10)
41+
store.set( // Test that we can delete a character associated with a single syntax run too
42+
runs: [
43+
.empty(length: 8),
44+
.init(length: 1, modifiers: [.abstract]),
45+
.init(length: 1, modifiers: [.declaration])],
46+
for: 0..<10
47+
)
48+
store.storageUpdated(replacedCharactersIn: 9..<10, withCount: 0)
49+
XCTAssertEqual(store.length, 9, "Failed to remove correct range")
50+
XCTAssertEqual(store.count, 2)
51+
}
52+
3953
func test_storageRemoveFromBeginning() {
4054
let store = StyledRangeStore(documentLength: 100)
4155
store.storageUpdated(replacedCharactersIn: 0..<15, withCount: 0)

0 commit comments

Comments
 (0)