From 3c0f8f73bcd559bf878921928fde8e6438da93f7 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Wed, 7 Oct 2026 13:13:26 +0300 Subject: [PATCH 1/3] Keep a one-line title's delimiter style when editing it adoc-title-descriptor decided whether a one-line title was enclosed (`== Title ==`) from match group 3, which also holds the line's trailing newline. Every title followed by a newline therefore looked enclosed, and promoting, demoting or adjusting it added a closing delimiter. Group 4 holds just the trailing delimiter. The existing specs only used titles at the end of the buffer, which hid this. --- CHANGELOG.md | 1 + adoc-mode.el | 2 +- test/adoc-mode-editing-test.el | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9b5803..d03a473 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - Following a cross-reference at point (`C-c C-o` / `M-.`, and the new `xref` commands) now works for a plain `<>` even when a captioned `<>` appears later on the same or an adjacent line, and ignores the whitespace in forms like `<>`. Previously `adoc-xref-id-at-point` could return nil or an id with a trailing space in those cases. - Heading navigation (`C-c C-n` and friends) and the imenu index no longer get confused by code and other delimited blocks. A `==`-style line inside a listing, source, literal, example, sidebar, quote, or open block, or a code line followed by `----` (which looks just like a two-line title underline), is no longer mistaken for a section title. Navigation and imenu now stay in step with what is actually highlighted as a title. - Heading navigation and imenu now honour `adoc-enable-two-line-title`. It is nil by default, so two-line (setext) titles are no longer picked up unless you opt in, matching their fontification. Previously they were always recognised, which was the main source of the code-block confusion above. +- [#89](https://github.com/bbatsov/adoc-mode/pull/89): Promoting, demoting or toggling a one-line title (`M-left` / `M-right`, `C-c C-t`) no longer turns it into the enclosed form when it's followed by a newline. `== Section` used to become `=== Section ===`. ## 0.9.0 (2026-06-02) diff --git a/adoc-mode.el b/adoc-mode.el index f883276..31076e7 100644 --- a/adoc-mode.el +++ b/adoc-mode.el @@ -3545,7 +3545,7 @@ trailing delimiter ('== my title =='). ((looking-at (adoc-re-one-line-title level)) (setq type 1) (setq text (match-string 2)) - (setq sub-type (if (< 0 (length (match-string 3))) 2 1)) + (setq sub-type (if (match-beginning 4) 2 1)) (setq found t)) ;; WARNING: if you decide to replace adoc-re-two-line-title with a ;; method ensuring the correct length of the underline, be aware that diff --git a/test/adoc-mode-editing-test.el b/test/adoc-mode-editing-test.el index be73ce0..1eb53ac 100644 --- a/test/adoc-mode-editing-test.el +++ b/test/adoc-mode-editing-test.el @@ -28,6 +28,20 @@ (adoc-test-trans "foo!\n+++!" "foo\n===" '(adoc-promote-title 1)) (adoc-test-trans "foo!\n---!" "foo\n^^^" '(adoc-promote-title 2))) + (it "keeps a title's delimiter style when it is followed by text" + (adoc-test-trans "== Sec!tion\n\nbody\n" "=== Section\n\nbody\n" + '(adoc-promote-title 1)) + (adoc-test-trans "=== Sec!tion\n\nbody\n" "== Section\n\nbody\n" + '(adoc-demote-title 1)) + (adoc-test-trans "== Sec!tion ==\n\nbody\n" "=== Section ===\n\nbody\n" + '(adoc-promote-title 1)) + (adoc-test-trans "== Sec!tion\n\nbody\n" "== Section\n\nbody\n" + '(adoc-adjust-title-del)) + (adoc-test-trans "== Sec!tion\n\nbody\n" "== Section ==\n\nbody\n" + '(adoc-toggle-title-type t)) + (adoc-test-trans "== Sec!tion ==\n\nbody\n" "== Section\n\nbody\n" + '(adoc-toggle-title-type t))) + (it "demotes titles" (adoc-test-trans "= foo" "====== foo" '(adoc-demote-title 1)) (adoc-test-trans "= foo =" "====== foo ======" '(adoc-demote-title 1)) From 73c502613e2b33ba2ec517768657ca083f1b6a0c Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Wed, 7 Oct 2026 13:13:48 +0300 Subject: [PATCH 2/3] Default the title commands to one level from Lisp adoc-promote-title and adoc-demote-title are documented to treat a nil ARG as 1, but only the interactive spec supplied it, so a Lisp call without an argument either did nothing or signalled wrong-type-argument in `(- nil)'. Point not being on a title is the user's mistake, so report it with user-error. --- CHANGELOG.md | 1 + adoc-mode.el | 6 +++--- test/adoc-mode-editing-test.el | 10 ++++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d03a473..e24755f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - Heading navigation (`C-c C-n` and friends) and the imenu index no longer get confused by code and other delimited blocks. A `==`-style line inside a listing, source, literal, example, sidebar, quote, or open block, or a code line followed by `----` (which looks just like a two-line title underline), is no longer mistaken for a section title. Navigation and imenu now stay in step with what is actually highlighted as a title. - Heading navigation and imenu now honour `adoc-enable-two-line-title`. It is nil by default, so two-line (setext) titles are no longer picked up unless you opt in, matching their fontification. Previously they were always recognised, which was the main source of the code-block confusion above. - [#89](https://github.com/bbatsov/adoc-mode/pull/89): Promoting, demoting or toggling a one-line title (`M-left` / `M-right`, `C-c C-t`) no longer turns it into the enclosed form when it's followed by a newline. `== Section` used to become `=== Section ===`. +- [#89](https://github.com/bbatsov/adoc-mode/pull/89): `adoc-promote-title` and `adoc-demote-title` default to one level when called from Lisp without an argument (`adoc-demote-title` used to signal an error), and the title commands signal a `user-error` instead of an `error` when point isn't on a title. ## 0.9.0 (2026-06-02) diff --git a/adoc-mode.el b/adoc-mode.el index 31076e7..ffc05a1 100644 --- a/adoc-mode.el +++ b/adoc-mode.el @@ -3120,12 +3120,12 @@ When ARG is nil (i.e. when no prefix arg is given), it defaults to 1. When ARG is negative, level is demoted that many levels. If ARG is 0, see `adoc-adjust-title-del'." (interactive "p") - (adoc-modify-title arg)) + (adoc-modify-title (or arg 1))) (defun adoc-demote-title (&optional arg) "Completely analogous to `adoc-promote-title'." (interactive "p") - (adoc-promote-title (- arg))) + (adoc-promote-title (- (or arg 1)))) (defun adoc-adjust-title-del () "Adjusts underline length to match the length of the title's text. @@ -3608,7 +3608,7 @@ BUG: In one line title case: number of spaces between delimiters and title's text are not preserved, afterwards its always one space." (let ((descriptor (adoc-title-descriptor))) (if (or create (not descriptor)) - (error "Point is not on a title")) + (user-error "Point is not on a title")) (let* ((type (nth 0 descriptor)) (new-type-val (cond diff --git a/test/adoc-mode-editing-test.el b/test/adoc-mode-editing-test.el index 1eb53ac..9f398e5 100644 --- a/test/adoc-mode-editing-test.el +++ b/test/adoc-mode-editing-test.el @@ -47,6 +47,12 @@ (adoc-test-trans "= foo =" "====== foo ======" '(adoc-demote-title 1)) (adoc-test-trans "foo!\n===!" "foo\n+++" '(adoc-demote-title 1))) + (it "defaults to one level when called from Lisp without an argument" + (adoc-test-trans "== f!oo\n" "=== foo\n" '(adoc-promote-title)) + (adoc-test-trans "== f!oo\n" "= foo\n" '(adoc-demote-title)) + (adoc-test-trans "== f!oo\n" "=== foo\n" '(adoc-promote)) + (adoc-test-trans "== f!oo\n" "= foo\n" '(adoc-demote))) + (it "toggles the title type" (adoc-test-trans "= one" "one\n===" '(adoc-toggle-title-type)) (adoc-test-trans "two!\n===!" "= two" '(adoc-toggle-title-type)) @@ -55,6 +61,10 @@ (adoc-test-trans "= five" "= five =" '(adoc-toggle-title-type t)) (adoc-test-trans "= six =" "= six" '(adoc-toggle-title-type t))) + (it "signals a user-error when point is not on a title" + (adoc-test-trans "just some pr!ose\n" "just some prose\n" + '(expect (adoc-promote-title 1) :to-throw 'user-error))) + (it "adjusts the two-line title underline length" (adoc-test-trans "lorem!\n===!" "lorem\n=====" '(adoc-adjust-title-del)) (adoc-test-trans "lorem!\n========!" "lorem\n=====" '(adoc-adjust-title-del)) From 53b7ca336bde329381387475da3fcc4ec1f4df39 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Wed, 7 Oct 2026 13:13:59 +0300 Subject: [PATCH 3/3] Honour adoc-enable-two-line-title in the title editing commands Highlighting, heading navigation and imenu only recognise two-line (setext) titles when they're enabled, but adoc-title-descriptor, which the editing commands go through, always did. With the option off (the default) any line above a block delimiter passed for a title, so M-left on `.Example' above `----' rewrote the delimiter as `~~~~~~~~', and C-c C-t deleted it. The descriptor now skips two-line titles unless they're enabled, and C-c C-t refuses to create one while they're disabled, or deeper than the levels two-line titles have (which used to crash). A numeric adoc-enable-two-line-title is documented as the length of delimiter lines that are never title underlines, but fontification compared it against the title text. Both places now share one check on the underline length. --- CHANGELOG.md | 4 +++ adoc-mode.el | 28 ++++++++++++++++--- test/adoc-mode-editing-test.el | 46 ++++++++++++++++++++++++-------- test/adoc-mode-font-lock-test.el | 5 ++++ 4 files changed, 68 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e24755f..6d6c50c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,10 @@ - Heading navigation and imenu now honour `adoc-enable-two-line-title`. It is nil by default, so two-line (setext) titles are no longer picked up unless you opt in, matching their fontification. Previously they were always recognised, which was the main source of the code-block confusion above. - [#89](https://github.com/bbatsov/adoc-mode/pull/89): Promoting, demoting or toggling a one-line title (`M-left` / `M-right`, `C-c C-t`) no longer turns it into the enclosed form when it's followed by a newline. `== Section` used to become `=== Section ===`. - [#89](https://github.com/bbatsov/adoc-mode/pull/89): `adoc-promote-title` and `adoc-demote-title` default to one level when called from Lisp without an argument (`adoc-demote-title` used to signal an error), and the title commands signal a `user-error` instead of an `error` when point isn't on a title. +- [#89](https://github.com/bbatsov/adoc-mode/pull/89): Title editing commands now honour `adoc-enable-two-line-title`, like highlighting, navigation and imenu already did. + - With two-line titles disabled (the default), `M-left` / `M-right` and `C-c C-t` no longer mistake a line above a `----` or `====` delimiter for a title and overwrite the delimiter. + - `C-c C-t` won't convert to a two-line title while they're disabled, or past level 4 (where it used to crash). + - A numeric value now skips underlines of that length, as documented. It used to compare the length of the title text instead. ## 0.9.0 (2026-06-02) diff --git a/adoc-mode.el b/adoc-mode.el index ffc05a1..3af2ec6 100644 --- a/adoc-mode.el +++ b/adoc-mode.el @@ -1831,6 +1831,13 @@ TEXT-FACE is a face name symbol." ;; TODO: highlight bogous 'two line titles' with warning face ;; TODO: completely remove keyword when adoc-enable-two-line-title is nil +(defun adoc--two-line-title-underline-p (length) + "Return non-nil when a line LENGTH chars long may underline a two-line title. +That takes two-line titles to be enabled, and LENGTH to differ from +a numeric `adoc-enable-two-line-title'." + (and adoc-enable-two-line-title + (not (eql adoc-enable-two-line-title length)))) + (defun adoc-kw-two-line-title (del text-face) "Creates a keyword for font-lock which highlights two line titles. TEXT-FACE is a face name symbol." @@ -1841,8 +1848,7 @@ TEXT-FACE is a face name symbol." (and adoc-enable-two-line-title (adoc-kwf-search ,(adoc-re-two-line-title del) end t) (< (abs (- (- (match-end 2) (match-beginning 2)) (- (match-end 3) (match-beginning 3)))) 3) - (or (not (numberp adoc-enable-two-line-title)) - (not (equal adoc-enable-two-line-title (- (match-end 2) (match-beginning 2))))) + (adoc--two-line-title-underline-p (- (match-end 3) (match-beginning 3))) (not (text-property-not-all (match-beginning 0) (match-end 0) 'adoc-reserved nil)))) ;; highlighers `(2 ,face-form t) @@ -3521,6 +3527,9 @@ Returns nil if there was no xref found." When STRICT-MATCH is t, and 2 line title is used, the lengths of the underline text and title must not differ by more than 2 characters. +Two line titles are only recognised when `adoc-enable-two-line-title' +is non-nil. + Title descriptor looks like this: (TYPE SUB-TYPE LEVEL TEXT START END) 0 TYPE: 1 fore one line title, 2 for two line title. @@ -3553,7 +3562,11 @@ trailing delimiter ('== my title =='). ;; the wrong underline length. ;; Two-line titles only cover the first N levels (one per entry in ;; adoc-two-line-title-del); skip the check for higher levels. - ((and (< level (length adoc-two-line-title-del)) + ;; They're also only recognised when enabled, like fontification, + ;; navigation and imenu do - otherwise a line of text above a + ;; block delimiter (`----', `====') would pass for one. + ((and adoc-enable-two-line-title + (< level (length adoc-two-line-title-del)) (or (looking-at (adoc-re-two-line-title (nth level adoc-two-line-title-del))) (save-excursion (forward-line -1) @@ -3564,7 +3577,8 @@ trailing delimiter ('== my title =='). (<= (abs (- (length (match-string 3)) (length (match-string 2)))) 2)) - (not (string-prefix-p "[" (match-string 2)))) + (not (string-prefix-p "[" (match-string 2))) + (adoc--two-line-title-underline-p (- (match-end 3) (match-beginning 3)))) (setq type 2) (setq text (match-string 2)) (setq found t)) @@ -3652,6 +3666,12 @@ and title's text are not preserved, afterwards its always one space." (end (nth 5 descriptor)) (saved-col (current-column))) + (when (eq new-type-val 2) + (unless adoc-enable-two-line-title + (user-error "Two-line titles are disabled, see `adoc-enable-two-line-title'")) + (when (>= new-level level-count) + (user-error "Two-line titles only support levels 0-%d" (1- level-count)))) + ;; set new title descriptor (setcar (nthcdr 0 descriptor) new-type-val) (setcar (nthcdr 1 descriptor) new-sub-type-val) diff --git a/test/adoc-mode-editing-test.el b/test/adoc-mode-editing-test.el index 9f398e5..49faef8 100644 --- a/test/adoc-mode-editing-test.el +++ b/test/adoc-mode-editing-test.el @@ -24,9 +24,10 @@ (adoc-test-trans "====== foo ======" "= foo =" '(adoc-promote-title 1)) (adoc-test-trans "== foo ==" "==== foo ====" '(adoc-promote-title 2)) ;; two-line titles span five levels (0-4), so promoting level 4 wraps to 0 - (adoc-test-trans "foo!\n===!" "foo\n---" '(adoc-promote-title 1)) - (adoc-test-trans "foo!\n+++!" "foo\n===" '(adoc-promote-title 1)) - (adoc-test-trans "foo!\n---!" "foo\n^^^" '(adoc-promote-title 2))) + (let ((adoc-enable-two-line-title t)) + (adoc-test-trans "foo!\n===!" "foo\n---" '(adoc-promote-title 1)) + (adoc-test-trans "foo!\n+++!" "foo\n===" '(adoc-promote-title 1)) + (adoc-test-trans "foo!\n---!" "foo\n^^^" '(adoc-promote-title 2)))) (it "keeps a title's delimiter style when it is followed by text" (adoc-test-trans "== Sec!tion\n\nbody\n" "=== Section\n\nbody\n" @@ -45,7 +46,8 @@ (it "demotes titles" (adoc-test-trans "= foo" "====== foo" '(adoc-demote-title 1)) (adoc-test-trans "= foo =" "====== foo ======" '(adoc-demote-title 1)) - (adoc-test-trans "foo!\n===!" "foo\n+++" '(adoc-demote-title 1))) + (let ((adoc-enable-two-line-title t)) + (adoc-test-trans "foo!\n===!" "foo\n+++" '(adoc-demote-title 1)))) (it "defaults to one level when called from Lisp without an argument" (adoc-test-trans "== f!oo\n" "=== foo\n" '(adoc-promote-title)) @@ -54,21 +56,43 @@ (adoc-test-trans "== f!oo\n" "= foo\n" '(adoc-demote))) (it "toggles the title type" - (adoc-test-trans "= one" "one\n===" '(adoc-toggle-title-type)) - (adoc-test-trans "two!\n===!" "= two" '(adoc-toggle-title-type)) - (adoc-test-trans "= three!\nbar" "three\n=====\nbar" '(adoc-toggle-title-type)) - (adoc-test-trans "four!\n====!\nbar" "= four\nbar" '(adoc-toggle-title-type)) + (let ((adoc-enable-two-line-title t)) + (adoc-test-trans "= one" "one\n===" '(adoc-toggle-title-type)) + (adoc-test-trans "two!\n===!" "= two" '(adoc-toggle-title-type)) + (adoc-test-trans "= three!\nbar" "three\n=====\nbar" '(adoc-toggle-title-type)) + (adoc-test-trans "four!\n====!\nbar" "= four\nbar" '(adoc-toggle-title-type))) (adoc-test-trans "= five" "= five =" '(adoc-toggle-title-type t)) (adoc-test-trans "= six =" "= six" '(adoc-toggle-title-type t))) + (it "refuses to make a two-line title when they are disabled or too deep" + (adoc-test-trans "== f!oo\n" "== foo\n" + '(expect (adoc-toggle-title-type) :to-throw 'user-error)) + (let ((adoc-enable-two-line-title t)) + (adoc-test-trans "====== f!oo\n" "====== foo\n" + '(expect (adoc-toggle-title-type) :to-throw 'user-error)))) + + (it "leaves block delimiters alone when two-line titles are disabled" + (dolist (command '(adoc-promote adoc-toggle-title-type)) + (adoc-test-trans ".Ex!ample\n-!---\ncode\n----\n" ".Example\n----\ncode\n----\n" + `(expect (,command) :to-throw 'user-error)) + (adoc-test-trans "Some t!ext\n====\nexample\n====\n" "Some text\n====\nexample\n====\n" + `(expect (,command) :to-throw 'user-error)))) + + (it "honours a numeric `adoc-enable-two-line-title' when editing" + (let ((adoc-enable-two-line-title 4)) + (adoc-test-trans ".Ex!ample\n----\ncode\n----\n" ".Example\n----\ncode\n----\n" + '(expect (adoc-promote) :to-throw 'user-error)) + (adoc-test-trans "foo!\n---" "== foo" '(adoc-toggle-title-type)))) + (it "signals a user-error when point is not on a title" (adoc-test-trans "just some pr!ose\n" "just some prose\n" '(expect (adoc-promote-title 1) :to-throw 'user-error))) (it "adjusts the two-line title underline length" - (adoc-test-trans "lorem!\n===!" "lorem\n=====" '(adoc-adjust-title-del)) - (adoc-test-trans "lorem!\n========!" "lorem\n=====" '(adoc-adjust-title-del)) - (adoc-test-trans "lorem!\n=====!" "lorem\n=====" '(adoc-adjust-title-del))) + (let ((adoc-enable-two-line-title t)) + (adoc-test-trans "lorem!\n===!" "lorem\n=====" '(adoc-adjust-title-del)) + (adoc-test-trans "lorem!\n========!" "lorem\n=====" '(adoc-adjust-title-del)) + (adoc-test-trans "lorem!\n=====!" "lorem\n=====" '(adoc-adjust-title-del)))) (it "builds a two-line title underline" (expect (adoc-make-two-line-title-underline 0 6) :to-equal "======") diff --git a/test/adoc-mode-font-lock-test.el b/test/adoc-mode-font-lock-test.el index f25d82a..72b45e5 100644 --- a/test/adoc-mode-font-lock-test.el +++ b/test/adoc-mode-font-lock-test.el @@ -41,6 +41,11 @@ "Document Title\n==============" '(("Document Title" adoc-title-0-face))))) + (it "skips two-line titles whose underline has the excluded length" + (let ((adoc-enable-two-line-title 5)) + (adoc-test--check-face-specs "Titles\n-----\n" '(("Titles" nil))) + (adoc-test--check-face-specs "Title\n------\n" '(("Title" adoc-title-1-face))))) + (when-fontifying-it "fontifies a block title" (".Block Title\nsome text" ("Block Title" adoc-gen-face))))