Subject: Re: bug#77256: Treesit language-at-point

From: Juri Linkov <[email-masked]>
Date: Tue, 01 Apr 2025 19:53:37 +0300

>> @@ -185,15 +185,15 @@ treesit-language-at
>> This function assumes that parser ranges are up-to-date.  It
>> returns the return value of `treesit-language-at-point-function'
>> if it's non-nil, otherwise it returns the language of the first
>> -parser in `treesit-parser-list', or nil if there is no parser.
>> +parser from `treesit-parsers-at', or the primary parser.
>
> If we handle the fallback case in treesit-language-at directly, rather than
> defining a separate default function, IMO we should describe the fallback
> behavior as the default. So something like “Return the language at
> POS. When there are multiple parsers that covers POS, determine the most
> relevant parser (hence language) by their embed level. If
> treesit-language-at-point-function is non-nil, return the return value of
> that function instead.” (Not saying we should use this exact docstring but
> to illustrate the point.)

Now fixed in the next patch.

>> +(defun treesit-parsers-at (&optional pos language with-host local-only)
>> +  "Return all the non-primary parsers at POS.
>
> I get why you used treesit-parsers-at rather than a more technically
> correct name like treesit-non-primary-parsers-at, or
> treesit-embeded-parsers-at. But this is confusing for not much benefit
> IMO. I suggest either use treesit-parsers-at, and add an optional argument
> exclude-primary; or use a more correct name and don’t include the primary
> parser. Also, instead of using LOCAL-ONLY, we might be more future-prove to
> use a ONLY parameter, and let user pass ‘local to mean local-only. This way
> we can add the option of returning only non-local non-primary parsers in
> the future, should the need arises (it already kinda does in
> treesit-node-at and treesit-simple-imenu, technically global-parser should
> be picked from non-local parsers).

I can't find a better function signature than to add the argument ONLY
with the following combinations of symbols for its list, wrt existing uses
of treesit-parsers-at:

  nil                treesit-language-at (nil returns all parsers)
  (local)            treesit-local-parsers-at
  (primary global)   treesit-node-at, treesit-simple-imenu
  (global local)     treesit-outline-level, treesit-up-list

> We should also update manual sections that mentions treesit-language-at.

Will do when everything is ok with this patch:

diff --git a/lisp/treesit.el b/lisp/treesit.el
index 54c29326df2..4ba46e5b7ce 100644
--- a/lisp/treesit.el
+++ b/lisp/treesit.el
@@ -182,18 +182,14 @@ treesit-language-at-point-function
 (defun treesit-language-at (position)
   "Return the language at POSITION.
 
-This function assumes that parser ranges are up-to-date.  It
-returns the return value of `treesit-language-at-point-function'
-if it's non-nil, otherwise it returns the language of the first
-parser in `treesit-parser-list', or nil if there is no parser.
-
-In a multi-language buffer, make sure
-`treesit-language-at-point-function' is implemented!  Otherwise
-`treesit-language-at' wouldn't return the correct result."
+When there are multiple parsers that covers POSITION, determine
+the most relevant parser (hence language) by their embed level.
+If `treesit-language-at-point-function' is non-nil, return
+the return value of that function instead."
   (if treesit-language-at-point-function
       (funcall treesit-language-at-point-function position)
-    (when-let* ((parser (car (treesit-parser-list))))
-      (treesit-parser-language parser))))
+    (treesit-parser-language
+     (car (treesit-parsers-at position)))))
 
 ;;; Node API supplement
 
@@ -247,8 +243,9 @@ treesit-node-at
                 (parser-or-lang
                  (let* ((local-parser (car (treesit-local-parsers-at
                                             pos parser-or-lang)))
-                        (global-parser (car (treesit-parser-list
-                                             nil parser-or-lang)))
+                        (global-parser (car (treesit-parsers-at
+                                             pos parser-or-lang nil
+                                             '(primary global))))
                         (parser (or local-parser global-parser)))
                    (when parser
                      (treesit-parser-root-node parser))))
@@ -267,13 +264,10 @@ treesit-node-at
                         (local-parser
                          ;; Find the local parser with highest
                          ;; embed-level at point.
-                         (car (seq-sort-by #'treesit-parser-embed-level
-                                           (lambda (a b)
-                                             (> (or a 0) (or b 0)))
-                                           (treesit-local-parsers-at
-                                            pos lang))))
-                        (global-parser (car (treesit-parser-list
-                                             nil lang)))
+                         (car (treesit-local-parsers-at pos lang)))
+                        (global-parser (car (treesit-parsers-at
+                                             pos lang nil
+                                             '(primary global))))
                         (parser (or local-parser global-parser)))
                    (when parser
                      (treesit-parser-root-node parser))))))
@@ -851,6 +845,56 @@ treesit--clip-ranges
            if (<= start (car range) (cdr range) end)
            collect range))
 
+(defun treesit-parsers-at (&optional pos language with-host only)
+  "Return all parsers at POS.
+
+POS defaults to point.  The returned parsers are sorted by
+the decreasing embed level.
+
+If LANGUAGE is non-nil, only return parsers for LANGUAGE.
+
+If WITH-HOST is non-nil, return a list of (PARSER . HOST-PARSER)
+instead.  HOST-PARSER is the host parser which created the PARSER.
+
+If ONLY is nil, return all parsers including the primary parser.
+
+The argument ONLY can be a list of symbols that specify what
+parsers to include in the return value.
+
+If ONLY contains the symbol `local', include local parsers.
+Local parsers are those which only parse a limited region marked
+by an overlay with non-nil `treesit-parser' property.
+
+If ONLY contains the symbol `global', include non-local parsers
+excluding the primary parser.
+
+If ONLY contains the symbol `primary', include the primary parser."
+  (let ((res nil))
+    ;; Refer to (ref:local-parser-overlay) for more explanation of local
+    ;; parser overlays.
+    (dolist (ov (overlays-at (or pos (point))))
+      (when-let* ((parser (overlay-get ov 'treesit-parser))
+                  (host-parser (or (null with-host)
+                                   (overlay-get ov 'treesit-host-parser)))
+                  (_ (or (null language)
+                         (eq (treesit-parser-language parser)
+                             language)))
+                  (_ (or (null only)
+                         (and (memq 'local only) (memq 'global only))
+                         (and (memq 'local only)
+                              (overlay-get ov 'treesit-parser-local-p))
+                         (and (memq 'global only)
+                              (not (overlay-get ov 'treesit-parser-local-p))))))
+        (push (if with-host (cons parser host-parser) parser) res)))
+    (when (or (null only) (memq 'primary only))
+      (setq res (cons treesit-primary-parser res)))
+    (seq-sort-by (lambda (p)
+                   (treesit-parser-embed-level
+                    (or (car-safe p) p)))
+                 (lambda (a b)
+                   (> (or a 0) (or b 0)))
+                 res)))
+
 (defun treesit-local-parsers-at (&optional pos language with-host)
   "Return all the local parsers at POS.
 
@@ -862,19 +906,7 @@ treesit-local-parsers-at
 If WITH-HOST is non-nil, return a list of (PARSER . HOST-PARSER)
 instead.  HOST-PARSER is the host parser which created the local
 PARSER."
-  (let ((res nil))
-    ;; Refer to (ref:local-parser-overlay) for more explanation of local
-    ;; parser overlays.
-    (dolist (ov (overlays-at (or pos (point))))
-      (let ((parser (overlay-get ov 'treesit-parser))
-            (host-parser (overlay-get ov 'treesit-host-parser))
-            (local-p (overlay-get ov 'treesit-parser-local-p)))
-        (when (and parser host-parser local-p
-                   (or (null language)
-                       (eq (treesit-parser-language parser)
-                           language)))
-          (push (if with-host (cons parser host-parser) parser) res))))
-    (nreverse res)))
+  (treesit-parsers-at pos language with-host '(local)))
 
 (defun treesit-local-parsers-on (&optional beg end language with-host)
   "Return the list of local parsers that cover the region between BEG and END.
@@ -3135,9 +3173,7 @@ treesit-up-list
           (setq parent (treesit-parent-until parent pred)))
 
         (unless parent
-          (let ((parsers (seq-keep (lambda (o)
-                                     (overlay-get o 'treesit-host-parser))
-                                   (overlays-at (point) t))))
+          (let ((parsers (mapcar #'cdr (treesit-parsers-at (point) nil t '(global local)))))
             (while (and (not parent) parsers)
               (setq parent (treesit-parent-until
                             (treesit-node-at (point) (car parsers)) pred)
@@ -3887,9 +3943,8 @@ treesit-simple-imenu
       (lambda (entry)
         (let* ((lang (car entry))
                (settings (cdr entry))
-               (global-parser (car (treesit-parser-list nil lang)))
-               (local-parsers
-                (treesit-parser-list nil lang 'embedded)))
+               (global-parser (car (treesit-parsers-at nil lang nil '(primary global))))
+               (local-parsers (treesit-local-parsers-at nil lang)))
           (cons (treesit-language-display-name lang)
                 ;; No one says you can't have both global and local
                 ;; parsers for the same language.  E.g., Rust uses
@@ -4029,9 +4087,7 @@ treesit-outline-level
       (setq level (1+ level)))
 
     ;; Continue counting the host nodes.
-    (dolist (parser (seq-keep (lambda (o)
-                                (overlay-get o 'treesit-host-parser))
-                              (overlays-at (point) t)))
+    (dolist (parser (mapcar #'cdr (treesit-parsers-at (point) nil t '(global local))))
       (let* ((node (treesit-node-at (point) parser))
              (lang (treesit-parser-language parser))
              (pred (alist-get lang treesit-aggregated-outline-predicate)))