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

    Complete Antora xref targets and fragments
    
    Inside an xref: in an Antora component, completion now offers the
    component's pages as targets (pages in other modules prefixed with
    module:), and after a # it offers the target page's section ids and
    anchors (read from the resolved page). A same-page xref:# completes
    against the current buffer. Dot-prefixed entries (lock files, hidden
    dirs) are skipped, and the page list falls through to the in-buffer xref
    completion outside Antora.
    
    Phase 2b of the Antora/xref work; project-wide find-references is next.
---
 CHANGELOG.md                  |  1 +
 README.adoc                   |  2 +-
 adoc-mode.el                  | 84 +++++++++++++++++++++++++++++++++++++++++++
 test/adoc-mode-antora-test.el | 66 ++++++++++++++++++++++++++++++++++
 4 files changed, 152 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 83e156ee5a..aa242d2237 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@
 - Make references clickable. Cross-references (`<<id>>`, `xref:id[]`), links 
and URLs (`link:`, `https:`, `mailto:`, ...), and `include::` macros now 
highlight on hover and follow with a `mouse-1` (or `mouse-2`) click - the same 
action as `C-c C-o` / `M-.`. As part of this, `adoc-follow-thing-at-point` now 
also follows `link:` macros (opening a local target or a URL) and no longer 
passes the `[label]` along when opening a URL macro.
 - Add an `xref` backend over AsciiDoc anchors. In an `adoc-mode` buffer, `M-?` 
(`xref-find-references`) lists every cross-reference to the anchor at point, 
and the standard xref machinery (the marker stack, the completion-read prompt, 
`consult-xref`, ...) now works for AsciiDoc ids. Definitions are anchors 
(`[[id]]`, `[#id]`, `[[[biblio]]]`) and references are `<<id>>` / `xref:id[]` 
usages, resolved within the current buffer. `M-.` keeps following URLs and 
`include::` too, via `adoc-foll [...]
 - Follow Antora cross-file cross-references. In a file inside an Antora 
component (one with an `antora.yml` above it), following an `xref:` that 
targets a page - e.g. `xref:basics/install.adoc[]` or 
`xref:other.adoc#a-section[]`, including a `module:` prefix - now opens the 
resolved page (under the target module's `pages/` directory) and jumps to the 
`#fragment` section. Works from `C-c C-o` / `M-.` and a mouse click, and `M-,` 
(`xref-go-back`) returns. Resolution is limited to the curre [...]
+- Complete Antora `xref:` targets. Inside an `xref:` in an Antora component, 
completion offers the component's pages as targets (pages in other modules 
prefixed with `module:`), and after a `#` it offers the target page's section 
ids and anchors. A same-page `xref:#` completes against the current buffer.
 - Treat section titles as cross-reference targets. `adoc-mode` now derives 
each section's auto-id the way Asciidoctor does, so completion (`<<` / 
`xref:`), the `xref` backend, and `adoc-goto-ref-label` offer and resolve 
section ids - not just explicit anchors. The id style is detected 
automatically: a document's own `:idprefix:` / `:idseparator:` win, otherwise 
files inside an Antora component (an `antora.yml` above them) use Antora's 
kebab-case style (`My Title` -> `my-title`) and every [...]
 
 ### Changes
diff --git a/README.adoc b/README.adoc
index 56ae50e525..2bcf03b200 100644
--- a/README.adoc
+++ b/README.adoc
@@ -50,7 +50,7 @@ Here are some of the main features of `adoc-mode`:
 - list editing: `M-left` / `M-right` nest the list item at point deeper or 
shallower, `M-RET` inserts a sibling item (incrementing the number for 
explicitly-numbered lists), `M-up` / `M-down` move an item (with its sub-items) 
past its siblings, and `M-x adoc-renumber-list` renumbers an 
explicitly-numbered list
 - navigate to anchors and sections with completion (`C-c C-a`), and follow 
URLs, `link:` and `include::` macros, and xrefs at point (`C-c C-o` / `M-.`), 
or by clicking them with the mouse
 - section titles act as cross-reference targets too: their Asciidoctor 
auto-ids are offered in completion and resolved by navigation, with the id 
style (Asciidoctor `_my_title` vs Antora `my-title`) detected automatically or 
set via `adoc-section-id-style`
-- Antora awareness: in a component (a directory with an `antora.yml`), 
following an `xref:` to another page (e.g. `xref:topic/page.adoc#section[]`) 
opens the resolved page under the right module's `pages/` directory and jumps 
to the section
+- Antora awareness: in a component (a directory with an `antora.yml`), 
following an `xref:` to another page (e.g. `xref:topic/page.adoc#section[]`) 
opens the resolved page under the right module's `pages/` directory and jumps 
to the section, and completion inside an `xref:` offers the component's pages 
and, after a `#`, the target page's sections
 - an `xref` backend over anchors and sections: `M-?` lists every 
cross-reference to the id at point, with the usual xref marker stack and 
completion UI
 - context-aware completion via `completion-at-point`: cross-reference ids 
inside `<<` / `xref:`, attribute names inside `{`, file paths after 
`include::`, and source-block languages inside `[source,`
 - nested `imenu` index with hierarchical heading structure
diff --git a/adoc-mode.el b/adoc-mode.el
index caccd62742..d474fcb9d7 100644
--- a/adoc-mode.el
+++ b/adoc-mode.el
@@ -3833,6 +3833,59 @@ in-buffer id) qualifies."
                           target)))))
         nil))))
 
+(defun adoc--antora-page-targets ()
+  "Return the component's pages as xref targets, or nil outside Antora.
+Pages in the current module are listed by their path relative to that
+module's `pages' directory; pages in other modules are prefixed with
+`module:'."
+  (let ((root (adoc--antora-root)))
+    (when (and root buffer-file-name)
+      (let ((current (adoc--antora-current-module root buffer-file-name))
+            (modules-dir (expand-file-name "modules" root))
+            (targets '()))
+        (when (file-directory-p modules-dir)
+          (dolist (mdir (directory-files modules-dir t "\\`[^.]"))
+            (let ((pages (expand-file-name "pages" mdir)))
+              (when (file-directory-p pages)
+                (let ((module (file-name-nondirectory mdir)))
+                  (dolist (file (directory-files-recursively pages 
"\\.adoc\\'"))
+                    (let ((rel (file-relative-name file pages)))
+                      ;; skip any dot-prefixed segment: lock files
+                      ;; (`.#foo.adoc') and hidden dirs (`.git/...'), which
+                      ;; Antora ignores
+                      (unless (string-match-p "\\(?:\\`\\|/\\)\\." rel)
+                        (push (if (equal module current) rel
+                                (concat module ":" rel))
+                              targets)))))))))
+        (nreverse targets)))))
+
+(defun adoc--antora-page-fragments (page)
+  "Return the section ids and anchors defined in the xref target PAGE.
+PAGE is an xref page target (e.g. `topic/p.adoc' or `mod:p.adoc')
+resolved relative to the current buffer's Antora component."
+  (let ((resolved (adoc--antora-resolve-page page)))
+    (when (and resolved
+               (file-exists-p (car resolved))
+               (not (file-directory-p (car resolved))))
+      (with-current-buffer (find-file-noselect (car resolved))
+        (delete-dups (append (adoc--collect-anchor-ids)
+                             (adoc--collect-section-ids)))))))
+
+(defun adoc--completion-xref-target-bounds ()
+  "Return (START . END) of the `xref:' target text up to point, or nil.
+Only matches when point is within the target portion of an `xref:'
+macro (after `xref:', before the `[' or any whitespace)."
+  (save-excursion
+    (let ((pos (point))
+          (bol (line-beginning-position)))
+      (when (re-search-backward "xref:" bol t)
+        (let ((start (match-end 0)))
+          (when (and (<= start pos)
+                     (not (save-excursion
+                            (goto-char start)
+                            (re-search-forward "[][ \t]" pos t))))
+            (cons start pos)))))))
+
 ;;;; Completion
 
 (defconst adoc-intrinsic-attributes
@@ -3993,6 +4046,37 @@ inside `[source,'."
             :annotation-function (lambda (_) " attribute")
             :company-kind (lambda (_) 'variable)
             :exclusive 'no))
+     ;; Antora `xref:' target - complete page paths, and section ids/anchors
+     ;; after a `#fragment'.  Falls through to the generic xref branch outside
+     ;; an Antora component.
+     ((and (adoc--antora-root)
+           (setq bounds (adoc--completion-xref-target-bounds)))
+      (let* ((start (car bounds))
+             (end (cdr bounds))
+             (text (buffer-substring-no-properties start end))
+             (hash (string-match "#" text)))
+        (if hash
+            (let ((page (substring text 0 hash)))
+              (list (+ start hash 1) end
+                    (completion-table-dynamic
+                     (lambda (_)
+                       ;; an empty page (`xref:#frag') is a same-page
+                       ;; reference - complete against this buffer
+                       (if (string-empty-p page)
+                           (delete-dups (append (adoc--collect-anchor-ids)
+                                                (adoc--collect-section-ids)))
+                         (adoc--antora-page-fragments page))))
+                    :annotation-function (lambda (_) " section")
+                    :company-kind (lambda (_) 'reference)
+                    :exclusive 'no))
+          (list start end
+                (completion-table-dynamic
+                 (lambda (_) (append (adoc--antora-page-targets)
+                                     (adoc--collect-anchor-ids)
+                                     (adoc--collect-section-ids))))
+                :annotation-function (lambda (_) " page")
+                :company-kind (lambda (_) 'file)
+                :exclusive 'no))))
      ((setq bounds (adoc--completion-xref-bounds))
       (list (car bounds) (cdr bounds)
             (completion-table-dynamic
diff --git a/test/adoc-mode-antora-test.el b/test/adoc-mode-antora-test.el
index c050965604..515f2bee25 100644
--- a/test/adoc-mode-antora-test.el
+++ b/test/adoc-mode-antora-test.el
@@ -146,6 +146,72 @@ FILES is an alist of (RELPATH . CONTENT) created under
               (kill-buffer)))
         (delete-directory root t)))))
 
+(defun adoc-test--antora-capf (root relpath line)
+  "Return the capf candidates with point after LINE in component page RELPATH.
+LINE is inserted at end of the page (under ROOT's ROOT module) first."
+  (let ((file (expand-file-name (concat "modules/ROOT/pages/" relpath) root)))
+    (with-current-buffer (find-file-noselect file)
+      (unwind-protect
+          (progn
+            (goto-char (point-max))
+            (insert "\n" line)
+            (goto-char (point-max))
+            (let ((capf (adoc-completion-at-point)))
+              (when capf (all-completions "" (nth 2 capf)))))
+        (set-buffer-modified-p nil)
+        (kill-buffer)))))
+
+(describe "Antora xref completion"
+  (it "lists component pages as xref targets, other modules prefixed"
+    (let ((root (adoc-test--make-antora
+                 '(("src.adoc" . "= Src\n")
+                   ("sub/target.adoc" . "= T\n")))))
+      (unwind-protect
+          (progn
+            ;; add a page in another module
+            (let ((other (expand-file-name "modules/extra/pages/o.adoc" root)))
+              (make-directory (file-name-directory other) t)
+              (with-temp-file other (insert "= O\n")))
+            (let ((cands (adoc-test--antora-capf root "src.adoc" "see xref:")))
+              (expect (member "sub/target.adoc" cands) :to-be-truthy)
+              (expect (member "extra:o.adoc" cands) :to-be-truthy)))
+        (delete-directory root t))))
+
+  (it "completes #fragments from the target page"
+    (let ((root (adoc-test--make-antora
+                 '(("src.adoc" . "= Src\n")
+                   ("target.adoc" . "= T\n\n== Deep 
Section\n\n[[explicit]]\nx\n")))))
+      (unwind-protect
+          (let ((cands (adoc-test--antora-capf
+                        root "src.adoc" "see xref:target.adoc#")))
+            (expect (member "deep-section" cands) :to-be-truthy) ; section 
auto-id
+            (expect (member "explicit" cands) :to-be-truthy))    ; explicit 
anchor
+        (delete-directory root t))))
+
+  (it "does not offer lock files, dotfiles, or pages in hidden dirs"
+    (let ((root (adoc-test--make-antora
+                 '(("src.adoc" . "= Src\n")
+                   (".hidden/buried.adoc" . "= H\n")))))
+      (unwind-protect
+          (progn
+            ;; simulate an editor lock file next to a page
+            (with-temp-file (expand-file-name "modules/ROOT/pages/.#busy.adoc" 
root)
+              (insert "x"))
+            (let ((cands (adoc-test--antora-capf root "src.adoc" "see xref:")))
+              (expect (cl-find-if (lambda (c) (string-match-p 
"\\(?:\\`\\|/\\)\\." c))
+                                  cands)
+                      :to-be nil)))
+        (delete-directory root t))))
+
+  (it "completes a same-page #fragment against the current buffer"
+    (let ((root (adoc-test--make-antora '(("src.adoc" . "= Src\n")))))
+      (unwind-protect
+          (let ((cands (adoc-test--antora-capf
+                        root "src.adoc"
+                        "== Local Bit\n\nsee xref:#")))
+            (expect (member "local-bit" cands) :to-be-truthy))
+        (delete-directory root t)))))
+
 (provide 'adoc-mode-antora-test)
 
 ;;; adoc-mode-antora-test.el ends here

Reply via email to