Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- 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-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.
- Find cross-references project-wide in an Antora component. `M-?` (`xref-find-references`) on an id now searches the whole component (not just the current buffer) and lists every cross-page `xref:this/page.adoc#id[]` as well as the same-page `<<id>>` / `xref:id[]` usages.
- 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
Expand Down
4 changes: 3 additions & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ 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
`<<id>>` / `xref:id[]` that points at the id under point, and kbd:[M-,]
(`xref-go-back`) returns after a jump.
(`xref-go-back`) returns after a jump. In an Antora component kbd:[M-?]
searches the whole component, so cross-page references (`xref:this/page.adoc#id[]`
from other pages) are included.

=== Preview and Export

Expand Down
38 changes: 37 additions & 1 deletion adoc-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -3871,6 +3871,38 @@ resolved relative to the current buffer's Antora component."
(delete-dups (append (adoc--collect-anchor-ids)
(adoc--collect-section-ids)))))))

(defun adoc--antora-current-page-targets ()
"Return the xref target form(s) other pages use to reference this page.
A list of the module-pages-relative path and its `module:'-qualified
form, or nil when the buffer is not a page in an Antora component."
(let ((root (adoc--antora-root)))
(when (and root buffer-file-name)
(let* ((module (adoc--antora-current-module root buffer-file-name))
(pages (and module
(expand-file-name (concat "modules/" module "/pages")
root)))
(rel (and pages (file-relative-name buffer-file-name pages))))
(when (and rel (not (string-prefix-p ".." rel)))
(list rel (concat module ":" rel)))))))

(defun adoc--antora-references (id)
"Return cross-references to ID across the current Antora component.
Searches the component's `.adoc' files for same-page references
\(`<<id>>', `xref:id[]', `xref:#id[]') and cross-page references to this
page's id (`xref:this/page.adoc#id[]')."
(let* ((root (adoc--antora-root))
(qid (regexp-quote id))
(targets (adoc--antora-current-page-targets))
(same (concat "<<" qid "[,>]\\|xref:#?" qid "\\["))
;; A flat alternation (no shy group): `grep -E' under POSIX/GNU grep
;; rejects the `(?:...)' that a shy group would translate to.
(cross (when targets
(mapconcat (lambda (target)
(concat "xref:" (regexp-quote target) "#" qid "\\["))
targets "\\|")))
(regexp (if cross (concat same "\\|" cross) same)))
(xref-matches-in-directory regexp "*.adoc" root nil)))

(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:'
Expand Down Expand Up @@ -4173,7 +4205,11 @@ the match."
(adoc--section-definitions identifier)))

(cl-defmethod xref-backend-references ((_backend (eql adoc)) identifier)
(adoc--xref-collect (adoc--re-xref-to identifier)))
;; In an Antora component search the whole component (so cross-page
;; references show up); otherwise stay within the current buffer.
(if (adoc--antora-root)
(adoc--antora-references identifier)
(adoc--xref-collect (adoc--re-xref-to identifier))))

(cl-defmethod xref-backend-apropos ((_backend (eql adoc)) pattern)
(require 'apropos) ; for `apropos-parse-pattern'
Expand Down
44 changes: 44 additions & 0 deletions test/adoc-mode-antora-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,50 @@ LINE is inserted at end of the page (under ROOT's ROOT module) first."
(expect (member "local-bit" cands) :to-be-truthy))
(delete-directory root t)))))

(describe "Antora project-wide references"
(it "computes this page's reference target forms"
(let ((root (adoc-test--make-antora '(("sub/p.adoc" . "= P\n")))))
(unwind-protect
(with-current-buffer
(find-file-noselect
(expand-file-name "modules/ROOT/pages/sub/p.adoc" root))
(unwind-protect
(expect (adoc--antora-current-page-targets)
:to-equal '("sub/p.adoc" "ROOT:sub/p.adoc"))
(kill-buffer)))
(delete-directory root t))))

(it "finds same-page, same-module and other-module references"
(let* ((root (make-temp-file "adoc-antora-" t))
(rootpages (expand-file-name "modules/ROOT/pages" root))
(extrapages (expand-file-name "modules/extra/pages" root)))
(make-directory rootpages t)
(make-directory extrapages t)
(with-temp-file (expand-file-name "antora.yml" root) (insert "name: demo\n"))
(with-temp-file (expand-file-name "guide.adoc" rootpages)
(insert "= Guide\n\n== Deep Section\n\nSelf <<deep-section>>.\n"))
(with-temp-file (expand-file-name "intro.adoc" rootpages)
(insert "= Intro\n\nxref:guide.adoc#deep-section[x].\n"))
(with-temp-file (expand-file-name "o.adoc" extrapages)
(insert "= O\n\nxref:ROOT:guide.adoc#deep-section[y].\n"))
;; a different fragment and a same-id-but-different-page reference: neither
;; should match
(with-temp-file (expand-file-name "noise.adoc" rootpages)
(insert "xref:guide.adoc#other[a] xref:elsewhere.adoc#deep-section[b]\n"))
(unwind-protect
(with-current-buffer
(find-file-noselect (expand-file-name "guide.adoc" rootpages))
(unwind-protect
(let* ((refs (xref-backend-references 'adoc "deep-section"))
(summaries (mapcar #'xref-item-summary refs)))
(expect (length refs) :to-equal 3)
(expect (cl-some (lambda (s) (string-match-p "#other" s)) summaries)
:to-be nil)
(expect (cl-some (lambda (s) (string-match-p "elsewhere" s)) summaries)
:to-be nil))
(kill-buffer)))
(delete-directory root t)))))

(provide 'adoc-mode-antora-test)

;;; adoc-mode-antora-test.el ends here