Subject: treesit-aggregated-outline-predicate

From: Juri Linkov <[email-masked]>
Date: Tue, 18 Feb 2025 19:27:17 +0200

As discussed in bug#74610, in multi-language modes
treesit-outline-predicate ends abruptly after the first embedded range
since it can't find more matches in its range, it can't go out back
to the primary parser.

So this patch helps 'treesit-outline-search' to get out of the local parser
to the primary parser to continue search for the next outline predicate.

'treesit-outline-level' should do the same, but currently I can't find
a suitable function to break out of embedded confinement
and get the host node that contains the guest ranges.
I mean that e.g. (treesit-parser-root-node (treesit-node-parser node))
can get the root node of the local parser, but how to get its parent node
in the primary parser?  It's understandable that treesit-node-parent
doesn't go out of its parser.  But maybe there is another function?
If such function doesn't exist, this is fine, then could find that
node manually by calculating from treesit-parser-included-ranges.
diff --git a/lisp/textmodes/mhtml-ts-mode.el b/lisp/textmodes/mhtml-ts-mode.el
index 83f8879f427..7a481599310 100644
--- a/lisp/textmodes/mhtml-ts-mode.el
+++ b/lisp/textmodes/mhtml-ts-mode.el
@@ -580,7 +580,10 @@ mhtml-ts-mode
     (setq-local treesit-aggregated-simple-imenu-settings
                 mhtml-ts-mode--treesit-aggregated-simple-imenu-settings)
 
-    ;; (setq-local treesit-outline-predicate nil)
+    (setq-local treesit-aggregated-outline-predicate
+                `((html . ,#'html-ts-mode--outline-predicate)
+                  (javascript . "\\`function_declaration\\'")
+                  (css . "\\`rule_set\\'")))
 
     (treesit-major-mode-setup)
 
diff --git a/lisp/treesit.el b/lisp/treesit.el
index 30efd4d4599..ab9bfc33d3d 100644
--- a/lisp/treesit.el
+++ b/lisp/treesit.el
@@ -3601,6 +3601,16 @@ treesit-outline-predicate
 is constructed from the value of `treesit-simple-imenu-settings'
 when a major mode sets it.")
 
+(defvar-local treesit-aggregated-outline-predicate nil
+  "Settings that configure `treesit-outline-search' for multi-language modes.
+
+The value should be an alist of (LANG . SETTINGS), where LANG is a
+language symbol, and SETTINGS has the same form as
+`treesit-outline-predicate'.
+
+When both this variable and `treesit-outline-predicate' are non-nil,
+this variable takes priority.")
+
 (defun treesit-outline-predicate--from-imenu (node)
   ;; Return an outline searching predicate created from Imenu.
   ;; Return the value suitable to set `treesit-outline-predicate'.
@@ -3618,7 +3628,10 @@ treesit-outline-predicate--from-imenu
 
 (defun treesit-outline--at-point ()
   "Return the outline heading node at the current line."
-  (let* ((pred treesit-outline-predicate)
+  (let* ((pred (if treesit-aggregated-outline-predicate
+                   (alist-get (treesit-language-at (point))
+                              treesit-aggregated-outline-predicate)
+                 treesit-outline-predicate))
          (bol (pos-bol))
          (eol (pos-eol))
          (current (treesit-thing-at (point) pred))
@@ -3649,9 +3662,35 @@ treesit-outline-search
               (if (eq (point) (pos-bol))
                   (if (bobp) (point) (1- (point)))
                 (pos-eol))))
+           (pred (if treesit-aggregated-outline-predicate
+                     (alist-get (treesit-language-at pos)
+                                treesit-aggregated-outline-predicate)
+                   treesit-outline-predicate))
            (found (or bob-pos
-                      (treesit-navigate-thing pos (if backward -1 1) 'beg
-                                              treesit-outline-predicate))))
+                      (treesit-navigate-thing pos (if backward -1 1) 'beg pred))))
+
+      ;; Handle multi-language modes
+      (when-let* ((ranges (mapcar #'treesit-parser-included-ranges
+                                  (treesit-parser-list)))
+                  (ranges (delq nil (delete '((1 . 1)) ranges))))
+        (if found
+            nil
+          ;; Possibly was inside the local range, and when can't find
+          ;; more matches inside the local range then need to go out
+          (when-let* ((bounds (seq-filter
+                               (lambda (p) (if backward (< p pos) (> p pos)))
+                               (flatten-list
+                                (mapcar (lambda (rr)
+                                          (mapcar (if backward #'car #'cdr) rr))
+                                        ranges))))
+                      (closest (when bounds (if backward (seq-max bounds) (seq-min bounds)))))
+            (setq found (treesit-navigate-thing
+                         closest (if backward -1 1) 'beg
+                         (if treesit-aggregated-outline-predicate
+                             (alist-get (treesit-language-at closest)
+                                        treesit-aggregated-outline-predicate)
+                           treesit-outline-predicate))))))
+
       (if found
           (if (or (not bound) (if backward (>= found bound) (<= found bound)))
               (progn
@@ -3667,10 +3706,25 @@ treesit-outline-search
 (defun treesit-outline-level ()
   "Return the depth of the current outline heading."
   (let* ((node (treesit-outline--at-point))
-         (level 1))
-    (while (setq node (treesit-parent-until node treesit-outline-predicate))
+         (level 1)
+         (parser (when treesit-aggregated-outline-predicate
+                   (treesit-node-parser node)))
+         (pred (if treesit-aggregated-outline-predicate
+                   (alist-get (treesit-language-at (point))
+                              treesit-aggregated-outline-predicate)
+                 treesit-outline-predicate)))
+    (while (setq node (treesit-parent-until node pred))
       (setq level (1+ level)))
-    (if (zerop level) 1 level)))
+    (when-let* ((_ parser)
+                (host-lang (treesit-parser-language treesit-primary-parser))
+                (_ (not (eq (treesit-language-at (point)) host-lang)))
+                (host-pred (alist-get host-lang treesit-aggregated-outline-predicate)))
+      ;; Now need to break out of embedded confinement
+      ;; and get the host node that contains the guest ranges
+      (setq node (treesit-parser-root-node parser))
+      (while (setq node (treesit-parent-until node host-pred))
+        (setq level (1+ level))))
+    level))
 
 ;;; Hideshow mode
 
@@ -3955,11 +4009,14 @@ treesit-major-mode-setup
                 #'treesit-simple-imenu))
 
   ;; Outline minor mode.
-  (when (and (or treesit-outline-predicate treesit-simple-imenu-settings)
+  (when (and (or treesit-outline-predicate
+                 treesit-aggregated-outline-predicate
+                 treesit-simple-imenu-settings)
              (not (seq-some #'local-variable-p
                             '(outline-search-function
                               outline-regexp outline-level))))
-    (unless treesit-outline-predicate
+    (unless (or treesit-outline-predicate
+                treesit-aggregated-outline-predicate)
       (setq treesit-outline-predicate
             #'treesit-outline-predicate--from-imenu))
     (setq-local outline-search-function #'treesit-outline-search