branch: elpa/adoc-mode
commit c5c71940eb250501e453c62470a36ce7a21bf395
Author: Bozhidar Batsov <[email protected]>
Commit: Bozhidar Batsov <[email protected]>
Add list editing commands
Make adoc-promote / adoc-demote (M-left / M-right) context-aware: on a
list item they nest it one level deeper or shallower by rewriting the
marker, rather than only acting on section titles. Unordered (*, -) and
implicitly-numbered (.) lists are supported; the level is clamped to the
range the marker style allows.
Add adoc-insert-list-item (M-RET) to insert a sibling item below the one
at point, preserving its indentation and marker and incrementing the
number or letter of explicitly-numbered items.
---
CHANGELOG.md | 1 +
README.adoc | 3 +-
adoc-mode.el | 135 +++++++++++++++++++++++++++++++++++++++++++++----
test/adoc-mode-test.el | 53 +++++++++++++++++++
4 files changed, 180 insertions(+), 12 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd1d2a02bd..bf0f3eecea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
### New features
+- Add list editing: `adoc-promote` (`M-left`) and `adoc-demote` (`M-right`)
now nest the list item at point one level deeper or shallower (for unordered
`*`/`-` and implicitly-numbered `.` lists) in addition to acting on section
titles, and the new `adoc-insert-list-item` (`M-RET`) inserts a sibling item
below the current one, keeping its indentation and marker and incrementing the
number/letter of explicitly-numbered items.
- Add heading navigation commands modelled on `markdown-mode` and `org-mode`:
`adoc-next-visible-heading` (`C-c C-n`), `adoc-previous-visible-heading` (`C-c
C-p`), `adoc-forward-same-level` (`C-c C-f`), `adoc-backward-same-level` (`C-c
C-b`), and `adoc-up-heading` (`C-c C-u`). They understand both one-line (`==
Title`) and two-line (underlined) titles and skip headings hidden by folding.
`outline-minor-mode` is now enabled by default so the folding commands are
available out of the box.
- New `adoc-title-scaling` defcustom (default `t`) and
`adoc-title-scaling-values` list let users disable the variable-height title
faces or pick their own scale factors. Set the boolean to nil for
uniformly-sized headings, or customise the list to control the level-0..5
heights. Mirrors `markdown-header-scaling`.
- New `adoc-blockquote-face` for the body of `[quote]` and `[verse]` delimited
blocks (inherits `font-lock-doc-face`); previously the body was left
unfontified.
diff --git a/README.adoc b/README.adoc
index 3514377502..0cf8053a5a 100644
--- a/README.adoc
+++ b/README.adoc
@@ -41,6 +41,7 @@ Here are some of the main features of `adoc-mode`:
- ~50 tempo templates for inserting AsciiDoc markup (formatting, titles,
blocks, lists, macros, etc.)
- heading navigation modelled on `markdown-mode` / `org-mode`: next / previous
heading (`C-c C-n` / `C-c C-p`), forward / backward at the same level (`C-c
C-f` / `C-c C-b`), and up to the parent heading (`C-c C-u`)
- title management: promote / demote (`M-left` / `M-right`), toggle between
one-line and two-line styles, adjust underline length
+- list editing: `M-left` / `M-right` also nest the list item at point deeper
or shallower, and `M-RET` inserts a sibling item (incrementing the number for
explicitly-numbered lists)
- navigate to anchors (`C-c C-a`) and follow URLs, `include::` macros, and
xrefs at point (`C-c C-o` / `M-.`)
- nested `imenu` index with hierarchical heading structure
- `outline-minor-mode` enabled out of the box for folding (one-line title
style only)
@@ -232,7 +233,7 @@ These days all upstream packages (e.g. on MELPA) are built
from Bozhidar's fork.
Here are some features that we're considering to add in the future:
-- Demote / promote for list items
+- Moving list items up / down and renumbering explicitly-numbered lists
- Correctly highlighting backslash escapes
Check out the issue tracker for more details.
diff --git a/adoc-mode.el b/adoc-mode.el
index e4036b9d26..584ced5929 100644
--- a/adoc-mode.el
+++ b/adoc-mode.el
@@ -2826,23 +2826,32 @@ When point is on an xref or cross-reference, jump to
its anchor."
(user-error "Nothing to follow at point"))))
(defun adoc-promote (&optional arg)
- "Promotes the structure at point ARG levels.
+ "Promote the structure at point ARG levels.
When ARG is nil (i.e. when no prefix arg is given), it defaults
-to 1. When ARG is negative, level is demoted that many levels.
+to 1. When ARG is negative, the structure is demoted that many
+levels instead.
-The intention is that the structure can be a title or a list
-element or anything else which has a \='level\='. However currently
-it works only for titles."
+On a section title this adds title levels (e.g. `=' -> `==', see
+`adoc-promote-title'). On a list item it nests the item one
+level deeper (e.g. `*' -> `**', see `adoc-demote' for the
+opposite direction)."
(interactive "p")
- (adoc-promote-title arg))
+ (let ((item (adoc--list-item-at-point)))
+ (if item
+ (adoc--change-list-item-level item (or arg 1))
+ (adoc-promote-title arg))))
(defun adoc-demote (&optional arg)
- "Demotes the structure at point ARG levels.
+ "Demote the structure at point ARG levels.
-Analogous to `adoc-promote', see there."
+Analogous to `adoc-promote', see there. On a list item this
+nests the item one level shallower (e.g. `**' -> `*')."
(interactive "p")
- (adoc-demote-title arg))
+ (let ((item (adoc--list-item-at-point)))
+ (if item
+ (adoc--change-list-item-level item (- (or arg 1)))
+ (adoc-demote-title arg))))
(defun adoc-promote-title (&optional arg)
"Promotes the title at point ARG levels.
@@ -2876,6 +2885,108 @@ non-nil, the sub type is toggled."
(setq type-type t))
(adoc-modify-title nil nil (not type-type) type-type))
+;;;; List editing
+
+(defun adoc--list-item-at-point ()
+ "Return a description of the list item on the current line, or nil.
+The description is a plist with these keys:
+
+:type one of `unordered', `implicit-numbered',
+ `explicit-numbered'
+:marker the marker string, e.g. \"*\", \"..\" or \"1.\"
+:indent the leading whitespace string
+:marker-beg buffer position of the marker's first character
+:level 0-based nesting level
+
+Labeled lists and callouts are intentionally not recognised, as
+their markers are too easily confused with ordinary prose."
+ (save-excursion
+ (beginning-of-line)
+ (cond
+ ((looking-at (adoc-re-oulisti 'adoc-unordered 'adoc-all-levels))
+ (let ((marker (match-string-no-properties 2)))
+ (list :type 'unordered
+ :marker marker
+ :indent (match-string-no-properties 1)
+ :marker-beg (match-beginning 2)
+ :level (if (string-prefix-p "-" marker) 0 (length marker)))))
+ ((looking-at (adoc-re-oulisti 'adoc-implicitly-numbered 'adoc-all-levels))
+ (let ((marker (match-string-no-properties 2)))
+ (list :type 'implicit-numbered
+ :marker marker
+ :indent (match-string-no-properties 1)
+ :marker-beg (match-beginning 2)
+ :level (1- (length marker)))))
+ ((looking-at (adoc-re-oulisti 'adoc-explicitly-numbered))
+ (list :type 'explicit-numbered
+ :marker (match-string-no-properties 2)
+ :indent (match-string-no-properties 1)
+ :marker-beg (match-beginning 2)
+ :level 0)))))
+
+(defun adoc--unordered-marker (level)
+ "Return the unordered list marker for nesting LEVEL (0-based).
+Level 0 is the dash bullet; deeper levels use that many asterisks."
+ (if (<= level 0) "-" (make-string (min level 5) ?*)))
+
+(defun adoc--implicit-numbered-marker (level)
+ "Return the implicit-numbered list marker (dots) for LEVEL (0-based)."
+ (make-string (1+ (min (max level 0) 4)) ?.))
+
+(defun adoc--increment-marker (marker)
+ "Return list MARKER incremented for the next sibling item.
+Arabic numbers (\"1.\") and single letters (\"a.\") are
+incremented. Roman numerals, the last letter of the alphabet,
+and other markers are returned unchanged, leaving the user to fix
+them up."
+ (cond
+ ((string-match "\\`\\([0-9]+\\)\\(\\.\\)\\'" marker)
+ (concat (number-to-string (1+ (string-to-number (match-string 1 marker))))
+ (match-string 2 marker)))
+ ;; Stop before the alphabet wraps past `z'/`Z' into punctuation.
+ ((string-match "\\`\\([a-yA-Y]\\)\\(\\.\\)\\'" marker)
+ (concat (char-to-string (1+ (aref (match-string 1 marker) 0)))
+ (match-string 2 marker)))
+ (t marker)))
+
+(defun adoc--change-list-item-level (item delta)
+ "Change list ITEM's nesting level by DELTA, rewriting its marker.
+A positive DELTA nests the item deeper, a negative one shallower.
+The level is clamped to the range the marker style supports."
+ (let* ((type (plist-get item :type))
+ (level (plist-get item :level))
+ (old-marker (plist-get item :marker))
+ (beg (plist-get item :marker-beg))
+ new-marker)
+ (pcase type
+ ('unordered
+ (setq new-marker (adoc--unordered-marker (max 0 (min 5 (+ level
delta))))))
+ ('implicit-numbered
+ (setq new-marker (adoc--implicit-numbered-marker (max 0 (min 4 (+ level
delta))))))
+ (_ (user-error "Cannot change the nesting level of a %s list item"
+ (symbol-name type))))
+ (unless (string= new-marker old-marker)
+ (save-excursion
+ (goto-char beg)
+ (delete-region beg (+ beg (length old-marker)))
+ (insert new-marker)))))
+
+(defun adoc-insert-list-item (&optional _arg)
+ "Insert a new list item below the item at point.
+The new item keeps the indentation, marker style and nesting
+level of the current item; explicitly-numbered items get an
+incremented number or letter."
+ (interactive "p")
+ (let ((item (adoc--list-item-at-point)))
+ (unless item
+ (user-error "Not on a list item"))
+ (let ((indent (plist-get item :indent))
+ (new-marker (if (eq (plist-get item :type) 'explicit-numbered)
+ (adoc--increment-marker (plist-get item :marker))
+ (plist-get item :marker))))
+ (end-of-line)
+ (insert "\n" indent new-marker " "))))
+
(defun adoc-calc ()
"(Re-)calculates variables used in adoc-mode.
Needs to be called after changes to certain (customization)
@@ -3439,6 +3550,7 @@ ITEMS is a list of (name pos . level)."
(define-key map "\C-c\C-u" 'adoc-up-heading)
(define-key map (kbd "M-<left>") 'adoc-promote)
(define-key map (kbd "M-<right>") 'adoc-demote)
+ (define-key map (kbd "M-RET") 'adoc-insert-list-item)
(define-key map "\C-c\C-t" 'adoc-toggle-title-type)
(define-key map "\C-c\C-a" 'adoc-goto-ref-label)
(define-key map "\C-c\C-o" 'adoc-follow-thing-at-point)
@@ -3451,8 +3563,9 @@ ITEMS is a list of (name pos . level)."
["Backward (same level)" adoc-backward-same-level]
["Up to parent heading" adoc-up-heading]
"---"
- ["Promote" adoc-promote]
- ["Demote" adoc-demote]
+ ["Promote (title / list item)" adoc-promote]
+ ["Demote (title / list item)" adoc-demote]
+ ["Insert list item" adoc-insert-list-item]
["Toggle title type" adoc-toggle-title-type]
["Adjust title underline" adoc-adjust-title-del]
["Follow thing at point" adoc-follow-thing-at-point]
diff --git a/test/adoc-mode-test.el b/test/adoc-mode-test.el
index f2e923ebe9..8a544f113a 100644
--- a/test/adoc-mode-test.el
+++ b/test/adoc-mode-test.el
@@ -1013,6 +1013,59 @@ on top of the `<mark>' default)."
(adoc-up-heading 1)
(should (looking-at-p "Doc Title$"))))
+;;;; List editing
+
+(ert-deftest adoctest-test-list-promote-demote-unordered ()
+ ;; promote nests deeper, demote nests shallower
+ (adoctest-trans "* foo!" "** foo" '(adoc-promote 1))
+ (adoctest-trans "** foo!" "* foo" '(adoc-demote 1))
+ ;; level 1 (*) demotes to level 0 (-), and back
+ (adoctest-trans "* foo!" "- foo" '(adoc-demote 1))
+ (adoctest-trans "- foo!" "* foo" '(adoc-promote 1))
+ ;; clamped at the extremes
+ (adoctest-trans "- foo!" "- foo" '(adoc-demote 1))
+ (adoctest-trans "***** foo!" "***** foo" '(adoc-promote 1))
+ ;; leading indentation is preserved
+ (adoctest-trans " ** foo!" " *** foo" '(adoc-promote 1))
+ ;; a numeric argument changes several levels at once
+ (adoctest-trans "* foo!" "*** foo" '(adoc-promote 2)))
+
+(ert-deftest adoctest-test-list-promote-demote-numbered ()
+ (adoctest-trans ". foo!" ".. foo" '(adoc-promote 1))
+ (adoctest-trans ".. foo!" ". foo" '(adoc-demote 1))
+ (adoctest-trans ". foo!" ". foo" '(adoc-demote 1)))
+
+(ert-deftest adoctest-test-list-promote-explicit-numbered-errors ()
+ (with-temp-buffer
+ (adoc-mode)
+ (insert "1. foo")
+ (goto-char (point-min))
+ (should-error (adoc-promote 1) :type 'user-error)
+ ;; buffer is left untouched
+ (should (string-equal (buffer-string) "1. foo"))))
+
+(ert-deftest adoctest-test-insert-list-item ()
+ (adoctest-trans "* foo!" "* foo\n* " '(adoc-insert-list-item))
+ (adoctest-trans "- foo!" "- foo\n- " '(adoc-insert-list-item))
+ (adoctest-trans ".. bar!" ".. bar\n.. " '(adoc-insert-list-item))
+ ;; explicitly-numbered items get an incremented marker
+ (adoctest-trans "1. foo!" "1. foo\n2. " '(adoc-insert-list-item))
+ (adoctest-trans "a. foo!" "a. foo\nb. " '(adoc-insert-list-item))
+ (adoctest-trans "9. foo!" "9. foo\n10. " '(adoc-insert-list-item))
+ ;; roman numerals and the last letter of the alphabet are copied verbatim
+ (adoctest-trans "i) foo!" "i) foo\ni) " '(adoc-insert-list-item))
+ (adoctest-trans "z. foo!" "z. foo\nz. " '(adoc-insert-list-item))
+ ;; indentation is preserved
+ (adoctest-trans " ** foo!" " ** foo\n ** " '(adoc-insert-list-item)))
+
+(ert-deftest adoctest-test-insert-list-item-not-in-list ()
+ (with-temp-buffer
+ (adoc-mode)
+ (insert "just some prose")
+ (goto-char (point-min))
+ (should-error (adoc-insert-list-item) :type 'user-error)
+ (should (string-equal (buffer-string) "just some prose"))))
+
(ert-deftest adoctest-test-xref-at-point-1 ()
(unwind-protect
(progn