T-UYTR-md-engine-tables
State: closed/done · Impact: high · Complexity: medium
@codemirror/lang-markdown already parses GFM Table* nodes, but live mode shows raw pipes and
only read mode renders a table. This task adds the tables plugin: a rendered widget when the
table is inactive, mark-based styling when active, a cell-navigation keymap, insert.table, and
the table.* commands the chrome table menu runs.
| Location | Role today |
|---|---|
packages/ts/obsidian-editor/src/decorations.ts | The enter switch has no Table case; table markup renders as plain text in live mode |
packages/ts/obsidian-editor/src/commands.ts | No table command |
packages/ts/obsidian-editor/src/theme.ts | No table rules |
packages/ts/obsidian-md/src/vue/components/TableBlock.ts | Read-mode table component; the widget markup must match what read mode shows |
packages/ts/obsidian-md/src/processor.ts | Async renderer only; renderToHtmlSync comes from T-RCWI-md-obsidian-md-sync-helpers |
packages/ts/obsidian-editor/test/helpers.ts#makeState | Headless state builder for the plugin tests |
The plugin roster and dispatch come from T-4TE2-md-engine-plugin-contract; the command
catalogue from T-EXQ7-md-engine-command-catalogue; tableAt in the tree helpers from
T-MNRO-md-engine-anchors; the tables.md corpus fixture and the bench from
T-0GV4-md-engine-line-endings-corpus.
Proposed
Section titled “Proposed”packages/ts/obsidian-editor/src/plugins/tables.ts, a defineEditorPlugin with:
- Inactive
Table(no selection intersecting its range) → one blockDecoration.replacewidget whose DOM isrenderToHtmlSyncmarkup of the table source, filled from a cache keyed by that source so two rebuilds of an unchanged table reuse one render. A click on a rendered cell moves the cursor into that cell’s source position (pluginclicksentry). - Active
Table→ no widget; every table line gets acm-table-lineline decoration, pipes visible. - Keymap at
prec: 'high', active only when the cursor is inside aTable: Tab → next cell (from the last cell it appends an empty row and enters its first cell), Shift-Tab → previous cell, Enter → same column in the next row, Mod-Enter → insert a row below. - Catalogue additions:
insert.table(2×2 skeleton: header row, alignment row, one body row) andtable.row.insert-above,table.row.insert-below,table.col.insert-left,table.col.insert-right,table.row.delete,table.col.delete,table.align.left,table.align.center,table.align.right. Everytable.*run resolves its target throughtableAtand rewrites only the lines of that table; bytes outside it are untouched.
The theme module gains rules for cm-table-line and the widget’s table styling through the
existing --obsmd-* variables.
Approach
Section titled “Approach”- Write
packages/ts/obsidian-editor/test/plugins/tables.plugin.test.tsfirst, over a fixture with three tables: inactive table → one block widget whose markup contains the cell text; selection inside → widget gone,cm-table-lineon each line; render cache hit for identical source (spy onrenderToHtmlSync); each keymap binding’s cursor motion; Tab from the final cell appends a row. - Write the command cases in the same suite:
insert.tableat an empty line inserts the skeleton; eachtable.*command applied to the middle table leaves the other two tables byte-identical. - Implement the plugin: widget class, cache, click handler, line marks.
- Implement the keymap and the ten catalogue commands (runs live in the plugin module, ids registered in the catalogue).
- Add
tablestoBUILTIN_PLUGINS; add the theme rules. - Extend the swap-writer expectations if any command needs a fixture position the corpus lacks;
run
bun test,tsc --noEmit,moon run obsidian-editor:bench.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
packages/ts/obsidian-editor/src/plugins/tables.ts | new | tables plugin: widget, cache, marks, keymap, command runs |
packages/ts/obsidian-editor/src/plugins/index.ts | new | Created by T-4TE2-md-engine-plugin-contract; tables joins BUILTIN_PLUGINS |
packages/ts/obsidian-editor/src/commands/catalogue.ts | new | Created by T-EXQ7-md-engine-command-catalogue; gains insert.table and the nine table.* ids |
packages/ts/obsidian-editor/src/theme.ts | modify | cm-table-line and widget table rules |
packages/ts/obsidian-editor/test/plugins/tables.plugin.test.ts | new | Widget, cache, keymap and command tests |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
bun test packages/ts/obsidian-editorpasses, includingpackages/ts/obsidian-editor/test/plugins/tables.plugin.test.ts. - AC-2: an inactive table renders exactly one block widget whose DOM contains a
tableelement with the fixture’s cell text; moving the selection into it removes the widget and putscm-table-lineon each of its lines. - AC-3: Tab from the last cell appends one row (line count of the table grows by one) and places the cursor in the new row’s first cell; Enter moves to the same column of the next row.
- AC-4: each of the ten ids listed in Proposed exists in the compiled command set, and each
table.*command applied to the middle table of the three-table fixture leaves the other two tables byte-identical. - AC-5: two decoration rebuilds with an unchanged document call
renderToHtmlSynconce per distinct table source (spy assertion). - AC-6:
bun test packages/ts/obsidian-editor/test/swap-writer.test.tspasses with the new commands in the matrix overpackages/ts/obsidian-editor/test/fixtures/corpus/tables.md. - AC-7:
moon run obsidian-editor:benchstays within 1.5× of the recorded baseline.
Out of scope
Section titled “Out of scope”MdTableMenuchrome over thetable.*commands (T-8ZHI-md-ui-overlays-bubble-link-table).- The
tableanchor andtableAt(T-MNRO-md-engine-anchors provides both). - Editing text inside the rendered widget; a cell click moves the cursor into the source instead.
- Read-mode table rendering (
TableBlockis unchanged).
Dependencies
Section titled “Dependencies”- T-RCWI-md-obsidian-md-sync-helpers for
renderToHtmlSync. - T-4TE2-md-engine-plugin-contract for the plugin contract,
T-EXQ7-md-engine-command-catalogue for the catalogue the ids join,
T-MNRO-md-engine-anchors for
tableAt, T-0GV4-md-engine-line-endings-corpus for the corpus and bench. - D-7VMX: cell geometry comes from the
Table/TableRow/TableCelltree nodes, never a regex.
Discovery context
Section titled “Discovery context”Member of milestone M-931L; engine step E6 of decision D-K3EM-markdown-editor-chrome-and-plugin-environment.
Depends on
Section titled “Depends on”T-RCWI-md-obsidian-md-sync-helpers