Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix End Of Doc Crash #285

Merged
merged 2 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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