From b95f65eea463fdf8f1f66fc3d5a704b5e5957973 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Fri, 5 Jun 2026 15:08:10 +0300 Subject: [PATCH 1/2] 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 83e156e..aa242d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Make references clickable. Cross-references (`<>`, `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 `<>` / `xref:id[]` usages, resolved within the current buffer. `M-.` keeps following URLs and `include::` too, via `adoc-follow-thing-at-point`. - 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 current component. +- 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 everything else uses Asciidoctor's default (`_my_title`). The new `adoc-section-id-style` option forces a specific style. ### Changes diff --git a/README.adoc b/README.adoc index 56ae50e..2bcf03b 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 caccd62..d474fcb 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 c050965..515f2be 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 From 596b0d2bfb0f4d5817626fa4d2398b0e92f7c6d4 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Fri, 5 Jun 2026 16:05:50 +0300 Subject: [PATCH 2/2] Document section/Antora completion and cross-reference navigation in the README --- README.adoc | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/README.adoc b/README.adoc index 2bcf03b..c1e1764 100644 --- a/README.adoc +++ b/README.adoc @@ -122,8 +122,10 @@ completion UI like https://github.com/minad/corfu[Corfu] or https://github.com/company-mode/company-mode[Company]) offers candidates based on the construct at point: -- inside `<<` or `xref:` - cross-reference ids, taken from the explicit anchors - defined in the buffer (`[[id]]`, `[#id]`, `[[[biblio]]]`) +- inside `<<` or `xref:` - cross-reference ids: explicit anchors (`[[id]]`, + `[#id]`, `[[[biblio]]]`) and section auto-ids. In an Antora component an + `xref:` also completes the component's pages as targets and, after a `#`, + the sections of the target page - inside `{` - attribute names, both the ones defined with `:name:` in the buffer and a set of common built-in attributes - after `include::` - file paths, relative to the document @@ -132,6 +134,28 @@ on the construct at point: In plain prose the completion function yields to whatever else you have on `completion-at-point-functions`. +=== Cross-references and navigation + +Put point on a reference and follow it with kbd:[C-c C-o] (or kbd:[M-.]), or +just click it with the mouse - references highlight on hover. This follows +URLs, `link:` and `include::` macros, and cross-references: + +- an `<>` or `xref:id[]` jumps to the matching anchor or section in the + buffer (section auto-ids work as targets, so you can point at a section even + without an explicit anchor) +- in an Antora component, 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 `#fragment`. Resolution stays + within the current component + +kbd:[C-c C-a] jumps to an anchor or section by name, completing over the +buffer's ids. + +`adoc-mode` also registers an `xref` backend, so the standard cross-reference +keys work for AsciiDoc ids: kbd:[M-?] (`xref-find-references`) lists every +`<>` / `xref:id[]` that points at the id under point, and kbd:[M-,] +(`xref-go-back`) returns after a jump. + === Preview and Export `adoc-mode` can render and export documents by shelling out to the