diff --git a/CHANGELOG.md b/CHANGELOG.md index 43aada7..629fc7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,10 @@ - [#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`. +- [#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 d591456..d8ec7a2 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) @@ -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. `<>'); 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. @@ -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) @@ -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)) @@ -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) @@ -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 () @@ -4456,13 +4487,14 @@ 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. 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 @@ -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)))) 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..a8e9cbb 100644 --- a/test/adoc-mode-navigation-test.el +++ b/test/adoc-mode-navigation-test.el @@ -457,6 +457,68 @@ (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))) + + (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))) + + (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) + (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") + (adoc-mode) + (expect (length (xref-backend-definitions 'adoc "foo")) :to-equal 1)))) ;;; adoc-mode-navigation-test.el ends here