Skip to content

Commit

Permalink
Fix End Of Doc Crash (#285)
Browse files Browse the repository at this point in the history
  • Loading branch information
thecoolwinter authored Jan 7, 2025
1 parent b0688fa commit 63bae5a
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CodeEditSourceEditorTests"
BuildableName = "CodeEditSourceEditorTests"
BlueprintName = "CodeEditSourceEditorTests"
ReferencedContainer = "container:../..">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,6 @@

import _RopeModule

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)
}
}

extension StyledRangeStore {
/// Coalesce items before and after the given range.
///
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
84 changes: 66 additions & 18 deletions Tests/CodeEditSourceEditorTests/Highlighting/HighlighterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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}")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 63bae5a

Please sign in to comment.