Skip to content

Enter continues a block quote, as it already continues a list (#700) - #705

Merged
PathGao merged 1 commit into
masterfrom
fix/quote-continuation
Aug 22, 2026
Merged

Enter continues a block quote, as it already continues a list (#700)#705
PathGao merged 1 commit into
masterfrom
fix/quote-continuation

Conversation

@PathGao

@PathGao PathGao commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

What this is

Enter on a block quote writes the next line's >, and Enter on a quote with
nothing after the marker takes it away. Closes #700, reported by @17Archangel,
who asked for the behaviour lists already have — "回车两次再取消掉".

It also changes one thing about lists, deliberately: see Scope.

Mechanism

utils/listSyntax.ts has always spelled the list-marker prefix
[ \t]*(?:>[ \t]*)*, so > was understood — as the thing a list item can be
nested inside. listEnter asked parseListItem and nothing else, and that
pattern needs a -, * or 1. to match at all. > quoted is a line with a
prefix and no marker, so it parsed as no block, and Enter fell through to
Monaco's own.

The module answers for both blocks now and is named for it (blockEnter), with
QUOTE_MARKER factored out beside the fragments the prefix already used.

Where the caret joins the block

The interesting decision is not "does a quote continue" but "from which
column". Measuring to where the marker's TEXT starts — the obvious rule, and
the one the list branch used — answers two opposite things for one gesture:

"> text"    caret just after the `>`   ->  inside a two-character marker
">text"     caret just after the `>`   ->  head of the content

Same pixel, same keystroke, and the user cannot see which spelling they are in
without counting spaces. So the boundary is the end of the marker's
characters instead, and both answer "continue". That is CodeMirror's rule,
reached for the same reason — @codemirror/lang-markdown's commands.ts
declines only when inner.to - inner.spaceAfter.length > pos, subtracting
exactly that whitespace.

The separator, once instead of twice

A caret parked inside the separator splits it. The half after the caret rides
down with the text, because that is what a line break does, so a new marker
carrying a whole separator lands one space more than was typed — and another on
every Enter after that, which is what the reporter would have hit first:

"- item", caret between `-` and its space, Enter five times
  before   "-  item" -> "-   item" -> "-    item" -> ...
  after    "- item"  -> "- item"   -> "- item"

markerUpTo rations the separator to the half already behind the caret, so the
two halves add back up to what was there. The marker's characters are never
rationed: an ordered marker still counts up (9. -> 10.) and a task box still
comes back unchecked.

Alternatives considered

Suppressing continuation inside fenced code. Built and then removed. - item
inside a ```diff block is prose-shaped text in somebody's code and
continuing it is wrong, and utils/pasteContext.ts already answers "is the
caret in code" for Ctrl+V, so the guard was four lines. It costs
monaco.editor.tokenize() over every line above the caret: 5.5 ms at 500
lines, 27 ms at 2 000, 275 ms at 20 000
(Monarch's own markdown grammar,
realistic prose). Ctrl+V has paid that since it shipped because a paste is one
keystroke in a while; Enter is every second keystroke of a paragraph. Nothing
public in Monaco is cheaper — ITextModel.getLineTokens is internal, and the
encoded tokens it holds have lost the type strings this needs (a bare fence's
body encodes as Other, indistinguishable from prose), while this app's own
semantic layer arrives a tick late inside Monaco's sparse store. CodeMirror
does bail on FencedCode, cheaply, because it has a Lezer tree to resolve
against; we do not. So lists and quotes are both still continued inside a fence,
as lists always have been, and listEditing.ts says so with the numbers.

Declining >text, as VS Code's Markdown All in One does
(/^> /.test(textBeforeCursor)). It sidesteps the caret problem above rather
than answering it, and it cannot be copied here: comrak renders >text as a
quote — semantic_spans returns quote.marker for it, exactly as for
> text — so Enter would be disagreeing with the preview drawn beside it.

Normalising the separator. CodeMirror collapses it to one space, Markdown
All in One writes a hard-coded > . Neither preserves a hand-aligned list
either, and this app does (- item continues as - ), so > quoted
keeps its spacing for the same reason.

Trimming the whitespace behind the caret, which CodeMirror does before
breaking (while (from > line.from && /\s/...) from--). It would make the
abandoned line byte-identical whichever side of the separator Enter is pressed,
and render.hardbreaks means trailing spaces carry no meaning here, so nothing
would break. Left out anyway: the line it cleans is one nobody can see the
difference in, and Enter quietly deleting characters to the left of the caret is
more than this key was asked to do.

Scope

Lists change too. The boundary and the separator rule are one implementation
for both blocks, so -| item now continues instead of dropping out of the
list, and - [x]| task continues from after the box. That is the same defect
one marker over; fixing it only for quotes would leave quotes one column more
forgiving than lists with nothing to point at for why.

Not touched: Tab and Shift+Tab, which read the same lines through
parseListItem and have their own reasons to; the fence blind spot above; and
the empty-quote ladder, which stays at one Enter per block rather than
CodeMirror's and Markdown All in One's two empty > lines — the reporter asked
for the list's two-Enter exit and this app's lists give it.

Tests

scripts/listContinuation.test.ts: 968 -> 969 node tests, all green, plus
npm run check (815 files, 0 errors), npm run test:vitest (398), npm audit
(0), cargo test.

The pure function is CALLED, once per shape: every quote depth and spelling,
the caret at each column across the marker of a quote, a bullet, an ordered
item and a task item, and the split-doubling case as a before/after pair. The
two Editor.svelte handlers are lifted out and run against the stub editor as
before, so the edits asserted are the real ones.

Reverting each half with the tests left in place: the quote branch -> 5 red;
the boundary -> the caret and doubling tests red; both restored -> green.

Verification

npm audit, npm run check, npm test, npm run test:vitest,
cd src-tauri && cargo test — all pass locally.

Driven by hand in a release build on macOS 15 (Apple silicon): every case above,
plus Enter held down on a quote and on a list to watch the separator stay one
space wide.

Not verified: Windows and Linux — the reporter is on Windows 10 — and that
Monaco delivers Enter to this handler at runtime, which no test here can
establish. What is pinned instead is the when clause the app declares.

…a list (#700)

Writing a quote meant typing `>` on every line: Enter continued lists and
task items and left `> quoted` alone, because `listEnter` recognised `>`
only as the PREFIX a list item can be nested inside, never as a marker of
its own. A line with no `-`, `*` or `1.` therefore parsed as no block at
all.

`listEditing.ts` now answers for both — `blockEnter`, since what it decides
is no longer only about lists — and the two share one rule for where the
caret joins the block: after the marker's CHARACTERS, not after the space
behind them. `>|text` and `> |text` are the same gesture in the same pixel
and now get the same answer; the separator each new marker writes is cut
back to the half the caret has passed, so the half riding down with the
text is not doubled, and pressing Enter there repeatedly no longer widens
the gap a space at a time. Lists answer to that rule too, which is a
behaviour change: `-|  item` used to drop out of the list.

An empty quote clears in one keystroke, at any depth — the reporter asked
for the list's own "twice to cancel", and `> > ` should not need one Enter
per level to escape.
@PathGao
PathGao merged commit 5e245cb into master Aug 22, 2026
4 checks passed
@PathGao
PathGao deleted the fix/quote-continuation branch August 22, 2026 14:50
PathGao added a commit that referenced this pull request Aug 25, 2026
Four things shipped since 2.7.4 that the file which documents what Markpad can
do never heard about. It carries the editing behaviour around each construct,
not only the spellings, so each one belongs to a section that already exists.

- Lists: `Tab` moved a line by tabSize and left the marker alone. #713 makes it
  a level change -- the parent's content column, and both numbered lists
  renumbered -- which is what the file already claimed and now describes
  accurately.
- Quotes: `Enter` continues a block quote (#705), so the section gets the
  paragraph Lists has had. Including that one keystroke clears an empty quoted
  line at any depth, which is the way out.
- Images: where a pasted or dropped image lands, and `${filename}` in that
  setting (#716). It expands to a folder name, not a path, and the note says so
  -- `./images/${filename}/` is not a thing you can write here.
- Not-syntax: copying from the preview keeps its formatting (#680), and the
  split panes can trade sides (#693).

Tests: 984 pass.
alecdotdev pushed a commit that referenced this pull request Aug 25, 2026
…own again (#719)

* chore: bump version to 2.7.5

* docs(syntax): bring the reference up to 2.7.5, in both languages

Four things shipped since 2.7.4 that the file which documents what Markpad can
do never heard about. It carries the editing behaviour around each construct,
not only the spellings, so each one belongs to a section that already exists.

- Lists: `Tab` moved a line by tabSize and left the marker alone. #713 makes it
  a level change -- the parent's content column, and both numbered lists
  renumbered -- which is what the file already claimed and now describes
  accurately.
- Quotes: `Enter` continues a block quote (#705), so the section gets the
  paragraph Lists has had. Including that one keystroke clears an empty quoted
  line at any depth, which is the way out.
- Images: where a pasted or dropped image lands, and `${filename}` in that
  setting (#716). It expands to a folder name, not a path, and the note says so
  -- `./images/${filename}/` is not a thing you can write here.
- Not-syntax: copying from the preview keeps its formatting (#680), and the
  split panes can trade sides (#693).

Tests: 984 pass.

* docs(release): the download table's two warnings describe 2.7.5, not 2.7.4

The table is composed in `build.yml` and printed on every release page, so both
notes ship with whatever master holds when the workflow is dispatched. Both are
about to be wrong.

macOS: the last paragraph told users the app grants file access per prompt and
that self-signing in Keychain Access is the way out, redone after every update.
#707 is what that paragraph asks for, so it would print the workaround on the
first release that no longer needs it. Replaced with what is now true, worded so
it stays true for 2.7.6: the grant survives an update, and only a user coming
from a release older than 2.7.5 is asked once more, because the signature
changes the identity the old grants belonged to. The Gatekeeper paragraph above
it is untouched -- signing is not notarization, and the first-launch dialog is
unaffected.

Windows: the "false positive Trojan" half is the stale one -- VirusTotal no
longer flags the portable `.exe`, which is what #334 and #466 were. The
SmartScreen half is not: it fires on an unsigned binary regardless of what any
scanner says, and stays true until an Authenticode certificate exists (#562).
Dropping the whole note would leave the release page silent about a dialog every
Windows user still meets. So the antivirus claim goes and the unrecognized-app
one stays, in one shorter sentence.

Tests: 984 pass.
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