Rename a tag in every note (bulk-edit job) (#101) #216

Closed
kayg wants to merge 3 commits from issue/101-rename-tag-2 into dev
Owner

Closes #101. Replaces #201 (the forge ignores pushes to an open PR); adds the in-place sidebar rename found in my own screenshots.

What it does

  • notes.rename-tag {from, to}: renames a tag, and every tag nested under it (#work/q4 → #job/q4), in every note, including tags: in the properties. It runs as one #9 bulk-edit job: the plan is stored first, each note's room applies only the edits to the name (so they merge with anyone typing), it picks up where it stopped after a crash, and notes.undo takes it all back as one step. The request answers within 5 s; a bigger job keeps going in the background and notes.job reports when it's done.
  • notes.tag-preview {tag}: how many notes carry the tag. It only reads the note list.
  • Never touched: fenced code (also inside quotes and lists), inline code, link targets, reference definitions, web addresses and <…>. The note list, search and the rename all use the same scanner (tags/scan), so the sidebar count is exactly what a rename changes.
  • Web: the sidebar row takes the new name in place at once (the list renames its own entries; failure or Undo puts them back). Open the field with Rename… in the tag's sidebar menu, or by double-clicking the row. While you type, the row shows "N notes" where its count was, so nothing shifts. Enter starts the rename, and a single toast offers Undo for every note. The row keeps its place, and an open tag list switches to the new name.

Bulk-job changes made along the way

  • Each batch now saves only its own results to a new job_steps table (history migration 4). Before, the whole plan was rewritten after every batch, which for 20k steps meant several MB per batch going through the same writer as typing.
  • While a job runs, notes.job answers from memory. Before, every poll parsed the full plan.
  • Rooms opened only to read a note during planning now close right away (Cmd::Rest), so planning thousands of notes no longer keeps thousands of rooms open.

InlineRename fixes (shared component)

  • It saves once. Before, Enter saved and then the blur saved again, and Escape could still save.
  • A live update to the list behind the field no longer resets what you're typing.
  • The field is as wide as its text, so the first letter no longer scrolls out of view.

Tests

  • Unit tests for edge cases: #tag inside words, escaped, bold, in headings, in code, and in link text vs link targets.
  • Property tests: random notes built from pieces with known right answers (catches any rewrite inside code, link targets or URLs; I confirmed it fails against a scanner with code and link skipping removed), and rename-and-back on arbitrary text.
  • Notes integration tests: rename + undo, typing during a rename is kept, a 600-note rename never stalls a typing client, and an in-process crash mid-job.
  • notes_crash.rs: the real binary is killed with SIGKILL 4× during a 400-note rename. Each tag is renamed exactly once, and code and link targets keep #hub.
  • e2e in Chromium and WebKit: rename with the count and live editor/sidebar, Undo across notes, a change from another device while the name is being typed, and Escape.

Perf numbers and screenshots are on #101.

🤖 Generated with Claude Code

https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T

Closes #101. Replaces #201 (the forge ignores pushes to an open PR); adds the in-place sidebar rename found in my own screenshots. **What it does** - `notes.rename-tag {from, to}`: renames a tag, and every tag nested under it (`#work/q4` → `#job/q4`), in every note, including `tags:` in the properties. It runs as one #9 bulk-edit job: the plan is stored first, each note's room applies only the edits to the name (so they merge with anyone typing), it picks up where it stopped after a crash, and `notes.undo` takes it all back as one step. The request answers within 5 s; a bigger job keeps going in the background and `notes.job` reports when it's done. - `notes.tag-preview {tag}`: how many notes carry the tag. It only reads the note list. - Never touched: fenced code (also inside quotes and lists), inline code, link targets, reference definitions, web addresses and `<…>`. The note list, search and the rename all use the same scanner (`tags/scan`), so the sidebar count is exactly what a rename changes. - Web: the sidebar row takes the new name in place at once (the list renames its own entries; failure or Undo puts them back). Open the field with Rename… in the tag's sidebar menu, or by double-clicking the row. While you type, the row shows "N notes" where its count was, so nothing shifts. Enter starts the rename, and a single toast offers Undo for every note. The row keeps its place, and an open tag list switches to the new name. **Bulk-job changes made along the way** - Each batch now saves only its own results to a new `job_steps` table (history migration 4). Before, the whole plan was rewritten after every batch, which for 20k steps meant several MB per batch going through the same writer as typing. - While a job runs, `notes.job` answers from memory. Before, every poll parsed the full plan. - Rooms opened only to read a note during planning now close right away (`Cmd::Rest`), so planning thousands of notes no longer keeps thousands of rooms open. **InlineRename fixes (shared component)** - It saves once. Before, Enter saved and then the blur saved again, and Escape could still save. - A live update to the list behind the field no longer resets what you're typing. - The field is as wide as its text, so the first letter no longer scrolls out of view. **Tests** - Unit tests for edge cases: `#tag` inside words, escaped, bold, in headings, in code, and in link text vs link targets. - Property tests: random notes built from pieces with known right answers (catches any rewrite inside code, link targets or URLs; I confirmed it fails against a scanner with code and link skipping removed), and rename-and-back on arbitrary text. - Notes integration tests: rename + undo, typing during a rename is kept, a 600-note rename never stalls a typing client, and an in-process crash mid-job. - `notes_crash.rs`: the real binary is killed with SIGKILL 4× during a 400-note rename. Each tag is renamed exactly once, and code and link targets keep `#hub`. - e2e in Chromium and WebKit: rename with the count and live editor/sidebar, Undo across notes, a change from another device while the name is being typed, and Escape. Perf numbers and screenshots are on #101. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
`notes.rename-tag {from, to}` renames a tag, and every tag nested under
it, in every note: one bulk-edit job (planned first, applied through each
note's room, resumable after a crash, undone as one step).
`notes.tag-preview {tag}` says how many notes carry it, from the note
list alone.

- tags/scan, tags/front: one reader of tags for the list, search and
  renames. Code blocks (also in quotes and lists), inline code, link
  targets, reference definitions, web addresses and <...> are never
  tags, so a rename never touches them.
- tags/rewrite: minimal edits (the name only), checked by property tests
  built from pieces with known answers, and by rename-and-back on any
  text.
- Bulk jobs save each batch's results in a small job_steps table instead
  of rewriting the whole plan, and answer notes.job from memory while
  they run (a 20k-step plan is several MB).
- A job that only reads a note lets its room close at once (Cmd::Rest),
  so planning thousands of notes keeps few rooms open.
- The request answers within 5 s; a bigger job goes on in the
  background and notes.job tells when it is done.
- Tests: rename + undo, typing during a rename, a big rename never
  stalls typing, a crash mid-job, and SIGKILL of the real binary again
  and again mid-rename (each tag renamed exactly once). Benchmark:
  examples/tag_bench on the perf fixture Homes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
Web: rename a tag from the sidebar, with a count and Undo (#101)
Some checks failed
CI / rust (pull_request) Successful in 2m8s
CI / web (pull_request) Failing after 8m39s
ca7633bae7
Rename… in a tag's menu (or a double-click on its row) opens the name in
place; while you type, the row says how many notes will change, where
its count was, so nothing moves. Enter renames it in every note on the
server; the row keeps its spot, an open list follows the new name, and
one toast offers Undo for every note at once. A long rename says it's
working first.

InlineRename fixes found on the way: it saves once (Enter, then the
field's blur on the way out, saved twice; Escape could still save), a
live change to the list behind it no longer resets what's being typed,
and the field is as wide as its text instead of scrolling the first
letter away. Double-click on a sidebar tag reaches the row again (the
row holds the pointer to drag, so the name never saw it).

e2e (Chromium and WebKit): rename with count and live editor/sidebar,
Undo across notes, a change from another device while typing the name,
and Escape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
Web: a renamed tag changes name in place in the sidebar (#101)
Some checks failed
CI / rust (pull_request) Successful in 1m14s
CI / web (pull_request) Failing after 4m49s
4fb7427004
The list renames the tag (and the tags under it) in its own entries at
once, so the row keeps its spot with its new name instead of vanishing
and coming back at the end until the server's changes arrive. A failed
rename, or Undo, puts the entries back. The count preview appears
without a fade (it follows typing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
Author
Owner

Superseded by the -3 branch PR (merge + drag-to-merge, rebased on dev); closing.

Superseded by the -3 branch PR (merge + drag-to-merge, rebased on dev); closing.
kayg closed this pull request 2026-10-14 10:19:14 +00:00
Some checks failed
CI / rust (pull_request) Successful in 1m14s
CI / web (pull_request) Failing after 4m49s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal!216
No description provided.