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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions