Skip to content

Native /edit: a small, safe editor for one text file (#402) - #414

Merged
raiseCatError merged 2 commits into
devfrom
feature/v021-edit
Oct 18, 2026
Merged

raiseCatError merged 2 commits into
devfrom
feature/v021-edit

Conversation

@raiseCatError

Copy link
Copy Markdown
Owner

Stacked on #413 (/view) → #412 → #411. Merge in order.

What

/edit <path>, or e in /view on a file, opens one small text file in a full-height panel. It owns keys only while open; the composer draft and the shell are untouched.

  • Editing: typing; Enter and Tab insert a newline and a tab; caret by character, word, line and page; Shift selection; select-all.
  • Undo and redo: Ctrl+Z / Ctrl+Y, bounded to 500 steps, with typing and deleting bursts coalesced. Ctrl+Y now decodes as redo; it was dropped before, and the composer still ignores it.
  • Other keys: Ctrl+F find (smart case, wrapping); Ctrl+C / Ctrl+X copy and cut the selection; Ctrl+S saves; Esc closes, asking first if there are unsaved changes.
  • Dialogs use the existing src/ui/actionControls.ts renderer (Terminal-native action controls: reusable styles, semantic states and safe fallbacks #336). Unsaved changes: Save / Discard changes / Keep editing. Conflicts: Keep editing / Overwrite / Reload. The destructive choice is marked without relying on color.

Saving safely (src/viewer/fileEdit.ts)

  • Opens only files that can be saved back exactly:
    • regular UTF-8 text up to 1 MiB, owned by this user, with one hard link;
    • the file and its folder are writable;
    • one kind of line ending (LF or CRLF); the BOM and the final newline are kept.
    • Anything else is refused with the reason and stays readable in /view.
  • New files: a missing path in a writable folder opens empty and is created with link(), which fails rather than replaces if something appeared meanwhile.
  • Save steps:
    1. Exclusive, no-follow temporary file beside the original, at 0600, then the original's mode and group (best effort).
    2. fsync.
    3. Re-check that the original is still the file opened: device, inode, size, mtime, and a sha256 when size or time changed.
    4. rename over its real path, so a symlink stays a symlink.
    5. Best-effort directory sync.
  • Conflicts: a changed, replaced or deleted original writes nothing until the person chooses.
  • Failures (permission, read-only file system, full disk) keep the text in the editor and leave no temporary file.

Tests

  • textEditor.test.ts, 12 tests:
    • Buffer: grapheme, word and vertical movement across tabs and wide characters; selection; coalesced undo and redo; clean after undo to the saved text; a saved state pushed out of the history never reads as clean.
    • Find.
    • Open: every refusal.
    • Save: mode, BOM, CRLF, missing final newline, symlink kept; changed, replaced, deleted and exists conflicts; forced Overwrite; failures leave nothing behind.
    • Panel: keys decoded from real bytes, dialogs, hostile content drawn inert at narrow sizes.
  • viewerApp.test.ts: /edit → type → Ctrl+S saves → Esc closes; e from /view; a refused binary is reported.
  • viewerLive.test.ts: a real PTY creates a file through /edit; typed text and a real Ctrl+S byte save it; Esc returns to the composer.
  • 84 related test files (decoder, slash routing, Ask catalog) are green.

Not verified

Physical terminal checks are in docs/testing/v021-physical-qa.md, section 3, and have not been run.

Known limitation: unsaved text is lost if the NMSh window itself is closed.

Refs #402, #336, #410

/edit <path> (or e in /view) opens one small text file in a full-height
panel: typing, multi-line, caret by character/word/line/page, Shift
selection, Ctrl+Z/Ctrl+Y undo and redo (bounded, coalesced), Ctrl+F find,
Ctrl+C/Ctrl+X copy and cut, Ctrl+S save, Esc closes and asks about unsaved
changes. Ctrl+Y now decodes as redo (it was dropped before).

Only files that can be saved back exactly are opened: regular UTF-8 text up to
1 MiB, owned by this user, one hard link, writable file and folder, one kind
of line ending (BOM and final newline kept); a missing path in a writable
folder opens empty. Saving writes beside the original (exclusive create, its
mode and group), syncs, re-checks that the original is still the file opened
(device, inode, size, time, content hash) and renames over its real path; a
symlink stays a symlink. A changed, replaced or deleted original is a
conflict: Keep editing, Overwrite, or Reload. A new file never replaces one
that appeared meanwhile.
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