Ihor Radchenko wrote:
I have a handful of comments.
I appreciate it. Replies are below, and a revised patch set is attached
after a good bit of performance testing.
It is OK to provide a group in regexp, but adding groups make the
matching much slower. So, let's not modify the default regexp. Instead,
I recommend adding an optional parameter to org-item-re that will make
it return a version of regexp with groups.
Makes sense. Done. I should note that default regexp also had one
superfluous group, which the attached patch removes. The updated tests
cover both the bullet capturing (7 groups) and default (previously 5,
now 4 group) regexps.
For context, compared to the 5 group regexp:
- The 7 group one adds 2% to non-GC execution time and 13% to total time
including GC on a 1000000 line (27MB) test file.
- The 4 group one reduces total time by 1.4% on the same file.
On a 100000 line (2.7MB) file, the relative gaps narrow substantially;
there is almost no measurable difference.
I also couldn't find any measurable difference between capturing and
non-capturing regexp groups at any scale.
+(defconst org-element-block-elements
+ '(center-block comment-block dynamic-block example-block
+ export-block quote-block special-block src-block verse-block)
+ "List of block element types.")
What about simply matching the element type against ^.*-block$?
Sure. Done. This was my original approach, but I thought the explicitly
declared version might be preferable. Happy to go back to this.
+(defconst org-element-block-element-names
+ '(("CENTER" . center-block)
+ ("COMMENT" . comment-block)
+ ("EXAMPLE" . example-block)
+ ("EXPORT" . export-block)
+ ("QUOTE" . quote-block)
+ ("SRC" . src-block)
+ ("VERSE" . verse-block))
+ "Alist of block names to element types.")
If we can match against the type, this will no longer be needed.
True indeed.
+(defun org--block-types (types)
+ "Convert block names to types and filter non-strings to block types.
+Name strings that do not map to a defined block type are returned unchanged."
Same here.
I kept a helper function for readability, but it uses the approach you
suggested.
(defun org-before-change-function (_beg _end)
- "Every change indicates that a table might need an update."
+ "Every change indicates that a plain list or table might need an update."
+ (setq org-list-outermost-end (org-element-end (org-list-outermost)))
This will fire the parser before every single change.
Sorry, but this will slow down Org too much. We need to find some other
way. We just got a report that even the existing approach is already too
slow. With your addition, Org performance will crawl.
With the example file provided in
https://orgmode.org/list/87y0g9hx38.fsf@localhost, your patch will make
typing impossible.
Thanks for steering me on this. I mistook your earlier suggestion to use
the org-element API for font locking to imply that it should be used for
extending the font lock region too.
The revised patch set is very lightweight in the after-change function.
No parser; it just checks line beginnings for a single character or
org-item-re.
From 41545accbf391bf9e9c9196b2afc347999ab8169 Mon Sep 17 00:00:00 2001
From: Jeff Valk <[email protected]>
Date: Sat, 23 May 2026 18:54:31 -0400
Subject: [PATCH 1/4] org-list: Allow list item regexp to isolate bullet
* lisp/org-list.el (org-item-re): Add option to generate regexp so
the bullet is matched as group 1, independently of leading
whitespace. Remove superfluous capture group from default regexp.
Use rx syntax.
(org-item-beginning-re): Revise argument list and docstring to reflect
updated org-item-re.
* testing/lisp/test-org-list.el (test-org-list/match-item): Add test
for identification of list items using both default and bullet
capturing regexps.
---
lisp/org-list.el | 58 ++++++++++++----------
testing/lisp/test-org-list.el | 90 +++++++++++++++++++++++++++++++++++
2 files changed, 124 insertions(+), 24 deletions(-)
diff --git a/lisp/org-list.el b/lisp/org-list.el
index 8df83663c..7b991afa9 100644
--- a/lisp/org-list.el
+++ b/lisp/org-list.el
@@ -374,34 +374,44 @@ group 4: description tag")
(defvar org--item-re-cache nil
"Results cache for `org-item-re'.")
-(defsubst org-item-re ()
- "Return the correct regular expression for plain lists."
+(defsubst org-item-re (&optional capture-bullet)
+ "Return the correct regular expression for plain lists.
+When CAPTURE-BULLET is non-nil, group 1 matches the bullet without
+leading whitespace. Otherwise, groups are minimized for performance."
(or (plist-get
- (plist-get org--item-re-cache
- org-list-allow-alphabetical)
+ (plist-get
+ (plist-get org--item-re-cache capture-bullet)
+ org-list-allow-alphabetical)
org-plain-list-ordered-item-terminator)
- (let* ((term (cond
- ((eq org-plain-list-ordered-item-terminator t) "[.)]")
- ((= org-plain-list-ordered-item-terminator ?\)) ")")
- ((= org-plain-list-ordered-item-terminator ?.) "\\.")
- (t "[.)]")))
- (alpha (if org-list-allow-alphabetical "\\|[A-Za-z]" ""))
- (re (concat "\\([ \t]*\\([-+]\\|\\(\\([0-9]+" alpha "\\)" term
- "\\)\\)\\|[ \t]+\\*\\)\\([ \t]+\\|$\\)")))
- (setq org--item-re-cache
- (plist-put
- org--item-re-cache
- org-list-allow-alphabetical
- (plist-put
- (plist-get org--item-re-cache
- org-list-allow-alphabetical)
- org-plain-list-ordered-item-terminator
- re)))
+ (let* ((counter `(regexp ,(if org-list-allow-alphabetical
+ "[0-9]+\\|[A-Za-z]"
+ "[0-9]+")))
+ (term (or (car (memq org-plain-list-ordered-item-terminator '(?\) ?.)))
+ '(any ".)")))
+ (bullet `(or (any "-+") (seq ,counter ,term)))
+ (ws '(any " \t"))
+ (re (if capture-bullet
+ (rx-to-string ; captures bullet as group 1
+ `(or (seq (0+ ,ws) (group-n 1 ,bullet (or ,ws eol)))
+ (seq (1+ ,ws) (group-n 1 "*" (or ,ws eol)))))
+ (concat (rx-to-string ; minimizes groups (4 vs 7)
+ `(or (seq (0+ ,ws) ,bullet)
+ (seq (1+ ,ws) "*")))
+ (rx-to-string `(or ,ws eol)))))
+ (c0 org--item-re-cache)
+ (c1 (plist-get c0 capture-bullet))
+ (c2 (plist-get c1 org-list-allow-alphabetical))
+ (c2* (plist-put c2 org-plain-list-ordered-item-terminator re))
+ (c1* (plist-put c1 org-list-allow-alphabetical c2*))
+ (c0* (plist-put c0 capture-bullet c1*)))
+ (setq org--item-re-cache c0*)
re)))
-(defsubst org-item-beginning-re ()
- "Regexp matching the beginning of a plain list item."
- (concat "^" (org-item-re)))
+(defsubst org-item-beginning-re (&optional capture-bullet)
+ "Regexp matching the beginning of a plain list item.
+When CAPTURE-BULLET is non-nil, group 1 matches the bullet without
+leading whitespace. Otherwise, groups are minimized for performance."
+ (concat "^" (org-item-re capture-bullet)))
(defun org-list-at-regexp-after-bullet-p (regexp)
"Is point at a list item with REGEXP after bullet?"
diff --git a/testing/lisp/test-org-list.el b/testing/lisp/test-org-list.el
index 1da621143..e2edeae04 100644
--- a/testing/lisp/test-org-list.el
+++ b/testing/lisp/test-org-list.el
@@ -28,6 +28,96 @@
(require 'org-list)
(require 'org)
+(ert-deftest test-org-list/match-item ()
+ "Test identification of list items.
+With bullet capture, match group 1 should contain the list item bullet.
+This tests `org-item-beginning-re' and by extension `org-item-re'."
+ (cl-flet ((match-group-1 ()
+ (re-search-forward (org-item-beginning-re t)) ; capture bullet
+ (match-string-no-properties 1)))
+ ;; Unordered, unindented
+ (org-test-with-temp-text "- item"
+ (should (looking-at (org-item-beginning-re)))
+ (should (equal "- " (match-group-1))))
+ (org-test-with-temp-text "+ item"
+ (should (looking-at (org-item-beginning-re)))
+ (should (equal "+ " (match-group-1))))
+ ;; Unordered, indented
+ (org-test-with-temp-text " - item"
+ (should (looking-at (org-item-beginning-re)))
+ (should (equal "- " (match-group-1))))
+ (org-test-with-temp-text " + item"
+ (should (looking-at (org-item-beginning-re)))
+ (should (equal "+ " (match-group-1))))
+ (org-test-with-temp-text " * item"
+ (should (looking-at (org-item-beginning-re)))
+ (should (equal "* " (match-group-1))))
+ ;; Ordered, default terminators
+ (let ((org-plain-list-ordered-item-terminator t))
+ ;; unindented
+ (org-test-with-temp-text "1. item"
+ (should (looking-at (org-item-beginning-re)))
+ (should (equal "1. " (match-group-1))))
+ (org-test-with-temp-text "123) item"
+ (should (looking-at (org-item-beginning-re)))
+ (should (equal "123) " (match-group-1))))
+ ;; indented
+ (org-test-with-temp-text " 1. item"
+ (should (looking-at (org-item-beginning-re)))
+ (should (equal "1. " (match-group-1))))
+ (org-test-with-temp-text " 123) item"
+ (should (looking-at (org-item-beginning-re)))
+ (should (equal "123) " (match-group-1))))
+ ;; alpha
+ (let ((org-list-allow-alphabetical t))
+ ;; unindented
+ (org-test-with-temp-text "a) item"
+ (should (looking-at (org-item-beginning-re)))
+ (should (equal "a) " (match-group-1))))
+ (org-test-with-temp-text "Z. item"
+ (should (looking-at (org-item-beginning-re)))
+ (should (equal "Z. " (match-group-1))))
+ ;; indented
+ (org-test-with-temp-text " a) item"
+ (should (looking-at (org-item-beginning-re)))
+ (should (equal "a) " (match-group-1))))
+ (org-test-with-temp-text " Z. item"
+ (should (looking-at (org-item-beginning-re)))
+ (should (equal "Z. " (match-group-1))))))
+ ;; Ordered, specific terminator
+ (let ((org-plain-list-ordered-item-terminator ?.))
+ (org-test-with-temp-text "1. item"
+ (should (looking-at (org-item-beginning-re)))
+ (should (equal "1. " (match-group-1))))
+ (org-test-with-temp-text "1) item"
+ (should-not (looking-at (org-item-beginning-re)))
+ (should-error (match-group-1))))
+ (let ((org-plain-list-ordered-item-terminator ?\)))
+ (org-test-with-temp-text "1. item"
+ (should-not (looking-at (org-item-beginning-re)))
+ (should-error (match-group-1)))
+ (org-test-with-temp-text "1) item"
+ (should (looking-at (org-item-beginning-re)))
+ (should (equal "1) " (match-group-1)))))
+ ;; Invalid list items
+ (org-test-with-temp-text "* item" ; star without indentation
+ (should-not (looking-at (org-item-beginning-re)))
+ (should-error (match-group-1)))
+ (org-test-with-temp-text "1 item" ; no ordinal terminator
+ (should-not (looking-at (org-item-beginning-re)))
+ (should-error (match-group-1)))
+ (org-test-with-temp-text "1] item" ; invalid ordinal terminator
+ (should-not (looking-at (org-item-beginning-re)))
+ (should-error (match-group-1)))
+ (let ((org-list-allow-alphabetical nil))
+ (org-test-with-temp-text "a) item" ; alpha without option
+ (should-not (looking-at (org-item-beginning-re)))
+ (should-error (match-group-1))))
+ (let ((org-list-allow-alphabetical t))
+ (org-test-with-temp-text "ab) item" ; multiple alpha characters
+ (should-not (looking-at (org-item-beginning-re)))
+ (should-error (match-group-1))))))
+
(ert-deftest test-org-list/list-ending ()
"Test if lists end at the right place."
;; With two blank lines.
--
2.54.0
From f03d9a56a02486b14133bca703483934404dc0c9 Mon Sep 17 00:00:00 2001
From: Jeff Valk <[email protected]>
Date: Sun, 7 Jun 2026 22:12:26 -0400
Subject: [PATCH 2/4] org: Improve org-in-block-p
* lisp/org.el (org-in-block-p): Replace regexp-based approach with
element API by renaming and generalizing existing function
org-in-src-block-p. This adds flexibility and is more reliable with
nested blocks.
(org-in-src-block-p): Preserve this function name as a thin wrapper
around the improved org-in-block-p.
(org--block-types): Add utility for org-in-block-p type arguments.
* lisp/org-element.el (org-element-block-elements): Add new constant.
* testing/lisp/test-org.el (test-org/in-block-p): Add tests for
org-in-block-p.
---
lisp/org-element.el | 5 ++
lisp/org.el | 59 +++++++++---------
testing/lisp/test-org.el | 125 +++++++++++++++++++++++++++++++++++++++
3 files changed, 162 insertions(+), 27 deletions(-)
diff --git a/lisp/org-element.el b/lisp/org-element.el
index fe78dfb2c..d523c8455 100644
--- a/lisp/org-element.el
+++ b/lisp/org-element.el
@@ -350,6 +350,11 @@ specially in `org-element--object-lex'.")
section table-row)
"List of paragraph-level node types that cannot have affiliated keywords.")
+(defconst org-element-block-elements
+ '( center-block comment-block dynamic-block example-block
+ export-block quote-block special-block src-block verse-block)
+ "List of block element types.")
+
(defconst org-element-affiliated-keywords
'("CAPTION" "DATA" "HEADER" "HEADERS" "LABEL" "NAME" "PLOT" "RESNAME" "RESULT"
"RESULTS" "SOURCE" "SRCNAME" "TBLNAME")
diff --git a/lisp/org.el b/lisp/org.el
index 294a5eb52..5434794ed 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -19271,17 +19271,39 @@ With prefix arg UNCOMPILED, load the uncompiled versions."
(setq s (replace-match "\\vert" t t s)))
s)
-(defun org-in-src-block-p (&optional inside element)
- "Return t when point is at a source block element.
-When INSIDE is non-nil, return t only when point is between #+BEGIN_SRC
-and #+END_SRC lines.
+(defun org--block-types (names-or-types)
+ "Convert block names to types and filter non-strings to defined block types.
+Name strings that do not map to a defined block type are presumed to
+be names of special blocks and are returned unchanged."
+ (mapcar (lambda (x)
+ (if (stringp x)
+ (let ((type (intern (format "%s-block" (downcase x)))))
+ (or (car (memq type org-element-block-elements)) x))
+ (car (memq x org-element-block-elements))))
+ (ensure-list names-or-types)))
+
+(defun org-in-block-p (&optional types inside element)
+ "Return t when point is in a block element.
+
+Block TYPES may be constrained using a type symbol, a name string,
+or a list including either. Name strings are mapped to type symbols
+for defined block types and compared as a \\='special-block :type
+property otherwise.
+
+When INSIDE is non-nil, return t only when point is between #+BEGIN
+and #+END lines.
Note that affiliated keywords and blank lines after are considered a
part of a source block.
When ELEMENT is provided, it is considered to be element at point."
(save-match-data (setq element (or element (org-element-at-point))))
- (when (org-element-type-p element 'src-block)
+ (when-let* ((types (or (org--block-types types) org-element-block-elements))
+ (element (or (org-element-lineage element types t)
+ (org-element-lineage-map element
+ `(let ((type (org-element-property :type node)))
+ (when (member type ',types) node))
+ 'special-block t t))))
(or (not inside)
(not (or (<= (line-beginning-position)
(org-element-post-affiliated element))
@@ -19290,6 +19312,11 @@ When ELEMENT is provided, it is considered to be element at point."
(skip-chars-backward " \t\n\r")
(point))))))))
+(defun org-in-src-block-p (&optional inside element)
+ "Return t when point is at a source block element.
+This simply wraps `org-in-block-p' for type \\='src-block."
+ (org-in-block-p 'src-block inside element))
+
(defun org-context ()
"Return a list of contexts of the current cursor position.
If several contexts apply, all are returned.
@@ -19442,28 +19469,6 @@ position before START-RE (resp. after END-RE)."
;; Return value.
(cons beg end))))))
-(defun org-in-block-p (names)
- "Non-nil when point belongs to a block whose name belongs to NAMES.
-
-NAMES is a list of strings containing names of blocks.
-
-Return first block name matched, or nil. Beware that in case of
-nested blocks, the returned name may not belong to the closest
-block from point."
- (save-match-data
- (catch 'exit
- (let ((case-fold-search t)
- (lim-up (save-excursion (outline-previous-heading)))
- (lim-down (save-excursion (outline-next-heading))))
- (dolist (name names)
- (let ((n (regexp-quote name)))
- (when (org-between-regexps-p
- (concat "^[ \t]*#\\+begin_" n)
- (concat "^[ \t]*#\\+end_" n)
- lim-up lim-down)
- (throw 'exit n)))))
- nil)))
-
;; Defined in org-agenda.el
(defvar org-agenda-restrict)
(defvar org-agenda-restrict-begin)
diff --git a/testing/lisp/test-org.el b/testing/lisp/test-org.el
index 9e9be9ebc..c5d1fb294 100644
--- a/testing/lisp/test-org.el
+++ b/testing/lisp/test-org.el
@@ -6042,6 +6042,131 @@ Text.
(org-next-block 1 "^[ \t]*#\\+BEGIN_QUOTE")
(looking-at "#\\+begin_quote")))))
+(ert-deftest test-org/in-block-p ()
+ "Test `org-in-block-p' specifications."
+ ;; Not in block
+ (should-not
+ (org-test-with-temp-text
+ "<point>\n#+NAME: A\n#+BEGIN_EXAMPLE\nB\n#+END_EXAMPLE\n\nC\n"
+ (org-in-block-p)))
+ ;; Without inside flag
+ ;;; On affiliated line
+ (should
+ (org-test-with-temp-text
+ "\n<point>#+NAME: A\n#+BEGIN_EXAMPLE\nB\n#+END_EXAMPLE\n\nC\n"
+ (org-in-block-p)))
+ ;;; On begin line
+ (should
+ (org-test-with-temp-text
+ "\n#+NAME: A\n<point>#+BEGIN_EXAMPLE\nB\n#+END_EXAMPLE\n\nC\n"
+ (org-in-block-p)))
+ ;;; Inside block
+ (should
+ (org-test-with-temp-text
+ "\n#+NAME: A\n#+BEGIN_EXAMPLE\n<point>B\n#+END_EXAMPLE\n\nC\n"
+ (org-in-block-p)))
+ ;;; On end line
+ (should
+ (org-test-with-temp-text
+ "\n#+NAME: A\n#+BEGIN_EXAMPLE\nB\n<point>#+END_EXAMPLE\n\nC\n"
+ (org-in-block-p)))
+ ;;; In post blank
+ (should
+ (org-test-with-temp-text
+ "\n#+NAME: A\n#+BEGIN_EXAMPLE\nB\n#+END_EXAMPLE\n<point>\nC\n"
+ (org-in-block-p)))
+ ;; With inside flag
+ ;;; On affiliated line
+ (should-not
+ (org-test-with-temp-text
+ "\n<point>#+NAME: A\n#+BEGIN_EXAMPLE\nB\n#+END_EXAMPLE\n\nC\n"
+ (org-in-block-p nil t)))
+ ;;; On begin line
+ (should-not
+ (org-test-with-temp-text
+ "\n#+NAME: A\n<point>#+BEGIN_EXAMPLE\nB\n#+END_EXAMPLE\n\nC\n"
+ (org-in-block-p nil t)))
+ ;;; Inside block
+ (should
+ (org-test-with-temp-text
+ "\n#+NAME: A\n#+BEGIN_EXAMPLE\n<point>B\n#+END_EXAMPLE\n\nC\n"
+ (org-in-block-p nil t)))
+ ;;; On end line
+ (should-not
+ (org-test-with-temp-text
+ "\n#+NAME: A\n#+BEGIN_EXAMPLE\nB\n<point>#+END_EXAMPLE\n\nC\n"
+ (org-in-block-p nil t)))
+ ;;; In post blank
+ (should-not
+ (org-test-with-temp-text
+ "\n#+NAME: A\n#+BEGIN_EXAMPLE\nB\n#+END_EXAMPLE\n<point>\nC\n"
+ (org-in-block-p nil t)))
+ ;; Nested blocks
+ ;;; Block within greater block
+ (should
+ (org-test-with-temp-text
+ "#+BEGIN_QUOTE\n#+BEGIN_EXAMPLE\n<point>A\n#+END_EXAMPLE\n#+END_QUOTE\n"
+ (org-in-block-p 'example-block)))
+ (should
+ (org-test-with-temp-text
+ "#+BEGIN_QUOTE\n#+BEGIN_EXAMPLE\n<point>A\n#+END_EXAMPLE\n#+END_QUOTE\n"
+ (org-in-block-p 'quote-block)))
+ ;;; Block within lesser block
+ (should
+ (org-test-with-temp-text
+ "#+BEGIN_SRC\n#+BEGIN_EXAMPLE\n<point>A\n#+END_EXAMPLE\n#+END_SRC\n"
+ (org-in-block-p 'src-block)))
+ (should-not
+ (org-test-with-temp-text
+ "#+BEGIN_SRC\n#+BEGIN_EXAMPLE\n<point>A\n#+END_EXAMPLE\n#+END_SRC\n"
+ (org-in-block-p 'quote-block)))
+ ;; Type selectors
+ ;;; Symbols
+ (should
+ (org-test-with-temp-text
+ "#+BEGIN_EXAMPLE\n<point>A\n#+END_EXAMPLE\n"
+ (org-in-block-p 'example-block)))
+ ;; Name strings
+ (should
+ (org-test-with-temp-text
+ "#+BEGIN_EXAMPLE\n<point>A\n#+END_EXAMPLE\n"
+ (org-in-block-p "example")))
+ ;; Special (named) blocks
+ (should
+ (org-test-with-temp-text
+ "#+BEGIN_abcd\n<point>A\n#+END_abcd\n"
+ (org-in-block-p "abcd")))
+ (should-not
+ (org-test-with-temp-text
+ "#+BEGIN_abcd\n<point>A\n#+END_abcd\n"
+ (org-in-block-p 'abcd-block)))
+ (should
+ (org-test-with-temp-text
+ "#+BEGIN_abcd\n<point>A\n#+END_abcd\n"
+ (org-in-block-p 'special-block)))
+ ;; Dynamic blocks
+ (should-not
+ (org-test-with-temp-text
+ "#+BEGIN: abcd\n<point>A\n#+END\n"
+ (org-in-block-p "abcd")))
+ (should
+ (org-test-with-temp-text
+ "#+BEGIN: abcd\n<point>A\n#+END\n"
+ (org-in-block-p 'dynamic-block)))
+ ;;; Multiple type selectors
+ (should
+ (org-test-with-temp-text
+ "#+BEGIN_EXAMPLE\n<point>A\n#+END_EXAMPLE\n"
+ (org-in-block-p '(example-block "src" "abcd"))))
+ (should
+ (org-test-with-temp-text
+ "#+BEGIN_SRC\n<point>A\n#+END_SRC\n"
+ (org-in-block-p '(example-block "src" "abcd"))))
+ (should
+ (org-test-with-temp-text
+ "#+BEGIN_abcd\n<point>A\n#+END_abcd\n"
+ (org-in-block-p '(example-block "src" "abcd")))))
+
;;; Outline structure
--
2.54.0
From 89a45ffd7be2d2ce5d55122abd08879d7acea9eb Mon Sep 17 00:00:00 2001
From: Jeff Valk <[email protected]>
Date: Mon, 8 Jun 2026 21:16:45 -0400
Subject: [PATCH 3/4] Add faces for structural syntax elements
This adds an identifying face and fontification for syntax elements
that convey document outline structure: headline stars, list bullets,
list indentation. Structure faces inherit from a newly introduced
common ancestor. The new faces are defined without any properties,
which adds useful semantic information but reserves any visual
customization to the user.
Fontification of indentation is context-sensitive: it is applied only
within a plain list. This is straightforward with one caveat: a
buffer change might either create or end an outermost plain list
around indented content. This would change the syntactic significance
of indentation, and thus requires the fontification region to be
extended.
* lisp/org-faces.el (org-structure): Add new face.
(org-headline-stars, org-list-bullet, org-list-indent): Add new faces,
inheriting from org-structure.
(org-hide): Modify face to inherit from org-structure. This face is
used for structural syntax, even if made invisible. Importantly, the
org-indent face inherits from org-hide.
* lisp/org.el (org-set-font-lock-defaults): Apply org-list-bullet face.
(org-get-level-face): Apply org-headline-stars face.
(org-fontify-leading-space): Add new function.
(org-fontify-extend-space): Extend region when change may create or
end a plain list item having indented child content other than nested
list items.
(org-fontify-extend-region, org-fontify-extend-block): Delegate
region extension to helper functions for readability.
---
lisp/org-faces.el | 23 +++++++++++++++++-
lisp/org.el | 61 +++++++++++++++++++++++++++++++++++++++++++----
2 files changed, 79 insertions(+), 5 deletions(-)
diff --git a/lisp/org-faces.el b/lisp/org-faces.el
index b0f5ef9d7..2c7496ee8 100644
--- a/lisp/org-faces.el
+++ b/lisp/org-faces.el
@@ -41,8 +41,29 @@
"Face used for default text."
:group 'org-faces)
+(defface org-structure
+ '((t ()))
+ "Face used for syntax that defines outline structure."
+ :group 'org-faces)
+
+(defface org-headline-stars
+ '((t (:inherit org-structure)))
+ "Face used for stars in headlines."
+ :group 'org-faces)
+
+(defface org-list-bullet
+ '((t (:inherit org-structure)))
+ "Face used for bullets or numerals in plain lists."
+ :group 'org-faces)
+
+(defface org-list-indent
+ '((t (:inherit org-structure)))
+ "Face used for significant leading whitespace in plain lists."
+ :group 'org-faces)
+
(defface org-hide
- '((((background light)) (:foreground "white"))
+ '((default :inherit org-structure)
+ (((background light)) (:foreground "white"))
(((background dark)) (:foreground "black")))
"Face used to hide leading stars in headlines.
The foreground color of this face should be equal to the background
diff --git a/lisp/org.el b/lisp/org.el
index 5434794ed..0f25fe5bb 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -5830,7 +5830,39 @@ by a #."
(add-text-properties closing-start end '(invisible t)))
t)))))
-(defun org-fontify-extend-region (beg end _old-len)
+(defun org-fontify-leading-space (limit)
+ "Fontify leading whitespace in plain lists."
+ (when (re-search-forward "^[ \t]+" limit t)
+ (let ((beg (match-beginning 0))
+ (end (match-end 0))
+ (elem (save-match-data (org-element-at-point))))
+ ;; Indentation is syntactically meaningful within plain lists
+ ;; including for paragraphs, etc that lack identifying syntax.
+ ;; Fontify, omitting blocks in which lists are not allowed.
+ (when (org-element-lineage elem 'plain-list t)
+ (unless (org-in-block-p org-list-forbidden-blocks t)
+ (add-face-text-property beg end 'org-list-indent))))
+ t))
+
+(defun org-fontify-extend-space (beg end _old-len)
+ "Extend the region to refontify for plain list indentation.
+
+If an outermost plain list is either created or ended by a change, it
+alters whether indentation is significant for child contents. To
+accommodate, extend the region to include any indented lines between
+the end of the change and where either the next list item begins or
+an outermost list would necessarily end (a non-indented line or the
+end of buffer)."
+ (goto-char end)
+ (forward-line)
+ (let ((re (concat "\\S-\\|\\'\\|" (org-item-re))))
+ (if (looking-at-p re)
+ (cons beg end)
+ (while (not (looking-at-p re)) (forward-line))
+ (cons beg (point)))))
+
+(defun org-fontify-extend-block (beg end _old-len)
+ "Extend the region to refontify when block begin/end lines change."
(let ((end (if (progn (goto-char end) (looking-at-p "^[*#]"))
(min (point-max) (1+ end))
;; See `font-lock-extend-jit-lock-region-after-change' and bug#68849.
@@ -5854,6 +5886,21 @@ by a #."
(cons beg (or (funcall extend "end" "]" 1) end)))
(t (cons beg end))))))
+(defun org-fontify-extend-region (&rest args)
+ "Return the region to refontify after a change.
+
+This dispatches to case-specific functions that return expanded
+bounds if needed and the default bounds otherwise. Each function
+executes independently of the others. The returned region to
+refontify is the aggregate of these.
+
+See `font-lock-extend-after-change-region-function'"
+ (let* ((fns '(org-fontify-extend-space
+ org-fontify-extend-block))
+ (bounds (mapcar (lambda (f) (apply f args)) fns)))
+ (cons (apply #'min (mapcar #'car bounds))
+ (apply #'max (mapcar #'cdr bounds)))))
+
(defun org-activate-footnote-links (limit)
"Add text properties for footnotes."
(let ((fn (org-footnote-next-reference-or-definition limit)))
@@ -6102,6 +6149,9 @@ needs to be inserted at a specific position in the font-lock sequence.")
(1 (org-get-level-face 1))
(2 (org-get-level-face 2))
(3 (org-get-level-face 3)))
+ ;; Plain lists
+ `(,(org-item-beginning-re t)
+ (1 'org-list-bullet))
;; Table lines
'("^[ \t]*\\(\\(|\\|\\+-[-+]\\).*\\S-\\)\n?"
(0 'org-table-row t)
@@ -6210,6 +6260,8 @@ needs to be inserted at a specific position in the font-lock sequence.")
(org-cite-try-load-processor org-cite-activate-processor))
;; prepends faces
'(org-cite-activate))
+ ;; Leading whitespace
+ '(org-fontify-leading-space)
;; COMMENT
;; Apply this last, after all the markup is highlighted, so
;; that even "bright" markup will become dim.
@@ -6318,10 +6370,11 @@ needs to be inserted at a specific position in the font-lock sequence.")
(org-l (if org-odd-levels-only (1+ (/ org-l0 2)) org-l0))
(org-f (if org-cycle-level-faces
(nth (% (1- org-l) org-n-level-faces) org-level-faces)
- (nth (1- (min org-l org-n-level-faces)) org-level-faces))))
+ (nth (1- (min org-l org-n-level-faces)) org-level-faces)))
+ (org-f-stars (list 'org-headline-stars org-f)))
(cond
- ((eq n 1) (if org-hide-leading-stars 'org-hide org-f))
- ((eq n 2) org-f)
+ ((eq n 1) (if org-hide-leading-stars 'org-hide org-f-stars))
+ ((eq n 2) org-f-stars)
(t (unless org-level-color-stars-only org-f)))))
(defun org-face-from-face-or-color (context inherit face-or-color)
--
2.54.0
From b83e86f66331ae048eb88e31f8a14ec86e220b7d Mon Sep 17 00:00:00 2001
From: Jeff Valk <[email protected]>
Date: Mon, 6 Jul 2026 23:27:40 -0400
Subject: [PATCH 4/4] org: Consolidate space related extend-region cases
* lisp/org.el (org-fontify-extend-space, org-fontify-extend-block):
Absorb the end-of-line region extension fix from
org-fontify-extend-block into org-fontify-extend-space, where it's
logically compatible. This simplifies the code for both,
and (modestly) improves performance in the extend-after-change
function. Specifically, this removes a conditional in which both
branches return the same result; and it observes that (forward-line)
will always move point at least as far as (min (point-max) (1+ end)).
---
lisp/org.el | 23 ++++++++++++-----------
1 file changed, 12 insertions(+), 11 deletions(-)
diff --git a/lisp/org.el b/lisp/org.el
index 0f25fe5bb..6cca54ac6 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -5845,29 +5845,30 @@ by a #."
t))
(defun org-fontify-extend-space (beg end _old-len)
- "Extend the region to refontify for plain list indentation.
+ "Extend the region to refontify for plain list indentation and line endings.
If an outermost plain list is either created or ended by a change, it
alters whether indentation is significant for child contents. To
accommodate, extend the region to include any indented lines between
the end of the change and where either the next list item begins or
an outermost list would necessarily end (a non-indented line or the
-end of buffer)."
+end of buffer).
+
+Always extend the region forward to the next line. Even when not
+needed for plain list indentation, this ensures that end of line
+fontification is correct for faces with :extend t. This digests edge
+case fixes from d74a82448 and 1abff3859.
+See `font-lock-extend-jit-lock-region-after-change' and bug#68849."
(goto-char end)
(forward-line)
(let ((re (concat "\\S-\\|\\'\\|" (org-item-re))))
- (if (looking-at-p re)
- (cons beg end)
- (while (not (looking-at-p re)) (forward-line))
- (cons beg (point)))))
+ (while (not (looking-at-p re))
+ (forward-line))
+ (cons beg (point))))
(defun org-fontify-extend-block (beg end _old-len)
"Extend the region to refontify when block begin/end lines change."
- (let ((end (if (progn (goto-char end) (looking-at-p "^[*#]"))
- (min (point-max) (1+ end))
- ;; See `font-lock-extend-jit-lock-region-after-change' and bug#68849.
- (min (point-max) (1+ end))))
- (begin-re "\\(\\\\\\[\\|\\(#\\+begin_\\|\\\\begin{\\)\\S-+\\)")
+ (let ((begin-re "\\(\\\\\\[\\|\\(#\\+begin_\\|\\\\begin{\\)\\S-+\\)")
(end-re "\\(\\\\\\]\\|\\(#\\+end_\\|\\\\end{\\)\\S-+\\)")
(extend
(lambda (r1 r2 dir)
--
2.54.0