Skip to content

feat: format the text selected in the preview - #11

Closed
bero wants to merge 6 commits into
GDKsoftware:mainfrom
bero:feat/preview-selection-source-mapping
Closed

bero wants to merge 6 commits into
GDKsoftware:mainfrom
bero:feat/preview-selection-source-mapping

Conversation

@bero

@bero bero commented Sep 16, 2026

Copy link
Copy Markdown

Builds on #10, which is included here until that merges. Independent of #7, #8 and #9.

What this does

Select text in the rendered preview, press Bold, and exactly those characters become bold in the markdown.

Before this, a formatting command always acted on the editor's own selection, so in preview only mode it applied at a caret you could not see.

Why it needed the parser

The rendered preview is not the source. Emphasis markers, link syntax, escapes and entities are gone by the time text reaches the screen, so a character offset in the preview does not line up with one in the markdown.

Worse, the mapping could not be a single offset. A block's inline content is assembled line by line with markers already stripped, a block quote losing its > and a list item its bullet, then trimmed before the inline parser sees it. Content and source drift apart repeatedly. Only block nodes carried a source range; inline nodes had none.

How it is built, one commit per layer

  1. Markdown4D.Parser.SourceMap records the content to source relationship as runs, each run being one stretch that is contiguous in the source. Characters with no source, such as the spaces a tab expands into, fall in no run.
  2. Inline nodes get source ranges. The staging block collects the map as lines are appended, the block parser adjusts it for the trim, and the inline parser sets a segment on each text node at the buffer flush. The span comes from positions in the content, never from the length of the rendered text.
  3. ATX headings are mapped too, which needed the heading matcher to report where its caption starts.
  4. TMarkdownSourceLookup and TrySelectedSourceSpan turn a selection into a source span. A display run already knew its node and where it starts inside that node's text, so no new field was needed on the display list.
  5. Both studios adopt that span before a formatting command runs, and switch out of preview only mode so the change lands where it can be seen.

What it refuses to do

Anything it cannot trace exactly reports nothing rather than guessing, and the command falls back to the editor's own selection:

  • A node whose rendered text is a different length from its source, meaning an escape or entity changed the length.
  • A run that is not contiguous in the source, such as one crossing a line break.
  • Table cells, which are split out of assembled row text and are not mapped yet.

Testing

19 new tests across four suites, each written before the code and confirmed failing first.

  • Suite: 1104 tests, all passing, no regressions.
  • All three projects build clean on Win32 Debug with Delphi 12 Athens.
  • Parser tests assert real documents: a block quote whose marker is stripped, a list item bullet, a heading's hashes, and text sitting next to emphasis.
  • Viewer tests select precise pixel ranges and assert the source span, including a selection after emphasis where the asterisks exist in the source but never on screen.
  • End to end in the VCL studio: selecting a word in the preview, pressing Bold and saving writes the markers around exactly that word.

The FMX studio has identical wiring and shares every tested rule, but was not driven interactively, since its Skia canvas does not capture reliably for scripted checks on my machine.

Preview only mode hides the editor, and the VCL refuses to focus a control
that is not visible. Clicking Bold, Italic, Link or Code then raised
EInvalidOperation with 'Cannot focus a disabled or invisible window', and so
did every command on the shared format path, clicking an entry in the
contents list, and the palette's undo, redo, select all, indent, outdent and
delete word.

Route every path that returns focus to the editor through a FocusEditor
helper carrying the CanFocus guard that the zen mode code already used. The
VCL applied that guard in four places out of sixteen. The FMX studio applied
it nowhere, so it could raise from leaving zen mode and from closing the
command palette as well.
First step towards formatting a selection made in the preview. To turn a
position in the rendered preview back into a source range, the parser has to
know where each character of a block's inline content came from.

That is not a simple offset. A block's content is assembled line by line with
markers stripped, a block quote losing its '> ' and a list item its bullet,
and the result is trimmed before the inline parser sees it, so content and
source drift apart repeatedly.

TMarkdownSourceMap records the relationship as runs, each run being one
stretch of content that is contiguous in the source. Characters with no
source, such as the spaces a tab expands into, fall in no run and report as
unmappable rather than guessing.

This commit adds the unit and its tests only. Nothing calls it yet.
Carries the content to source map through the parser so a text node knows
which characters of the markdown it came from. The staging block collects the
map as lines are appended, the block parser adjusts it for the trim it does
before parsing inlines, and the inline parser sets a segment on each text node
as the buffer flushes.

The span is taken from positions in the content rather than from the length of
the buffered text, because escapes and entities make the rendered text a
different length from its source. A run that is not contiguous in the source,
such as one crossing a line break, gets no segment rather than a wrong one.

Four tests parse real documents and assert that the recorded range points at
the right characters, including a block quote whose marker is stripped before
the inline parser ever sees the content, and a list item bullet.

Table cells and ATX headings are not mapped yet.
Heading content is appended straight from the matcher rather than through the
line path, so it had no entry in the source map and heading text ended up with
no segment. The matcher now also reports where the caption starts in the line,
past the hashes and the space after them, which is enough to record the run.
Adds the lookup that closes the gap between what is on screen and what is in
the markdown. A display run already knew its node and where it starts inside
that node's text, and the node now carries its source range, so a character in
the preview can be traced back to a character in the source.

TMarkdownSourceLookup holds the rule, including the case it refuses: when a
node's rendered text is a different length from the source it came from, an
escape or an entity has changed the length and positions inside it no longer
line up, so it reports nothing rather than landing on the wrong character.

TMarkdownViewerModel.TrySelectedSourceSpan walks the selected runs and returns
the span they cover, failing if any run in the selection cannot be mapped.

Eight tests, including a selection sitting after emphasis, where the asterisks
are in the source but never on screen.
Completes the path from a selection on screen to an edit in the source. The
editor control gains a public SetSelection so a caller that has worked out
where it wants to act can act there, and the viewer control exposes the source
span of its selection.

Both studios now adopt that span before a formatting command runs, so
selecting a word in the rendered preview and pressing Bold wraps exactly those
characters. A selection that cannot be traced back exactly is left alone and
the command falls back to the editor's own selection, rather than guessing at
the wrong characters.

When the command came from the preview while preview only mode is active, the
view switches to split first, so the change lands where it can be seen instead
of out of sight.

Verified end to end: selecting a word in the preview, pressing Bold and saving
writes the markers around exactly that word.
@bero

bero commented Sep 16, 2026

Copy link
Copy Markdown
Author

Withdrawing this for now: it needs more work before it is worth your time. I will reopen or resubmit once it is verified end to end.

@bero bero closed this Sep 16, 2026
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