Skip to content

Pull block placement and caret rules out of the views (#116) - #128

Merged
shovon merged 1 commit into
mainfrom
worktree-issue-116
Oct 28, 2026
Merged

shovon merged 1 commit into
mainfrom
worktree-issue-116

Conversation

@shovon

@shovon shovon commented Oct 28, 2026

Copy link
Copy Markdown
Owner

Closes #116.

What changed

Where a new block goes (app/src/renderer/project/placement/placement.ts, new, shared by both views):

  • placeNew(root, after): beneath after when there is one to go beneath; otherwise last under the zoomed root, or at the end of the page. It returns { after, indent }, where indent is the second step a bare root takes (written beside the root, then filed under it). PageView's box at the end of a zoom and ProjectView's images each worked this out on their own before. Now both call this.
  • imageTarget(...): the page a paste or drop with no box open lands on (dropped on, else showing, else today), and the zoomed block it goes under. That's only on the zoomed page, matched by pageKey.

The editing keys (app/src/renderer/project/PageView/editing/editing.ts, new):

  • afterDelete(blocks, block, { root, reference }): Backspace in an emptied block. It returns null for a block with children. Otherwise it returns where the caret goes, whether the page's end box opens, and whether the zoom moves out.
  • afterCancel(blocks, after, root): Backspace in an empty new box. Where the caret goes back to, or null to stay open.
  • newBlock(blocks, { root, after, text, then }): what a key in a new box does. nothing, pop (Enter on an empty child box) or write with its placement and move.
  • movable(blocks, id, by, root): whether Tab or Shift+Tab may move a block, given the zoom.

The views apply those decisions. commitDelete, commitIndent, commitNew and cancelNew in PageView.tsx, and placeImages in ProjectView.tsx, keep what is theirs: making the id, asking main to write, setting state, and the comments on why each rule is what it is.

For the reviewer

  • No behaviour change is intended. One comparison moves from object identity to id: found.parent === root becomes parent?.id === root.id. Both come from the same drawn page, and a block id is unique on it.
  • Placement sits at project/, not inside either view's folder, because both views use it (per "Encapsulate by folder"). The editing rules are PageView's alone, so they nest under it.
  • Collapsed subtrees (not built yet) will change afterDelete and afterCancel, which both take the lastLeaf walk. That's where the "uncollapse or skip" switch goes when they land.

Tests

  • placement.test.ts (5 tests): beneath a given block; the end of the page; last under a zoomed root with children, and the bare-root second step; an image's page for a drop, the page showing, and today; under the zoomed block only on the zoomed page, found by key in other case, and not for a zoom the page doesn't have.

  • editing.test.ts (13 tests): every Backspace case in commitDelete's comment (a block with children, the block above in reading order, a first child, the page's first block, the only block in a page and in a cut, deleting the zoomed root); backing out of a new box, and the cases where it stays open; Enter popping out a level, then making a gap at the top level, and not popping past a zoomed root; Shift+Tab not leaving a zoom; the bare-root second step; Tab in a new box; which blocks may move under a zoom.

  • End to end, in a scratch instance, with its own --user-data-dir and a scratch project. CDP's simulated input never reached the dev window (docked DevTools seems to take it), so the editor's own key and click handlers were triggered with DOM events. The run:

    • typed one, Enter, two, Tab: two was filed under one, and the box reopened on it with the caret at the end
    • Enter, three, Enter, then Enter on the empty box: the box popped out, and four landed at the top level
    • emptying three and pressing Backspace deleted it, and the box opened on two with the caret at its end
    • Backspace in a new box beneath four went back to four with the caret at its end
    • Shift+Tab on two brought it out beneath one

    The zoom and image paths weren't driven in the app; the unit tests above cover them.

  • npm test (196 tests across all workspaces), npm run typecheck and npm run lint pass.

🤖 Generated with Claude Code

The rules for where a block goes and where the caret lands after a key were
written inside handlers that also set React state, so only stories covered
them: the Backspace rules under nesting, Enter on an empty child box, Tab and
Shift+Tab under a zoom, and where a pasted image lands. ProjectView's image
paste also had its own copy of the "write beside a bare root, then file it
under" step that PageView's box at the end of a zoom takes.

placement/placement.ts, beside both views, now holds where a new block goes:
placeNew(root, after) for the box and the images alike, and imageTarget for
which page an image lands on and when it goes under the zoomed block.
PageView/editing/editing.ts holds the key rules: afterDelete (where the caret
goes, whether the page's end box opens, and whether the zoom moves out),
afterCancel, newBlock (write, pop out a level, or nothing), and movable.
Each is pure over the blocks as drawn and returns a decision; the views make
the id, ask main to write, set state, and keep the comments on why each rule
is what it is.

No behaviour changes. One comparison moves from object identity to id
(a block's parent against the zoomed root), which is the same thing since a
block id is unique on a page.

The new tests cover each Backspace case in commitDelete's comment, backing
out of a new box, Enter popping out level by level and not past a zoomed
root, Shift+Tab not leaving a zoom, the bare-root second step, Tab in a new
box, which blocks may move under a zoom, and where an image goes for a drop,
the page showing, today, and a zoomed page found by key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shovon
shovon merged commit 21ba332 into main Oct 28, 2026
1 check passed
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.

Pull block placement and caret rules out of PageView and ProjectView

1 participant