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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
- [#91](https://github.com/bbatsov/adoc-mode/pull/91): Two cross-references on one line, as in `<<foo>>, <<bar>>`, are highlighted as two instead of one with the id `foo>>`, and an xref to a section's auto-id like `<<_installation>>` is highlighted at all.
- [#91](https://github.com/bbatsov/adoc-mode/pull/91): Superscripts and subscripts can't span whitespace anymore, as in Asciidoctor, so `~/.emacs.d/init.el to ~/backup` and `C-^ to join and M-^` stay plain text.
- [#91](https://github.com/bbatsov/adoc-mode/pull/91): Superscripts are raised by their own `adoc-script-raise` value. They checked the subscript's, so setting that to 0 stopped superscripts from being raised.
- [#92](https://github.com/bbatsov/adoc-mode/pull/92): Looking up an anchor or section id is case-sensitive, as ids are in Asciidoctor. Following `<<foo>>` used to land on `[[FOO]]`, and `M-?` in an Antora component counted `#Deep-Section` as a reference to `deep-section`.
- [#92](https://github.com/bbatsov/adoc-mode/pull/92): A same-page `xref:#id[]` can be followed and is found by `M-?`, like `<<id>>`.
- [#92](https://github.com/bbatsov/adoc-mode/pull/92): An inline anchor with reftext, `[[id,Reftext]]`, can be found and followed by its id. It was offered in completion but nothing could resolve it.
- [#92](https://github.com/bbatsov/adoc-mode/pull/92): `M-?` on a section title lists the references to that section instead of prompting for an id. A section with an explicit id (`[[id]]` or `[#id]` above it, or an anchor at the end of the title) uses that id, and no longer offers an auto-id in completion, since Asciidoctor doesn't generate one for it.

## 0.9.0 (2026-06-02)

Expand Down
64 changes: 54 additions & 10 deletions adoc-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -1385,7 +1385,10 @@ this id. If ID is nil, the regexp matches any anchor."
((eq type 'inline-special)
;; [\\]?\[\[(?P<attrlist>[\w"_:].*?)\]\]
(concat "\\(\\[\\[\\)"
"\\(" (if id (concat (regexp-quote id) "[ \t]*?") "[a-zA-Z0-9\"_:].*?") "\\)"
"\\(" (if id
(concat (regexp-quote id) "[ \t]*\\(?:,.*?\\)?")
"[a-zA-Z0-9\"_:].*?")
"\\)"
"\\(\\]\\]\\)"))

((eq type 'biblio)
Expand Down Expand Up @@ -3731,8 +3734,9 @@ Returns nil if there was no xref found."
(or (< saved-point (match-beginning 0))
(> saved-point (match-end 0)))))
;; `adoc-re-xref' captures trailing whitespace inside the id group
;; (e.g. `<<foo >>'); the actual anchor id has none.
(and id (string-trim id)))))
;; (e.g. `<<foo >>'); the actual anchor id has none. And the `#' in
;; a same-page `xref:#id[]' isn't part of the id.
(and id (string-remove-prefix "#" (string-trim id))))))

(defun adoc-title-descriptor (&optional strict-match )
"Returns title descriptor of title point is in.
Expand Down Expand Up @@ -3978,10 +3982,31 @@ prefix."
(concat "\\`\\(?:" q "\\)+\\|\\(?:" q "\\)+\\'") "" id))))
(concat prefix id)))

(defun adoc--section-explicit-id (descriptor)
"Return the explicit id of the section title DESCRIPTOR describes, or nil.
That's a block anchor or id on the lines right above the title
\(`[[id]]', `[#id]'), or an anchor at the end of the title text. A
section that has one gets no auto-id."
(or (let ((text (nth 3 descriptor)))
(when (string-match "\\[\\[\\([^],[:space:]]+\\)[^]]*\\]\\][ \t]*\\'" text)
(substring-no-properties (match-string 1 text))))
(save-excursion
(goto-char (nth 4 descriptor))
(let (id)
(while (and (not id)
(zerop (forward-line -1))
(looking-at-p "\\[.*\\][ \t]*$"))
(when (or (looking-at (adoc-re-anchor 'block-id))
(looking-at (adoc-re-anchor 'block-id-shorthand)))
(setq id (match-string-no-properties 1))))
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."
so `==' lines inside code or other delimited blocks are skipped. So are
sections with an explicit id, which have no auto-id: their anchors
already define them."
(save-excursion
(save-match-data
(font-lock-ensure)
Expand All @@ -3997,6 +4022,8 @@ so `==' lines inside code or other delimited blocks are skipped."
;; section, so skip it (but advance past it).
((and descriptor (= (nth 2 descriptor) 0))
(goto-char (nth 5 descriptor)))
((and descriptor (adoc--section-explicit-id descriptor))
(goto-char (nth 5 descriptor)))
(descriptor
(let ((title (string-trim (nth 3 descriptor))))
(push (list (adoc--section-id title (car params) (cdr params))
Expand Down Expand Up @@ -4035,10 +4062,12 @@ cross-reference, when its title does."
"Move point to the anchor or section identified by ID in this buffer.
Search explicit anchors first (`[[id]]', `[#id]', ...), then fall back
to a section whose auto-id or title matches. Return non-nil on success,
leaving point on the target; return nil and do not move otherwise."
leaving point on the target; return nil and do not move otherwise.
Ids are case-sensitive, as in Asciidoctor."
(let ((pos (or (save-excursion
(goto-char (point-min))
(re-search-forward (adoc-re-anchor nil id) nil t))
(let ((case-fold-search nil))
(re-search-forward (adoc-re-anchor nil id) nil t)))
(adoc--section-position id))))
(when pos
(goto-char pos)
Expand Down Expand Up @@ -4183,7 +4212,9 @@ page's id (`xref:this/page.adoc#id[]')."
(mapconcat (lambda (target)
(concat "xref:" (regexp-quote target) "#" qid "\\["))
targets "\\|")))
(regexp (if cross (concat same "\\|" cross) same)))
(regexp (if cross (concat same "\\|" cross) same))
;; ids are case-sensitive (this also keeps grep from using -i)
(case-fold-search nil))
(xref-matches-in-directory regexp "*.adoc" root nil)))

(defun adoc--completion-xref-target-bounds ()
Expand Down Expand Up @@ -4456,13 +4487,14 @@ inside `[source,'."
Trailing whitespace is tolerated after the id (as in `<<foo >>'), the
same way `adoc-re-xref' permits it."
(let ((q (regexp-quote id)))
(concat "<<" q "[ \t\n]*\\(?:,\\|>>\\)\\|xref:" q "\\[")))
(concat "<<" q "[ \t\n]*\\(?:,\\|>>\\)\\|xref:#?" q "\\[")))

(defun adoc--xref-collect (regexp)
"Return a list of xref items, one per match of REGEXP in the buffer.
Each item's summary is the matched line; its location is the start of
the match."
the match. The search is case-sensitive, like ids."
(let ((buffer (current-buffer))
(case-fold-search nil)
(items '()))
(save-excursion
(save-match-data
Expand All @@ -4476,9 +4508,21 @@ the match."
items)))))
(nreverse items)))

(defun adoc--section-id-at-point ()
"Return the id of the section title point is on, or nil.
That's its explicit id, or else its auto-id. The document title isn't
a section, so it has none."
(when (adoc-title-descriptor) ; cheap, before fontifying
(font-lock-ensure)
(let ((descriptor (adoc--heading-descriptor-at-point)))
(when (and descriptor (> (nth 2 descriptor) 0))
(or (adoc--section-explicit-id descriptor)
(adoc--section-id (string-trim (nth 3 descriptor))))))))

(cl-defmethod xref-backend-identifier-at-point ((_backend (eql adoc)))
(or (adoc-xref-id-at-point)
(adoc--anchor-id-at-point)))
(adoc--anchor-id-at-point)
(adoc--section-id-at-point)))

(cl-defmethod xref-backend-identifier-completion-table ((_backend (eql adoc)))
(delete-dups (append (adoc--collect-anchor-ids) (adoc--collect-section-ids))))
Expand Down
7 changes: 4 additions & 3 deletions test/adoc-mode-antora-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,11 @@ LINE is inserted at end of the page (under ROOT's ROOT module) first."
(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
;; a different fragment, a same-id-but-different-page reference and
;; an id that only differs in case: none should match
(with-temp-file (expand-file-name "noise.adoc" rootpages)
(insert "xref:guide.adoc#other[a] xref:elsewhere.adoc#deep-section[b]\n"))
(insert "xref:guide.adoc#other[a] xref:elsewhere.adoc#deep-section[b]\n"
"xref:guide.adoc#Deep-Section[c]\n"))
(unwind-protect
(with-current-buffer
(find-file-noselect (expand-file-name "guide.adoc" rootpages))
Expand Down
64 changes: 63 additions & 1 deletion test/adoc-mode-navigation-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,68 @@
(insert "[[foo]]\n[[foobar]]\n<<foo>> <<foobar>> xref:foobar[x]\n")
(adoc-mode)
(expect (length (xref-backend-definitions 'adoc "foo")) :to-equal 1)
(expect (length (xref-backend-references 'adoc "foo")) :to-equal 1))))
(expect (length (xref-backend-references 'adoc "foo")) :to-equal 1)))

(it "treats ids as case-sensitive"
(with-temp-buffer
(insert "[[FOO]]\nupper\n\n[[foo]]\nlower\n\n<<foo>> <<FOO>>\n")
(adoc-mode)
(expect (length (xref-backend-definitions 'adoc "foo")) :to-equal 1)
(expect (length (xref-backend-references 'adoc "foo")) :to-equal 1)
(adoc-goto-ref-label "foo")
(expect (line-number-at-pos) :to-equal 4)))

(it "handles a same-page xref:#id[]"
(with-temp-buffer
(insert "[[foo]]\nanchor\n\nsee xref:#foo[the foo] and <<foo>>\n")
(adoc-mode)
(goto-char (point-min))
(search-forward "xref:#f")
(expect (xref-backend-identifier-at-point 'adoc) :to-equal "foo")
(expect (length (xref-backend-references 'adoc "foo")) :to-equal 2)
(adoc-follow-thing-at-point)
(expect (line-number-at-pos) :to-equal 1)))

(it "finds an inline anchor that has reftext"
(with-temp-buffer
(insert "Some text [[inl,Inline Ref]] here.\n\n<<inl>>\n")
(adoc-mode)
(expect (length (xref-backend-definitions 'adoc "inl")) :to-equal 1)
(goto-char (point-max))
(search-backward "<<inl")
(adoc-follow-thing-at-point)
(expect (line-number-at-pos) :to-equal 1)))

(it "uses a section's auto-id as the identifier on its title"
(with-temp-buffer
(insert "= Doc\n\n== Some Section\n\ntext <<_some_section>>\n")
(adoc-mode)
(goto-char (point-min))
(search-forward "Some S")
(expect (xref-backend-identifier-at-point 'adoc) :to-equal "_some_section")
;; the document title isn't a section
(goto-char (point-min))
(expect (xref-backend-identifier-at-point 'adoc) :to-be nil)))

(it "uses a section's explicit id instead of an auto-id"
(with-temp-buffer
(insert "= Doc\n\n[[custom]]\n== Some Section\n\n"
"== Other Section [[trail]]\n\n[#short]\n== Third\n\n== Plain One\n")
(adoc-mode)
(goto-char (point-min))
(search-forward "Some S")
(expect (xref-backend-identifier-at-point 'adoc) :to-equal "custom")
(search-forward "Other S")
(expect (xref-backend-identifier-at-point 'adoc) :to-equal "trail")
(search-forward "Third")
(expect (xref-backend-identifier-at-point 'adoc) :to-equal "short")
;; such sections have no auto-id to offer
(expect (adoc--collect-section-ids) :to-equal '("_plain_one"))))

(it "finds an inline anchor whose reftext has brackets"
(with-temp-buffer
(insert "x [[foo,Some [x] text]] y\n\n<<foo>>\n")
(adoc-mode)
(expect (length (xref-backend-definitions 'adoc "foo")) :to-equal 1))))

;;; adoc-mode-navigation-test.el ends here
Loading