Skip to content

fix: unify occurrence search through text maps (ISSUES.md #21) - #39

Merged
pablospe merged 2 commits into
mainfrom
task/f9d24701-Unify-occurrence-search-throug
Jul 14, 2026
Merged

pablospe merged 2 commits into
mainfrom
task/f9d24701-Unify-occurrence-search-throug

Conversation

@pablospe

Copy link
Copy Markdown
Owner

Problem

replace_text, suggest_deletion, and insert_text_after/before (document-wide path) tried an element-level w:t search first and fell back to the text-map search only on a miss. The two searches count occurrences over different sequences — the element-level one only sees whole w:t nodes containing the text, so a match split across fragmented runs is invisible to it. Result: occurrence=N could target different matches depending on which path won. Concretely, with occurrence 0 of "target" split "tar"+"get" in P1 and occurrence 1 whole in P2, replace_text("target", "X", occurrence=0) edited P2.

Same bug class as issue #5 (comments), which already routes exclusively through the text map.

Fix

  • New _find_document_wide(text, occurrence): single text-map lookup shared by all three methods, counting over the same sequence as count_matches. Each method's paragraph=None branch is now two lines.
  • Deleted _get_nth_match, _find_match_containing_node, and the three duplicate single-node edit bodies (~250 lines) — the cross-boundary helpers were already the mainline for paragraph-scoped calls and batch_edit.
  • Rewrote _insert_near_match to split the edge w:t at the match boundary instead of inserting relative to whole runs. This also fixes a pre-existing mid-node placement bug in the paragraph-scoped and batch_edit insert paths (insert_text_after("Hello", "XX") on <w:t>Hello world</w:t> produced "Hello worldXX"; now "HelloXX world"). Anchors inside w:ins are spliced at the exact offset with no nested w:ins; non-text run children (w:tab, w:br) are preserved.
  • TextNotFoundError.total_occurrences now reports the text-map count, consistent with count_matches (message wording unchanged).

Tests

  • TestDocumentWideOccurrenceStability (9): pins occurrence indexing on a split-match fixture for replace/delete/insert plus the error contract. Failed before the fix.
  • TestInsertPlacementMidNode (9): exact-output placement assertions across document-wide, paragraph-scoped, and batch_edit paths, sibling/w:ins edge cases. Six failed before the fix.
  • TestDocumentWideEditsRealXml (7): real-XML replacements for the deleted mock-driven edge-case tests (rPr preservation, in-place w:ins semantics, prefix/suffix splits).
  • Deleted 17 mock-based tests that drove the removed code paths; rewrote the two _get_nth_match unit tests against the public API.

Full suite: 760 passed, 2 skipped (baseline 746/8 — six formerly-skipped tests skipped on TextNotFoundError and now run and pass because the unified search finds their targets).

Out of scope

Author-aware edits inside foreign w:ins (ISSUES.md #19) and non-text run-children ordering (#20) are follow-up tasks that build on this unification.

Document-wide replace/delete/insert used an element-level w:t search
(_get_nth_match) with a text-map fallback, and the two counted
occurrences over different sequences: a match split across fragmented
runs was invisible to the element-level path, so occurrence=N could
target different matches depending on which path won.

Route all three methods through a single _find_document_wide text-map
lookup (the same sequence count_matches uses), delete the duplicate
single-node edit bodies and the drift shim (~250 lines), and rewrite
_insert_near_match to split the edge w:t at the match boundary,
fixing mid-node anchor placement for the paragraph-scoped and
batch_edit insert paths as well ("HelloXX world", not "Hello worldXX").

TextNotFoundError.total_occurrences now matches count_matches, and
runs with non-text children (w:tab, w:br) keep them during inserts.
@coderabbitai

coderabbitai Bot commented Jul 14, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@pablospe, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 28 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2a6260f4-65e9-4bb5-a46b-e7f44052c316

📥 Commits

Reviewing files that changed from the base of the PR and between 9c8bcc3 and 78851c4.

📒 Files selected for processing (3)
  • docx_editor/track_changes.py
  • tests/test_review_fixes.py
  • tests/test_track_changes.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task/f9d24701-Unify-occurrence-search-throug

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 14, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.21053% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.0%. Comparing base (9c8bcc3) to head (78851c4).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
docx_editor/track_changes.py 84.2% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##            main     #39     +/-   ##
=======================================
+ Coverage   93.5%   94.0%   +0.4%     
=======================================
  Files         11      11             
  Lines       2950    2812    -138     
  Branches     634     592     -42     
=======================================
- Hits        2761    2644    -117     
+ Misses        84      74     -10     
+ Partials     105      94     -11     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Multi-reviewer pass on PR #39. Fixed: missing `occurrence: int = 0`
default, matching the sibling finders (_find_in_paragraph,
_find_across_boundaries).

Verified as not applicable here and deferred with notes in ISSUES.md:
straddle-match insertion into a foreign w:ins (needs #19's
ins-splitting machinery; in-place rebuild would nest w:ins) and
front-hoisted non-text run children in the insert rebuild (#20, same
pattern as the existing replace/delete paths). xml:space on generated
w:t was a false positive — the editor's attribute injection adds it.
@pablospe
pablospe merged commit aefe715 into main Jul 14, 2026
9 of 10 checks passed
@pablospe
pablospe deleted the task/f9d24701-Unify-occurrence-search-throug branch July 14, 2026 10:27
pablospe added a commit that referenced this pull request Aug 3, 2026
… (ISSUES.md #39) (#74)

Replacing text inside our own pending insertion anchored the replacement at ins_elem.firstChild — the match position only when the match starts at the insertion's first character. Every other match came out reordered.

_replace_within_own_ins() rebuilds each affected run where it stands via rebuild_run_fragments: text before the match, the matched text dropped, the replacement emitted at the match's last node, text after, unmatched w:t siblings preserved. Runs are swapped one at a time and rebuilt deepest-first, so content never migrates between adjacent own insertions and a run nested in another matched run's w:drawing is mutated while still attached. An insertion left with no element children is removed; one holding another author's nested w:del is kept.

Behaviour change: a match covering a whole own insertion now amends it in place instead of minting a fresh w:ins, returning -1 / group_id=None like every partial splice. The insertion keeps its id, date and group; undoing an amendment means rejecting that group. group_id_of() resolves a negative revision id to None so the sentinel cannot collide with a document carrying a negative w:id.

Docs (api.md, SKILL.md, EditResult, replace(), rewrite_paragraph()) state the amend-in-place contract. Tests cover the repro across pending/accepted/rejected views, single-run, mid-run, sibling w:t, two adjacent own insertions, a consumed leading insertion, a consumed insertion holding a foreign w:del, both text-box shapes, and the start-anchored case.
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