Editor rework: explicit modes, line navigation, durable accepts, undo/redo (spec 1.0+1.1) - #45
Open
Vadim1987 wants to merge 52 commits into
Open
Editor rework: explicit modes, line navigation, durable accepts, undo/redo (spec 1.0+1.1)#45Vadim1987 wants to merge 52 commits into
Vadim1987 wants to merge 52 commits into
Conversation
- Enter on an empty input opens the selected block for editing (was: silent no-op); submit on non-empty input is unchanged - bare Esc no longer loads the block and performs no action, which also defuses the device RMB->Esc binding silently replacing typed input - Shift+Esc discards the edit (clears input and loaded state); on an empty input it closes the buffer / leaves the editor - Ctrl+Y no longer deletes the block; deliberate deletion stays on Ctrl+Delete - Ctrl+O follows the require if present and no longer silently pops the buffer otherwise Stopgap ahead of the full editor rework (stage 1); matches the spec direction so none of it is throwaway.
Saving is automatic on block accept, so bare Ctrl+S in the editor state now does nothing; the key is reserved for the checkpoint (rework spec 2.6). Ctrl+Shift+S still finishes the edit, and leaving the editor is Shift+Esc.
Adds an 'editing' flag as a submode of 'edit', without touching the mode enum or its call sites: - entering: Enter opens the selected block, or typing into an empty input starts a new block - leaving: accepting a block (Enter), discarding (Shift+Esc), or clearing (Ctrl+W) - the statusline shows [NAV] / [EDIT] before the filename; reorder and search render as before - while editing, Enter on an emptied input is a no-op instead of reloading the block, and Shift+Esc first drops back to navigation; a second Shift+Esc leaves - accepting a block no longer auto-loads the next one into the input, so accept lands back in navigation
- bare Up/Down no longer move the buffer selection while editing; they stay in the input and only navigate blocks in navigation submode - files open at the first block/line instead of the last (BufferModel selection defaults to 1); saved-state restore is unchanged, leaving room for real position memory later - statusline drops the [NAV] label and shows a single yellow E while editing, before the filename
Stores the active buffer's selection and scroll offset by file name when the buffer is closed or popped, and restores them on open when still in range. In-memory for the session only; not persisted across restarts. Fresh files with no remembered position keep opening at the first block.
BufferView opens scrolled to the end of the content, which stopped matching the selection once files started opening at the first block: the buffer showed the end while the selection sat at the top, and the first arrow snapped the view back to the start. On open, follow the selection when no remembered position was restored, so the view and selection agree from the first frame.
Files now open with the first block selected, so typing straight into a fresh file and pressing Enter went down the replace path and destroyed the first block with no undo (compy-ide-typing-overwrites-first-block). Plain Enter now replaces only when a block was deliberately opened (buf.loaded is set by Enter / load_selection); text composed from scratch in navigation goes through the existing insert path and lands at the selection, pushing content down. The destructive replace is unreachable without opening a block first. Also pads the yellow editing marker with a leading space so it no longer collides with the filetype label (compy-ide-edit-indicator-collides-with-filetype).
The 'edit' mode splits into 'nav' and 'edit' as first- class states, replacing the boolean submode flag from the demo series. Transitions are declared in an explicit table in set_mode: nav reaches edit/reorder/search, the special modes and edit only return to nav. Rejected transitions no longer log or repaint. Behavioral deltas from the flag version: - reorder and search are enterable from nav only; while editing, Ctrl+M / Ctrl+F do nothing until the block is accepted or discarded - a freshly opened buffer always starts in nav - the statusline receives the real mode, the display mapping in _generate_status is gone is_normal() treats both nav and edit as normal-key modes, so the external is_normal_mode() contract is unchanged.
Code: - load_selection loses its dead additive parameter (Shift+Esc no longer additively loads) - mode transition table is a module constant, the exit ritual is a leave_edit() method (also fixes it calling clear on the input model instead of the controller) - add() hoisted to submit scope: the plain-Enter insert dispatch referenced it out of scope, crashing on composing a fresh line in plaintext/markdown buffers - note on device textinput-before-keypressed ordering at the nav->edit transition Tests (suite was green on dev, 26 red after the editor changes; every red encoded pre-rework semantics): - open helper presses Enter, not the now-inert Esc - open-at-top expectations; scroll suites establish the historical EOF position explicitly (walk + scroll_to) - arrows staying in the input while editing, Shift+Esc as discard (additive-load spec repurposed) - insertion suites select the last block before adding 685 successes / 0 failures / 0 errors.
Adds the line coordinate under the block selection, per the rework spec's line-wise navigation: - active_line: absolute source line, always inside the selected block; clamped on every selection mutation (set_selection, move_selection, warps) - get_selection_lines(): source-line span of the selected block; singleton for plaintext and for the phantom past-end position - move_line(dir): steps by one line, crossing block boundaries — down lands on the next block's first line, up on the previous block's last Block selection stays the source of truth for open, replace, and insert; the line is a refinement inside it. For plaintext, blocks are lines, so move_line degenerates to block movement. Covered by a new line-navigation spec group; suite at 692 green.
- draw_highlight paints the active line's wrapped rows a shade brighter (fg at .125 alpha) inside the block highlight; no new palette keys, tunable later - BufferView:follow_line() scrolls just enough to keep the active line's wrapped rows in range, the line-wise sibling of follow_selection - covered by a spec walking the line out of the viewport in both directions; suite at 693 green
Spec 2.2 key semantics for navigation: - bare Up/Down in nav move the active line via _move_line + follow_line; Ctrl+Up/Down stay block-wise (_move_sel), Ctrl+Home/End warp as before - open() places the input cursor on the row of the active line inside the block, not on line 1 Spec migration: - select_block helper and lua block walks use Ctrl arrows, since bare arrows are line-wise now - the selection-scroll suite asserts visibility invariants (active line in range, caps at the ends) instead of offsets hand-tuned to follow_selection; the old TODO-marked fudge constants are gone - warp-to-bottom asserts selection and visibility 693 green.
… guard Implements the amended 2.2/2.7 navigation split: - Ctrl+Alt+Up/Down peek-scrolls one line, Ctrl+Alt+ PageUp/Down one page; the selection and any open block stay put; identical in nav and edit - bare PageUp/PageDown in nav move the active line by a viewport page (_move_line_page); in editing they do nothing; Ctrl+PageUp/Down keep the warp scroll - typing after a peek returns the view to the active line (nav) or the open block (edit), per 2.2/2.4 - the textinput guard widens from Ctrl+Shift to any Ctrl/Alt chord: device chords leak glyphs (compy-input-quirks, quirk 3); only Shift composes Specs: scroll suites drive peeking through the C-M- chord (the mock already holds lalt); new group covers peek keeping the selection, the chord-glyph drop, page moves, and the view returning on input. 697 green.
Single-step block swap with the neighbor, reusing the reorder path (move + rechunk + save): written through immediately, the selection follows the moved block, the view follows the selection. Capped at the buffer edges, refused on readonly buffers and on the phantom past-the-end position. In editing, Alt+arrows keep passing through to the input widget's line swap, completing the amended 2.7 row: Alt moves — the block in nav, the line in edit. 698 green.
Leave-block gate, part one: - the block size limit is the input view height (cfg.input_max = 14), not the buffer viewport; a 14- line block passes, 15 is refused (spec 9.6). The refusal names the excess in the input's error line, shown until the first fixing keystroke (2.5); the refusing keypress is blocked from the widget so it does not clear its own message - opening a Lua block runs it through the pretty-printer (spec 9.4): a sloppy block is reshaped in the input and thereby dirty from birth (2.4), while the file stays untouched until acceptance; the printer's trailing-empty artifact is trimmed so a properly formatted block loads byte-identical and stays clean Specs: 14-passes/15-refused with the message, and auto-format on open leaving the file alone. 700 green.
Crossing the open block's edge with an arrow, or Ctrl+Up/Down from editing, now goes through the gate: - untouched (input equals the file's block text): leaves freely, writes nothing, and opens the neighbor — downward on its first line, upward on its last (2.4.1/2.4.4). The model's active line is synced to the crossed edge first, since during editing the cursor lives in the input, not the model - changed: accepted through the standard pipeline (validate, auto-format, re-chunk, size check) and written; editing flows on to the neighbor - invalid: the leave is refused, the parser's message shows per 2.5, the block stays; Shift+Esc remains the way out, writing nothing (2.4.3) The refusing keypress is blocked from the widget, as with the size limit. Bare arrows inside the block keep moving the input cursor; only the edge crossing leaves. Specs cover all three paths on pprint-stable fixtures (the suite caught that sierpinski is not: auto-format on open makes it dirty at birth, by design). Note: tests' savefile() reads destructively — read once. 703 green.
Ctrl+Alt+PgUp/PgDn is a four-key chord on the device keyboard (PgUp/PgDn live behind Fn), too much for a child's hand. Left/Right on the same Ctrl+Alt anchor peek a page up/down; the PgUp/PgDn variants stay for full-size keyboards. The whole peek now sits on one chord: vertical arrows by a line, horizontal by a page.
The last unimplemented spec chunk. Console side:
write_checkpoint copies the file to {name}.~save via
FS.cp, restore_checkpoint copies it back, modtimes via
FS.getInfo; the REPL gains revert(name) (default
main.lua), restoring without a prompt and returning
false when no checkpoint exists.
Editor side, proposal for Open item 1 — confirmation by
repeated press on the 2.5 message channel (Compy has no
dialog machinery, and the inert Esc finally earns a
role):
- Ctrl+K writes the checkpoint; if one exists, the
first press shows its date and asks, the second
overwrites, any other key (Esc included) cancels
- Ctrl+Shift+K always asks, showing the checkpoint's
and the file's dates; the second press restores and
reloads the open buffer from disk
- in editing, Ctrl+K accepts the open block first, so
the checkpoint reflects the screen; a refused block
aborts the checkpoint with its message showing
Flow covered by specs with a stubbed console; the FS
copy path itself needs the device/LÖVE pass. 708 green.
The remaining 2.4/2.9 tails: Mouse (2.9), geometry split from semantics: - BufferView:line_at(y) maps a pixel row through the scroll offset and wrap_reverse to a source line - BufferModel:block_at_line(ln) finds the owning block - EditorController:mouse_select(ln): in nav, selects the clicked block and line; while editing, a click inside the open block places the input cursor on that line, a click outside flows out when the block is untouched, and asks to accept or discard when dirty - buffer-area clicks route editor:mousepressed via the console; input-strip clicks keep going to the widget Cursor-to-error (2.4.3): an eval refusal moves the input cursor to the first error's line before the message shows. _handle_submit now reports acceptance, and submit() blocks the widget passthrough on refusal — the same self-clearing-message bug the size limit had. Refusal frame: render_error draws a line rectangle in the error color around the message (the message replaces the input until the first fixing keystroke, so framing it is framing the refusal). Sound stays out pending an actual sound asset. 714 green; line_at and the frame are device-check items.
The Keys tables in README.md and doc/EDITOR.md were stale and mutually inconsistent. Correct them to the behavior verified against the shipped editor build 1b86c90: toggle edit/run = Ctrl+T (was F8/F9), stop project = Ctrl+S (was Ctrl+Shift+S), quit project = Ctrl+Q (EDITOR had Ctrl+Shift+Q), leave editor = Shift+Esc / Ctrl+Shift+S, drop the stale editor Esc/Ctrl+S/Ctrl+Y rows, and add the input clipboard/selection keys. These tables document commit 1b86c90. The editor-stage1 (2eed100) keymap update follows as a separate change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0177PwS4Xws9YFAGqJMADY5c
Retarget the editor-mode Keys tables in README.md and doc/EDITOR.md to the editor rework at 2eed100: line-wise navigation with Ctrl+arrow block jumps, Alt+arrows to move a block, Ctrl+Alt peek scrolling, block-level Copy/Cut/Paste, and Ctrl+K / Ctrl+Shift+K checkpoint/restore. Platform, input, and search bindings are unchanged from 1b86c90 and retained. Verified: every key literal in editorController.lua at 2eed100 maps to a documented row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0177PwS4Xws9YFAGqJMADY5c
The agreed 2.7 binding was never actually implemented: the spec and the amendments say Ctrl+J (F12 died with the F-row, which Android swallows without root), but the code still had follow_require on Ctrl+O. Ctrl+O now does nothing and stays reserved for a conventional 'open file', so children carry a habit that works in their next editor. The keymap tables inherited from dsent/dev are retargeted with it — they documented the code correctly, it was the code that lagged the spec. 715 green.
One sound for all refusals, as agreed: knock from the existing util.audio library — no new asset, and the IDE already ships the sound bank for projects. EditorController:refuse(msg) knocks and optionally shows the message; every refusal now goes through it: - a block the parser or the size check rejects - Ctrl+Shift+K with no checkpoint to restore - a mouse click away from a changed block - Ctrl+J with no require to follow (was silent) - an arrow at the end of the file (was silent) knock is deliberately neutral: hitting the end of a file is normal navigation, not a mistake, and it will sound dozens of times per lesson — 'wrong' would be judging the child, and anything musical would grate. util.audio is required inside refuse(), not at the top: it builds its sources on load and needs love.audio ready, while the editor controller is required before love is mocked in tests. The mock gains a love.audio stub that records what was played. 716 green; the mute-device behavior stays a device check.
Auditing the sound against the agreed list found four gaps — the reported coverage was wider than the code: - search text matching nothing never knocked at all, though it is on the list (SearchController checks resultset, not get_results(), which is only the visible slice, and only knocks on the transition to empty so every further keystroke stays quiet) - Ctrl+Up/Down block jumps at the file's edges - PageUp/PageDown when not a single line can move - Alt+Up/Down moving a block past an edge, and on a readonly buffer - leaving a clean block with nowhere further to go Spec drives each case through the keyboard and asserts the sound, including that a working move stays silent; the phantom line past the last block is a legitimate stop, so the first arrow there is a move, not a refusal. 718 green.
Per 2.7 the key means 'delete the block' in navigation and 'delete the next word' while editing, but the handler was not gated on the mode: Ctrl+Delete inside an open block dropped the whole block instead of a word — destructive, with no undo. 719 green.
Per 2.2 the paragraph-jump convention is asymmetric: Ctrl+Down goes to the next block's first line, while Ctrl+Up first returns to the current block's first line and only jumps to the previous block when the active line is already there. The controller called _move_sel, which always changed the block, so Ctrl+Up skipped past the head of a long block the reader was in the middle of. BufferModel:jump_block(dir) implements the rule and the controller gains _jump_block for the nav binding; _move_sel keeps serving reorder, the Ctrl+Home/End warps and the acceptance tails untouched. 720 green.
Enter moved the selection down by the number of blocks the acceptance produced, a leftover of the pre-rework 'submit and walk on' flow. Per 2.4.4 only an arrow transition opens a neighbor; acceptance in place stays and scrolls back to the block. The gate depended on that drift to land on the next block, so it now computes the target itself: sel + the accepted block count downwards (an acceptance may split one block into several), sel - 1 upwards, with the active line on the near edge — first line downwards, last upwards. Nowhere to go refuses with the knock instead of silently staying open. Four specs encoded the old drift; migrated, plus one new for the in-place case. 721 green.
Clicking away from a changed block refused with 'accept or discard first', but 2.9 says such a click follows 2.4 — the block is accepted and written, exactly as an arrow transition would. The mouse could not do that: the pipeline lived in closures inside _normal_mode_keys, reachable only from a keypress. The pipeline is now controller methods — _size_limit, _first_oversized, _reject_oversized and accept_block — so the keyboard gate and the mouse share one path, and the accepted block count moves onto the controller where the gate reads it. _handle_submit propagates the handler's verdict instead of always reporting success, which is what lets a refusal block the widget. That propagation exposed a real bug the extraction would otherwise have hidden: the Ctrl+Enter branch ignored the verdict, so an oversized-block refusal let Enter through to the widget, which cleared the message it had just raised. The size limit only looked enforced because reject_oversized reached into the closure to stop the key itself. 722 green.
Per 2.1 a printable character starts editing where the reader is: the active line's block opens and a blank line appears at that line, pushing the rest down, to type into. Only on a blank line — an empty block, an empty file — does the text compose a new block instead. The editor did the latter everywhere: typing anywhere opened an empty input and acceptance inserted a new block before the selection, so a child standing inside a function and typing got a new block above it rather than a line in it. That behavior was the demo fix for typing destroying the first block; the spec reaches the same safety a better way. open() is lifted to EditorController:open_block(), since typing needs the same load-and-format path the keyboard uses; the key handler now delegates to it. Row placement inherits the format-on-open caveat already there: a reshaped block can shift the line, so the insert clamps to the input's length. 725 green.
Per 2.7 Ctrl+Enter means 'a new empty block below the current one, open it' in navigation and 'accept the block, back to navigation' while editing; Ctrl+Shift+Enter opens one above. The key did neither: it inserted whatever the input held at the selection. The new block appears on acceptance rather than as a blank line in the file, since blank lines are structural (2.1) and re-chunk regenerates them — Ctrl+Enter just composes at that spot, which is what 'open an empty block' means in this model. The insertion suite composed text straight into the input while the editor stayed in navigation, a state 2.1 does not allow (text in the input means editing). The session helper now enters editing as typing would. 728 green. NB the table also puts a line break on plain Enter while editing, which would move acceptance onto Ctrl+Enter alone. Enter still accepts here — that is a product decision, not a bug, and it is raised separately.
Per 2.7 Home and End alone go to the file's first and last line while navigating, and Ctrl+Home/End belong to the input widget (block start/end) while editing. The bindings were inverted: the warp sat on Ctrl+Home/End in navigation, and bare Home/End fell through to the widget, which moved a cursor nobody was looking at. Specs that warped through Ctrl now use the bare keys, plus one covering both halves of the rule. 730 green.
The presses count LOVE passes was accepted and dropped; a double click now opens the block the first click selected, the mouse counterpart of Enter. 732 green.
1.4 asks for an actionable refusal — 'Too many lines in
a block. Remove N lines to save or press Shift+Esc to
cancel' — while the message reported the measurement
('block is 15 lines, the limit is 14'), leaving the
child to do the subtraction.
732 green.
The legacy newline() handler put an empty block into the file whenever Enter arrived with Shift or Ctrl held and the input empty, and it ran before the submit dispatch. Once Ctrl+Enter became 'open a fresh block' (2.7), both fired: the file silently gained a blank line, saved, and only then did the editor open a block to compose in. newline() is what the new binding replaces, so it goes. Shift+Enter keeps reaching the widget, where it is the line break; the empty block now appears where 2.1 says it should — from the re-chunk on acceptance. BufferModel:insert_newline stays, still covered by the buffer specs and reachable for reorder work. 733 green.
Spec 2.7 puts 'delete the previous word' on Ctrl+Backspace with Ctrl+W as its synonym, deliberately displacing the conventional close binding. Neither existed: the editor used Ctrl+W to leave the block — the very binding the spec displaces — and word-wise deletion was absent from the widget entirely, which only had per-character backspace, delete and Ctrl+Y for a line. UserInputModel:backspace_word() eats the word before the cursor together with the whitespace in front of it, the readline behavior both bindings come from, and falls back to backspace() at a line's start so the line join still works. The widget binds both keys; the editor's Ctrl+W handler is gone, and Shift+Esc remains the way out of a block, as 2.3 says. NB this touches the input widget, which is Gleb's area: the change is additive (a new model method plus two bindings in removers()), it does not alter how the controller dispatches or how existing keys behave. Worth a heads-up before the compy-toys#77 routing migration. 734 green.
The first half of the two-level model: EditHistory, a
32-step ring of {text, cursor} snapshots taken before
each mutation. Consecutive same-kind edits continuing at
the expected cursor coalesce, and a typed whitespace
starts a new step — so Ctrl+Z removes the word just
typed, not one letter, without any wall-clock timers
(deterministic, hence testable).
The history is born with the block: set_text (how every
block opens) resets it, so it never leaks across blocks
or into the file. All six mutators record: add_text
(insert/paste), backspace, delete, backspace_word,
delete_line, line_feed.
Ctrl+Z / Ctrl+Y while editing work this history via
undo_edit/redo_edit; empty history knocks. Navigation
undo is the block level, coming next.
735 green.
The second half of the model: a 32-step ring on the buffer recording every file write as a trimmed diff — the common prefix and suffix of the file before/after are cut, leaving exactly the affected line range, whatever the operation. One recording wrapper (record_write) serves all five write sites: acceptance, insertion, deletion/cut, the Alt block move and the reorder move. Applying a step is a splice plus the same re-chunk and save every write takes; selection restores to the step's remembered side. Ctrl+Z / Ctrl+Y in navigation walk this history; the history lives on the buffer, so it survives a follow- require round trip and dies with the file. Empty history, and any attempt on a readonly buffer, knock. A new write kills the redo tail. Found and fixed by the specs on the way: the dispatch used 'redo and buf:redo() or buf:undo()', and since redo() legitimately returns nil when its stack is empty, the chain fell through to undo — a redo past the tail silently undid instead of refusing. The same trap sat in the edit-level branch. Both are honest ifs now. 740 green.
Shift+Esc was the editor's only irreversible action: a changed block discarded on one keypress, the typed text gone for good. Two agreed guards close that. The ask: discarding a changed block now takes a second Shift+Esc, the same repeated-press confirmation the checkpoints use; any other key cancels. A clean block still leaves instantly. The pair: a confirmed discard of a draft that parses records two history steps — original->draft and draft->original. One Ctrl+Z in navigation puts the discarded text into the file, another takes it back out; redo mirrors. No flags, no block reopening, just two ordinary steps. A draft that does not parse records nothing: writing it would flip the buffer read-only (the 1.0 invariant), so the ask is its only guard. Specs migrated where they discarded dirty blocks with a single press; new specs cover the ask, the cancel, the pair round-trip and the broken-draft case. 743 green.
The last pieces of the layout the spec tied to undo: Bare Delete drops the block in navigation — 2.7 gated it on Undo/Redo existing, and it does now; Ctrl+Delete stays as the synonym. While editing, Delete remains the widget's character delete. Ctrl+Y in the input widget was delete-line; redo displaces it, per the agreed layout. Delete-line keeps no key for now (Home, Shift+End, Backspace covers it) until one is picked. A checkpoint restore or revert() rebuilds the buffer through reload_active -> open, so the history is born empty — the new-baseline boundary costs nothing; a spec pins it so a future in-place reload cannot silently lose the boundary. 745 green.
The boundary it was written for holds structurally on every path: Ctrl+Shift+K reloads the buffer through reload_active (fresh BufferModel, empty history) and revert() rewrites the file on disk while no buffer is open — the next edit() reads it fresh. A method nobody can call is a promise nobody keeps; the specs covering the restore boundary stay green without it. 745 green.
Vadim1987
force-pushed
the
deliver/editor-stage1
branch
from
July 21, 2026 08:54
082e2f1 to
c1184a8
Compare
Author
|
Depends on #46 (the FS durability API, split out at dsent's request; this branch carries the same API commit so it stays self-contained). |
FS.fsync(path) and FS.sync() over a pcall-guarded LuaJIT FFI cdef (open/fsync/close/sync), degrading to a no-op where the syscalls are unavailable. FS.write stays async by contract — bulk deploy/clone and the user-facing writefile must not stall on the card — with durability as an explicit opt-in for callers that promise it. Vendored nativefs is untouched. Interface only; the consumers (the editor's per-accept fsync, the app's lifecycle flushes) live in the editor series. Split from c5866b2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011WEJighCCuSFparAVqjsWH (cherry picked from commit e5e31ff)
Consumers of the FS durability API at the app lifecycle level: FS.sync() on love.quit, and on focus(false) / visible(false) — previously skipped handlers — so a child leaving the app flushes pending writes. One syscall per event; a force-stop mid-edit is covered by the editor's per-accept fsync, not by these nets. Split from c5866b2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011WEJighCCuSFparAVqjsWH
Editor half of the durable-writes work (the FS API it consumes is the preceding commit, split out as shared platform code): - controller/consoleController.lua (:edit): the editor's save callback fsyncs the file after a successful write — the accept path only. - controller/editorController.lua: :save returns ok and err; accept_block refuses, keeping the block open with a statusline error, when the write fails, so a failed save no longer reads as accepted. Verified on device: an FFI probe returns fsync=0 for a file on the SD card, and an accepted edit followed immediately by a force-stop and relaunch survives across repeated runs. Refs: compy-ide-edit-flush-loss Split from c5866b2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011WEJighCCuSFparAVqjsWH
Inside a multi-line block End jumped to the end of the whole input and Home to its start, so nothing reached the ends of the current line. The intended layout is the opposite of what was bound: bare Home/End are the line-scoped pair while editing, and the jump over the whole block belongs on Ctrl+Home/End. The line-scoped jumps were instead sitting on Alt, which is not a documented binding at all. Route bare Home/End to jump_line_start/jump_line_end and give the whole-block jump to Ctrl+Home/End. UserInputController is shared with the console, so its input line is corrected by the same change. Navigation Home/End in the editor (first/last block) is handled in the editor controller and is untouched. Verified on a 4-line block opened with the cursor on row 2: End -> 2:15, Ctrl+End -> 4:4, Home -> 4:1, Ctrl+Home -> 1:1. Refs: compy-ide-home-end-line-scope Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011WEJighCCuSFparAVqjsWH
Typing straight after Ctrl+Delete went into a detached input: the status bar showed a cursor like 0:32 with no block association, nothing appeared on screen, and Enter never accepted it into the file, so the keystrokes were lost outright. A navigation press between the delete and the typing hid it by re-anchoring the editor. Four things lined up: - delete_selected_text() never re-clamped the active line after the content shrank under the selection, leaving it stale. - Once the last block is gone the selection sits on the trailing gap, where the selected block reads as Empty. - start_typing() returned early for an empty block, setting edit mode without opening it, so the input belonged to no block. - The row it derives from the stale active line could reach 0, and set_cursor assigns straight to self.cursor without the clamping that move_cursor applies. Re-clamp the active line after a deletion, open the block for the empty and gap cases so the input is always anchored, and clamp the row in start_typing and open_block. Plain buffers keep their previous path. Verified on device: deleting the trailing block and typing at once lands the text (status B6 L9, cursor 1:8, text on screen), and the same holds when the deleted block is a non-empty one mid-file. Refs: compy-ide-delete-then-type-phantom Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011WEJighCCuSFparAVqjsWH
UserInputModel is the whole platform's input — the console, project inputs the games read, and search all build on it. The 1.1 work changed it as if it were the editor's own, and every surface felt it: - Ctrl+Backspace/Ctrl+W ate a word in every input - Ctrl+Y stopped deleting the line in the console - the edit history recorded every mutation of every input, cloning the text on each keystroke - the refusal frame drew around every error message, console errors included — which is what moved the screenshots in the user-guide generator An explicit editing flag on the model (off by default, the editor model opts in) now scopes all of it: word deletion and the frame gate on it, recording returns early without it, and the widget's Ctrl+Y delete-line binding is restored — in the editor it is shadowed by the controller's redo dispatch, which runs first and blocks the key. Regression specs pin the contract from the console's side: a flagless model records nothing, Ctrl+Backspace deletes one character, Ctrl+W does nothing, Ctrl+Y deletes the line. 748 green.
The mode is evident from the input strip itself: editing shows the strip with a cursor and the loaded-block highlight, navigation shows an empty strip and the active line. The tag added nothing — and collided with the left label: the filename block is right-anchored (its x is computed back from the right cluster), the 'lua' label left-anchored, and the gap between them is whatever happens to remain, so prepending ' E ' to the name pushed it left into the label on longer filenames. Spec criterion 9.9 amended accordingly. 748 green.
Key repeat on the device fires the invoking chord many times, and dialogs that confirmed on a repeated press were a hair-trigger: a held Shift+Esc could blow through the discard, a held Ctrl+Shift+K through the restore. The scheme is now repeat-proof by construction, as agreed: the confirming key differs from the invoking one, so repeats land on the idempotent cancel. - Every dialog (discard, checkpoint overwrite, restore) confirms on Enter or Space and cancels on anything else — the invoking chord and Esc included. Held, it oscillates ask/cancel and never fires. - One executor (EditorController:_confirm) runs the confirmed action; the key handlers only raise dialogs. - Space arrives as textinput on the device, so the dialog eats the glyph there and swallows the paired event on either delivery order. - A plain error message (a refusal) closes on Enter, Esc or Shift+Esc without re-submitting or leaving the block, matching the REPL's Enter; printables keep closing it via textinput as before. 749 green.
… to reorder only As decided after the device pass: Alt+arrows moving blocks in navigation confused more than it helped (the blank-line churn around every swap), and the line swap while editing likewise. Alt now means one thing in both modes — the peek: Alt+Up/Down by a line, PageUp/PageDown (and Left/Right, the device shortcut) by a page, and Alt+Home/End to the file's edges, all without moving the selection. Ctrl+Alt keeps working as a synonym. Blocks move through the reorder mode (Ctrl+M) alone; _move_block is gone with its binding. The widget's line swap stays for the console, unreachable in the editor since the Alt branch consumes the chord. 750 green.
Ctrl+J into a file and Shift+Esc back parked the view at the file's end; only an arrow press snapped it to where you were. The buffer kept its position all along — open() places the view at the end range and pop_buffer never asked it to follow, unlike the file-open path. It follows now; the probe spec jumps from the middle of a 30-block file, where the end-parking and the stored line genuinely diverge. 752 green.
Deleting a block wrote it to the system clipboard — cut semantics on a delete key. On the device that clobbered whatever the child had copied, and every clipboard write pops the Android share overlay on top of the screen, so a plain deletion produced both a surprise and a popup. Delete now just deletes (undoable since 1.1, which is what gated it); Ctrl+X remains copy + delete. The spec holds a 'precious' clipboard across a deletion and sees it survive. 753 green.
…el [Esc] One uniform trailer on all three dialogs (discard, checkpoint overwrite, restore), in the agreed format. 753 green.
Vadim1987
force-pushed
the
deliver/editor-stage1
branch
from
July 24, 2026 13:24
c1184a8 to
16eb33d
Compare
dsent
added a commit
to dsent/compy-ide
that referenced
this pull request
Jul 27, 2026
…rable accepts, undo/redo (spec 1.0+1.1)
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.
This is the editor rework — releases 1.0 and 1.1 of the behavior spec. 44 commits on top of dev@945a5d1
What it does
editflag; thestatusline always shows the mode, Esc semantics per spec.
bare Home/End to the file edges, page moves, peeking on Ctrl+Alt.
accept/refuse; acceptance writes to the file immediately and stays on
the block. Typing in navigation opens the block at the active line.
(dsent's work) — an accepted edit survives a force-stop, which
matters on the device's exfat/FUSE storage.
{file}.~save, Ctrl+Shift+K restoreswith a repeated-press confirmation,
revert()from the REPL.open; unparseable files open read-only.
click opens, a click away goes through the same acceptance gate.
open block; a 32-step ring of file operations in navigation
(accept, move, delete, insert, discard). A discarded-but-valid draft
is recoverable; checkpoint restore is a boundary. Shift+Esc on a
changed block asks first.
frame around the message, cursor to the first error.
frame) is scoped behind an explicit flag on the shared input widget —
the console, project inputs and search keep the plain widget,
regression-pinned from their side.
Review notes
doc/EDITOR.md); Ctrl+W now deletes the previous word (readline),
leaving a block is Shift+Esc.
compynamespace compy-toys/compy#77 inputrouting and is intentionally absent here.
BufferModel:_text_changecallsrechunk()twice back to back —looks like a copy-paste; happy to fix in a follow-up if you confirm
it's not intentional.
live FS, chord glyph suppression, sound, undo paths); that pass is
what surfaced the fsync work.
If this is easier to review split by stage (modes / navigation / gate
/ checkpoints / mouse / undo), say so and I'll break it up — the
history is linear and cuts cleanly.