Repository navigation
Pull block placement and caret rules out of the views (#116) - #128
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #116.
What changed
Where a new block goes (
app/src/renderer/project/placement/placement.ts, new, shared by both views):placeNew(root, after): beneathafterwhen there is one to go beneath; otherwise last under the zoomed root, or at the end of the page. It returns{ after, indent }, whereindentis the second step a bare root takes (written beside the root, then filed under it).PageView's box at the end of a zoom andProjectView'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 bypageKey.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) orwritewith 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,commitNewandcancelNewinPageView.tsx, andplaceImagesinProjectView.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
found.parent === rootbecomesparent?.id === root.id. Both come from the same drawn page, and a block id is unique on it.project/, not inside either view's folder, because both views use it (per "Encapsulate by folder"). The editing rules arePageView's alone, so they nest under it.afterDeleteandafterCancel, which both take thelastLeafwalk. 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 incommitDelete'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-dirand 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:one, Enter,two, Tab:twowas filed underone, and the box reopened on it with the caret at the endthree, Enter, then Enter on the empty box: the box popped out, andfourlanded at the top levelthreeand pressing Backspace deleted it, and the box opened ontwowith the caret at its endfourwent back tofourwith the caret at its endtwobrought it out beneathoneThe 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 typecheckandnpm run lintpass.🤖 Generated with Claude Code