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

Reply via email to