diff --git a/README.md b/README.md index 8fa2ef93..bd175a44 100644 --- a/README.md +++ b/README.md @@ -506,6 +506,69 @@ can obtain a list of all keybindings by pressing C-c C-h. to the end of the current or following defun, and C-M-h will put the region around the entire defun. + * Table Editing: + + Markdown Mode includes support for editing tables, which + have the following basic format: + + | Right | Left | Center | Default | + |------:|:-----|:------:|---------| + | 12 | 12 | 12 | 12 | + | 123 | 123 | 123 | 123 | + | 1 | 1 | 1 | 1 | + + The first line contains column headers. The second line + contains a separator line between the headers and the content. + Each following line is a row in the table. Columns are always + separated by the pipe character. The colons indicate column + alignment. + + A table is re-aligned automatically each time you press TAB + or RET inside the table. TAB also moves to the next + field (RET to the next row) and creates new table rows at + the end of the table or before horizontal separator lines. The + indentation of the table is set by the first line. Column + centering inside Emacs is not supported. + + Beginning pipe characters are required for proper detection of + table borders inside Emacs. Any line starting with `|-` or `|:` + is considered as a horizontal separator line and will be + expanded on the next re-align to span the whole table width. No + padding is allowed between the beginning pipe character and + header separator symbol. So, to create the above table, you + would only type + + |Right|Left|Center|Default| + |- + + and then press TAB to align the table and start filling in + cells. + + Then you can jump with TAB from one cell to the next or with + S-TAB to the previous one. RET will jump to the to the + next cell in the same column, and create a new row if there is + no such cell or if the next row is beyond a separator line. + + You can also convert selected region to a table. Basic editing + capabilities include inserting, deleting, and moving of columns + and rows, and table re-alignment, sorting, transposition: + + - C-c UP or C-c DOWN - Move the current row up or down. + - C-c LEFT or C-c RIGHT - Move the current column left or right. + - C-c S-UP - Kill the current row. + - C-c S-DOWN - Insert a row above the current row. With a + prefix argument, row line is created below the current one. + - C-c S-LEFT - Kill the current column. + - C-c S-RIGHT - Insert a new column to the left of the current one. + - C-c C-d - Re-align the current table (`markdown-do`). + - C-c C-c ^ - Sort table lines alpabetically or numerically. + - C-c C-c | - Convert selected region to a table. + - C-c C-c t - Transpose table at point. + + The table editing functions try to handle markup hiding + correctly when calculating column widths, however, columns + containig hidden markup may not always be aligned properly. + * Miscellaneous Commands: When the [`edit-indirect`][ei] package is installed, C-c ` diff --git a/markdown-mode.el b/markdown-mode.el index 9b657bba..891b1d69 100644 --- a/markdown-mode.el +++ b/markdown-mode.el @@ -522,6 +522,69 @@ ;; to the end of the current or following defun, and `C-M-h` will ;; put the region around the entire defun. ;; +;; * Table Editing: +;; +;; Markdown Mode includes support for editing tables, which +;; have the following basic format: +;; +;; | Right | Left | Center | Default | +;; |------:|:-----|:------:|---------| +;; | 12 | 12 | 12 | 12 | +;; | 123 | 123 | 123 | 123 | +;; | 1 | 1 | 1 | 1 | +;; +;; The first line contains column headers. The second line +;; contains a separator line between the headers and the content. +;; Each following line is a row in the table. Columns are always +;; separated by the pipe character. The colons indicate column +;; alignment. +;; +;; A table is re-aligned automatically each time you press `TAB` +;; or `RET` inside the table. `TAB` also moves to the next +;; field (`RET` to the next row) and creates new table rows at +;; the end of the table or before horizontal separator lines. The +;; indentation of the table is set by the first line. Column +;; centering inside Emacs is not supported. +;; +;; Beginning pipe characters are required for proper detection of +;; table borders inside Emacs. Any line starting with `|-` or `|:` +;; is considered as a horizontal separator line and will be +;; expanded on the next re-align to span the whole table width. No +;; padding is allowed between the beginning pipe character and +;; header separator symbol. So, to create the above table, you +;; would only type +;; +;; |Right|Left|Center|Default| +;; |- +;; +;; and then press `TAB` to align the table and start filling in +;; cells. +;; +;; Then you can jump with `TAB` from one cell to the next or with +;; `S-TAB` to the previous one. `RET` will jump to the to the +;; next cell in the same column, and create a new row if there is +;; no such cell or if the next row is beyond a separator line. +;; +;; You can also convert selected region to a table. Basic editing +;; capabilities include inserting, deleting, and moving of columns +;; and rows, and table re-alignment, sorting, transposition: +;; +;; - `C-c UP` or `C-c DOWN` - Move the current row up or down. +;; - `C-c LEFT` or `C-c RIGHT` - Move the current column left or right. +;; - `C-c S-UP` - Kill the current row. +;; - `C-c S-DOWN` - Insert a row above the current row. With a +;; prefix argument, row line is created below the current one. +;; - `C-c S-LEFT` - Kill the current column. +;; - `C-c S-RIGHT` - Insert a new column to the left of the current one. +;; - `C-c C-d` - Re-align the current table (`markdown-do`). +;; - `C-c C-c ^` - Sort table lines alpabetically or numerically. +;; - `C-c C-c |` - Convert selected region to a table. +;; - `C-c C-c t` - Transpose table at point. +;; +;; The table editing functions try to handle markup hiding +;; correctly when calculating column widths, however, columns +;; containig hidden markup may not always be aligned properly. +;; ;; * Miscellaneous Commands: ;; ;; When the [`edit-indirect'][ei] package is installed, `C-c '` @@ -5445,7 +5508,9 @@ duplicate positions, which are handled up by calling functions." (reverse positions))) (defun markdown-enter-key () - "Handle RET according to value of `markdown-indent-on-enter'. + "Handle RET depending on the context. +If the point is at a table, move to the next row. Otherwise, +indent according to value of `markdown-indent-on-enter'. When it is nil, simply call `newline'. Otherwise, indent the next line following RET using `markdown-indent-line'. Furthermore, when it is set to 'indent-and-new-item and the point is in a list item, @@ -5453,8 +5518,12 @@ start a new item with the same indentation. If the point is in an empty list item, remove it (so that pressing RET twice when in a list simply adds a blank line)." (interactive) - (if (not markdown-indent-on-enter) - (newline) + (cond + ;; Table + ((markdown-table-at-point-p) + (call-interactively #'markdown-table-next-row)) + ;; Indent non-table text + (markdown-indent-on-enter (let (bounds) (if (and (memq markdown-indent-on-enter '(indent-and-new-item)) (setq bounds (markdown-cur-list-item-bounds))) @@ -5471,7 +5540,9 @@ list simply adds a blank line)." (call-interactively #'markdown-insert-list-item))) ;; Point is not in a list (newline) - (markdown-indent-line))))) + (markdown-indent-line)))) + ;; Insert a raw newline + (t (newline)))) (define-obsolete-function-alias 'markdown-exdent-or-delete 'markdown-outdent-or-delete "v2.3") @@ -5836,6 +5907,9 @@ Assumes match data is available for `markdown-regex-italic'." (define-key map (kbd "c") 'markdown-check-refs) (define-key map (kbd "n") 'markdown-cleanup-list-numbers) (define-key map (kbd "]") 'markdown-complete-buffer) + (define-key map (kbd "^") 'markdown-table-sort-lines) + (define-key map (kbd "|") 'markdown-table-convert-region) + (define-key map (kbd "t") 'markdown-table-transpose) map) "Keymap for Markdown buffer-wide commands.") @@ -5859,7 +5933,7 @@ Assumes match data is available for `markdown-regex-italic'." (define-key map (kbd "C-c >") 'markdown-indent-region) (define-key map (kbd "C-c <") 'markdown-outdent-region) ;; Visibility cycling - (define-key map (kbd "TAB") 'markdown-cycle) + (define-key map (kbd "TAB") 'markdown-tab) (define-key map (kbd "") 'markdown-shifttab) (define-key map (kbd "") 'markdown-shifttab) (define-key map (kbd "") 'markdown-shifttab) @@ -5871,11 +5945,15 @@ Assumes match data is available for `markdown-regex-italic'." (define-key map (kbd "C-c C-u") 'markdown-outline-up) ;; Buffer-wide commands (define-key map (kbd "C-c C-c") markdown-mode-command-map) - ;; Subtree and list editing + ;; Subtree, list, and table editing (define-key map (kbd "C-c ") 'markdown-move-up) (define-key map (kbd "C-c ") 'markdown-move-down) (define-key map (kbd "C-c ") 'markdown-promote) (define-key map (kbd "C-c ") 'markdown-demote) + (define-key map (kbd "C-c S-") 'markdown-table-delete-row) + (define-key map (kbd "C-c S-") 'markdown-table-insert-row) + (define-key map (kbd "C-c S-") 'markdown-table-delete-column) + (define-key map (kbd "C-c S-") 'markdown-table-insert-column) (define-key map (kbd "C-c C-M-h") 'markdown-mark-subtree) (define-key map (kbd "C-x n s") 'markdown-narrow-to-subtree) (define-key map (kbd "M-") 'markdown-insert-list-item) @@ -5950,6 +6028,14 @@ Assumes match data is available for `markdown-regex-italic'." "Keymap for `gfm-mode'. See also `markdown-mode-map'.") +(defun markdown-tab () + "Handle TAB key based on context." + (interactive) + (cond + ((markdown-table-at-point-p) + (call-interactively #'markdown-table-forward-cell)) + (t (call-interactively #'markdown-cycle)))) + ;;; Menu ================================================================== @@ -7239,10 +7325,14 @@ as appropriate, by calling `indent-for-tab-command'." (indent-for-tab-command)))) (defun markdown-shifttab () - "Global visibility cycling. -Calls `markdown-cycle' with argument t." + "Handle S-TAB keybinding based on context. +When in a table, move backward one cell. +Otherwise, cycle global heading visibility by calling +`markdown-cycle' with argument t." (interactive) - (markdown-cycle t)) + (cond ((markdown-table-at-point-p) + (call-interactively #'markdown-table-backward-cell)) + (t (markdown-cycle t)))) (defun markdown-outline-level () "Return the depth to which a statement is nested in the outline." @@ -7459,14 +7549,17 @@ This puts point at the start of the current subtree, and mark at the end." (defun markdown-move-up () "Move thing at point up. When in a list item, call `markdown-move-list-item-up'. +When in a table, call `markdown-table-move-row-up'. Otherwise, move the current heading subtree up with `markdown-move-subtree-up'." (interactive) (cond ((markdown-list-item-at-point-p) - (markdown-move-list-item-up)) + (call-interactively #'markdown-move-list-item-up)) + ((markdown-table-at-point-p) + (call-interactively #'markdown-table-move-row-up)) (t - (markdown-move-subtree-up)))) + (call-interactively #'markdown-move-subtree-up)))) (defun markdown-move-down () "Move thing at point down. @@ -7476,14 +7569,17 @@ Otherwise, move the current heading subtree up with (interactive) (cond ((markdown-list-item-at-point-p) - (markdown-move-list-item-down)) + (call-interactively #'markdown-move-list-item-down)) + ((markdown-table-at-point-p) + (call-interactively #'markdown-table-move-row-down)) (t - (markdown-move-subtree-down)))) + (call-interactively #'markdown-move-subtree-down)))) (defun markdown-promote () - "Either promote header or list item at point or cycle markup. -See `markdown-cycle-atx', `markdown-cycle-setext', and -`markdown-promote-list-item'." + "Promote or move element at point to the left. +Depending on the context, this function will promote a heading or +list item at the point, move a table column to the left, or cycle +markup." (interactive) (let (bounds) (cond @@ -7499,6 +7595,9 @@ See `markdown-cycle-atx', `markdown-cycle-setext', and ;; Promote list item ((setq bounds (markdown-cur-list-item-bounds)) (markdown-promote-list-item bounds)) + ;; Move table column to the left + ((markdown-table-at-point-p) + (call-interactively #'markdown-table-move-column-left)) ;; Promote bold ((thing-at-point-looking-at markdown-regex-bold) (markdown-cycle-bold)) @@ -7509,9 +7608,10 @@ See `markdown-cycle-atx', `markdown-cycle-setext', and (user-error "Nothing to promote at point"))))) (defun markdown-demote () - "Either demote header or list item at point or cycle or remove markup. -See `markdown-cycle-atx', `markdown-cycle-setext', and -`markdown-demote-list-item'." + "Demote or move element at point to the right. +Depending on the context, this function will demote a heading or +list item at the point, move a table column to the right, or cycle +or remove markup." (interactive) (let (bounds) (cond @@ -7527,6 +7627,9 @@ See `markdown-cycle-atx', `markdown-cycle-setext', and ;; Demote list item ((setq bounds (markdown-cur-list-item-bounds)) (markdown-demote-list-item bounds)) + ;; Move table column to the right + ((markdown-table-at-point-p) + (call-interactively #'markdown-table-move-column-right)) ;; Demote bold ((thing-at-point-looking-at markdown-regex-bold) (markdown-cycle-bold)) @@ -8356,6 +8459,9 @@ markers and footnote text." ;; GFM task list item ((markdown-gfm-task-list-item-at-point) (markdown-toggle-gfm-checkbox)) + ;; Align table + ((markdown-table-at-point-p) + (call-interactively #'markdown-table-align)) ;; Otherwise (t (markdown-insert-gfm-checkbox)))) @@ -8927,6 +9033,643 @@ position." (package-install 'edit-indirect) (markdown-edit-code-block)))))) + +;;; Table Editing + +;; These functions were originally adapted from `org-table.el'. + +;; General helper functions + +(defmacro markdown--with-gensyms (symbols &rest body) + (declare (debug (sexp body)) (indent 1)) + `(let ,(mapcar (lambda (s) + `(,s (make-symbol (concat "--" (symbol-name ',s))))) + symbols) + ,@body)) + +(defun markdown--split-string (string &optional separators) + "Splits STRING into substrings at SEPARATORS. +SEPARATORS is a regular expression. If nil it defaults to +`split-string-default-separators'. This version returns no empty +strings if there are matches at the beginning and end of string." + (let ((start 0) notfirst list) + (while (and (string-match + (or separators split-string-default-separators) + string + (if (and notfirst + (= start (match-beginning 0)) + (< start (length string))) + (1+ start) start)) + (< (match-beginning 0) (length string))) + (setq notfirst t) + (or (eq (match-beginning 0) 0) + (and (eq (match-beginning 0) (match-end 0)) + (eq (match-beginning 0) start)) + (push (substring string start (match-beginning 0)) list)) + (setq start (match-end 0))) + (or (eq start (length string)) + (push (substring string start) list)) + (nreverse list))) + +(defun markdown--string-width (s) + "Return width of string S. +This version ignores characters with invisibility property +`markdown-markup'." + (let (b) + (when (or (eq t buffer-invisibility-spec) + (member 'markdown-markup buffer-invisibility-spec)) + (while (setq b (text-property-any + 0 (length s) + 'invisible 'markdown-markup s)) + (setq s (concat + (substring s 0 b) + (substring s (or (next-single-property-change + b 'invisible s) + (length s)))))))) + (string-width s)) + +(defun markdown--remove-invisible-markup (s) + "Remove Markdown markup from string S. +This version removes characters with invisibility property +`markdown-markup'." + (let (b) + (while (setq b (text-property-any + 0 (length s) + 'invisible 'markdown-markup s)) + (setq s (concat + (substring s 0 b) + (substring s (or (next-single-property-change + b 'invisible s) + (length s))))))) + s) + +;; Functions for maintaining tables + +(defconst markdown-table-line-regexp "^[ \t]*|" + "Regexp matching any line inside a table.") + +(defconst markdown-table-hline-regexp "^[ \t]*|[-:]" + "Regexp matching hline inside a table.") + +(defconst markdown-table-dline-regexp "^[ \t]*|[^-:]" + "Regexp matching dline inside a table.") + +(defconst markdown-table-border-regexp "^[ \t]*[^| \t]" + "Regexp matching any line outside a table.") + +(defun markdown-table-at-point-p () + "Return non-nil when point is inside a table." + (save-excursion + (beginning-of-line) + (and (looking-at-p markdown-table-line-regexp) + (not (markdown-code-block-at-point-p))))) + +(defun markdown-table-hline-at-point-p () + "Return non-nil when point is on a hline in a table. +This function assumes point is on a table." + (save-excursion + (beginning-of-line) + (looking-at-p markdown-table-hline-regexp))) + +(defun markdown-table-begin () + "Find the beginning of the table and return its position. +This function assumes point is on a table." + (cond + ((save-excursion + (and (re-search-backward markdown-table-border-regexp nil t) + (line-beginning-position 2)))) + (t (point-min)))) + +(defun markdown-table-end () + "Find the end of the table and return its position. +This function assumes point is on a table." + (save-excursion + (cond + ((re-search-forward markdown-table-border-regexp nil t) + (match-beginning 0)) + (t (goto-char (point-max)) + (skip-chars-backward " \t") + (if (bolp) (point) (line-end-position)))))) + +(defun markdown-table-get-dline () + "Return index of the table data line at point. +This function assumes point is on a table." + (let ((pos (point)) (end (markdown-table-end)) (cnt 0)) + (save-excursion + (goto-char (markdown-table-begin)) + (while (and (re-search-forward + markdown-table-dline-regexp end t) + (setq cnt (1+ cnt)) + (< (point-at-eol) pos)))) + cnt)) + +(defun markdown-table-get-column () + "Return table column at point. +This function assumes point is on a table." + (let ((pos (point)) (cnt 0)) + (save-excursion + (beginning-of-line) + (while (search-forward "|" pos t) (setq cnt (1+ cnt)))) + cnt)) + +(defun markdown-table-get-cell (&optional n) + "Return the content of the cell in column N of current row. +N defaults to column at point. This function assumes point is on +a table." + (and n (markdown-table-goto-column n)) + (skip-chars-backward "^|\n") (backward-char 1) + (if (looking-at "|[^|\r\n]*") + (let* ((pos (match-beginning 0)) + (val (buffer-substring (1+ pos) (match-end 0)))) + (goto-char (min (point-at-eol) (+ 2 pos))) + ;; Trim whitespaces + (setq val (replace-regexp-in-string "\\`[ \t]+" "" val) + val (replace-regexp-in-string "[ \t]+\\'" "" val))) + (forward-char 1) "")) + +(defun markdown-table-goto-dline (N) + "Go to the Nth data line in the table at point. +Return t when the line exists, nil otherwise. This function +assumes point is on a table." + (goto-char (markdown-table-begin)) + (let ((end (markdown-table-end)) (cnt 0)) + (while (and (re-search-forward + markdown-table-dline-regexp end t) + (< (setq cnt (1+ cnt)) N))) + (= cnt N))) + +(defun markdown-table-goto-column (n &optional on-delim) + "Go to the Nth column in the table line at point. +With optional argument ON-DELIM, stop with point before the left +delimiter of the cell. If there are less than N cells, just go +beyond the last delimiter. This function assumes point is on a +table." + (beginning-of-line 1) + (when (> n 0) + (while (and (> (setq n (1- n)) -1) + (search-forward "|" (point-at-eol) t))) + (if on-delim + (backward-char 1) + (when (looking-at " ") (forward-char 1))))) + +(defmacro markdown-table-save-cell (&rest body) + "Save cell at point, execute BODY and restore cell. +This function assumes point is on a table." + (declare (debug (body))) + (markdown--with-gensyms (line column) + `(let ((,line (copy-marker (line-beginning-position))) + (,column (markdown-table-get-column))) + (unwind-protect + (progn ,@body) + (goto-char ,line) + (markdown-table-goto-column ,column) + (set-marker ,line nil))))) + +(defun markdown-table-blank-line (s) + "Convert a table line S into a line with blank cells." + (if (string-match "^[ \t]*|-" s) + (setq s (mapconcat + (lambda (x) (if (member x '(?| ?+)) "|" " ")) + s "")) + (while (string-match "|\\([ \t]*?[^ \t\r\n|][^\r\n|]*\\)|" s) + (setq s (replace-match + (concat "|" (make-string (length (match-string 1 s)) ?\ ) "|") + t t s))) + s)) + +(defun markdown-table-colfmt (fmtspec) + "Process column alignment specifier FMTSPEC for tables." + (when (stringp fmtspec) + (mapcar (lambda (x) + (cond ((string-match-p "^:.*:$" x) 'c) + ((string-match-p "^:" x) 'l) + ((string-match-p ":$" x) 'r) + (t 'd))) + (markdown--split-string fmtspec "\\s-*|\\s-*")))) + +(defun markdown-table-align () + "Align table at point. +This function assumes point is on a table." + (interactive) + (let ((begin (markdown-table-begin)) + (end (copy-marker (markdown-table-end)))) + (markdown-table-save-cell + (goto-char begin) + (let* (fmtspec + ;; Store table indent + (indent (progn (looking-at "[ \t]*") (match-string 0))) + ;; Split table in lines and save column format specifier + (lines (mapcar (lambda (l) + (if (string-match-p "\\`[ \t]*|[-:]" l) + (progn (setq fmtspec (or fmtspec l)) nil) l)) + (markdown--split-string (buffer-substring begin end) "\n"))) + ;; Split lines in cells + (cells (mapcar (lambda (l) (markdown--split-string l "\\s-*|\\s-*")) + (remq nil lines))) + ;; Calculate maximum number of cells in a line + (maxcells (if cells + (apply #'max (mapcar #'length cells)) + (user-error "Empty table"))) + ;; Empty cells to fill short lines + (emptycells (make-list maxcells "")) maxwidths) + ;; Calculate maximum width for each column + (dotimes (i maxcells) + (let ((column (mapcar (lambda (x) (or (nth i x) "")) cells))) + (push (apply #'max 1 (mapcar #'markdown--string-width column)) + maxwidths))) + (setq maxwidths (nreverse maxwidths)) + ;; Process column format specifier + (setq fmtspec (markdown-table-colfmt fmtspec)) + ;; Compute formats needed for output of table lines + (let ((hfmt (concat indent "|")) + (rfmt (concat indent "|")) + hfmt1 rfmt1 fmt) + (dolist (width maxwidths (setq hfmt (concat (substring hfmt 0 -1) "|"))) + (setq fmt (pop fmtspec)) + (cond ((equal fmt 'l) (setq hfmt1 ":%s-|" rfmt1 " %%-%ds |")) + ((equal fmt 'r) (setq hfmt1 "-%s:|" rfmt1 " %%%ds |")) + ((equal fmt 'c) (setq hfmt1 ":%s:|" rfmt1 " %%-%ds |")) + (t (setq hfmt1 "-%s-|" rfmt1 " %%-%ds |"))) + (setq rfmt (concat rfmt (format rfmt1 width))) + (setq hfmt (concat hfmt (format hfmt1 (make-string width ?-))))) + ;; Replace modified lines only + (dolist (line lines) + (let ((line (if line + (apply #'format rfmt (append (pop cells) emptycells)) + hfmt)) + (previous (buffer-substring (point) (line-end-position)))) + (if (equal previous line) + (forward-line) + (insert line "\n") + (delete-region (point) (line-beginning-position 2)))))) + (set-marker end nil))))) + +(defun markdown-table-insert-row (&optional arg) + "Insert a new row above the row at point into the table. +With optional argument ARG, insert below the current row." + (interactive "P") + (unless (markdown-table-at-point-p) + (user-error "Not at a table")) + (let* ((line (buffer-substring + (line-beginning-position) (line-end-position))) + (new (markdown-table-blank-line line))) + (beginning-of-line (if arg 2 1)) + (unless (bolp) (insert "\n")) + (insert-before-markers new "\n") + (beginning-of-line 0) + (re-search-forward "| ?" (line-end-position) t))) + +(defun markdown-table-delete-row () + "Delete row or horizontal line at point from the table." + (interactive) + (unless (markdown-table-at-point-p) + (user-error "Not at a table")) + (let ((col (current-column))) + (kill-region (point-at-bol) + (min (1+ (point-at-eol)) (point-max))) + (unless (markdown-table-at-point-p) (beginning-of-line 0)) + (move-to-column col))) + +(defun markdown-table-move-row (&optional up) + "Move table line at point down. +With optional argument UP, move it up." + (interactive "P") + (unless (markdown-table-at-point-p) + (user-error "Not at a table")) + (let* ((col (current-column)) (pos (point)) + (tonew (if up 0 2)) txt) + (beginning-of-line tonew) + (unless (markdown-table-at-point-p) + (goto-char pos) (user-error "Cannot move row further")) + (goto-char pos) (beginning-of-line 1) (setq pos (point)) + (setq txt (buffer-substring (point) (1+ (point-at-eol)))) + (delete-region (point) (1+ (point-at-eol))) + (beginning-of-line tonew) + (insert txt) (beginning-of-line 0) + (move-to-column col))) + +(defun markdown-table-move-row-up () + "Move table row at point up." + (interactive) + (markdown-table-move-row 'up)) + +(defun markdown-table-move-row-down () + "Move table row at point down." + (interactive) + (markdown-table-move-row nil)) + +(defun markdown-table-insert-column () + "Insert a new table column." + (interactive) + (unless (markdown-table-at-point-p) + (user-error "Not at a table")) + (let* ((col (max 1 (markdown-table-get-column))) + (begin (markdown-table-begin)) + (end (copy-marker (markdown-table-end)))) + (markdown-table-save-cell + (goto-char begin) + (while (< (point) end) + (markdown-table-goto-column col t) + (if (markdown-table-hline-at-point-p) + (insert "|---") + (insert "| ")) + (forward-line))) + (set-marker end nil) + (markdown-table-align))) + +(defun markdown-table-delete-column () + "Delete column at point from table." + (interactive) + (unless (markdown-table-at-point-p) + (user-error "Not at a table")) + (let ((col (markdown-table-get-column)) + (begin (markdown-table-begin)) + (end (copy-marker (markdown-table-end)))) + (markdown-table-save-cell + (goto-char begin) + (while (< (point) end) + (markdown-table-goto-column col t) + (and (looking-at "|[^|\n]+|") + (replace-match "|")) + (forward-line))) + (set-marker end nil) + (markdown-table-goto-column (max 1 (1- col))) + (markdown-table-align))) + +(defun markdown-table-move-column (&optional left) + "Move table column at point to the right. +With optional argument LEFT, move it to the left." + (interactive "P") + (unless (markdown-table-at-point-p) + (user-error "Not at a table")) + (let* ((col (markdown-table-get-column)) + (col1 (if left (1- col) col)) + (colpos (if left (1- col) (1+ col))) + (begin (markdown-table-begin)) + (end (copy-marker (markdown-table-end)))) + (when (and left (= col 1)) + (user-error "Cannot move column further left")) + (when (and (not left) (looking-at "[^|\n]*|[^|\n]*$")) + (user-error "Cannot move column further right")) + (markdown-table-save-cell + (goto-char begin) + (while (< (point) end) + (markdown-table-goto-column col1 t) + (when (looking-at "|\\([^|\n]+\\)|\\([^|\n]+\\)|") + (replace-match "|\\2|\\1|")) + (forward-line))) + (set-marker end nil) + (markdown-table-goto-column colpos) + (markdown-table-align))) + +(defun markdown-table-move-column-left () + "Move table column at point to the left." + (interactive) + (markdown-table-move-column 'left)) + +(defun markdown-table-move-column-right () + "Move table column at point to the right." + (interactive) + (markdown-table-move-column nil)) + +(defun markdown-table-next-row () + "Go to the next row (same column) in the table. +Create new table lines if required." + (interactive) + (unless (markdown-table-at-point-p) + (user-error "Not at a table")) + (if (or (looking-at "[ \t]*$") + (save-excursion (skip-chars-backward " \t") (bolp))) + (newline) + (markdown-table-align) + (let ((col (markdown-table-get-column))) + (beginning-of-line 2) + (if (or (not (markdown-table-at-point-p)) + (markdown-table-hline-at-point-p)) + (progn + (beginning-of-line 0) + (markdown-table-insert-row 'below))) + (markdown-table-goto-column col) + (skip-chars-backward "^|\n\r") + (when (looking-at " ") (forward-char 1))))) + +(defun markdown-table-forward-cell () + "Go to the next cell in the table. +Create new table lines if required." + (interactive) + (unless (markdown-table-at-point-p) + (user-error "Not at a table")) + (markdown-table-align) + (let ((end (markdown-table-end))) + (when (markdown-table-hline-at-point-p) (end-of-line 1)) + (condition-case nil + (progn + (re-search-forward "|" end) + (if (looking-at "[ \t]*$") + (re-search-forward "|" end)) + (if (and (looking-at "[-:]") + (re-search-forward "^[ \t]*|\\([^-:]\\)" end t)) + (goto-char (match-beginning 1))) + (if (looking-at "[-:]") + (progn + (beginning-of-line 0) + (markdown-table-insert-row 'below)) + (when (looking-at " ") (forward-char 1)))) + (error (markdown-table-insert-row 'below))))) + +(defun markdown-table-backward-cell () + "Go to the previous cell in the table." + (interactive) + (unless (markdown-table-at-point-p) + (user-error "Not at a table")) + (markdown-table-align) + (when (markdown-table-hline-at-point-p) (end-of-line 1)) + (condition-case nil + (progn + (re-search-backward "|" (markdown-table-begin)) + (re-search-backward "|" (markdown-table-begin))) + (error (user-error "Cannot move to previous table cell"))) + (while (looking-at "|\\([-:]\\|[ \t]*$\\)") + (re-search-backward "|" (markdown-table-begin))) + (when (looking-at "| ?") (goto-char (match-end 0)))) + +(defun markdown-table-transpose () + "Transpose table at point. +Horizontal separator lines will be eliminated." + (interactive) + (unless (markdown-table-at-point-p) + (user-error "Not at a table")) + (let* ((table (buffer-substring-no-properties + (markdown-table-begin) (markdown-table-end))) + ;; Convert table to a Lisp structure + (table (delq nil + (mapcar + (lambda (x) + (unless (string-match-p + markdown-table-hline-regexp x) + (markdown--split-string x "\\s-*|\\s-*"))) + (markdown--split-string table "[ \t]*\n[ \t]*")))) + (dline_old (markdown-table-get-dline)) + (col_old (markdown-table-get-column)) + (contents (mapcar (lambda (_) + (let ((tp table)) + (mapcar + (lambda (_) + (prog1 + (pop (car tp)) + (setq tp (cdr tp)))) + table))) + (car table)))) + (goto-char (markdown-table-begin)) + (re-search-forward "|") (backward-char) + (delete-region (point) (markdown-table-end)) + (insert (mapconcat + (lambda(x) + (concat "| " (mapconcat 'identity x " | " ) " |\n")) + contents "")) + (markdown-table-goto-dline col_old) + (markdown-table-goto-column dline_old)) + (markdown-table-align)) + +(defun markdown-table-sort-lines (&optional sorting-type) + "Sort table lines according to the column at point. + +The position of point indicates the column to be used for +sorting, and the range of lines is the range between the nearest +horizontal separator lines, or the entire table of no such lines +exist. If point is before the first column, user will be prompted +for the sorting column. If there is an active region, the mark +specifies the first line and the sorting column, while point +should be in the last line to be included into the sorting. + +The command then prompts for the sorting type which can be +alphabetically or numerically. Sorting in reverse order is also +possible. + +If SORTING-TYPE is specified when this function is called from a +Lisp program, no prompting will take place. SORTING-TYPE must be +a character, any of (?a ?A ?n ?N) where the capital letters +indicate that sorting should be done in reverse order." + (interactive) + (unless (markdown-table-at-point-p) + (user-error "Not at a table")) + ;; Set sorting type and column used for sorting + (let ((column (let ((c (markdown-table-get-column))) + (cond ((> c 0) c) + ((called-interactively-p 'any) + (read-number "Use column N for sorting: ")) + (t 1)))) + (sorting-type + (or sorting-type + (read-char-exclusive + "Sort type: [a]lpha [n]umeric (A/N means reversed): ")))) + (save-restriction + ;; Narrow buffer to appropriate sorting area + (if (region-active-p) + (narrow-to-region + (save-excursion + (progn + (goto-char (region-beginning)) (line-beginning-position))) + (save-excursion + (progn + (goto-char (region-end)) (line-end-position)))) + (let ((start (markdown-table-begin)) + (end (markdown-table-end))) + (narrow-to-region + (save-excursion + (if (re-search-backward + markdown-table-hline-regexp start t) + (line-beginning-position 2) + start)) + (if (save-excursion (re-search-forward + markdown-table-hline-regexp end t)) + (match-beginning 0) + end)))) + ;; Determine arguments for `sort-subr' + (let* ((extract-key-from-cell + (cl-case sorting-type + ((?a ?A) #'markdown--remove-invisible-markup) ;; #'identity) + ((?n ?N) #'string-to-number) + (t (user-error "Invalid sorting type: %c" sorting-type)))) + (predicate + (cl-case sorting-type + ((?n ?N) #'<) + ((?a ?A) #'string<)))) + ;; Sort selected area + (goto-char (point-min)) + (sort-subr (memq sorting-type '(?A ?N)) + (lambda () + (forward-line) + (while (and (not (eobp)) + (not (looking-at + markdown-table-dline-regexp))) + (forward-line))) + #'end-of-line + (lambda () + (funcall extract-key-from-cell + (markdown-table-get-cell column))) + nil + predicate) + (goto-char (point-min)))))) + +(defun markdown-table-convert-region (begin end &optional separator) + "Convert region from BEGIN to END to table with SEPARATOR. + +If every line contains at least one TAB character, the function +assumes that the material is tab separated (TSV). If every line +contains a comma, comma-separated values (CSV) are assumed. If +not, lines are split at whitespace into cells. + +You can use a prefix argument to force a specific separator: +\\[universal-argument] once forces CSV, \\[universal-argument] +twice forces TAB, and \\[universal-argument] three times will +prompt for a regular expression to match the separator, and a +numeric argument N indicates that at least N consecutive +spaces, or alternatively a TAB should be used as the separator." + + (interactive "r\nP") + (let* ((begin (min begin end)) (end (max begin end)) re) + (goto-char begin) (beginning-of-line 1) + (setq begin (point-marker)) + (goto-char end) + (if (bolp) (backward-char 1) (end-of-line 1)) + (setq end (point-marker)) + (when (equal separator '(64)) + (setq separator (read-regexp "Regexp for cell separator: "))) + (unless separator + ;; Get the right cell separator + (goto-char begin) + (setq separator + (cond + ((not (re-search-forward "^[^\n\t]+$" end t)) '(16)) + ((not (re-search-forward "^[^\n,]+$" end t)) '(4)) + (t 1)))) + (goto-char begin) + (if (equal separator '(4)) + ;; Parse CSV + (while (< (point) end) + (cond + ((looking-at "^") (insert "| ")) + ((looking-at "[ \t]*$") (replace-match " |") (beginning-of-line 2)) + ((looking-at "[ \t]*\"\\([^\"\n]*\\)\"") + (replace-match "\\1") (if (looking-at "\"") (insert "\""))) + ((looking-at "[^,\n]+") (goto-char (match-end 0))) + ((looking-at "[ \t]*,") (replace-match " | ")) + (t (beginning-of-line 2)))) + (setq re + (cond + ((equal separator '(4)) "^\\|\"?[ \t]*,[ \t]*\"?") + ((equal separator '(16)) "^\\|\t") + ((integerp separator) + (if (< separator 1) + (user-error "Cell separator must contain one or more spaces") + (format "^ *\\| *\t *\\| \\{%d,\\}" separator))) + ((stringp separator) (format "^ *\\|%s" separator)) + (t (error "Invalid cell separator")))) + (while (re-search-forward re end t) (replace-match "| " t t))) + (goto-char begin) + (markdown-table-align))) + ;;; ElDoc Support