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 @@ -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-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
Expand Down
30 changes: 27 additions & 3 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 `<<id>>` 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
`<<id>>` / `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
Expand Down
84 changes: 84 additions & 0 deletions adoc-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
66 changes: 66 additions & 0 deletions test/adoc-mode-antora-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -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