From 63bae5a4f752aba5295ff991e843b3eefe907473 Mon Sep 17 00:00:00 2001
From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com>
Date: Mon, 6 Jan 2025 21:52:34 -0600
Subject: [PATCH] Fix End Of Doc Crash (#285)
---
.../CodeEditSourceEditorExample.xcscheme | 12 +++
....swift => StyledRangeStore+Coalesce.swift} | 11 +--
.../StyledRangeStore+FindIndex.swift | 15 ++++
.../Highlighting/HighlighterTests.swift | 84 +++++++++++++++----
.../Highlighting/StyledRangeStoreTests.swift | 14 ++++
5 files changed, 108 insertions(+), 28 deletions(-)
rename Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/{StyledRangeStore+Internals.swift => StyledRangeStore+Coalesce.swift} (78%)
create mode 100644 Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore+FindIndex.swift
diff --git a/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample.xcodeproj/xcshareddata/xcschemes/CodeEditSourceEditorExample.xcscheme b/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample.xcodeproj/xcshareddata/xcschemes/CodeEditSourceEditorExample.xcscheme
index 8a70ab4de..643fa8b4a 100644
--- a/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample.xcodeproj/xcshareddata/xcschemes/CodeEditSourceEditorExample.xcscheme
+++ b/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample.xcodeproj/xcshareddata/xcschemes/CodeEditSourceEditorExample.xcscheme
@@ -29,6 +29,18 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
+
+
+
+
+
+
(index: Index, remaining: Int) {
- _guts.find(at: offset, in: OffsetMetric(), preferEnd: false)
- }
-}
-
extension StyledRangeStore {
/// Coalesce items before and after the given range.
///
@@ -32,7 +23,7 @@ extension StyledRangeStore {
}
index = findIndex(at: range.lowerBound).index
- if index > _guts.startIndex && _guts.count > 1 {
+ if index > _guts.startIndex && index < _guts.endIndex && _guts.count > 1 {
index = _guts.index(before: index)
coalesceRunAfter(index: &index)
}
diff --git a/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore+FindIndex.swift b/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore+FindIndex.swift
new file mode 100644
index 000000000..a07076b58
--- /dev/null
+++ b/Sources/CodeEditSourceEditor/Highlighting/StyledRangeContainer/StyledRangeStore/StyledRangeStore+FindIndex.swift
@@ -0,0 +1,15 @@
+//
+// StyledRangeStore+FindIndex.swift
+// CodeEditSourceEditor
+//
+// Created by Khan Winter on 1/6/25.
+//
+
+extension StyledRangeStore {
+ /// Finds a Rope index, given a string offset.
+ /// - Parameter offset: The offset to query for.
+ /// - Returns: The index of the containing element in the rope.
+ func findIndex(at offset: Int) -> (index: Index, remaining: Int) {
+ _guts.find(at: offset, in: OffsetMetric(), preferEnd: false)
+ }
+}
diff --git a/Tests/CodeEditSourceEditorTests/Highlighting/HighlighterTests.swift b/Tests/CodeEditSourceEditorTests/Highlighting/HighlighterTests.swift
index fd33ddaaa..e57bde99f 100644
--- a/Tests/CodeEditSourceEditorTests/Highlighting/HighlighterTests.swift
+++ b/Tests/CodeEditSourceEditorTests/Highlighting/HighlighterTests.swift
@@ -38,12 +38,30 @@ final class HighlighterTests: XCTestCase {
func attributesFor(_ capture: CaptureName?) -> [NSAttributedString.Key: Any] { [:] }
}
+ class SentryStorageDelegate: NSObject, NSTextStorageDelegate {
+ var editedIndices: IndexSet = IndexSet()
+
+ func textStorage(
+ _ textStorage: NSTextStorage,
+ didProcessEditing editedMask: NSTextStorageEditActions,
+ range editedRange: NSRange,
+ changeInLength delta: Int) {
+ editedIndices.insert(integersIn: editedRange)
+ }
+ }
+
+ var attributeProvider: MockAttributeProvider!
+ var textView: TextView!
+
+ override func setUp() {
+ attributeProvider = MockAttributeProvider()
+ textView = Mock.textView()
+ textView.frame = NSRect(x: 0, y: 0, width: 1000, height: 1000)
+ }
+
@MainActor
func test_canceledHighlightsAreInvalidated() {
let highlightProvider = MockHighlightProvider()
- let attributeProvider = MockAttributeProvider()
- let textView = Mock.textView()
- textView.frame = NSRect(x: 0, y: 0, width: 1000, height: 1000)
textView.setText("Hello World!")
let highlighter = Mock.highlighter(
textView: textView,
@@ -62,23 +80,8 @@ final class HighlighterTests: XCTestCase {
@MainActor
func test_highlightsDoNotInvalidateEntireTextView() {
- class SentryStorageDelegate: NSObject, NSTextStorageDelegate {
- var editedIndices: IndexSet = IndexSet()
-
- func textStorage(
- _ textStorage: NSTextStorage,
- didProcessEditing editedMask: NSTextStorageEditActions,
- range editedRange: NSRange,
- changeInLength delta: Int) {
- editedIndices.insert(integersIn: editedRange)
- }
- }
-
let highlightProvider = TreeSitterClient()
highlightProvider.forceSyncOperation = true
- let attributeProvider = MockAttributeProvider()
- let textView = Mock.textView()
- textView.frame = NSRect(x: 0, y: 0, width: 1000, height: 1000)
textView.setText("func helloWorld() {\n\tprint(\"Hello World!\")\n}")
let highlighter = Mock.highlighter(
@@ -97,4 +100,49 @@ final class HighlighterTests: XCTestCase {
XCTAssertEqual(sentryStorage.editedIndices, invalidSet) // Should only cause highlights on the first line
}
+
+ // This test isn't testing much highlighter functionality. However, we've seen crashes and other errors after normal
+ // editing that were caused by the highlighter and would only have been caught by an integration test like this.
+ @MainActor
+ func test_editFile() {
+ let highlightProvider = TreeSitterClient()
+ highlightProvider.forceSyncOperation = true
+ textView.setText("func helloWorld() {\n\tprint(\"Hello World!\")\n}") // 44 chars
+
+ let highlighter = Mock.highlighter(
+ textView: textView,
+ highlightProvider: highlightProvider,
+ attributeProvider: attributeProvider
+ )
+ textView.addStorageDelegate(highlighter)
+ highlighter.setLanguage(language: .swift)
+ highlighter.invalidate()
+
+ // Delete Characters
+ textView.replaceCharacters(in: [NSRange(location: 43, length: 1)], with: "")
+ textView.replaceCharacters(in: [NSRange(location: 0, length: 4)], with: "")
+ textView.replaceCharacters(in: [NSRange(location: 6, length: 5)], with: "")
+ textView.replaceCharacters(in: [NSRange(location: 25, length: 5)], with: "")
+
+ XCTAssertEqual(textView.string, " hello() {\n\tprint(\"Hello !\")\n")
+
+ // Insert Characters
+ textView.replaceCharacters(in: [NSRange(location: 29, length: 0)], with: "}")
+ textView.replaceCharacters(
+ in: [NSRange(location: 25, length: 0), NSRange(location: 6, length: 0)],
+ with: "World"
+ )
+ // emulate typing with a cursor
+ textView.selectionManager.setSelectedRange(NSRange(location: 0, length: 0))
+ textView.insertText("f")
+ textView.insertText("u")
+ textView.insertText("n")
+ textView.insertText("c")
+ XCTAssertEqual(textView.string, "func helloWorld() {\n\tprint(\"Hello World!\")\n}")
+
+ // Replace contents
+ textView.replaceCharacters(in: textView.documentRange, with: "")
+ textView.insertText("func helloWorld() {\n\tprint(\"Hello World!\")\n}")
+ XCTAssertEqual(textView.string, "func helloWorld() {\n\tprint(\"Hello World!\")\n}")
+ }
}
diff --git a/Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeStoreTests.swift b/Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeStoreTests.swift
index 0395e74b1..5b43d5d2e 100644
--- a/Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeStoreTests.swift
+++ b/Tests/CodeEditSourceEditorTests/Highlighting/StyledRangeStoreTests.swift
@@ -36,6 +36,20 @@ final class StyledRangeStoreTests: XCTestCase {
XCTAssertEqual(store.count, 1, "Failed to coalesce")
}
+ func test_storageRemoveSingleCharacterFromEnd() {
+ let store = StyledRangeStore(documentLength: 10)
+ store.set( // Test that we can delete a character associated with a single syntax run too
+ runs: [
+ .empty(length: 8),
+ .init(length: 1, modifiers: [.abstract]),
+ .init(length: 1, modifiers: [.declaration])],
+ for: 0..<10
+ )
+ store.storageUpdated(replacedCharactersIn: 9..<10, withCount: 0)
+ XCTAssertEqual(store.length, 9, "Failed to remove correct range")
+ XCTAssertEqual(store.count, 2)
+ }
+
func test_storageRemoveFromBeginning() {
let store = StyledRangeStore(documentLength: 100)
store.storageUpdated(replacedCharactersIn: 0..<15, withCount: 0)