diff --git a/CHANGELOG.md b/CHANGELOG.md index aa242d2..ec6cb40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `<>` / `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 `<>` / `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 diff --git a/README.adoc b/README.adoc index c1e1764..ec975f6 100644 --- a/README.adoc +++ b/README.adoc @@ -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 `<>` / `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 diff --git a/adoc-mode.el b/adoc-mode.el index d474fcb..b006183 100644 --- a/adoc-mode.el +++ b/adoc-mode.el @@ -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 +\(`<>', `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:' @@ -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' diff --git a/test/adoc-mode-antora-test.el b/test/adoc-mode-antora-test.el index 515f2be..2c27bd9 100644 --- a/test/adoc-mode-antora-test.el +++ b/test/adoc-mode-antora-test.el @@ -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 <>.\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