Make :range-fn in treesit-range-rules actually work

* lisp/treesit.el (treesit--range-start):
(treesit--range-end):
(treesit--flatten-ranges): New helpers.
(treesit-query-range):
(treesit-query-range-by-language): Don't flatten the ranges
returned by each range-fn.
(treesit--update-ranges-non-local):
(treesit--update-ranges-local): Handle the case where the range
is a complicated ((START . END)...).
* test/src/treesit-tests.el (treesit-range-helper): New test.
This commit is contained in:
Yuan Fu 2026-04-23 01:13:05 -07:00
parent 28f828e8a6
commit d3debef28d
No known key found for this signature in database
GPG key ID: 56E19BC57664A442
2 changed files with 139 additions and 20 deletions

View file

@ -545,6 +545,44 @@ See `treesit-query-capture' for QUERY."
(treesit-parser-root-node parser)
query))))
(defsubst treesit--range-start (range)
"Return the start of RANGE.
RANGE can be either a simple range (START . END), or a list of ranges
\((START . END)...). If RANGE is empty, return nil."
(if (consp (car range))
(caar range)
(car range)))
(defsubst treesit--range-end (range)
"Return the end of RANGE.
RANGE can be either a simple range (START . END), or a list of ranges
\((START . END)...). If RANGE is empty, return nil."
(let ((last-link (last range)))
(if (consp (car last-link))
(cdr (car last-link))
(cdr last-link))))
(defun treesit--flatten-ranges (ranges)
"Flatten RANGES to be a list of (START . END) in-place and return it.
Each element in RANGES can be either (STRT . END) or ((START . END)...)."
(let (head prev elm)
(while (setq elm (car ranges))
(if (consp (car elm))
;; ELM is ((START . END)...).
(progn
(if (null head)
(setq head elm)
(setcdr prev elm))
(setq prev (last elm)))
;; ELM is (START . END).
(if (null head)
(setq head ranges)
(setcdr prev ranges))
(setq prev ranges))
(setq ranges (cdr ranges)))
head))
(defun treesit-query-range (node query &optional beg end offset range-fn)
"Query the current buffer and return ranges of captured nodes.
@ -557,7 +595,12 @@ being returned. Capture names generally don't matter, but names
that starts with an underscore are ignored.
RANGE-FN, if non-nil, is a function that takes a NODE and OFFSET, and
returns the ranges to use for that NODE."
returns the ranges to use for that NODE, it can return either a simple
range (START . END) or a list ((START . END)...).
Note that each range in the returned list can be either a simple (START
. END), or a list itself ((START . END)...). The latter case means the
embedded parser should have ranges set to that."
(let ((offset-left (or (car offset) 0))
(offset-right (or (cdr offset) 0)))
(cl-loop for capture
@ -565,11 +608,11 @@ returns the ranges to use for that NODE."
for name = (car capture)
for node = (cdr capture)
if (not (string-prefix-p "_" (symbol-name name)))
append
collect
(if range-fn
(funcall range-fn node offset)
(list (cons (+ (treesit-node-start node) offset-left)
(+ (treesit-node-end node) offset-right)))))))
(cons (+ (treesit-node-start node) offset-left)
(+ (treesit-node-end node) offset-right))))))
(defun treesit-query-range-by-language
(node query language-fn &optional beg end offset range-fn)
@ -584,9 +627,14 @@ symbols with LANGUAGE-FN. LANGUAGE-FN can return nil, meaning no
valid language is detected, in which case the range is skipped.
RANGE-FN, if non-nil, is a function that takes a NODE and OFFSET, and
returns the ranges to use for that NODE.
returns the ranges to use for that NODE, it can return either a simple
range (START . END) or a list ((START . END)...).
BEG, END, OFFSET are the same as in `treesit-query-range'."
BEG, END, OFFSET are the same as in `treesit-query-range'.
Note that each range in the returned list can be either (START . END)
or ((START . END)...). The latter case means the embedded parser should
have ranges set to that."
(let ((offset-left (or (car offset) 0))
(offset-right (or (cdr offset) 0))
(ranges-by-language nil))
@ -601,12 +649,11 @@ BEG, END, OFFSET are the same as in `treesit-query-range'."
(not (string-prefix-p "_" (symbol-name name))))
(push (if range-fn
(funcall range-fn node offset)
(list (cons (+ (treesit-node-start node) offset-left)
(+ (treesit-node-end node) offset-right))))
(cons (+ (treesit-node-start node) offset-left)
(+ (treesit-node-end node) offset-right)))
(alist-get lang ranges-by-language))))))))
(mapcar (lambda (entry)
(cons (car entry)
(apply #'append (nreverse (cdr entry)))))
(cons (car entry) (nreverse (cdr entry))))
ranges-by-language)))
(defun treesit-query-valid-p (language query)
@ -1053,27 +1100,32 @@ Return updated parsers as a list."
;; Lay an overlay over each range to mark the start & end of
;; it for other functions to access (e.g., outline wants to
;; know this). Refer to (ref:local-parser-overlay) for more
;; explanation of local parser overlays.
;; explanation of local parser overlays. Each RANGE can be
;; either a simple (START . END) or a ((START . END)...).
(dolist (range new-ranges)
(let ((has-existing-ov nil))
(let ((has-existing-ov nil)
(r-start (treesit--range-start range))
(r-end (treesit--range-end range)))
(setq has-existing-ov
(catch 'done
(dolist (ov (overlays-in (car range) (cdr range)))
(dolist (ov (overlays-in r-start r-end))
(when (eq (overlay-get ov 'treesit-parser)
embed-parser)
(move-overlay ov (car range) (cdr range))
(move-overlay ov r-start r-end)
(overlay-put ov 'treesit-parser-ov-timestamp
modified-tick)
(throw 'done t)))))
(unless has-existing-ov
(let ((ov (make-overlay (car range) (cdr range))))
(let ((ov (make-overlay r-start r-end)))
(overlay-put ov 'treesit-parser embed-parser)
(overlay-put ov 'treesit-parser-local-p nil)
(overlay-put ov 'treesit-host-parser host-parser)
(overlay-put ov 'treesit-parser-ov-timestamp
modified-tick)))))
;; Set ranges for the embed parser.
(let* ((old-ranges (treesit-parser-included-ranges
(let* ((new-ranges
(treesit--flatten-ranges new-ranges))
(old-ranges (treesit-parser-included-ranges
embed-parser))
(set-ranges (treesit--clip-ranges
(treesit--merge-ranges
@ -1133,8 +1185,12 @@ Return the created local parsers as a list."
(dolist (lang-and-range ranges-by-lang)
(let ((embedded-lang (car lang-and-range))
(ranges (cdr lang-and-range)))
(pcase-dolist (`(,beg . ,end) ranges)
(let ((existing-local-parser
;; Each element of RANGES can be either (START . END) or ((START
;; . END)...).
(dolist (range ranges)
(let ((beg (treesit--range-start range))
(end (treesit--range-end range))
(existing-local-parser
(catch 'done
(dolist (ov (overlays-in beg end) nil)
;; Update range of local parser.
@ -1148,7 +1204,9 @@ Return the created local parsers as a list."
(eq parser-lang embedded-lang)
(eq embed-level parser-level))
(treesit-parser-set-included-ranges
embedded-parser `((,beg . ,end)))
embedded-parser (if (consp (car range))
range
(list range)))
(move-overlay ov beg end)
(overlay-put ov 'treesit-parser-ov-timestamp
modified-tick)
@ -1168,7 +1226,9 @@ Return the created local parsers as a list."
(overlay-put ov 'treesit-parser-ov-timestamp
modified-tick)
(treesit-parser-set-included-ranges
embedded-parser `((,beg . ,end)))
embedded-parser (if (consp (car range))
range
(list range)))
(push embedded-parser touched-parsers)))))))
touched-parsers))

View file

@ -524,6 +524,65 @@ BODY is the test body."
;;; Query
(ert-deftest treesit-range-helper ()
"Test range helpers."
;; Simple.
(should (equal (treesit--range-start '(1 . 2)) 1))
(should (equal (treesit--range-end '(1 . 2)) 2))
;; Empty.
(should (equal (treesit--range-start '()) nil))
(should (equal (treesit--range-end '()) nil))
;; List.
(should (equal (treesit--range-start '((1 . 2) (3 . 4))) 1))
(should (equal (treesit--range-end '((1 . 2) (3 . 4))) 4))
;; The complicated range is...
;; ...in the middle.
(should (equal (treesit--flatten-ranges (list (cons 1 2)
(cons 3 4)
(list (cons 5 6)
(cons 7 8))
(cons 9 10)))
'((1 . 2) (3 . 4) (5 . 6) (7 . 8) (9 . 10))))
;; ... the second.
(should (equal (treesit--flatten-ranges (list (cons 1 2)
(list (cons 5 6)
(cons 7 8))
(cons 3 4)
(cons 9 10)))
'((1 . 2) (5 . 6) (7 . 8) (3 . 4) (9 . 10))))
;; ...last.
(should (equal (treesit--flatten-ranges (list (cons 1 2)
(cons 3 4)
(cons 9 10)
(list (cons 5 6)
(cons 7 8))))
'((1 . 2) (3 . 4) (9 . 10) (5 . 6) (7 . 8))))
;; ...first.
(should (equal (treesit--flatten-ranges (list (list (cons 5 6)
(cons 7 8))
(cons 1 2)
(cons 3 4)
(cons 9 10)))
'((5 . 6) (7 . 8) (1 . 2) (3 . 4) (9 . 10))))
;; ...doubled.
(should (equal (treesit--flatten-ranges (list (list (cons 5 6)
(cons 7 8))
(list (cons 5 6)
(cons 7 8))
(cons 1 2)
(cons 3 4)
(cons 9 10)))
'((5 . 6) (7 . 8) (5 . 6) (7 . 8) (1 . 2) (3 . 4) (9 . 10))))
;; ...has one element.
(should (equal (treesit--flatten-ranges (list (cons 1 2)
(cons 3 4)
(cons 9 10)
(list (cons 5 6))))
'((1 . 2) (3 . 4) (9 . 10) (5 . 6)))))
(defun treesit--ert-pred-last-sibling (node)
(null (treesit-node-next-sibling node t)))