Skip to content

Skip evaluation of a Modifying block iff it doesn't do anything #9

@DivineDominion

Description

@DivineDominion

The Modifying(<Range>) { <Block> } construct always evaluates even if the block is empty.

With TextKit integration, this means

  1. an undo group is being started (and ended)
  2. NSTextView.shouldChangeText(in:replacementString:) and didChangeText() are being run to guard against unwanted changes

With syntax highlighting in the text storage, you may end up processing the text for what's essentially a no-op.

How to test

To get an empty block, use a for-loop to trigger the buildArray path of the result builder, but without any actual iterations:

Modifying(selectedRange) { _ in
    for _ in 0 ..< 0 {
        Insert(0) { "loop never runs" }
    }
}

I'm not sure whether we can figure out at all whether a result builder produces nothing (i.e. empty array).

Complete test case

This test fails with a thrown error at try buffer.evaluate because the text view doesn't permit changes in the range.

This should not be a problem, because the range is not actually changed.

    func testModifying_EmptyLoopBlock_SkipsEvaluation() throws {
         class TextViewSpy: NSTextView {
            var didCallShouldChangeText = false
            var didCallDidChangeText = false

            override func shouldChangeText(in affectedCharRange: NSRange, replacementString: String?) -> Bool {
                didCallShouldChangeText = true
                return false  // Would abort modification an error
            }

            override func didChangeText() {
                didCallDidChangeText = true
            }
        }

        let textViewSpy = TextViewSpy()
        textViewSpy.string = "Lorem ipsum."
        let buffer = NSTextViewBuffer(textView: textViewSpy)
        let selectedRange: SelectedRange = .init(location: 6, length: 5)

        assertBufferState(buffer, "Lorem ipsum.ˇ")

        try buffer.evaluate {
            Modifying(selectedRange) { _ in
                for _ in 0 ..< 0 {
                    Insert(0) { "loop never runs" }
                }
            }
        }

        XCTAssertFalse(textViewSpy.didCallShouldChangeText)
        XCTAssertFalse(textViewSpy.didCallDidChangeText)
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions