branch: elpa/adoc-mode
commit 1dbb39c83301ab0c952058ad649bd5fb21d8adc5
Author: Bozhidar Batsov <[email protected]>
Commit: Bozhidar Batsov <[email protected]>

    [Fix #59] Add nested imenu index support
    
    Add `adoc-imenu-create-nested-index` that builds a hierarchical imenu
    reflecting the document's heading structure, so subheadings appear as
    nested entries under their parent headings.
    
    The new `adoc-imenu-create-index-function` customization variable
    controls which index style is used (nested by default, flat available
    via `adoc-imenu-create-index`).
---
 CHANGELOG.md           |  1 +
 adoc-mode.el           | 70 +++++++++++++++++++++++++++++++++++++++++++++++++-
 test/adoc-mode-test.el | 24 +++++++++++++++++
 3 files changed, 94 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 33cfdc6e4b..1b0e6171fb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@
 - [#21](https://github.com/bbatsov/adoc-mode/pull/21): Add support for native 
font-locking in code blocks.
 - [#48](https://github.com/bbatsov/adoc-mode/pull/48): Add support for 
displaying images.
 - Add font-lock support for Asciidoctor inline macros: `kbd:[]`, `btn:[]`, 
`menu:[]`, `pass:[]`, `stem:[]`, `latexmath:[]`, `asciimath:[]`.
+- [#59](https://github.com/bbatsov/adoc-mode/issues/59): Add nested `imenu` 
index support (enabled by default via `adoc-imenu-create-index-function`).
 
 ### Changes
 
diff --git a/adoc-mode.el b/adoc-mode.el
index 8d354d700b..60ceb0a673 100644
--- a/adoc-mode.el
+++ b/adoc-mode.el
@@ -221,6 +221,15 @@ delimited block lines have a certain length."
                  number)
   :group 'adoc)
 
+(defcustom adoc-imenu-create-index-function 'adoc-imenu-create-nested-index
+  "Function to create the imenu index.
+Use `adoc-imenu-create-nested-index' for a hierarchical index
+reflecting heading structure, or `adoc-imenu-create-index' for a
+flat list."
+  :type '(choice (function-item adoc-imenu-create-nested-index)
+                 (function-item adoc-imenu-create-index))
+  :group 'adoc)
+
 (defcustom adoc-title-style 'adoc-title-style-one-line
   "Title style used for title tempo templates.
 
@@ -3533,6 +3542,65 @@ LOCAL-ATTRIBUTE-FACE-ALIST before it is looked up in
              (cons (cons title-text title-pos) index-alist))))))
     (nreverse index-alist)))
 
+(defun adoc-imenu-create-nested-index ()
+  "Create a nested imenu index reflecting the heading hierarchy."
+  (let ((flat-index (adoc-imenu-create-index)))
+    (adoc--imenu-nest flat-index)))
+
+(defun adoc--imenu-heading-level (_title-text pos)
+  "Return the heading level (0-4) for heading at POS.
+_TITLE-TEXT is unused but accepted for interface consistency."
+  (save-excursion
+    (goto-char pos)
+    (let ((descriptor (adoc-title-descriptor t)))
+      (if descriptor
+          (nth 2 descriptor)
+        0))))
+
+(defun adoc--imenu-nest (flat-index)
+  "Convert FLAT-INDEX (a flat list of (name . pos)) into a nested alist.
+Each heading contains its sub-headings as a nested menu."
+  (let ((items (mapcar (lambda (item)
+                         (cons (car item)
+                               (cons (cdr item)
+                                     (adoc--imenu-heading-level
+                                      (car item) (cdr item)))))
+                       flat-index)))
+    ;; items is now ((name pos . level) ...)
+    (adoc--imenu-build-tree items 0)))
+
+(defun adoc--imenu-build-tree (items min-level)
+  "Build a nested imenu tree from ITEMS starting at MIN-LEVEL.
+ITEMS is a list of (name pos . level)."
+  (let (result)
+    (while items
+      (let* ((item (car items))
+             (name (car item))
+             (pos (cadr item))
+             (level (cddr item)))
+        (cond
+         ;; Item is at a higher level than we're collecting — return
+         ((< level min-level)
+          (setq items nil))
+         ;; Item is at a deeper level — shouldn't happen if called correctly
+         ((> level min-level)
+          (setq items (cdr items)))
+         ;; Item is at our level — collect it and its children
+         (t
+          (setq items (cdr items))
+          ;; Collect children (items with level > current level, up to
+          ;; next item at same or higher level)
+          (let (children)
+            (while (and items (> (cddr (car items)) level))
+              (push (car items) children)
+              (setq items (cdr items)))
+            (if children
+                (let ((subtree (adoc--imenu-build-tree
+                                (nreverse children) (1+ level))))
+                  (push (cons name (cons (cons nil pos) subtree)) result))
+              (push (cons name pos) result)))))))
+    (nreverse result)))
+
 (defvar adoc-mode-syntax-table
   (let ((table (make-syntax-table)))
     (modify-syntax-entry ?$ "." table)
@@ -3783,7 +3851,7 @@ Turning on Adoc mode runs the normal hook 
`adoc-mode-hook'."
 
   ;; it's the user's decision whether he wants to set imenu-sort-function to
   ;; nil, or even something else. See also similar comment in sgml-mode.
-  (setq-local imenu-create-index-function 'adoc-imenu-create-index)
+  (setq-local imenu-create-index-function adoc-imenu-create-index-function)
 
   ;; compilation
   (when (boundp 'compilation-error-regexp-alist-alist)
diff --git a/test/adoc-mode-test.el b/test/adoc-mode-test.el
index 1827c07d0b..69c789acf1 100644
--- a/test/adoc-mode-test.el
+++ b/test/adoc-mode-test.el
@@ -1118,6 +1118,30 @@ Don't use it for anything real.")
            (cons "sub chapter 2.1" 262)))))
     (kill-buffer "adoc-test")))
 
+(ert-deftest adoctest-test-imenu-create-nested-index ()
+  (unwind-protect
+      (progn
+        (set-buffer (get-buffer-create "adoc-test"))
+        (insert "= document title\n"
+                "== chapter 1\n"
+                "=== sub chapter 1.1\n"
+                "== chapter 2\n"
+                "=== sub chapter 2.1\n"
+                "=== sub chapter 2.2\n")
+        (should
+         (equal
+          (adoc-imenu-create-nested-index)
+          '(("document title"
+             (nil . 1)
+             ("chapter 1"
+              (nil . 18)
+              ("sub chapter 1.1" . 31))
+             ("chapter 2"
+              (nil . 51)
+              ("sub chapter 2.1" . 64)
+              ("sub chapter 2.2" . 84)))))))
+    (kill-buffer "adoc-test")))
+
 (ert-deftest adoctest-adoc-kw-replacement ()
   (unwind-protect
       (progn

Reply via email to