Skip to content

fix: formatting, hyperlinks and undo in and around tables; text before images - #41

Merged
michaelstonis merged 7 commits into
mainfrom
housekeeping/table-editing-fixes
Oct 21, 2026
Merged

michaelstonis merged 7 commits into
mainfrom
housekeeping/table-editing-fixes

Conversation

@michaelstonis

Copy link
Copy Markdown
Contributor

Summary

  • Deleting across a table:
    • Behaviour: deleting a selection from a paragraph before a table to one after it still deletes the table, which is the usual editor behaviour.
    • The fix: undo and redo now restore everything exactly, putting the table object and both paragraphs back in place. Undo snapshots now record every block in the range (SnapshotHelper.CaptureBlocks / RestoreBlocks, keeping object identity).
    • Side effect: undoing a deletion inside a table cell (for example Backspace) now restores the cell text.
  • Hyperlinks in table cells: inserting a link in a cell, at the caret or over a selection, no longer throws. Undo and redo work, and the caret ends up after the link in that cell. A link can't span cells, so a selection that runs into another cell links only its part in the start cell.
  • Character formatting in table cells:
    • What changed: the cell paragraphs in the range are now included by DocumentEditor.ApplyTextStyle, FormattingEngine.IsAppliedToEntireRange, FontFormattingEngine.GetTextStyleForRange, DocumentEditor.ApplyCharacterFormatting and ApplyStyleAction. Cells are taken in reading order from the start cell to the end cell, the same rule FormattingStateTracker uses.
    • Result: bold and the other formats now apply inside a cell and across several cells. Toggling them a second time turns them off again, and undo and redo restore the exact runs.
    • Side effect: GetSelectedText, which copy and cut use, now returns the selected cell text instead of an empty string.
  • Text before images: DocumentEditor.InsertText at offset 0 of an image or line break now inserts the text before it instead of after it. If there is a text run before the image, the text is added to that run.

Not in this PR

  • Deleting or typing over a selection that spans several cells is still wrong: DeleteRange treats it as a range inside the start cell.
  • The remove and open hyperlink commands don't find links inside cells, because HyperlinkEngine.GetHyperlinkAtPosition only checks top-level paragraphs.

Tests

  • New TableEditingFixesTests with 15 tests, each written to fail before its fix.
  • Core.Tests pass (1121) and SkiaSharp.Tests pass (33).
  • The SkiaSharp Release build and the MAUI compile check report 0 warnings.

Generated by Claude Code

…e images

- Deleting a selection from a paragraph before a table to one after it
  deletes the table with the selected text (unchanged), but undo lost the
  table and put the last paragraph back in the wrong place, because
  DeleteRangeAction only snapshotted paragraphs. Undo now records every
  block in the range (SnapshotHelper.CaptureBlocks/RestoreBlocks, keeping
  object identity) and reinserts the table at its index. The same snapshot
  covers table-cell paragraphs, so undoing a deletion inside a cell (e.g.
  Backspace) now restores the cell text instead of doing nothing.
- Inserting a hyperlink with the caret or selection in a table cell threw
  (HyperlinkEngine looked up the paragraph by block index only). It now
  uses the cell's paragraph, undo restores it, and the caret lands after
  the link in the cell. A selection that runs out of the start cell links
  only its part in that cell.
- Character formatting skipped table cells but still recorded an undo
  step. DocumentEditor.ApplyTextStyle, FormattingEngine.IsAppliedToEntireRange,
  FontFormattingEngine.GetTextStyleForRange, ApplyCharacterFormatting and
  ApplyStyleAction now walk the paragraphs of the cells in the range
  (reading order from the start cell to the end cell, as
  FormattingStateTracker already did), so bold etc. apply in cells and
  across several cells, toggles turn off again, and undo/redo restore the
  exact runs. GetSelectedText returns cell text too (it returned "").
- DocumentEditor.InsertText at offset 0 of an image (or other non-text
  inline) put the text after it; it now goes before it, appended to the
  preceding run when there is one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197tHRWhTiagB1MTsDuPkoa
Copilot AI balanced review requested due to automatic review settings October 21, 2026 17:22
awk compared the hunk line number (a string from substr) with the section
bounds as strings, so an entry on line 40 was not counted as after line 9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197tHRWhTiagB1MTsDuPkoa

Copy link
Copy Markdown
Contributor Author

Changelog entry check failed on 85bc0ab: the bug was in the check, not this PR's entry. The PR does add a ### Fixed line under [Unreleased] (line 40). The workflow's awk compared that line number with the section start (line 9) as strings, so "40" > "9" came out false and the entry wasn't counted. 6f9a49c makes the comparison numeric. With that change the same diff counts 1 entry. The fix lands on main when this PR merges.


Generated by Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Clipboard extraction loses linked text, merged-cell traversal includes hidden content, and redo can change hyperlink membership.

3 open issues
What changed in this PR

Improves table editing in the core editor, including formatting, hyperlink insertion, undo restoration, and text insertion before images.

Changes:

  • Adds table-aware range traversal and block snapshots.
  • Fixes hyperlink insertion in cells and text placement before non-text inlines.
  • Adds regression tests and documents supported behavior.
File Description
tests/​MintedTextEditor.Core.Tests/​TableEditingFixesTests.cs Adds 15 editing regression tests.
src/​MintedTextEditor.Core/​Formatting/​HyperlinkEngine.cs Supports insertion in cells and clamps cross-cell selections.
src/​MintedTextEditor.Core/​Formatting/​FormattingEngine.cs Checks formatting across selected cells.
src/​MintedTextEditor.Core/​Formatting/​FontFormattingEngine.cs Reads styles across selected cells.
src/​MintedTextEditor.Core/​Editing/​UndoableActions.cs Captures table content and restores block identity.
src/​MintedTextEditor.Core/​Document/​DocumentEditor.cs Adds cell traversal and insertion-before-image handling.
docs/​tables.md Documents cell editing behavior.
CHANGELOG.md Records editing fixes.

Review effort: Balanced

Comment thread src/MintedTextEditor.Core/Document/DocumentEditor.cs
Comment thread src/MintedTextEditor.Core/Document/DocumentEditor.cs
Comment thread src/MintedTextEditor.Core/Document/DocumentEditor.cs Outdated
GetSelectedText walked only the TextRun inlines of each paragraph, so in a
cell holding "A", a link showing "B" and "C" copy returned "AC", cutting
the linked text deleted it without copying it, and linking already-linked
text with no display text produced an empty link. It now extracts text by
character offsets, recursing into hyperlink children and adding a newline
for each line break, so a partly selected link contributes only its
selected characters. This applies to top-level paragraphs too, which had
the same gap.

HyperlinkEngine.InsertHyperlink also re-resolves its insertion point by
character offset after deleting the selection: deleting a whole link
between two runs merges them, which left the old inline index pointing
past the merged run and put the new link after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197tHRWhTiagB1MTsDuPkoa
GetParagraphsInRange walked every cell between the start and end cell,
including slots covered by another cell's RowSpan/ColumnSpan. MergeCells
keeps those cells' blocks but layout hides them, so after merging the top
row of a 2x2 table, a selection from (0,0) into (1,0) copied the hidden
text of (0,1) and formatting changed it. The traversal now works out span
ownership once per table with TableBlock.BuildSpanOwners(), as layout
does, and skips covered slots (unless the range starts or ends in one).

FormattingStateTracker had its own copy of the traversal with the same
problem (hidden text could make the toolbar state look mixed); it now
uses GetParagraphsInRange.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197tHRWhTiagB1MTsDuPkoa
…e a preceding link

With [link("site"), image], an InsertTextAction at the image's offset 0
gives [link, "X", image]. Its start anchor is a paragraph character
offset, which after undo resolves to the end of the link, and because
EscapeHyperlinkBoundary is false the redo put "X" inside the link.

ParagraphAnchor's keepHyperlinkStart option becomes keepHyperlinkSide: it
still keeps a position recorded at the start of a link there, and now
also resolves a position recorded outside any link to the start of the
inline after a link's end (when that inline is not another link), via the
new DocumentEditor.PreferAfterHyperlinkEnd. Only InsertTextAction's start
anchor uses the option, so insertions before images, line breaks or runs
replay on the same side of the link; a position recorded at a link's end
still extends the link on redo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197tHRWhTiagB1MTsDuPkoa
@michaelstonis
michaelstonis merged commit 2f71035 into main Oct 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants