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)