CleanCocoa / CleanCocoa/DeclarativeTextKit

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

Open
#9 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
Swift
Stars
51
Forks
0
PR merge metrics
No merged PRs in 30d

Description

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

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at the Modifying construct and the buffer.evaluate entry point, then inspect how an empty result-builder block is represented for the buildArray path. Reproduce the issue with testModifying_EmptyLoopBlock_SkipsEvaluation and the provided TextViewSpy. Done means the empty loop block skips evaluation and neither text-change callback is called.

Written by the indexing model from the issue text.

Assessment

Tech stack
swift
Domain
desktop
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.