Add numbered paste placeholders and true multi-row input - #699
Open
kk1987 wants to merge 7 commits into
Open
Conversation
The editor models the input as one logical line, so a pasted or recalled newline that reached the terminal desynced the refresh cursor math and corrupted the display. Substitute U+21B5 for every newline outside a fold, which is one column on every terminal and needs no escape sequence, so horizontal scrolling can still slice the rendered text. Byte offsets are no longer preserved by rendering, so translating the edit position into a render position now scans the buffer. The no-fold fast path is gone for the same reason, and the insert shortcut bails out when the buffer already holds newlines: it accounts for width with utf8StrWidth(), which counts them as zero columns. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UazT9herrhVF4gFz3VwSar
A prompt holding several placeholders gave no way to tell them apart. Each fold created by a paste now keeps a stable number, assigned in creation order and never reused, so "[Pasted text antirez#2 +40 lines]" still names the same text after antirez#1 is removed. The array stays sorted by offset, hence the explicit id beside each range. Folds rebuilt from a recalled history entry keep the old unnumbered text: they were not pasted on this line, so a number would be a lie. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UazT9herrhVF4gFz3VwSar
Folded text had no way back: the bytes were in the buffer but nothing could reveal them. Pasting bytes that exactly match a fold now drops that fold instead of inserting a duplicate, showing the text with its newlines as markers, with the cursor left where the paste would have left it. The revealed text is one long logical line, so a file pasted twice by mistake could push the prompt past the terminal: measure the expanded render first and keep the fold, with a beep, when it would not fit. The newest placeholder advertises this with an inline suffix, dropped as soon as the buffer content changes or the line is submitted, so the frozen line above the answer shows the plain placeholder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UazT9herrhVF4gFz3VwSar
Any paste containing a newline became a placeholder, so a three line snippet was hidden behind "[... 3 pasted lines ...]" even though it fits the prompt. Fold only what would take the prompt over: long text, or more than PASTE_FOLD_LINES lines. The collector also refused pastes that could not fit the remaining edit buffer, and dropped every paste once the sixteen fold slots were used, in both cases without a word. Collect the bytes with only the PASTE_MAX_BYTES cap, since identical bytes may expand an existing fold rather than be inserted, and let the single insert decide: text with no free fold slot goes in unfolded, and only a full buffer beeps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UazT9herrhVF4gFz3VwSar
The paste behaviour of the line editor had no test at all, and nothing told the user that a placeholder can be opened again. Add a unit test driving the editor through its non blocking API with a fake terminal, pinning the newline markers, the numbered placeholders, expanding by pasting again, the screen height guard, the fold slot exhaustion and the transient hint. Mention the gesture in both help screens and add the manual checks to the release QA list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UazT9herrhVF4gFz3VwSar
The editor treated the input as one long logical line: rows and cursor came from dividing the total display width by the terminal width, which is only right while the text holds no newline. A newline typed with Ctrl+J went out as a bare LF, and with OPOST off that drops a row while keeping the column, so the input stair-stepped away and every later redraw was off by the rows nobody had counted. Walk the rendered text instead. Each hard newline opens a fresh row, each segment wraps at the right margin, and the walk reports the row, the column and the deferred wrap state terminals keep when text stops exactly on the margin: knowing it is what lets a newline right after such text resolve the wrap instead of skipping a row. The multi row refresh writes CR LF for every newline and takes its clearing, cursor and reserved area numbers from the same walk, so typed newlines, short pastes and expanded placeholders are now edited as the rows they look like, and the screen fit guard measures real rows. The marker survives where several rows are impossible: the single row refresh and masked input, which the render buffer now serves through a flag. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UazT9herrhVF4gFz3VwSar
The gesture has always been there, undocumented and unusable: it is a supported way to write a multi row prompt now that the editor paints the rows. Add it to both help screens and give the release QA list the multi row editing checks a terminal has to make by hand. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UazT9herrhVF4gFz3VwSar
Author
|
Since opening this PR, two more commits landed ( 🤖 Generated with Claude Code |
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 upgrades the line editing of the bundled linenoise fork, shared by the
ds4REPL and theds4-agentTUI, in two related ways: Claude Code-style paste placeholders, and real multi-row rendering of hard newlines.Real multi-row input. The editor previously modeled the input as one logical row: a hard newline that reached the terminal (typed with Ctrl+J, or recalled from multiline history) moved the cursor down without a carriage return (raw mode clears OPOST) and desynchronized the refresh row math, stair-stepping and corrupting the prompt.
refreshMultiLinenow derives its geometry from a newline-aware walk of the rendered text: every hard newline paints as a real terminal row (written as CR LF), and the row, cursor, and clear accounting is exact, including the deferred-wrap state terminals keep when text ends on the right margin. Ctrl+J is now a documented way to type a newline. In the paths confined to a single row — single-line mode and masked input — newlines render as a one-column↵marker (U+21B5, East Asian Neutral, exactly one column everywhere).Short pastes go inline. A paste of up to 5 lines and under 200 bytes is inserted directly and shows as real rows. Previously any paste containing a newline was folded behind a placeholder.
Long pastes collapse to a numbered placeholder.
[Pasted text #1 +123 lines](or+N charsfor a long single-line paste). Numbers are assigned in creation order per edited line and never renumbered, so#2still names the same text after#1is gone. History folds keep the old unnumbered[... N lines ...]text. The newest placeholder carries a(paste again to expand)suffix that disappears as soon as the buffer content changes or the line is submitted.Paste again to expand. Pasting bytes that exactly match a fold reveals that text in place as real rows, fully editable, instead of inserting a duplicate. A screen-height guard measures the true expanded row count first: a big file pasted twice by mistake beeps and keeps the placeholder rather than pushing the prompt past the terminal.
No silent drops. Running out of the 16 fold slots used to discard the paste with only a beep; the text now goes in unfolded. The only refusals left are a paste over
PASTE_MAX_BYTESor an edit buffer that genuinely cannot grow.Enter semantics are unchanged: folds were always display-only and the real bytes are submitted exactly as before, so nothing downstream of the line editor changes.
Testing:
tests/test_linenoise_paste.cdrives the non-blocking editor API with a fake terminal (LINENOISE_ASSUME_TTY, pipe stdin, tmpfile stdout so emitted bytes can be asserted) — 81 checks covering the newline geometry (including a differential test asserting the new math reproduces the previous formulas exactly on newline-free input, so wrapping behavior for ordinary lines is provably unchanged), CR LF emission, marker rendering in single-line mode, numbered placeholders, expand/re-fold, the screen-height guard, fold-slot exhaustion, the >1MB refusal, history folds, and the transient hint. Wired intomake test;make ds4 ds4-agentbuilds warning-free. Manual terminal checks were added toQA_BEFORE_RELEASES.md.🤖 Generated with Claude Code
https://claude.ai/code/session_01UazT9herrhVF4gFz3VwSar