Skip to content

fix(find): search both sides of the diff during AI review (#111) - #156

Merged
Razee4315 merged 1 commit into
mainfrom
fix/find-during-ai-review
Jul 29, 2026
Merged

fix(find): search both sides of the diff during AI review (#111)#156
Razee4315 merged 1 commit into
mainfrom
fix/find-during-ai-review

Conversation

@Razee4315

Copy link
Copy Markdown
Owner

Fixes the Ctrl+F-during-review bug from #111, reported by @Blaze-Leo.

The bug

While an AI review is open the editor runs unifiedMergeView. The document holds only the proposed text; the removed original lines are block widgets that are not part of the document at all. The find controller searched state.doc, so a match inside a removed line did not exist as far as find was concerned.

consider that I have exactly two same words in a list, and I asked to divide the list into multiple groups, now before accepting or rejecting I search for the words, I find only 2 instances instead of expected 4

That is exactly it: four on screen, two counted, and next/previous silently skipping the ones you are looking at.

On the second half of the report ("positions of rendering ... not visually correct"): I could not reproduce mispositioning as a separate defect. Decorations are placed by document offset and CodeMirror accounts for widget heights when it lays out, so the surviving highlights land correctly. The wrong-looking positions follow from the missing matches, and go away with them.

The fix

Rebuild the searchable surface the way it is actually rendered: the document, plus original[fromA..toA] for each chunk, ordered by on-screen position. A chunk''s removed lines sort before its new text because @codemirror/merge mounts the deletion as a block widget at fromB.

Outside review the region list is empty and the search collapses to exactly the previous document-only behaviour, so the normal path is unchanged.

Replace is deliberately asymmetric

Removed text is the version being replaced. It is not in the document, and its offsets index a different string, so splicing the document at them would corrupt unrelated text. replaceActive no-ops on a removed match and replaceAll only ever takes document offsets. There are tests specifically for this.

Two numbering systems

The bar counts every match; decorations can only mark document text. Those indices are not the same, so activeDocIndex translates between them. Passing the bar index straight through would have emphasised the wrong match whenever a removed one appeared earlier in the list. Also tested.

The highlight is best-effort on purpose

Removed lines get the active highlight via the CSS Custom Highlight API, because decorations cannot reach widget DOM. That path is deliberately isolated: counting, ordering, navigation and replace safety never call into it. If it is skipped you still get the right count and still land on the right chunk.

It bails rather than mispaint when:

  • the query spans lines (the model counts such a match, but rendered lines are separate elements with no newline between them, so ordinals would not line up)
  • the DOM yields fewer occurrences than the model did (a line split at a change boundary mid-match)
  • the platform has no Custom Highlight API

It also skips the chunk''s Accept/Reject buttons, so those labels neither inflate the count nor shift the ordinal. Searching "Accept" during a review finds nothing in the chrome, as it should.

Refactor included

previewFind''s text-node walking moved into domTextMatches so reader-mode find and this share one implementation. It had no tests before; it does now.

Verification

  • bun run build (tsc + vite) clean
  • bun run test: 359 tests, 36 files, up from 313. 46 new tests.
    • reviewFind.test.ts (20) — offset and ordering arithmetic, including the interleaving of multiple chunks and the seam case where a regex must not match across removed/new text
    • reviewFind.integration.test.ts (8) — builds a real unifiedMergeView state and asserts the fromA/toA/fromB contract, so a future @codemirror/merge change cannot silently break this while the unit tests still pass. Includes finding text that exists only in removed lines.
    • reviewFindHighlight.test.ts (10) — drives the highlighter against a stand-in chunk, including the button-skip and refuse-to-mispaint cases
    • domTextMatches.test.ts (8) — new coverage for the extracted walker
  • Ran the full suite cold (cleared the Vite cache) as well as warm.

What I could not verify

I have no Rust toolchain or real webview here, so the Custom Highlight paint on the removed side is unverified on a real build — jsdom has no Custom Highlight API, so the tests exercise it through a stand-in. The logic it guards is fully tested and the failure mode is benign (scroll without paint). A Test Build is queued so this can be confirmed on a real window.

No version bump and no release in this PR.

While an AI review is open the editor runs unifiedMergeView: the DOCUMENT
holds only the proposed text and the removed original lines are block
widgets that are not part of the document. The find controller searched
state.doc, so any match inside a removed line simply did not exist to it.
Blaze-Leo reported seeing four hits on screen while the bar said two, with
next/previous skipping the ones he was looking at.

Rebuild the searchable surface the way it is rendered: the document plus,
for each chunk, original[fromA..toA], ordered by on-screen position. A
chunk's removed lines sort before its new text because @codemirror/merge
mounts the deletion as a block widget at fromB.

Replace is deliberately asymmetric. Removed text is the version being
replaced, it is not in the document, and its offsets index a different
string, so splicing at them would corrupt unrelated text. replaceActive
no-ops on a removed match and replaceAll only takes document offsets.

The bar counts every match while decorations can only mark document text,
so those are two different numbering systems; activeDocIndex translates.
Passing the bar index straight through would have emphasised the wrong
match whenever a removed one appeared earlier in the list.

Removed lines get the active highlight through the CSS Custom Highlight
API, since decorations cannot reach widget DOM. That path is best-effort
and isolated: counting, ordering, navigation and replace safety never call
into it, so if it is skipped the count is still right and the user is still
scrolled to the correct chunk. It bails rather than mispaint when the query
spans lines or the DOM yields fewer occurrences than the model did, and it
skips the chunk's Accept/Reject buttons so their labels neither inflate the
count nor shift the ordinal.

previewFind's text-node walking moved to domTextMatches so reader-mode find
and this share one implementation; it had no tests before and now does.

Outside review the region list is empty and the search collapses to exactly
the previous document-only behaviour.

Verified: 46 new tests. reviewFind.test.ts covers the offset and ordering
arithmetic, reviewFind.integration.test.ts asserts the fromA/toA/fromB
contract against the real @codemirror/merge so a library change cannot
silently break it while unit tests still pass, and reviewFindHighlight.test.ts
drives the highlighter against a stand-in chunk. Build clean, 359 tests pass.
@Razee4315
Razee4315 merged commit 33db492 into main Jul 29, 2026
7 checks passed
@Razee4315
Razee4315 deleted the fix/find-during-ai-review branch July 29, 2026 23:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant