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