From 872838981589776aca390d5c060cad67d0d05b5b Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Wed, 7 Oct 2026 15:08:45 +0300 Subject: [PATCH 1/4] Look ids up case-sensitively Asciidoctor ids are case-sensitive, but adoc--goto-id and adoc--xref-collect searched with the default case-fold-search, and the Antora project-wide search handed grep `-i' for the same reason. So `<>' could jump to `[[FOO]]', and definitions and references mixed ids that only differ in case. --- CHANGELOG.md | 1 + adoc-mode.el | 13 +++++++++---- test/adoc-mode-antora-test.el | 7 ++++--- test/adoc-mode-navigation-test.el | 11 ++++++++++- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43aada7..e42a036 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ - [#91](https://github.com/bbatsov/adoc-mode/pull/91): Two cross-references on one line, as in `<>, <>`, 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 `<>` used to land on `[[FOO]]`, and `M-?` in an Antora component counted `#Deep-Section` as a reference to `deep-section`. ## 0.9.0 (2026-06-02) diff --git a/adoc-mode.el b/adoc-mode.el index d591456..e5fcfc1 100644 --- a/adoc-mode.el +++ b/adoc-mode.el @@ -4035,10 +4035,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) @@ -4183,7 +4185,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 () @@ -4461,8 +4465,9 @@ same way `adoc-re-xref' permits it." (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 diff --git a/test/adoc-mode-antora-test.el b/test/adoc-mode-antora-test.el index 2c27bd9..58b3a84 100644 --- a/test/adoc-mode-antora-test.el +++ b/test/adoc-mode-antora-test.el @@ -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)) diff --git a/test/adoc-mode-navigation-test.el b/test/adoc-mode-navigation-test.el index 7adc2d8..66fb24c 100644 --- a/test/adoc-mode-navigation-test.el +++ b/test/adoc-mode-navigation-test.el @@ -457,6 +457,15 @@ (insert "[[foo]]\n[[foobar]]\n<> <> 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<> <>\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)))) ;;; adoc-mode-navigation-test.el ends here From 1c3de26cf480cb399be8e6792396f24ce9f6a2e2 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Wed, 7 Oct 2026 15:08:46 +0300 Subject: [PATCH 2/4] Follow same-page xref:#id[] references The `#' in `xref:#id[]' isn't part of the id, but adoc-xref-id-at-point returned `#id', so following it failed ("Can't find an anchor defining '#id'") and M-? found no references. Strip it there, and let adoc--re-xref-to match the `#' form like the Antora search already did. --- CHANGELOG.md | 1 + adoc-mode.el | 7 ++++--- test/adoc-mode-navigation-test.el | 13 ++++++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e42a036..c321377 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ - [#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 `<>` 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 `<>`. ## 0.9.0 (2026-06-02) diff --git a/adoc-mode.el b/adoc-mode.el index e5fcfc1..0ff9875 100644 --- a/adoc-mode.el +++ b/adoc-mode.el @@ -3731,8 +3731,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. `<>'); the actual anchor id has none. - (and id (string-trim id))))) + ;; (e.g. `<>'); 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. @@ -4460,7 +4461,7 @@ inside `[source,'." Trailing whitespace is tolerated after the id (as in `<>'), 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. diff --git a/test/adoc-mode-navigation-test.el b/test/adoc-mode-navigation-test.el index 66fb24c..49bca34 100644 --- a/test/adoc-mode-navigation-test.el +++ b/test/adoc-mode-navigation-test.el @@ -466,6 +466,17 @@ (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)))) + (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 <>\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)))) ;;; adoc-mode-navigation-test.el ends here From 25911f99d88f3bbdfb81c9f498c91d5c2a21e20e Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Wed, 7 Oct 2026 15:08:46 +0300 Subject: [PATCH 3/4] Resolve inline anchors that carry reftext adoc-re-anchor's inline-special regexp, given an id, only allowed whitespace between the id and the closing `]]', so `[[inl,Inline Ref]]' never matched `inl': completion offered the id (it splits at the comma) but definitions and following came up empty. Allow the reftext, brackets in it included. --- CHANGELOG.md | 1 + adoc-mode.el | 5 ++++- test/adoc-mode-navigation-test.el | 18 +++++++++++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c321377..c896e18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ - [#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 `<>` 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 `<>`. +- [#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. ## 0.9.0 (2026-06-02) diff --git a/adoc-mode.el b/adoc-mode.el index 0ff9875..abca2d2 100644 --- a/adoc-mode.el +++ b/adoc-mode.el @@ -1385,7 +1385,10 @@ this id. If ID is nil, the regexp matches any anchor." ((eq type 'inline-special) ;; [\\]?\[\[(?P[\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) diff --git a/test/adoc-mode-navigation-test.el b/test/adoc-mode-navigation-test.el index 49bca34..d7b1b1b 100644 --- a/test/adoc-mode-navigation-test.el +++ b/test/adoc-mode-navigation-test.el @@ -477,6 +477,22 @@ (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)))) + (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<>\n") + (adoc-mode) + (expect (length (xref-backend-definitions 'adoc "inl")) :to-equal 1) + (goto-char (point-max)) + (search-backward "<>\n") + (adoc-mode) + (expect (length (xref-backend-definitions 'adoc "foo")) :to-equal 1)))) ;;; adoc-mode-navigation-test.el ends here From b3b78243e33120371d6894104a6c10482de6373f Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Wed, 7 Oct 2026 15:08:47 +0300 Subject: [PATCH 4/4] Use a section's id as the xref identifier on its title xref-backend-identifier-at-point only knew about xrefs and anchors, so M-? on a section title prompted instead of finding the references to that section. It now returns the section's id. That id is the explicit one when the section has one - a block anchor or id right above the title, or an anchor at the end of it - since Asciidoctor then doesn't generate an auto-id at all. For the same reason such sections no longer contribute an auto-id to completion and section lookups; their anchors already define them. --- CHANGELOG.md | 1 + adoc-mode.el | 39 +++++++++++++++++++++++++++++-- test/adoc-mode-navigation-test.el | 26 +++++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c896e18..629fc7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ - [#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 `<>` 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 `<>`. - [#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) diff --git a/adoc-mode.el b/adoc-mode.el index abca2d2..d8ec7a2 100644 --- a/adoc-mode.el +++ b/adoc-mode.el @@ -3982,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) @@ -4001,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)) @@ -4485,9 +4508,21 @@ the match. The search is case-sensitive, like ids." 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)))) diff --git a/test/adoc-mode-navigation-test.el b/test/adoc-mode-navigation-test.el index d7b1b1b..a8e9cbb 100644 --- a/test/adoc-mode-navigation-test.el +++ b/test/adoc-mode-navigation-test.el @@ -489,6 +489,32 @@ (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<>\n")