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 @@ -9,6 +9,7 @@
- Add a Flymake backend (`adoc-flymake`) that runs the buffer through Asciidoctor and reports its parser errors and warnings inline. It's registered automatically, so enabling `flymake-mode` is enough. The check feeds the buffer to Asciidoctor over its standard input, so it works on unsaved edits.
- 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`.
- 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
5 changes: 3 additions & 2 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ Here are some of the main features of `adoc-mode`:
- heading navigation modelled on `markdown-mode` / `org-mode`: next / previous heading (`C-c C-n` / `C-c C-p`), forward / backward at the same level (`C-c C-f` / `C-c C-b`), and up to the parent heading (`C-c C-u`)
- title management: promote / demote (`M-left` / `M-right`), toggle between one-line and two-line styles, adjust underline length
- 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 with completion over the buffer's anchors (`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
- an `xref` backend over anchors: `M-?` lists every cross-reference to the anchor at point, with the usual xref marker stack and completion UI
- 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`
- 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
- outline folding built on `outline-minor-mode` (enabled out of the box): `TAB` cycles the subtree at point, `S-TAB` cycles the whole buffer (overview / contents / show all), one-line title style only
Expand Down
171 changes: 161 additions & 10 deletions adoc-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,27 @@ delimited block lines have a certain length."
number)
:group 'adoc)

(defcustom adoc-section-id-style 'auto
"How section auto-ids are derived from section titles.

Asciidoctor turns a section title into an id using the `idprefix' and
`idseparator' attributes. This option controls which style `adoc-mode'
assumes when offering and resolving section ids (completion, the `xref'
backend, `adoc-goto-ref-label').

`auto' Honour `:idprefix:' / `:idseparator:' set in the document;
otherwise use the Antora style when the file lives in an
Antora component (an `antora.yml' is found above it), and
the Asciidoctor default style elsewhere.
`asciidoctor' Asciidoctor's default: prefix and separator both `_'
(e.g. `My Title' -> `_my_title').
`antora' Antora's default: empty prefix, `-' separator
(e.g. `My Title' -> `my-title')."
:type '(choice (const :tag "Auto-detect" auto)
(const :tag "Asciidoctor default (_my_title)" asciidoctor)
(const :tag "Antora (my-title)" antora))
:group 'adoc)

(defcustom adoc-imenu-create-index-function 'adoc-imenu-create-nested-index
"Function to create the imenu index.
Use `adoc-imenu-create-nested-index' for a hierarchical index
Expand Down Expand Up @@ -2909,15 +2930,19 @@ for multiline constructs to be matched."
(interactive (let* ((default (adoc-xref-id-at-point))
(default-str (if default (concat "(default " default ")") "")))
(list
;; Offer the buffer's anchors as candidates, but stay
;; permissive (require-match nil) so a not-yet-defined id can
;; still be entered.
;; Offer the buffer's anchors and section ids as candidates,
;; but stay permissive (require-match nil) so a not-yet-defined
;; id can still be entered.
(completing-read
(concat "Goto anchor of reference/label " default-str ": ")
(adoc--collect-anchor-ids) nil nil nil nil default))))
(let ((pos (save-excursion
(goto-char (point-min))
(re-search-forward (adoc-re-anchor nil id) nil t))))
(delete-dups (append (adoc--collect-anchor-ids)
(adoc--collect-section-ids)))
nil nil nil nil default))))
(let ((pos (or (save-excursion
(goto-char (point-min))
(re-search-forward (adoc-re-anchor nil id) nil t))
;; fall back to a section whose auto-id (or title) matches
(adoc--section-position id))))
(if (null pos) (user-error "Can't find an anchor defining '%s'" id))
(push-mark)
(goto-char pos)))
Expand Down Expand Up @@ -3580,6 +3605,130 @@ and title's text are not preserved, afterwards its always one space."
(forward-line -1))
(move-to-column saved-col))))

;;;; Section auto-ids

;; Asciidoctor derives an id for every section from its title (unless
;; `sectids' is off). These ids are what cross-references point at in
;; practice - far more often than explicit `[[id]]' anchors - so we generate
;; them and feed them to completion, the `xref' backend and
;; `adoc-goto-ref-label'.

(defun adoc--doc-attribute (name)
"Return the value of document attribute NAME, or nil when it is not set.
An attribute set to an empty value (e.g. `:idprefix:') returns the empty
string, which is distinct from nil."
(when buffer-file-name
(save-excursion
(save-match-data
(goto-char (point-min))
(when (re-search-forward
(concat "^:" (regexp-quote name) ":[ \t]*\\(.*\\)$") nil t)
(string-trim-right (match-string-no-properties 1)))))))

(defun adoc--antora-p ()
"Return non-nil when the buffer's file lives in an Antora component."
(and buffer-file-name
(locate-dominating-file buffer-file-name "antora.yml")
t))

(defun adoc--section-id-params ()
"Return (PREFIX . SEPARATOR) for section id generation in this buffer.
See `adoc-section-id-style'."
(pcase adoc-section-id-style
('asciidoctor (cons "_" "_"))
('antora (cons "" "-"))
(_
(let ((prefix (adoc--doc-attribute "idprefix"))
(separator (adoc--doc-attribute "idseparator")))
(cond
;; The document sets the attributes explicitly; an unset one keeps
;; Asciidoctor's `_' default.
((or prefix separator)
(cons (or prefix "_") (or separator "_")))
((adoc--antora-p) (cons "" "-"))
(t (cons "_" "_")))))))

(defun adoc--section-id (title &optional prefix separator)
"Return the Asciidoctor auto-id for the section titled TITLE.
PREFIX and SEPARATOR default to those of `adoc--section-id-params'.
Mirrors Asciidoctor's id generation: downcase, drop characters outside
letters/digits/`_'/space/`.'/`-', translate runs of space, `.' and `-'
to the separator, strip a leading/trailing separator, then prepend the
prefix."
(let* ((params (unless (and prefix separator) (adoc--section-id-params)))
(prefix (or prefix (car params)))
(separator (or separator (cdr params)))
(id (downcase title)))
(setq id (replace-regexp-in-string "<[^>]*>" "" id)) ; inline tags
(setq id (replace-regexp-in-string "[^[:alnum:]_ .-]" "" id)) ; invalid chars
(if (string-empty-p separator)
;; An empty separator only deletes spaces; `.' and `-' are kept.
(setq id (replace-regexp-in-string " +" "" id t t))
;; Collapse runs of space, `.', `-' AND the separator itself to a single
;; separator (so e.g. `foo_ bar' -> `foo_bar', not `foo__bar'), then
;; strip a leading/trailing separator. `-' is kept last in the class so
;; it stays a literal rather than forming a range.
(let* ((extra (if (member separator '("." "-")) "" separator))
(class (concat "[ ." extra "-]+")))
(setq id (replace-regexp-in-string class separator id t t)))
(let ((q (regexp-quote separator)))
(setq id (replace-regexp-in-string
(concat "\\`\\(?:" q "\\)+\\|\\(?:" q "\\)+\\'") "" id))))
(concat prefix id)))

(defun adoc--collect-sections ()
"Return a list of (ID TITLE POSITION) for the buffer's section titles.
Only headings that font-lock actually fontifies as titles are included,
so `==' lines inside code or other delimited blocks are skipped."
(save-excursion
(save-match-data
(font-lock-ensure)
(let ((re (adoc--re-all-titles))
(params (adoc--section-id-params))
(result '()))
(goto-char (point-min))
(while (re-search-forward re nil t)
(goto-char (match-beginning 0))
(let ((descriptor (adoc--heading-descriptor-at-point)))
(cond
;; A level-0 title is the document title, not a referenceable
;; section, so skip it (but advance past it).
((and descriptor (= (nth 2 descriptor) 0))
(goto-char (nth 5 descriptor)))
(descriptor
(let ((title (string-trim (nth 3 descriptor))))
(push (list (adoc--section-id title (car params) (cdr params))
title (nth 4 descriptor))
result)
(goto-char (nth 5 descriptor))))
(t (forward-line 1)))))
(nreverse result)))))

(defun adoc--collect-section-ids ()
"Return the auto-ids of the buffer's section titles."
(delete-dups (mapcar #'car (adoc--collect-sections))))

(defun adoc--section-position (id)
"Return the start position of the section matching ID, or nil.
A section matches when its auto-id equals ID or, for a natural
cross-reference, when its title does."
(seq-some (lambda (s)
(when (or (string= (nth 0 s) id)
(string= (nth 1 s) id))
(nth 2 s)))
(adoc--collect-sections)))

(defun adoc--section-definitions (id)
"Return a list of xref items for sections matching ID (auto-id or title)."
(let ((buffer (current-buffer)))
(delq nil
(mapcar (lambda (s)
(when (or (string= (nth 0 s) id)
(string= (nth 1 s) id))
(xref-make (nth 1 s)
(xref-make-buffer-location buffer (nth 2 s)))))
(adoc--collect-sections)))))

;;;; Completion

(defconst adoc-intrinsic-attributes
Expand Down Expand Up @@ -3743,7 +3892,8 @@ inside `[source,'."
((setq bounds (adoc--completion-xref-bounds))
(list (car bounds) (cdr bounds)
(completion-table-dynamic
(lambda (_) (adoc--collect-anchor-ids)))
(lambda (_) (delete-dups (append (adoc--collect-anchor-ids)
(adoc--collect-section-ids)))))
:annotation-function (lambda (_) " anchor")
:company-kind (lambda (_) 'reference)
:exclusive 'no))
Expand Down Expand Up @@ -3828,10 +3978,11 @@ the match."
(adoc--anchor-id-at-point)))

(cl-defmethod xref-backend-identifier-completion-table ((_backend (eql adoc)))
(adoc--collect-anchor-ids))
(delete-dups (append (adoc--collect-anchor-ids) (adoc--collect-section-ids))))

(cl-defmethod xref-backend-definitions ((_backend (eql adoc)) identifier)
(adoc--xref-collect (adoc-re-anchor nil identifier)))
(append (adoc--xref-collect (adoc-re-anchor nil identifier))
(adoc--section-definitions identifier)))

(cl-defmethod xref-backend-references ((_backend (eql adoc)) identifier)
(adoc--xref-collect (adoc--re-xref-to identifier)))
Expand Down
Loading