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

From: Juri Linkov <[email-masked]>
Date: Mon, 31 Mar 2025 19:53:14 +0300

>> Do we still need such complicated functions as
>> mhtml-ts-mode--language-at-point, js--treesit-language-at-point, etc.
>> that duplicate the rules from 'treesit-range-rules'
>> when now the default language-at-point function could be implemented
>> just as
>> 
>>  (treesit-parser-language
>>   (or (seq-some (lambda (o) (overlay-get o 'treesit-parser))
>>                 (overlays-at (point) t))
>>       treesit-primary-parser))
>
> Yeah, we can provide a default language-at-point function now that
> determines the “most relevant parser” by embed level. But we should keep
> treesit-language-at-point-function because a) it’s already in a release
> version and b) we want major modes to be able to customize what parser to
> pick at any given point.

Ok, so here is a complete patch:

diff --git a/lisp/textmodes/mhtml-ts-mode.el b/lisp/textmodes/mhtml-ts-mode.el
index 22c0455a4ee..fdb286fc8c4 100644
--- a/lisp/textmodes/mhtml-ts-mode.el
+++ b/lisp/textmodes/mhtml-ts-mode.el
@@ -221,21 +222,6 @@ mhtml-ts-mode-menu
   "Menu bar for `mhtml-ts-mode'."
   css-mode--menu)
 
-;; To enable some basic treesiter functionality, you should define
-;; a function that recognizes which grammar is used at-point.
-;; This function should be assigned to `treesit-language-at-point-function'
-(defun mhtml-ts-mode--language-at-point (point)
-  "Return the language at POINT assuming the point is within a HTML buffer."
-  (let* ((node (treesit-node-at point 'html))
-         (parent (treesit-node-parent node))
-         (node-query (format "(%s (%s))"
-                             (treesit-node-type parent)
-                             (treesit-node-type node))))
-    (cond
-     ((equal "(script_element (raw_text))" node-query) (js--treesit-language-at-point point))
-     ((equal "(style_element (raw_text))" node-query) 'css)
-     (t 'html))))
-
 ;; Custom font-lock function that's used to apply color to css color
 ;; The signature of the function should be conforming to signature
 ;; QUERY-SPEC required by `treesit-font-lock-rules'.
@@ -448,7 +434,7 @@ mhtml-ts-mode-flymake-mhtml
 
 ;;;###autoload
 (define-derived-mode mhtml-ts-mode html-ts-mode
-  '("HTML+" (:eval (let ((lang (mhtml-ts-mode--language-at-point (point))))
+  '("HTML+" (:eval (let ((lang (treesit-language-at (point))))
                      (cond ((eq lang 'html) "")
                            ((eq lang 'javascript) "JS")
                            ((eq lang 'css) "CSS")))))
@@ -527,10 +520,6 @@ mhtml-ts-mode
       (setq-local c-ts-common--comment-regexp
                   js--treesit-jsdoc-comment-regexp))
 
-
-    ;; Many treesit functions need to know the language at-point.
-    ;; So you should define such a function.
-    (setq-local treesit-language-at-point-function #'mhtml-ts-mode--language-at-point)
     (setq-local prettify-symbols-alist mhtml-ts-mode--prettify-symbols-alist)
 
     ;; Indent.
diff --git a/lisp/treesit.el b/lisp/treesit.el
index 54c29326df2..75c04912216 100644
--- a/lisp/treesit.el
+++ b/lisp/treesit.el
@@ -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.
 
-In a multi-language buffer, make sure
-`treesit-language-at-point-function' is implemented!  Otherwise
-`treesit-language-at' wouldn't return the correct result."
+In a multi-language buffer, optionally you can implement
+`treesit-language-at-point-function' to return more correct result."
   (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
+     (or (car (treesit-parsers-at position))
+         treesit-primary-parser))))
 
 ;;; Node API supplement
 
@@ -247,8 +247,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 (or (car (treesit-parsers-at
+                                                 pos parser-or-lang))
+                                           treesit-primary-parser))
                         (parser (or local-parser global-parser)))
                    (when parser
                      (treesit-parser-root-node parser))))
@@ -267,13 +268,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 (or (car (treesit-parsers-at
+                                                 pos lang))
+                                           treesit-primary-parser))
                         (parser (or local-parser global-parser)))
                    (when parser
                      (treesit-parser-root-node parser))))))
@@ -851,6 +849,38 @@ treesit--clip-ranges
            if (<= start (car range) (cdr range) end)
            collect range))
 
+(defun treesit-parsers-at (&optional pos language with-host local-only)
+  "Return all the non-primary parsers at POS.
+
+POS defaults to point.
+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 LOCAL-ONLY is non-nil, return only local parsers.
+Local parsers are those which only parse a limited region marked
+by an overlay with non-nil `treesit-parser' property."
+  (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 local-only)
+                         (overlay-get ov 'treesit-parser-local-p)))
+                  (_ (or (null language)
+                         (eq (treesit-parser-language parser)
+                             language))))
+        (push (if with-host (cons parser host-parser) 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 +892,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 t))
 
 (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 +3159,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))))
             (while (and (not parent) parsers)
               (setq parent (treesit-parent-until
                             (treesit-node-at (point) (car parsers)) pred)
@@ -3887,9 +3929,9 @@ 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 (or (car (treesit-parsers-at nil lang))
+                                  treesit-primary-parser))
+               (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 +4074,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)))
       (let* ((node (treesit-node-at (point) parser))
              (lang (treesit-parser-language parser))
              (pred (alist-get lang treesit-aggregated-outline-predicate)))