Count only content lines and show the selection word count (#199) - #204
Merged
Conversation
- getDocumentStats now counts non-blank lines: Muya serializes blank
separator lines between blocks and a trailing newline adds a phantom
segment, so short documents reported inflated line counts ("10 lines"
for a handful of visible ones).
- The expanded-toolbar stats line appends the selected-text word count
while a selection lives inside the editor host — a port of the
maintainer''s unmerged upstream marktext#4457, reshaped for this app:
one document-level selectionchange source feeding component-local
state (no store), rAF-coalesced so handle drags pay at most one count
per frame, with the upstream math/ruby preview exclusion kept (same
mu- class names in the vendored engine). Cleared on collapse, host
swap, and editor teardown. The source-mode leg is deliberately out:
the toolbar — the stats surface the owner chose — does not render in
source mode.
- New locale key toolbar.statsSelection across all ten languages; unit
tests for the line semantics and an e2e that asserts content-line
counting, the appended selection count, and its clearing.
- countContentLines understands fenced code: every interior line is visible content, blank ones included, while the fence markers are syntax and do not count. Closing requires the same marker at least as long as the opener, so tildes stay content inside a backtick fence. Regression cases cover interior blanks, lone-blank interiors, marker mixing, longer closers, and composition with separator filtering. - The selection text read moves to selectionStats.ts with a genuinely allocation-free probe: ancestor closest() plus scoped intersectsNode decide before anything is cloned, so plain selections - every frame of a handle drag - stay on the toString() fast path. A selection entirely inside a rendered preview now counts as nothing (render output is not document text; its clone would lack the wrapper the strip keys on). Contract tests prove the fast path never clones (the fake range throws on cloneContents) alongside the strip and containment behavior.
- Fence detection gains explicit per-delimiter syntax checks: delimiters allow at most three columns of indentation (a leading tab already disqualifies), so four-space-indented backticks are indented-code content; a backtick fence info string may not contain backticks, so a paragraph opening with inline code no longer opens a phantom fence; tilde info strings stay unrestricted. The closing check (a pure marker run, at least opener length) already rejected trailing text - it now also refuses over-indented closers. Seven new delimiter-rule cases. - The preview probe now runs a querySelector existence test first: it early-exits on a match and materializes no NodeList, so the common no-preview path pays one native subtree scan and nothing else; the full querySelectorAll walk only happens when a preview actually exists under the range ancestor. Comments and the PR text now state the honest cost model - clone-free, not allocation-free - and a contract test pins that querySelectorAll is never called on the no-preview path.
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.
Closes #199 — both halves: the line-count bug and the selection word count port, surfaced on the toolbar stats line per the owner's decision (not the ⋮ menu).
Bug: line count
getDocumentStatscounted every newline-delimited segment of the serialized markdown — Muya's blank separator lines between blocks and the trailing-newline phantom segment included — which is how a handful of visible lines reported as "10 lines" on device. It now counts non-blank lines only:# Title\n\npara one\n\npara two\nis 3 lines, a single-line document is 1 (previously 2). The same stats feed the draft-log diagnostics, which become meaningful too.Port: selection word count (upstream marktext#4457, reshaped)
While a selection lives inside the editor host, the expanded-toolbar stats line appends its word count —
4 words · 23 chars · 1 lines · 2 selected— and drops it when the selection collapses.The port deliberately reshapes the upstream implementation rather than copying it:
selection-changeevent and a documentselectionchangelistener, reconciled through a subtlelastSelectedText/store-consistency dedupe. Here a single document-levelselectionchangelistener feeds component-local state inMobileEditorToolbar— the component already owns such a listener for command restore-ranges, and the count never leaves the component that displays it. No store, no dedupe, no race.toString()fast path. The vendored engine uses the samemu-math-render/mu-ruby-renderclass names, so the selector ports verbatim.toString()), and the source-mode leg — the toolbar, the stats surface the owner chose, does not render in source mode. If a source-mode surface is wanted later, that is a new issue.New locale key
toolbar.statsSelectionacross all ten languages, phrased to each locale's existing counter style (选中 {words} 字,{words} selected, …).Codex review round 1 (at
d5d7db9) — both P2s addressed in6683395countContentLinesnow tracks fence state — every interior line counts (blanks included), fences don't, and closing requires the same marker at least as long as the opener (tildes stay content inside a backtick fence, a longer run of the same marker closes, per CommonMark). Codex's example (```js / alpha / blank / beta / ```) now counts 3. Regression cases cover interior blanks, lone-blank interiors, marker mixing, longer closers, and composition with the separator filtering.selectionStats.ts: an ancestorclosest()plus scopedintersectsNodeprobe decides without cloning, so plain selections stay ontoString(). A selection entirely inside a rendered preview now counts as nothing (render output is not document text — and its clone would lack the wrapper the strip keys on, a gap the upstream implementation shares). Contract tests pin the cost model: the fake range throws oncloneContents, so the fast-path tests passing is the proof no clone happens; strip and containment behavior covered alongside.Codex review round 2 (at
6683395) — addressed inf2c5218```not-a-closing-fencealready stayed inside the block — a regression case now pins it. The genuine hole was on the open side: a backtick fence's info string may not contain backticks, so a paragraph starting with inline code (```foo``` bar) no longer opens a phantom fence; tilde info strings stay unrestricted. Both delimiters now have explicit, separate syntax checks as suggested.querySelectorexistence test first — engine early-exit, no NodeList — so the common no-preview path pays exactly one native scan of the range-ancestor subtree;querySelectorAllonly materializes when a preview actually exists there. A new contract test pins thatquerySelectorAllis never called on the no-preview path (alongside the existing throws-on-clone proof). The deeper options codex sketched (preview index via MutationObserver) trade ongoing mutation-tracking cost for probe cost; at realistic preview counts the existence test is the better bargain, and the comment now documents the model honestly.Verification
vue-tsc -b+ Vite build green, focused lint clean, locale contract satisfied.mobile-editor-toolbar37/37 — new test types a heading + paragraph, asserts5 wordsand2 lines(the old code would say 4), selects two words via a real DOM range and asserts2 selected, then collapses and asserts the suffix is gone.alpha beta gamma delta, expanded the toolbar →4 words · 23 chars · 1 lines(single content line, previously 2); drove abeta gammaselection over CDP →4 words · 23 chars · 1 lines · 2 selected; cleared the selection → suffix dropped. Long-press caret placement (no selection) correctly shows no suffix.