From acdaeac5405eadd0db50f5b562c2ed7a4acc34b7 Mon Sep 17 00:00:00 2001 From: Sayre Blades Date: Fri, 31 Jul 2026 17:31:02 -0400 Subject: [PATCH 1/5] Add markdown-table-wrap-buffer.el: in-buffer table wrapping (markdown/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. --- markdown-table-wrap-buffer.el | 495 ++++++++++++++++++++++++++++++++++ 1 file changed, 495 insertions(+) create mode 100644 markdown-table-wrap-buffer.el diff --git a/markdown-table-wrap-buffer.el b/markdown-table-wrap-buffer.el new file mode 100644 index 0000000..5053b5d --- /dev/null +++ b/markdown-table-wrap-buffer.el @@ -0,0 +1,495 @@ +;;; markdown-table-wrap-buffer.el --- Wrap tables in place to a target width -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 SayreBlades + +;; Author: SayreBlades +;; Maintainer: SayreBlades +;; URL: https://github.com/SayreBlades/markdown-table-wrap +;; Package-Requires: ((emacs "28.1") (markdown-table-wrap "0.2.0")) +;; Keywords: text, markdown, org, tables +;; SPDX-License-Identifier: GPL-3.0-or-later + +;; This file is part of the markdown-table-wrap package (fork). +;; +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; In-buffer table wrapping that rewrites the raw table text to fit a +;; given character width, using `markdown-table-wrap' as the engine. +;; +;; Unlike a display-overlay approach (which leaves raw text canonical +;; and shows a wrapped view), this REWRITES the buffer so that the +;; wrapped text *is* the buffer text: links stay clickable, +;; isearch/yank/copy work, and point moves naturally. +;; +;; Supports GFM pipe tables (markdown-mode / gfm-mode / md-ts-mode) and +;; Org tables. Org tables are converted to pipe form via +;; `org-table-to-lisp', wrapped, and reinserted as pipe text (which Org +;; still recognizes as a table). Org tables carrying a `#+TBLFM:' +;; formula line are refused (feature to be added later). +;; +;; Entry points: +;; `markdown-table-wrap-table-at-point' -- wrap the table at point +;; `markdown-table-wrap-buffer' -- wrap every table in buffer +;; `markdown-table-wrap-buffer-region' -- wrap tables in a region +;; `markdown-table-wrap-buffer-mode' -- minor mode: TAB wraps on table +;; +;; Width selection: +;; - default: `markdown-table-wrap-buffer-width' or window width +;; - prefix arg: `SPC u 60' (doom) or `C-u 60' (plain emacs) +;; +;; TAB behavior (minor mode on): +;; - markdown / gfm / md-ts: TAB wraps when on a table, else falls +;; through to `markdown-cycle'. +;; - org: a function on `org-tab-first-hook' wraps when +;; `org-at-table-p' and returns t to consume the TAB; otherwise +;; returns nil so `org-cycle' runs normally (visibility cycling, +;; src-block indent, etc. unaffected off-table). This mirrors how +;; doom's own `+org-*' hooks participate in `org-tab-first-hook'. +;; Org's native TAB already does layout (via `org-table-next-field' +;; which calls `org-table-align' when the table is dirty), so this +;; slots into the same layout role. +;; +;; Re-wrapping at a new width: +;; The command unwraps first (via `markdown-table-wrap-unwrap') *only +;; when the table shows wrap markers* (the all-empty-cell spacer rows +;; that `markdown-table-wrap' inserts between wrapped logical rows), +;; then wraps at the requested width, within a single undo boundary. +;; Unwrap is best-effort: force-broken headers at very narrow widths +;; may not rejoin cleanly; undo to recover. We do NOT unwrap +;; never-wrapped tables because `markdown-table-wrap-unwrap' is not +;; idempotent on them (its continuation-row heuristic can merge +;; consecutive full rows). +;; +;; All behaviors are opt-in: loading this file changes nothing until +;; `markdown-table-wrap-buffer-mode' is enabled (typically via hooks). + +;;; Code: + +(require 'cl-lib) +(require 'subr-x) +(require 'markdown-table-wrap) + +;; Quiet the byte compiler for mode-specific functions we call lazily. +(declare-function markdown-cycle "markdown-mode") +(declare-function org-cycle "org-cycle") +(declare-function org-at-table-p "org-table") +(declare-function org-table-to-lisp "org-table") +(declare-function markdown-table-wrap-inside-code-fence-p + "markdown-table-wrap") + + +;;;; Customization + +(defgroup markdown-table-wrap-buffer nil + "In-buffer table wrapping to a target width." + :group 'text + :prefix "markdown-table-wrap-buffer-") + +(defcustom markdown-table-wrap-buffer-width nil + "Default wrap width in characters. +nil means use `window-max-chars-per-line' of the selected window. +A positive integer pins the width regardless of window size." + :type '(choice (const :tag "Window width" nil) + (integer :tag "Fixed width")) + :group 'markdown-table-wrap-buffer) + +(defcustom markdown-table-wrap-buffer-max-cell-height nil + "Cap cell height when wrapping; nil means unlimited. +Passed through to `markdown-table-wrap' as MAX-CELL-HEIGHT." + :type '(choice (const :tag "Unlimited" nil) + (integer :tag "Max lines per cell")) + :group 'markdown-table-wrap-buffer) + +(defcustom markdown-table-wrap-buffer-tab-override t + "When the minor mode is on, bind TAB to wrap when on a table. +Off-table, TAB falls through to the major mode's normal binding. +If nil, only `markdown-table-wrap-buffer-key' wraps." + :type 'boolean + :group 'markdown-table-wrap-buffer) + +(defcustom markdown-table-wrap-buffer-key (kbd "C-c C-w") + "Key that wraps the table at point (always active in the minor mode)." + :type 'key-sequence + :group 'markdown-table-wrap-buffer) + + +;;;; Width resolution + +(defun markdown-table-wrap-buffer--effective-width (&optional pfx) + "Return the wrap width to use. +PFX, when a numeric prefix, overrides the configured width." + (let ((p (and pfx (prefix-numeric-value pfx)))) + (cond + ((and p (> p 0)) p) + (markdown-table-wrap-buffer-width) + (t (max 10 (or (window-max-chars-per-line) 80)))))) + + +;;;; Table region detection + +;; A table region is a maximal run of lines beginning with `|' (after +;; optional leading whitespace). This matches GFM pipe tables and Org +;; pipe tables alike, and works in markdown-mode, gfm-mode, md-ts-mode +;; and org-mode without mode-specific APIs. + +(defconst markdown-table-wrap-buffer--table-line-re "^[ \t]*|" + "Regexp matching the first column of a pipe/org table line.") + +(defun markdown-table-wrap-buffer--table-region-at-point () + "Return (BEG . END) for the table surrounding point, or nil. +BEG is the start of the first table line; END is the start of the +line following the last table line (an insertion position). Returns +nil if point is not on a table line." + (save-excursion + (beginning-of-line) + (if (not (looking-at-p markdown-table-wrap-buffer--table-line-re)) + nil + (let ((start (point))) + (while (looking-at-p markdown-table-wrap-buffer--table-line-re) + (forward-line 1)) + (cons start (point)))))) + +(defun markdown-table-wrap-buffer--table-regions-in (beg end) + "Return a list of (BEG . END) table regions between BEG and END. +Skips tables inside fenced code blocks (markdown only, via +`markdown-table-wrap-inside-code-fence-p')." + (save-excursion + (save-restriction + (narrow-to-region beg end) + (goto-char (point-min)) + (let (regions) + (while (re-search-forward markdown-table-wrap-buffer--table-line-re + nil t) + (let ((line-start (line-beginning-position))) + (unless (and (fboundp 'markdown-table-wrap-inside-code-fence-p) + (markdown-table-wrap-inside-code-fence-p line-start)) + (goto-char line-start) + (let ((start (point))) + (while (looking-at-p markdown-table-wrap-buffer--table-line-re) + (forward-line 1)) + (push (cons start (point)) regions))))) + (nreverse regions))))) + + +;;;; Org conversion + +;; Org tables use `|---+---|' separators and optional width cookies +;; `|<[lrc]?[0-9]*>|'. `org-table-to-lisp' parses robustly and returns +;; a list of rows, each a list of strings or the symbol `hline'. We +;; emit canonical GFM pipe text, wrap it, and reinsert: GFM pipe text +;; is itself valid Org pipe text, so no back-conversion is needed. + +(defconst markdown-table-wrap-buffer--width-cookie-re + "<\\([lrc]\\)?[0-9]*>" + "Regexp matching an Org column width cookie like `<5>' or `'.") + +(defun markdown-table-wrap-buffer--org-table-has-formula-p (_beg end) + "Return non-nil if a `#+TBLFM:' line follows the org table ending at END." + (save-excursion + (goto-char end) + (skip-chars-forward " \t\n") + (looking-at-p "#\\+TBLFM:"))) + +(defun markdown-table-wrap-buffer--org-to-pipe (beg end) + "Return canonical GFM pipe-table text for the org table at BEG..END. +Strips width cookies. Assumes no `#+TBLFM' (caller guards)." + (let* ((raw (buffer-substring-no-properties beg end)) + (parsed (with-temp-buffer + (insert raw) + (goto-char (point-min)) + (org-table-to-lisp)))) + (if (not parsed) + raw + (let* ((rows (mapcar + (lambda (row) + (if (eq row 'hline) + 'hline + (mapcar (lambda (cell) + (string-trim + (replace-regexp-in-string + markdown-table-wrap-buffer--width-cookie-re + "" cell))) + row))) + parsed)) + (ncols (apply #'max (mapcar + (lambda (r) (if (listp r) (length r) 1)) + rows))) + (hline-text (concat "| " + (mapconcat #'identity + (make-list ncols "---") + " | ") + " |"))) + (mapconcat + (lambda (row) + (if (eq row 'hline) + hline-text + (let ((cells (append row + (make-list (max 0 (- ncols (length row))) + "")))) + (concat "| " + (mapconcat #'identity cells " | ") + " |")))) + rows + "\n"))))) + + +;;;; Wrap core + +(defun markdown-table-wrap-buffer--spacer-line-p (line) + "Return non-nil if LINE is an all-empty-cell pipe row (a wrap marker). +Such rows are inserted by `markdown-table-wrap' between wrapped +logical data rows; their presence signals a previously-wrapped table." + (and (string-prefix-p "|" (string-trim-left line)) + (string-blank-p (replace-regexp-in-string "[| \\t]" "" line)))) + +(defun markdown-table-wrap-buffer--previously-wrapped-p (text) + "Return non-nil if TEXT looks like output of `markdown-table-wrap'. +Detects the spacer rows (all-empty-cell pipe rows) that +`markdown-table-wrap' inserts between wrapped logical rows." + (with-temp-buffer + (insert text) + (goto-char (point-min)) + (let (found) + (while (and (not found) (not (eobp))) + (let ((line (buffer-substring (line-beginning-position) + (line-end-position)))) + (when (markdown-table-wrap-buffer--spacer-line-p line) + (setq found t))) + (forward-line 1)) + found))) + +(defun markdown-table-wrap-buffer--wrap-text (text width) + "Wrap table TEXT to WIDTH, returning the wrapped text. +TEXT is canonical pipe text (org tables are converted to pipe form +by the caller first; the wrapped GFM output is valid Org pipe text). + +If TEXT was previously wrapped (contains spacer rows), unwrap it +first so we never double-wrap. `markdown-table-wrap-unwrap' is NOT +safe on arbitrary never-wrapped tables (its continuation-row +heuristic can merge consecutive full rows), so we only unwrap when +spacer rows indicate a prior wrap." + (let ((base (if (markdown-table-wrap-buffer--previously-wrapped-p text) + (markdown-table-wrap-unwrap text) + text))) + (markdown-table-wrap base + width + markdown-table-wrap-buffer-max-cell-height + nil nil))) + +(defun markdown-table-wrap-buffer--wrap-region-1 (beg end width) + "Wrap the single table at BEG..END to WIDTH in place. +BEG..END is a table region as returned by detection. Returns t if +the buffer was modified, nil if the table already fit. +Dialect is inferred from the major mode." + (let* ((dialect (if (derived-mode-p 'org-mode) 'org 'gfm)) + (raw-text (if (eq dialect 'org) + (markdown-table-wrap-buffer--org-to-pipe beg end) + (buffer-substring-no-properties beg end))) + ;; Normalize: compare on trailing-newline-stripped text so a + ;; table that fits (wrap returns it aligned, no trailing nl) + ;; compares equal to the raw content sans its trailing nl. + (raw-norm (string-trim-right raw-text "\n")) + (wrapped (condition-case err + (markdown-table-wrap-buffer--wrap-text raw-norm width) + (error + (message "markdown-table-wrap: %s" + (error-message-string err)) + nil)))) + (when (and wrapped (not (equal wrapped raw-norm))) + (let ((inhibit-read-only t) + (handle (prepare-change-group))) + ;; A single undo boundary so the user can recover the previous + ;; form with one `undo'. Preserve the trailing newline that the + ;; detected region includes (the start of the line after the + ;; table) so blank-line separation after the table survives. + (let ((inserted (if (string-suffix-p "\n" wrapped) + wrapped + (concat wrapped "\n")))) + (unwind-protect + (progn + (delete-region beg end) + (save-excursion + (goto-char beg) + (insert inserted))) + (undo-amalgamate-change-group handle)))) + t))) + +(defun markdown-table-wrap-buffer-region (beg end &optional width) + "Wrap every table between BEG and END to WIDTH. +WIDTH, when non-nil (e.g. from a numeric prefix arg), overrides the +configured width. Returns the number of tables wrapped. Leaves one +undo boundary per table. Org tables with `#+TBLFM' are skipped with +a message." + (interactive "r\nP") + (let ((width (markdown-table-wrap-buffer--effective-width + (and (consp width) (car width)))) + (count 0)) + ;; Process regions back-to-front so earlier replacements don't + ;; invalidate the buffer positions of later regions. + (dolist (region (nreverse + (markdown-table-wrap-buffer--table-regions-in beg end))) + (if (and (derived-mode-p 'org-mode) + (markdown-table-wrap-buffer--org-table-has-formula-p + (car region) (cdr region))) + (message "markdown-table-wrap: skipping org table with #+TBLFM at %d" + (car region)) + (when (markdown-table-wrap-buffer--wrap-region-1 + (car region) (cdr region) width) + (cl-incf count)))) + (when (called-interactively-p 'interactive) + (message "markdown-table-wrap: %d table(s) wrapped to width %d" + count width)) + count)) + +(defun markdown-table-wrap-buffer (&optional _width) + "Wrap every table in the current buffer to the effective width. +A numeric prefix overrides it (`SPC u 60' in doom, `C-u 60' otherwise)." + (interactive "P") + (markdown-table-wrap-buffer-region + (point-min) (point-max) + (and (consp current-prefix-arg) current-prefix-arg))) + +(defun markdown-table-wrap-table-at-point (&optional width) + "Wrap the table at point to WIDTH. +WIDTH defaults to the effective width; a numeric prefix overrides +it (`SPC u 60' in doom). Signals `user-error' if point is not on a +table." + (interactive "P") + (if-let* ((region (markdown-table-wrap-buffer--table-region-at-point))) + (let ((width (markdown-table-wrap-buffer--effective-width width))) + (if (and (derived-mode-p 'org-mode) + (markdown-table-wrap-buffer--org-table-has-formula-p + (car region) (cdr region))) + (user-error + "Org table has #+TBLFM: wrapping would drop the formula; remove it first") + (when (markdown-table-wrap-buffer--wrap-region-1 + (car region) (cdr region) width) + (message "markdown-table-wrap: wrapped table to width %d" width)))) + (user-error "Not on a table"))) + +(defun markdown-table-wrap-buffer-set-width (width) + "Set the default wrap width for this buffer to WIDTH." + (interactive "nWidth: ") + (setq-local markdown-table-wrap-buffer-width width) + (message "markdown-table-wrap: buffer width set to %d" width)) + + +;;;; Minor mode: context-sensitive TAB + +(defun markdown-table-wrap-buffer--fallback-tab () + "Invoke the major mode's normal TAB binding. +In markdown/gfm/md-ts, `markdown-cycle'; in org, `org-cycle'; else +whatever the local map binds to TAB, else the raw TAB key." + (let ((cmd (cond + ((derived-mode-p 'org-mode) #'org-cycle) + ((derived-mode-p 'markdown-mode) #'markdown-cycle) + ((lookup-key (current-local-map) (kbd "TAB"))) + (t nil)))) + (if cmd + (call-interactively cmd) + (let ((current-prefix-arg nil)) + (execute-kbd-macro (kbd "TAB")))))) + +(defun markdown-table-wrap-buffer--markdown-tab (&optional arg) + "Context-sensitive TAB for markdown/gfm/md-ts. +On a table, wrap it (ARG overrides width like a numeric prefix). +Off-table, call `markdown-cycle'." + (interactive "P") + (if-let* ((region (markdown-table-wrap-buffer--table-region-at-point))) + (let ((width (markdown-table-wrap-buffer--effective-width arg))) + (markdown-table-wrap-buffer--wrap-region-1 + (car region) (cdr region) width)) + (markdown-table-wrap-buffer--fallback-tab))) + +(defun markdown-table-wrap-buffer--org-tab-h () + "Hook for `org-tab-first-hook': wrap the org table at point. +Returns t to consume the TAB when on a table (after wrapping), nil +otherwise so `org-cycle' runs normally. Refuses tables with +`#+TBLFM' (falls through to `org-cycle')." + (when (and (derived-mode-p 'org-mode) + (org-at-table-p)) + (if-let* ((region (markdown-table-wrap-buffer--table-region-at-point))) + (if (markdown-table-wrap-buffer--org-table-has-formula-p + (car region) (cdr region)) + nil ; let org-cycle handle it + (let ((width (markdown-table-wrap-buffer--effective-width))) + (markdown-table-wrap-buffer--wrap-region-1 + (car region) (cdr region) width) + t)) ; consume the TAB + nil))) + +(defvar markdown-table-wrap-buffer-mode-map + (let ((map (make-sparse-keymap))) + (define-key map markdown-table-wrap-buffer-key + #'markdown-table-wrap-table-at-point) + map) + "Keymap for `markdown-table-wrap-buffer-mode'. +TAB is bound context-sensitively via `:lighter' nil and a pre-command +check, OR (simpler and more reliable) bound directly when +`markdown-table-wrap-buffer-tab-override' is non-nil. We bind directly +in markdown-family modes; org uses `org-tab-first-hook' instead.") + +;;;###autoload +(define-minor-mode markdown-table-wrap-buffer-mode + "Toggle in-buffer table wrapping on TAB. +When on, TAB wraps the pipe/org table at point to the effective +width (window width, or `markdown-table-wrap-buffer-width'). +Off-table, TAB falls through to the major mode's normal binding. +A numeric prefix (`SPC u 60' in doom, `C-u 60' otherwise) wraps to +that width. + +`C-c C-w' wraps the table at point regardless of TAB override. + +Tables are rewritten in place, so wrapped text is real buffer text: +links stay clickable, isearch/yank/copy work. Re-wrapping at a new +width unwraps first (best-effort; undo to recover if a force-broken +header mangles). Org tables with `#+TBLFM' are refused. + +In org, TAB is wired via `org-tab-first-hook' (so visibility cycling +and src-block indent off-table are unaffected). In markdown-family +modes, TAB is bound in the minor-mode map with a context check." + :lighter " TWrap" + :group 'markdown-table-wrap-buffer + (cond + (markdown-table-wrap-buffer-mode + ;; Markdown-family: bind TAB in the minor mode map. + (when markdown-table-wrap-buffer-tab-override + (define-key markdown-table-wrap-buffer-mode-map (kbd "TAB") + #'markdown-table-wrap-buffer--markdown-tab) + (define-key markdown-table-wrap-buffer-mode-map [tab] + #'markdown-table-wrap-buffer--markdown-tab)) + ;; Org: register on `org-tab-first-hook' (buffer-local). + (add-hook 'org-tab-first-hook + #'markdown-table-wrap-buffer--org-tab-h nil t)) + (t + (define-key markdown-table-wrap-buffer-mode-map (kbd "TAB") nil) + (define-key markdown-table-wrap-buffer-mode-map [tab] nil) + (remove-hook 'org-tab-first-hook + #'markdown-table-wrap-buffer--org-tab-h t)))) + +;;;###autoload +(defun markdown-table-wrap-buffer-turn-on () + "Enable `markdown-table-wrap-buffer-mode' in the current buffer. +Suitable for `markdown-mode-hook', `md-ts-mode-hook', `org-mode-hook'. +Covers `gfm-mode' automatically (it derives from `markdown-mode', so +`markdown-mode-hook' fires for it). Skips `pi-coding-agent-chat-mode' +(which derives from `md-ts-mode') so this minor mode doesn't shadow +pi's own TAB in chat buffers — pi handles its chat tables separately." + (unless (derived-mode-p 'pi-coding-agent-chat-mode) + (markdown-table-wrap-buffer-mode 1))) + +(provide 'markdown-table-wrap-buffer) +;;; markdown-table-wrap-buffer.el ends here From 99cf97d7746f27352adba9f5c21275a354acc5d9 Mon Sep 17 00:00:00 2001 From: Sayre Blades Date: Fri, 31 Jul 2026 19:22:53 -0400 Subject: [PATCH 2/5] Fix TAB under evil: advise markdown-cycle instead of minor-mode-map binding 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. --- markdown-table-wrap-buffer.el | 66 ++++++++++++++++------------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/markdown-table-wrap-buffer.el b/markdown-table-wrap-buffer.el index 5053b5d..bb024b6 100644 --- a/markdown-table-wrap-buffer.el +++ b/markdown-table-wrap-buffer.el @@ -389,30 +389,25 @@ table." ;;;; Minor mode: context-sensitive TAB -(defun markdown-table-wrap-buffer--fallback-tab () - "Invoke the major mode's normal TAB binding. -In markdown/gfm/md-ts, `markdown-cycle'; in org, `org-cycle'; else -whatever the local map binds to TAB, else the raw TAB key." - (let ((cmd (cond - ((derived-mode-p 'org-mode) #'org-cycle) - ((derived-mode-p 'markdown-mode) #'markdown-cycle) - ((lookup-key (current-local-map) (kbd "TAB"))) - (t nil)))) - (if cmd - (call-interactively cmd) - (let ((current-prefix-arg nil)) - (execute-kbd-macro (kbd "TAB")))))) - -(defun markdown-table-wrap-buffer--markdown-tab (&optional arg) - "Context-sensitive TAB for markdown/gfm/md-ts. -On a table, wrap it (ARG overrides width like a numeric prefix). -Off-table, call `markdown-cycle'." - (interactive "P") +(defun markdown-table-wrap-buffer--markdown-cycle-a (orig &rest args) + "Around-advice on `markdown-cycle': wrap when on a table. +When point is on a pipe table, wrap it (consuming the TAB) and +return t; otherwise call ORIG with ARGS (the normal +`markdown-cycle' behavior). This works under evil because doom +binds normal-state TAB to `markdown-cycle' in +`evil-markdown-mode-map', so intercepting `markdown-cycle' catches +TAB regardless of evil state, without fighting evil's keymap +priority (a minor-mode-map TAB binding would be overridden by +the evil state map). Mirrors the org `org-tab-first-hook' pattern. + +`current-prefix-arg' is honored: a numeric prefix (`SPC u 60' in +doom, `C-u 60' otherwise) wraps to that width." (if-let* ((region (markdown-table-wrap-buffer--table-region-at-point))) - (let ((width (markdown-table-wrap-buffer--effective-width arg))) + (let ((width (markdown-table-wrap-buffer--effective-width + current-prefix-arg))) (markdown-table-wrap-buffer--wrap-region-1 (car region) (cdr region) width)) - (markdown-table-wrap-buffer--fallback-tab))) + (apply orig args))) (defun markdown-table-wrap-buffer--org-tab-h () "Hook for `org-tab-first-hook': wrap the org table at point. @@ -437,10 +432,10 @@ otherwise so `org-cycle' runs normally. Refuses tables with #'markdown-table-wrap-table-at-point) map) "Keymap for `markdown-table-wrap-buffer-mode'. -TAB is bound context-sensitively via `:lighter' nil and a pre-command -check, OR (simpler and more reliable) bound directly when -`markdown-table-wrap-buffer-tab-override' is non-nil. We bind directly -in markdown-family modes; org uses `org-tab-first-hook' instead.") +Only `markdown-table-wrap-buffer-key' (default `C-c C-w') is bound +here. TAB is intercepted via advice on `markdown-cycle' (markdown) +and `org-tab-first-hook' (org), so it works under evil without +fighting evil's state-map key priority.") ;;;###autoload (define-minor-mode markdown-table-wrap-buffer-mode @@ -458,25 +453,26 @@ links stay clickable, isearch/yank/copy work. Re-wrapping at a new width unwraps first (best-effort; undo to recover if a force-broken header mangles). Org tables with `#+TBLFM' are refused. -In org, TAB is wired via `org-tab-first-hook' (so visibility cycling -and src-block indent off-table are unaffected). In markdown-family -modes, TAB is bound in the minor-mode map with a context check." +In markdown-family modes, TAB is intercepted via `:around' advice on +`markdown-cycle' (works under evil because doom binds normal-state +TAB to `markdown-cycle'). In org, TAB is wired via +`org-tab-first-hook' (so visibility cycling and src-block indent +off-table are unaffected). Both off-table paths are unchanged." :lighter " TWrap" :group 'markdown-table-wrap-buffer (cond (markdown-table-wrap-buffer-mode - ;; Markdown-family: bind TAB in the minor mode map. + ;; Markdown-family: advise `markdown-cycle' (catches TAB under evil + ;; because doom binds normal-state TAB to `markdown-cycle'). (when markdown-table-wrap-buffer-tab-override - (define-key markdown-table-wrap-buffer-mode-map (kbd "TAB") - #'markdown-table-wrap-buffer--markdown-tab) - (define-key markdown-table-wrap-buffer-mode-map [tab] - #'markdown-table-wrap-buffer--markdown-tab)) + (advice-add 'markdown-cycle :around + #'markdown-table-wrap-buffer--markdown-cycle-a)) ;; Org: register on `org-tab-first-hook' (buffer-local). (add-hook 'org-tab-first-hook #'markdown-table-wrap-buffer--org-tab-h nil t)) (t - (define-key markdown-table-wrap-buffer-mode-map (kbd "TAB") nil) - (define-key markdown-table-wrap-buffer-mode-map [tab] nil) + (advice-remove 'markdown-cycle + #'markdown-table-wrap-buffer--markdown-cycle-a) (remove-hook 'org-tab-first-hook #'markdown-table-wrap-buffer--org-tab-h t)))) From 1382623c6397bf5547ebec7451e36c5d18b92ca3 Mon Sep 17 00:00:00 2001 From: Sayre Blades Date: Fri, 31 Jul 2026 20:45:05 -0400 Subject: [PATCH 3/5] TEMPORARILY DISABLE org TAB hook while diagnosing an org regression --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. --- markdown-table-wrap-buffer.el | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/markdown-table-wrap-buffer.el b/markdown-table-wrap-buffer.el index bb024b6..dd3bb64 100644 --- a/markdown-table-wrap-buffer.el +++ b/markdown-table-wrap-buffer.el @@ -413,18 +413,12 @@ doom, `C-u 60' otherwise) wraps to that width." "Hook for `org-tab-first-hook': wrap the org table at point. Returns t to consume the TAB when on a table (after wrapping), nil otherwise so `org-cycle' runs normally. Refuses tables with -`#+TBLFM' (falls through to `org-cycle')." - (when (and (derived-mode-p 'org-mode) - (org-at-table-p)) - (if-let* ((region (markdown-table-wrap-buffer--table-region-at-point))) - (if (markdown-table-wrap-buffer--org-table-has-formula-p - (car region) (cdr region)) - nil ; let org-cycle handle it - (let ((width (markdown-table-wrap-buffer--effective-width))) - (markdown-table-wrap-buffer--wrap-region-1 - (car region) (cdr region) width) - t)) ; consume the TAB - nil))) +`#+TBLFM' (falls through to `org-cycle'). + +TEMPORARILY DISABLED: returns nil always while we diagnose an org +regression. Org tables can still be wrapped via +`markdown-table-wrap-table-at-point' (\[markdown-table-wrap-table-at-point])." + nil) (defvar markdown-table-wrap-buffer-mode-map (let ((map (make-sparse-keymap))) From def40a486753138d8eb2b93c70c211b5b6a51328 Mon Sep 17 00:00:00 2001 From: Sayre Blades Date: Fri, 31 Jul 2026 21:11:24 -0400 Subject: [PATCH 4/5] Fix --table-region-at-point: walk backward to table start 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. --- markdown-table-wrap-buffer.el | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/markdown-table-wrap-buffer.el b/markdown-table-wrap-buffer.el index dd3bb64..397b122 100644 --- a/markdown-table-wrap-buffer.el +++ b/markdown-table-wrap-buffer.el @@ -157,6 +157,14 @@ nil if point is not on a table line." (beginning-of-line) (if (not (looking-at-p markdown-table-wrap-buffer--table-line-re)) nil + ;; Walk backward to the first table line so point anywhere in + ;; the table (header, separator, or a data row) captures the + ;; whole table, not just the suffix from point onward. + (while (and (not (bobp)) + (save-excursion + (forward-line -1) + (looking-at-p markdown-table-wrap-buffer--table-line-re))) + (forward-line -1)) (let ((start (point))) (while (looking-at-p markdown-table-wrap-buffer--table-line-re) (forward-line 1)) From 3b6ea6a26f10bd321b9fc0f3134d2e633a03d075 Mon Sep 17 00:00:00 2001 From: Sayre Blades Date: Fri, 31 Jul 2026 21:54:11 -0400 Subject: [PATCH 5/5] Re-enable org TAB hook: regression was the pre-def40a4 backward-walk bug 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). --- markdown-table-wrap-buffer.el | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/markdown-table-wrap-buffer.el b/markdown-table-wrap-buffer.el index 397b122..0b22eeb 100644 --- a/markdown-table-wrap-buffer.el +++ b/markdown-table-wrap-buffer.el @@ -423,10 +423,33 @@ Returns t to consume the TAB when on a table (after wrapping), nil otherwise so `org-cycle' runs normally. Refuses tables with `#+TBLFM' (falls through to `org-cycle'). -TEMPORARILY DISABLED: returns nil always while we diagnose an org -regression. Org tables can still be wrapped via -`markdown-table-wrap-table-at-point' (\[markdown-table-wrap-table-at-point])." - nil) +When point is on an org table, wrap it to the effective width and +return t to consume the TAB; off-table return nil so `org-cycle' runs +(visibility cycling, src-block indent, etc.). This mirrors how +doom's own `+org-*' hooks participate in `org-tab-first-hook'. + +`current-prefix-arg' is honored: a numeric prefix (`SPC u 60' in +doom, `C-u 60' otherwise) wraps to that width. Tables carrying a +`#+TBLFM:' formula line are refused (return nil) so `org-cycle' +handles them untouched. + +Note: `--table-region-at-point' walks backward to the table start, +so TAB anywhere in the table (header, hline, or a data row) wraps +the whole table. (An earlier revision that did not walk backward +mangled tables when TAB was pressed on a data row; that was the +regression that prompted temporarily disabling this hook.)" + (when (and (derived-mode-p 'org-mode) + (org-at-table-p)) + (if-let* ((region (markdown-table-wrap-buffer--table-region-at-point))) + (if (markdown-table-wrap-buffer--org-table-has-formula-p + (car region) (cdr region)) + nil ; let org-cycle handle it + (let ((width (markdown-table-wrap-buffer--effective-width + current-prefix-arg))) + (markdown-table-wrap-buffer--wrap-region-1 + (car region) (cdr region) width) + t)) ; consume the TAB + nil))) (defvar markdown-table-wrap-buffer-mode-map (let ((map (make-sparse-keymap)))