Skip to content

feat(text): a plain file joins the editing mode and can be saved - #878

Merged
andiwand merged 1 commit into
mainfrom
feat/txt-editing
Sep 10, 2026
Merged

feat(text): a plain file joins the editing mode and can be saved#878
andiwand merged 1 commit into
mainfrom
feat/txt-editing

Conversation

@andiwand

@andiwand andiwand commented Sep 10, 2026

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

#877 is merged, so this now sits on main. Closes the open question
editing.md has carried since the mode was built.

What it was

text.js was the one editor that answered to nobody — its own beforeinput
gate, its own undo stacks, its own paste handling, and no odr.editing on
the page at all
, because text_file.cpp writes text.js and not
editing.js. So a host that wired the mode once and opened a .txt found
odr.generateDiff undefined, could not ask isEditable(), got no
onEditChange to light its save button, and had no undo() for its toolbar.

Decision 11 of editing.md says that cannot happen — "odr.editing is on
every document view either way… odr.generateDiff() never goes missing". It
was true of every view except this one.

And nothing persisted: a .txt is a TextFile, not a DocumentFile, so
Document::edit / save do not reach it.

What it is now

It attaches. The view writes editing.js and the page-level
data-odr-editable / data-odr-keyboard, and text.js becomes an editor on
the mode rather than beside it. Its undo needed nothing new — it is the one
editor that recorded an inverse per change before any of this work started, so
canUndo / canRedo / undo / redo answer honestly on day one.

One operation. A plain file has no element tree — no runs, no paragraphs,
no ids — so none of the document operations apply. What it has instead is
setContent {text}, carrying the file's whole text, and coalescing keeps the
log at exactly one operation however long the session runs.

Not a line at a time, which was the obvious alternative: a line number is a
path
, and decision 1 is that an operation must not address by one — inserting
a line shifts every line after it. A document escapes that with ids from its
registry; a plain file has no registry, because lines are not elements. Finer
than a line would want offsets, which decision 2 refused over UTF-16 versus
UTF-8. And write_edited produces the complete bytes either way, so a finer
log would only be reassembled before writing. Where it bites is a large file
re-crossing the bridge; the answer there is one replaceLines {from, to, text}
diff hunk, not per-line ops — recorded in the design doc.

A write path, shaped like PdfFile::annotate rather than Document::save,
because a .txt is a TextFile:

if (text_file.is_savable())
  text_file.write_edited(odr_editing_get_operations(), out);

txt now states edit and save in FileTypeCapabilities.

The encoding, stated rather than hidden

encoding/transcode.hpp has to_utf8 and nothing in the other direction.
So write_edited writes UTF-8 whatever the source encoding was — a Shift-JIS
file that saves comes back as UTF-8. The API doc and the changelog say so, so a
host can warn; is_savable() refuses only what cannot be decoded at all,
since the view hands those bytes to the browser as they are and what comes
back could not be put back. A real from_utf8 is recorded as the open question
it is.

Breaking

HtmlConfig::editable no longer writes contenteditable on the lines —
odr.editing.enable() does, so one render serves both modes as it already does
for a document.

Checks

test/browser/plaintext/ is new: 36 checks over the mode, the log, the gutter
and undo. The plain-text editor has never had a check page.

Reference output

Every .txt page changes: data-odr-keyboard on the body, the editing.js
link, and no contenteditable. Say the word and I'll regen and advance the
pins once this and #877 are in.

Base automatically changed from fix/edit-envelope-version to main September 10, 2026 14:24
`text.js` was the one editor that answered to nobody: its own
`beforeinput` gate, its own undo, and no `odr.editing` on the page at all.
So a host that wired the mode once and opened a `.txt` found
`odr.generateDiff` missing - which decision 11 of `editing.md` said could
not happen, and which was true of every view except this one.

It attaches now. The view writes `editing.js` and the page-level state,
and `text.js` is an editor on the mode rather than beside it. Its undo
needed nothing new: it is the one editor that recorded an inverse per
change before any of this work started.

A plain file has no element tree, so none of the document operations
reach it. What it has instead is one operation, `setContent {text}`,
carrying the file's whole text. Not a line at a time: a line number is a
path, and a plain file has no registry to hang an id on, so per-line
operations would walk back into what decision 1 removed.

`TextFile::write_edited` is the write path, shaped like `PdfFile::annotate`
rather than `Document::save`, because a `.txt` is a `TextFile` and neither
`Document::edit` nor the element adapters reach it. `is_savable()` refuses
an encoding we cannot decode, since the view hands those bytes to the
browser as they are. What it writes is UTF-8 whatever the source was -
`encoding/transcode.hpp` decodes and does not encode.

`HtmlConfig::editable` no longer writes `contenteditable` on the lines;
`odr.editing.enable()` does, so one render serves both modes as it does
for a document.

`docs/design/txt-editing.md` holds its decisions, beside the two documents
the other editors already have. It is not a document view, so the document
one is the wrong place for any of it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKFKbUVCYF2VhujdmjhhPW
@andiwand
andiwand merged commit 911c93d into main Sep 10, 2026
10 checks passed
@andiwand
andiwand deleted the feat/txt-editing branch September 10, 2026 14:39
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