Skip to content

Commit ce69750

Browse files
committed
Merge branch 'main' into feat/highlighter-diffing
2 parents 0c7ccee + 63bae5a commit ce69750

File tree

5 files changed

+108
-25
lines changed

5 files changed

+108
-25
lines changed

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

Lines changed: 12 additions & 0 deletions
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"

Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore+Internals.swift renamed to Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore+Coalesce.swift

Lines changed: 1 addition & 10 deletions
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
}
Lines changed: 15 additions & 0 deletions
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

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,27 @@ final class HighlighterTests: XCTestCase {
4141
func attributesFor(_ capture: CaptureName?) -> [NSAttributedString.Key: Any] { [:] }
4242
}
4343

44+
class SentryStorageDelegate: NSObject, NSTextStorageDelegate {
45+
var editedIndices: IndexSet = IndexSet()
46+
47+
func textStorage(
48+
_ textStorage: NSTextStorage,
49+
didProcessEditing editedMask: NSTextStorageEditActions,
50+
range editedRange: NSRange,
51+
changeInLength delta: Int) {
52+
editedIndices.insert(integersIn: editedRange)
53+
}
54+
}
55+
56+
var attributeProvider: MockAttributeProvider!
57+
var textView: TextView!
58+
59+
override func setUp() {
60+
attributeProvider = MockAttributeProvider()
61+
textView = Mock.textView()
62+
textView.frame = NSRect(x: 0, y: 0, width: 1000, height: 1000)
63+
}
64+
4465
@MainActor
4566
func test_canceledHighlightsAreInvalidated() {
4667
var didQueryOnce = false
@@ -75,23 +96,8 @@ final class HighlighterTests: XCTestCase {
7596

7697
@MainActor
7798
func test_highlightsDoNotInvalidateEntireTextView() {
78-
class SentryStorageDelegate: NSObject, NSTextStorageDelegate {
79-
var editedIndices: IndexSet = IndexSet()
80-
81-
func textStorage(
82-
_ textStorage: NSTextStorage,
83-
didProcessEditing editedMask: NSTextStorageEditActions,
84-
range editedRange: NSRange,
85-
changeInLength delta: Int) {
86-
editedIndices.insert(integersIn: editedRange)
87-
}
88-
}
89-
9099
let highlightProvider = TreeSitterClient()
91100
highlightProvider.forceSyncOperation = true
92-
let attributeProvider = MockAttributeProvider()
93-
let textView = Mock.textView()
94-
textView.frame = NSRect(x: 0, y: 0, width: 1000, height: 1000)
95101
textView.setText("func helloWorld() {\n\tprint(\"Hello World!\")\n}")
96102

97103
let highlighter = Mock.highlighter(
@@ -226,4 +232,49 @@ final class HighlighterTests: XCTestCase {
226232
XCTAssertTrue(thirdSet.allSatisfy({ $0.queryCount >= 1 }), "Not all in third batch were queried")
227233
}
228234
}
235+
236+
// This test isn't testing much highlighter functionality. However, we've seen crashes and other errors after normal
237+
// editing that were caused by the highlighter and would only have been caught by an integration test like this.
238+
@MainActor
239+
func test_editFile() {
240+
let highlightProvider = TreeSitterClient()
241+
highlightProvider.forceSyncOperation = true
242+
textView.setText("func helloWorld() {\n\tprint(\"Hello World!\")\n}") // 44 chars
243+
244+
let highlighter = Mock.highlighter(
245+
textView: textView,
246+
highlightProvider: highlightProvider,
247+
attributeProvider: attributeProvider
248+
)
249+
textView.addStorageDelegate(highlighter)
250+
highlighter.setLanguage(language: .swift)
251+
highlighter.invalidate()
252+
253+
// Delete Characters
254+
textView.replaceCharacters(in: [NSRange(location: 43, length: 1)], with: "")
255+
textView.replaceCharacters(in: [NSRange(location: 0, length: 4)], with: "")
256+
textView.replaceCharacters(in: [NSRange(location: 6, length: 5)], with: "")
257+
textView.replaceCharacters(in: [NSRange(location: 25, length: 5)], with: "")
258+
259+
XCTAssertEqual(textView.string, " hello() {\n\tprint(\"Hello !\")\n")
260+
261+
// Insert Characters
262+
textView.replaceCharacters(in: [NSRange(location: 29, length: 0)], with: "}")
263+
textView.replaceCharacters(
264+
in: [NSRange(location: 25, length: 0), NSRange(location: 6, length: 0)],
265+
with: "World"
266+
)
267+
// emulate typing with a cursor
268+
textView.selectionManager.setSelectedRange(NSRange(location: 0, length: 0))
269+
textView.insertText("f")
270+
textView.insertText("u")
271+
textView.insertText("n")
272+
textView.insertText("c")
273+
XCTAssertEqual(textView.string, "func helloWorld() {\n\tprint(\"Hello World!\")\n}")
274+
275+
// Replace contents
276+
textView.replaceCharacters(in: textView.documentRange, with: "")
277+
textView.insertText("func helloWorld() {\n\tprint(\"Hello World!\")\n}")
278+
XCTAssertEqual(textView.string, "func helloWorld() {\n\tprint(\"Hello World!\")\n}")
279+
}
229280
}

Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeStoreTests.swift

Lines changed: 14 additions & 0 deletions
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)