Add markdown-table-wrap-buffer.el — opt-in in-buffer table wrapping (markdown / gfm / md-ts / org) - #4
Closed
SayreBlades wants to merge 5 commits into
Closed
Conversation
…/gfm/md-ts/org) New file, purely additive. No change to upstream markdown-table-wrap.el. Features: - markdown-table-wrap-buffer-mode: context-sensitive TAB wraps the pipe/org table at point (off-table falls through to markdown-cycle / org-cycle). - markdown-table-wrap-table-at-point (C-c C-w), -buffer, -region. - GFM pipe tables (markdown-mode/gfm-mode/md-ts-mode) and Org tables (org-table-to-lisp -> pipe -> wrap -> reinsert). - Width: window width default, defcustom override, numeric prefix (SPC u N in doom, C-u N in plain emacs). - Org: wired via org-tab-first-hook (returns t to consume TAB on table, nil off-table so org-cycle runs). Mirrors doom's +org-* hooks. - Org #+TBLFM tables refused (formula would be dropped). - Re-wrap unwraps first only when spacer rows present (unwrap is not idempotent on never-wrapped tables). Single undo boundary per table. - markdown-table-wrap-buffer-turn-on skips pi-coding-agent-chat-mode (derived from md-ts-mode) so it doesn't shadow pi's chat TAB. All behaviors opt-in: loading the file changes nothing until the minor mode is enabled (typically via markdown-mode-hook / md-ts-mode-hook / org-mode-hook). Known limitation: markdown-table-wrap drops link URLs when a column is narrower than the full [text](url) token (upstream engine behavior); clickability holds when the column fits the link markup.
…inding doom binds normal-state TAB to markdown-cycle in evil-markdown-mode-map; evil state maps override minor-mode maps, so our minor-mode-map TAB binding never fired under evil (TAB just collapsed folds / did nothing). Replace the keymap binding with :around advice on markdown-cycle: when on a table, wrap (honoring current-prefix-arg); otherwise call the original markdown-cycle. This mirrors the org org-tab-first-hook interception pattern and works regardless of evil state, without fighting evil's keymap priority. Remove the now-unused --fallback-tab and --markdown-tab helpers. Verified: on-table wrap@30 via prefix; off-table markdown-cycle fallthrough confirmed via advice.
--org-tab-h now always returns nil, so it never consumes TAB and never wraps in org via TAB. Org tables can still be wrapped via markdown-table-wrap-table-at-point (C-c C-w). This is a diagnostic step: org mode was reportedly broken by the hook. Disabling it isolates whether the hook is the cause. Markdown/gfm/md-ts TAB (via markdown-cycle advice) is unaffected.
Point on a non-header row (separator or data row) captured only the suffix from point onward, so TAB / C-c C-w wrapped a partial table. Walk backward to the first table line so point anywhere in the table captures the whole table. Verified: header, separator, data-row, and table-at-bob all return the full region; off-table returns nil.
The org TAB hook (--org-tab-h on org-tab-first-hook) was temporarily disabled in 1382623 to diagnose 'org mode reportedly broken by the hook.' Root cause identified: --table-region-at-point did not walk backward to the table start (fixed in def40a4), so TAB pressed on a non-header (data) row captured only the suffix from point onward and mangled the table. With def40a4 in place, TAB anywhere in the table (header, hline, or data row) wraps the whole table correctly. Verified headlessly (verify-org-tab.el) across five cases: - A. hook on a data row -> wraps whole table, returns t - B. integrated org-cycle on a data row (minor mode on) -> wraps cleanly - C. off-table -> returns nil (org-cycle unaffected) - D. #+TBLFM table -> returns nil (refused, table untouched) - E. current-prefix-arg honored (SPC u 60 -> width 60) Also pass current-prefix-arg to --effective-width for parity with the markdown-cycle advice path (so V1.7 prefix-arg wrap works in org too).
Contributor
Author
|
Closing in favor of a display-overlay approach. In-buffer mutation produces semantically broken pipe tables (wrapped continuation rows render as extra blank data rows under any real markdown/org exporter). We're moving to a read-only pretty rendering over canonical source, toggled like inline images / |
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.
Add
markdown-table-wrap-buffer.el— opt-in in-buffer table wrapping (markdown / gfm / md-ts / org)Summary
This PR adds a new, purely additive file —
markdown-table-wrap-buffer.el— that turnsmarkdown-table-wrap's string→string transform into a set of in-buffer commands and a minor mode for Emacs. It rewrites raw table text in place so the wrapped result is the buffer text: links stay clickable,isearch/yank/copywork, and point moves naturally.No behavior changes for existing users. The new file is opt-in: loading it changes nothing until
markdown-table-wrap-buffer-modeis enabled.markdown-table-wrap.elis byte-for-byte unchanged.Why
markdown-table-wrapis an excellent engine (pure function, no buffer state, markup-aware wrapping with a clean unwrap round-trip). Today it's consumed bypi-coding-agent's chat buffer via display overlays — the raw text stays canonical, a wrapped view is shown on top. That overlay approach is great for a read-only chat render, but it doesn't help users editing ordinary Markdown/Org files, and it doesn't generalize to other consumers.This PR provides the missing "use it in your own buffers" layer:
.mdor.orgfile with a wide table can hitTABon the table and have it wrap to the window width — directly in the source, multi-line, instead of the existing single-linemarkdown-table-align/org-table-alignthat blows past the viewport.Everything is built on the existing public API of
markdown-table-wrap(markdown-table-wrap,markdown-table-wrap-unwrap,markdown-table-wrap-inside-code-fence-p,markdown-table-wrap--split-table-row). No new core engine code, no changes to existing functions.What's added
One new file:
markdown-table-wrap-buffer.el.Commands
markdown-table-wrap-table-at-pointC-c C-w)markdown-table-wrap-buffermarkdown-table-wrap-buffer-regionmarkdown-table-wrap-buffer-set-widthMinor mode
markdown-table-wrap-buffer-mode(lighterTWrap) — context-sensitiveTAB:TAB.markdown-table-wrap-buffer-turn-on— hook-friendly enabler formarkdown-mode-hook,md-ts-mode-hook,org-mode-hook. Coversgfm-modeautomatically (derives frommarkdown-mode). Skipspi-coding-agent-chat-mode(which derives frommd-ts-mode) so it doesn't shadow pi's own chatTAB.Customization
markdown-table-wrap-buffer-widthnil(window width)markdown-table-wrap-buffer-max-cell-heightnil(unlimited)markdown-table-wrap.markdown-table-wrap-buffer-tab-overridetTABto wrap on table when the minor mode is on.markdown-table-wrap-buffer-keyC-c C-wHow it works (the interesting bits)
Width selection
Default:
window-max-chars-per-line. Override:markdown-table-wrap-buffer-width, then a numeric prefix arg. In doom+evil the universal argument isSPC u(doom rebindsC-utoevil-scroll-up), soSPC u 60 TABwraps to 60; in plain emacs it'sC-u 60 TAB. The file detects the prefix uniformly viaprefix-numeric-value.Table detection — universal, mode-agnostic
A table region is a maximal run of lines matching
^[ \t]*|(the first column of a GFM pipe table or an Org pipe table). This works inmarkdown-mode,gfm-mode,md-ts-mode, andorg-modewithout mode-specific APIs, so the file has no hard dependency onmarkdown-modeororg. Code fences are respected viamarkdown-table-wrap-inside-code-fence-p(so a pipe table inside```is left alone).GFM path
Pipe text is passed straight to
markdown-table-wrap.Org path
Org tables use
|---+---|separators and optional width cookies (|<5>|,|<l5>|). The file:org-table-to-lisp(robust; returns rows of strings orhline).+-joined hline becomes|---|---|).markdown-table-wrap.org-at-table-p→t).Org tables carrying a
#+TBLFM:formula line are refused with a clearuser-error(wrapping would silently drop the formula). This is a deliberate, conservative first cut; a follow-up could preserve#+TBLFMacross a wrap.TABintegration (the non-trivial part)The two major-mode families have different native
TABconventions, both verified in source:markdown-mode/gfm-mode/md-ts-mode:TAB→markdown-cycle→markdown-table-align(single-line layout). So "TAB in table = layout" is already the native convention; this PR slots wrap into that same role.org-mode:TAB→org-cycle→org-table-next-field, which (when the table is flagged dirty) callsorg-table-alignas its first act (org-table.el:1057–1065, gated onorg-table-automatic-realignwhich defaults tot). So in practice, org'sTABin a table is also "layout when dirty, then move cell" — the layout role is already there.To slot in non-invasively (not clobber off-table behavior):
TABin its own keymap with a context check — on a table, wrap; off a table, callmarkdown-cycle. This replaces only themarkdown-table-alignbranch ofmarkdown-cycle.org-tab-first-hook(buffer-local, on enable; removed on disable). Whenorg-at-table-p, it wraps and returnstto consume theTAB; otherwise it returnsnilsoorg-cycleruns unchanged (visibility cycling, src-block indent, etc. are all unaffected off-table). This mirrors how doom's own+org-*hooks (+org-yas-expand-maybe-h,+org-indent-maybe-h) participate inorg-tab-first-hook.The tradeoff (documented in the file): in org, enabling the minor mode means
TABno longer callsorg-table-next-field's cell-move on a table — it wraps instead. Cell-move remains reachable viaRETand direct motion. This is the deliberate choice for layout-parity; users who prefer org's cell-nav can leave the minor mode off and useC-c C-wdirectly.Re-wrap at a new width
Naïve "unwrap then wrap" is unsafe:
markdown-table-wrap-unwrapis not idempotent on never-wrapped tables — its continuation-row heuristic can merge consecutive full data rows (e.g. an "Auth" row and a "DB" row collapse into one logical row). I verified this empirically while building this. So the file only unwraps when the table shows wrap markers (the all-empty-cell spacer rows thatmarkdown-table-wrapinserts between wrapped logical rows); otherwise it wraps the raw text directly. Each wrap is wrapped in a single undo boundary (prepare-change-group+undo-amalgamate-change-group) so oneundorecovers the previous form.Buffer-position safety
When wrapping multiple tables in a region/buffer, regions are processed back-to-front so an earlier replacement (which changes buffer length) doesn't invalidate the positions of later regions. The trailing newline that the detection includes (start of the line after the table) is preserved on insert, so blank-line separation after a table survives.
Scenarios enabled
.mdwith a table that runs off the right edge.TABon the table → it wraps to the window width, multi-line, in the source.TABoff the table → normalmarkdown-cycle. Resize the window,SPC u 80 TAB→ re-wrap to 80.undo→ back to the previous form.org-mode.TABon the table → wrapped pipe text (still a valid org table).TABoff the table →org-cycle(visibility cycling) unchanged.#+TBLFMtables are protected.(markdown-table-wrap-buffer-region beg end 60)wraps all tables in a region to width 60 — no keybindings, no minor mode required. Useful for any tool that renders or normalizes markdown tables.pi-coding-agentchat buffers: can adopt this as an alternative "inline" render style (gated on a defcustom) where tables are rewritten in place rather than overlay-displayed — making links clickable,isearch/yankwork, etc. (That integration is in pi, not this PR; this PR provides the reusable primitive.)Opt-in by default — nothing changes for existing users
markdown-table-wrap.elis unchanged (byte-identical to upstreammain).markdown-table-wrap-buffer.eldefines commands, a minor mode, and defcustoms, but enables nothing. The minor mode is off by default.Known limitations (documented in the file)
Link URLs are dropped when a column is narrower than the full
[text](url)token. This is the upstream engine'smarkdown-table-wrap--force-break-markupbehavior: when a link token doesn't fit a column, it degrades to the visible text (URL discarded) to preserve legibility. The file preserves links intact when the column fits the markup. A future:preserve-linksoption could refuse to wrap a column narrower than its widest link token. (Worth noting because it bears on read-only renders like pi's chat.)Re-wrap of force-broken headers can be lossy at very narrow widths.
markdown-table-wrap-unwraprejoins continuation rows with spaces, so a header force-broken to "Featu"+"re" at width 30 rejoins as "Featu re" rather than "Feature". The file mitigates by only unwrapping when spacer rows are present and leaving an undo boundary; the underlying limitation is inmarkdown-table-wrap-unwrap.#+TBLFM:Org tables are refused, not wrapped. (Future enhancement.)org-table-next-fieldcell-nav onTABis replaced by wrap when the minor mode is on in org. Deliberate (layout-parity); off-tableTABis unaffected. Users who want cell-nav keep the minor mode off and useC-c C-w.Verification performed
Batch-tested against
markdown-mode2.8,org9.x,md-ts-mode, in Emacs 30:markdown-table-wrap's own output.markdown-table-wrap-bufferon a buffer with mixed content + two tables — both wrapped, surrounding paragraphs/lists/blank lines preserved.|---+---|hline + width cookies stripped;org-at-table-pistafterwards.#+TBLFM:table —user-error, table untouched.markdown-table-wrap-buffer-modeon:TABon a table wraps; off a table falls through tomarkdown-cycle(markdown) /org-cycle(org). Verified viaorg-tab-first-hookmembership and fallthrough advice.undorestores the original single-line table.point-minand atpoint-max(no trailing newline) both wrap without error.pi-coding-agent-chat-modeis skipped by the guard.Compatibility
Package-Requires: ((emacs "28.1") (markdown-table-wrap "0.2.0"))— same baseline as the core package.markdown-modeororgat load time (those functions are declared and called lazily, so the file loads even without them present).What this PR does not do
markdown-table-wrap.el.defcustomto the core engine.markdown-table-wrap-unwrapidempotency issue (separate, worth its own discussion).#+TBLFMpreservation (future work).Happy to iterate on naming, key choices, or the org
TABtradeoff. The orgTABdecision in particular is the one I'd most like feedback on — I went with "wrap replaces layout" for parity across modes, but I can see an argument for leaving orgTABas cell-nav and usingC-c C-wonly.