;;; treesit-auto.el --- Automatically use tree-sitter enhanced major modes -*- lexical-binding: t -*-

;; Copyright (C) 2023 Robert Enzmann

;; Author: Robb Enzmann <robbenzmann@gmail.com>
;; Keywords: treesitter auto automatic major mode fallback convenience
;; URL: https://github.com/renzmann/treesit-auto.git
;; Package-Version: 20260817.1304
;; Package-Revision: c227806dddd4
;; Package-Requires: ((emacs "29.0"))

;; This file is not part of GNU Emacs.

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:
;; If a tree-sitter grammar is available and installed, use it instead of the
;; corresponding default mode.  Conversely, when a tree-sitter grammar is not
;; available and a fallback major mode is available/specified, use it instead.
;;
;; This package also provides a `treesit-auto-install-all' function, which will
;; scan for tree-sitter grammars listed in `treesit-auto-recipe-list' that are
;; not installed or otherwise available on `treesit-extra-load-path'.  Automatic
;; installation of grammars when visiting a file is controlled by the
;; `treesit-auto-install' variable, which can be t, nil or `prompt'.  When t,
;; opening a file with a compatible tree-sitter mode will clone and install the
;; grammar defined by its recipe, if it isn't already installed.  `prompt' will
;; display a yes/no question in the minibuffer and wait for confirmation before
;; attempting the installation.

;;; Code:

(require 'treesit)
(eval-when-compile
  (require 'cl-lib)
  (require 'files))

(defcustom treesit-auto-install nil
  "If non-nil, auto install missing tree-sitter grammars.

This variable enables the automatic clone, compile, and
installation of tree-sitter grammars whenever visiting a file
that has a compatible tree-sitter mode.  If set to `prompt'
treesit-auto will ask for confirmation before downloading the
grammar.  Additionally, `treesit-auto-install-all' will skip the
yes/no prompt when this variable is t."
  :type '(choice (const :tag "Yes" t)
                 (const :tag "No" nil)
                 (const :tag "Ask" prompt))
  :group 'treesit)

(defcustom treesit-auto-fallback-alist nil
  "Ignored.

Formerly the method of defining fallback & promotion modes
between tree-sitter and original modes.  This is handled instead
by manipulating the `treesit-auto-recipe-list' variable."
  :type '(alist :key-type symbol :value-type function)
  :group 'treesit)

(cl-defstruct treesit-auto-recipe
  "Emacs metadata for a tree-sitter language grammar."
  lang ts-mode remap requires url revision abi14-revision source-dir cc c++ ext)

(defvar treesit-auto-recipe-list
  `(,(make-treesit-auto-recipe
      :lang 'awk
      :ts-mode 'awk-ts-mode
      :remap 'awk-mode
      :url "https://github.com/Beaglefoot/tree-sitter-awk"
      :ext "\\.awk\\'")
    ,(make-treesit-auto-recipe
      :lang 'bash
      :ts-mode 'bash-ts-mode
      :remap 'sh-mode
      :url "https://github.com/tree-sitter/tree-sitter-bash"
      :abi14-revision "v0.23.3"
      :ext "\\.sh\\'")
    ,(make-treesit-auto-recipe
      :lang 'bibtex
      :ts-mode 'bibtex-ts-mode
      :remap 'bibtex-mode
      :url "https://github.com/latex-lsp/tree-sitter-bibtex"
      :ext "\\.bib\\'")
    ,(make-treesit-auto-recipe
      :lang 'blueprint
      :ts-mode 'blueprint-ts-mode
      :remap 'blueprint-mode
      :url "https://github.com/huanie/tree-sitter-blueprint"
      :ext "\\.blp\\'")
    ,(make-treesit-auto-recipe
      :lang 'c
      :ts-mode 'c-ts-mode
      :remap 'c-mode
      :url "https://github.com/tree-sitter/tree-sitter-c"
      :abi14-revision "v0.23.6"
      :requires 'cpp
      :ext "\\.c\\'")
    ,(make-treesit-auto-recipe
      :lang 'c-sharp
      :ts-mode 'csharp-ts-mode
      :remap 'csharp-mode
      :url "https://github.com/tree-sitter/tree-sitter-c-sharp"
      :abi14-revision "v0.23.1"
      :ext "\\.cs\\'")
    ,(make-treesit-auto-recipe
      :lang 'clojure
      :ts-mode 'clojure-ts-mode
      :remap '(clojure-mode clojurescript-mode clojurec-mode)
      :url "https://github.com/sogaiu/tree-sitter-clojure"
      :ext "\\.cljc?s?d?\\'")
    ,(make-treesit-auto-recipe
      :lang 'cmake
      :ts-mode 'cmake-ts-mode
      :remap 'cmake-mode
      :url "https://github.com/uyha/tree-sitter-cmake"
      :ext "\\.cmake\\'")
    ,(make-treesit-auto-recipe
      :lang 'cobol
      :ts-mode 'cobol-ts-mode
      :remap 'cobol-mode
      :url "https://github.com/yutaro-sakamoto/tree-sitter-cobol"
      :ext "\\.\\(cob\\|cbl\\)\\'")
    ,(make-treesit-auto-recipe
      :lang 'commonlisp
      :ts-mode 'commonlisp-ts-mode
      :remap 'common-lisp-mode
      :url "https://github.com/tree-sitter-grammars/tree-sitter-commonlisp"
      :ext "\\.cl\\'")
    ,(make-treesit-auto-recipe
      :lang 'cpp
      :ts-mode 'c++-ts-mode
      :remap 'c++-mode
      :requires 'c
      :url "https://github.com/tree-sitter/tree-sitter-cpp"
      :abi14-revision "v0.23.4"
      :ext "\\.cpp\\'")
    ,(make-treesit-auto-recipe
      :lang 'css
      :ts-mode 'css-ts-mode
      :remap 'css-mode
      :url "https://github.com/tree-sitter/tree-sitter-css"
      :abi14-revision "v0.23.2"
      :ext "\\.css\\'")
    ,(make-treesit-auto-recipe
      :lang 'dart
      :ts-mode 'dart-ts-mode
      :remap 'dart-mode
      :url "https://github.com/UserNobody14/tree-sitter-dart"
      :ext "\\.dart\\'")
    ,(make-treesit-auto-recipe
      :lang 'dockerfile
      :ts-mode 'dockerfile-ts-mode
      :remap 'dockerfile-mode
      :url "https://github.com/camdencheek/tree-sitter-dockerfile"
      :ext "[/\\]\\(?:Containerfile\\|Dockerfile\\)\\(?:\\.[^/\\]*\\)?\\'")
    ,(make-treesit-auto-recipe
      :lang 'elixir
      :ts-mode 'elixir-ts-mode
      :remap 'elixir-mode
      :requires 'heex
      :url "https://github.com/elixir-lang/tree-sitter-elixir"
      :ext "\\.exs?\\'")
    ,(make-treesit-auto-recipe
      :lang 'gitcommit
      :ts-mode 'git-commit-ts-mode
      :url "https://github.com/gbprod/tree-sitter-gitcommit"
      :abi14-revision "v0.4.0"
      :ext "\\COMMIT_EDITMSG\\'")
    ,(make-treesit-auto-recipe
      :lang 'glsl
      :ts-mode 'glsl-ts-mode
      :remap 'glsl-mode
      :url "https://github.com/tree-sitter-grammars/tree-sitter-glsl")
    ,(make-treesit-auto-recipe
      :lang 'go
      :ts-mode 'go-ts-mode
      :remap 'go-mode
      :requires 'gomod
      :url "https://github.com/tree-sitter/tree-sitter-go"
      :abi14-revision "v0.23.4"
      :ext "\\.go\\'")
    ,(make-treesit-auto-recipe
      :lang 'gomod
      :ts-mode 'go-mod-ts-mode
      :remap 'go-mod-mode
      :requires 'go
      :url "https://github.com/camdencheek/tree-sitter-go-mod"
      :abi14-revision "v1.1.0"
      :ext "go\\.mod\\'")
    ,(make-treesit-auto-recipe
      :lang 'gowork
      :ts-mode 'go-work-ts-mode
      :requires 'go
      :url "https://github.com/omertuc/tree-sitter-go-work"
      :ext "go\\.work\\'")
    ,(make-treesit-auto-recipe
      :lang 'haskell
      :ts-mode 'haskell-ts-mode
      :remap 'haskell-mode
      :url "https://github.com/tree-sitter/tree-sitter-haskell"
      :ext "\\.hs\\'")
    ,(make-treesit-auto-recipe
      :lang 'heex
      :ts-mode 'heex-ts-mode
      :remap 'heex-mode
      :url "https://github.com/phoenixframework/tree-sitter-heex"
      :abi14-revision "v0.8.1"
      :ext "\\.heex\\'")
    ,(make-treesit-auto-recipe
      :lang 'html
      :ts-mode 'html-ts-mode
      :remap '(mhtml-mode sgml-mode)
      :url "https://github.com/tree-sitter/tree-sitter-html"
      :ext "\\.html\\'")
    ,(make-treesit-auto-recipe
      :lang 'hyprlang
      :ts-mode 'hyprlang-ts-mode
      :url "https://github.com/tree-sitter-grammars/tree-sitter-hyprlang"
      :abi14-revision "v3.0.0"
      :ext "/hypr/.*\\.conf\\'")
    ,(make-treesit-auto-recipe
      :lang 'janet-simple
      :ts-mode 'janet-ts-mode
      :remap 'janet-mode
      :url "https://github.com/sogaiu/tree-sitter-janet-simple"
      :ext "\\.janet\\'")
    ,(make-treesit-auto-recipe
      :lang 'java
      :ts-mode 'java-ts-mode
      :remap 'java-mode
      :url "https://github.com/tree-sitter/tree-sitter-java"
      :ext "\\.java\\'")
    ,(make-treesit-auto-recipe
      :lang 'javascript
      :ts-mode 'js-ts-mode
      :remap '(js-mode javascript-mode js2-mode)
      :url "https://github.com/tree-sitter/tree-sitter-javascript"
      :revision "master"
      :abi14-revision "v0.23.1"
      :source-dir "src"
      :ext "\\.js\\'")
    ,(make-treesit-auto-recipe
      :lang 'json
      :ts-mode 'json-ts-mode
      :remap 'js-json-mode
      :url "https://github.com/tree-sitter/tree-sitter-json"
      :ext "\\.jsonc?\\'")
    ,(make-treesit-auto-recipe
      :lang 'julia
      :ts-mode 'julia-ts-mode
      :remap 'julia-mode
      :url "https://github.com/tree-sitter/tree-sitter-julia"
      :abi14-revision "v0.23.1"
      :ext "\\.jl\\'")
    ,(make-treesit-auto-recipe
      :lang 'kotlin
      :ts-mode 'kotlin-ts-mode
      :remap 'kotlin-mode
      :url "https://github.com/fwcd/tree-sitter-kotlin"
      :ext "\\.kts?\\'")
    ,(make-treesit-auto-recipe
      :lang 'lua
      :ts-mode 'lua-ts-mode
      :remap 'lua-mode
      :url "https://github.com/tree-sitter-grammars/tree-sitter-lua"
      :abi14-revision "v0.3.0"
      :ext "\\.lua\\'")
    ,(make-treesit-auto-recipe
      :lang 'magik
      :ts-mode 'magik-ts-mode
      :remap 'magik-mode
      :url "https://github.com/krn-robin/tree-sitter-magik"
      :abi14-revision "0.0.1"
      :ext "\\.magik\\'")
    ,(make-treesit-auto-recipe
      :lang 'make
      :ts-mode 'makefile-ts-mode
      :remap 'makefile-mode
      :url "https://github.com/tree-sitter-grammars/tree-sitter-make"
      :abi14-revision "v1.1.1"
      :ext "\\([Mm]akefile\\|.*\\.\\(mk\\|make\\)\\)\\'")
    ,(make-treesit-auto-recipe
      :lang 'markdown
      :ts-mode 'markdown-ts-mode
      :remap '(poly-markdown-mode markdown-mode)
      :url "https://github.com/tree-sitter-grammars/tree-sitter-markdown"
      :abi14-revision "v0.4.1"
      :source-dir "tree-sitter-markdown/src"
      :ext "\\.md\\'")
    ,(make-treesit-auto-recipe
      :lang 'nix
      :ts-mode 'nix-ts-mode
      :remap 'nix-mode
      :url "https://github.com/nix-community/tree-sitter-nix"
      :ext "\\.nix\\'")
    ,(make-treesit-auto-recipe
      :lang 'nu
      :ts-mode 'nushell-ts-mode
      :remap 'nushell-mode
      :url "https://github.com/nushell/tree-sitter-nu"
      :ext "\\.nu\\'")
    ,(make-treesit-auto-recipe
      :lang 'org
      :ts-mode 'org-ts-mode
      :remap 'org-mode
      :url "https://github.com/milisims/tree-sitter-org"
      :ext "\\.org\\'")
    ,(make-treesit-auto-recipe
      :lang 'perl
      :ts-mode 'perl-ts-mode
      :remap 'perl-mode
      :url "https://github.com/ganezdragon/tree-sitter-perl"
      :abi14-revision "v1.1.1"
      :ext "\\.pl\\'")
    ,(make-treesit-auto-recipe
      :lang 'php
      :ts-mode 'php-ts-mode
      :remap 'php-mode
      :url "https://github.com/tree-sitter/tree-sitter-php"
      :abi14-revision "v0.23.9"
      :source-dir "php/src"
      :ext "\\.php\\'")
    ,(make-treesit-auto-recipe
      :lang 'proto
      :ts-mode 'protobuf-ts-mode
      :remap 'protobuf-mode
      :url "https://github.com/mitchellh/tree-sitter-proto"
      :ext "\\.proto\\'")
    ,(make-treesit-auto-recipe
      :lang 'python
      :ts-mode 'python-ts-mode
      :remap 'python-mode
      :url "https://github.com/tree-sitter/tree-sitter-python"
      :abi14-revision "v0.23.6"
      :ext "\\.py[iw]?\\'")
    ,(make-treesit-auto-recipe
      :lang 'r
      :ts-mode 'r-ts-mode
      :remap 'ess-mode
      :url "https://github.com/r-lib/tree-sitter-r"
      :ext "\\.r\\'")
    ,(make-treesit-auto-recipe
      :lang 'ruby
      :ts-mode 'ruby-ts-mode
      :remap 'ruby-mode
      :url "https://github.com/tree-sitter/tree-sitter-ruby"
      :ext "\\(?:\\.\\(?:rbw?\\|ru\\|rake\\|thor\\|jbuilder\\|rabl\\|gemspec\\|podspec\\)\\|/\\(?:Gem\\|Rake\\|Cap\\|Thor\\|Puppet\\|Berks\\|Brew\\|Vagrant\\|Guard\\|Pod\\)file\\)\\'")
    ,(make-treesit-auto-recipe
      :lang 'rust
      :ts-mode 'rust-ts-mode
      :remap 'rust-mode
      :url "https://github.com/tree-sitter/tree-sitter-rust"
      :abi14-revision "v0.23.2"
      :ext "\\.rs\\'")
    ,(make-treesit-auto-recipe
      :lang 'scala
      :ts-mode 'scala-ts-mode
      :remap 'scala-mode
      :url "https://github.com/tree-sitter/tree-sitter-scala"
      :abi14-revision "v0.24.1"
      :ext "\\.\\(scala\\|sbt\\)\\'")
    ,(make-treesit-auto-recipe
      :lang 'solidity
      :ts-mode 'solidity-ts-mode
      :remap 'solidity-mode
      :url "https://github.com/JoranHonig/tree-sitter-solidity"
      :abi14-revision "v1.2.9"
      :ext "\\.sol\\'")
    ,(make-treesit-auto-recipe
      :lang 'sql
      :ts-mode 'sql-ts-mode
      :remap 'sql-mode
      :revision "gh-pages"
      :url "https://github.com/DerekStride/tree-sitter-sql"
      :source-dir "src"
      :ext "\\.sql\\'")
    ,(make-treesit-auto-recipe
      :lang 'surface
      :ts-mode 'surface-ts-mode
      :remap 'surface-mode
      :url "https://github.com/connorlay/tree-sitter-surface")
    ,(make-treesit-auto-recipe
      :lang 'swift
      :ts-mode 'swift-ts-mode
      :remap 'swift-mode
      :url "https://github.com/alex-pinkus/tree-sitter-swift")
    ,(make-treesit-auto-recipe
      :lang 'toml
      :ts-mode 'toml-ts-mode
      :remap '(conf-toml-mode toml-mode)
      :url "https://github.com/tree-sitter-grammars/tree-sitter-toml"
      :ext "\\.toml\\'")
    ,(make-treesit-auto-recipe
      :lang 'tsx
      :ts-mode 'tsx-ts-mode
      :remap '(typescript-tsx-mode)
      :requires 'typescript
      :url "https://github.com/tree-sitter/tree-sitter-typescript"
      :revision "master"
      :abi14-revision "v0.20.3"
      :source-dir "tsx/src"
      :ext "\\.tsx\\'")
    ,(make-treesit-auto-recipe
      :lang 'typescript
      :ts-mode 'typescript-ts-mode
      :remap 'typescript-mode
      :requires 'tsx
      :url "https://github.com/tree-sitter/tree-sitter-typescript"
      :revision "master"
      :abi14-revision "v0.20.3"
      :source-dir "typescript/src"
      :ext "\\.ts\\'")
    ,(make-treesit-auto-recipe
      :lang 'typespec
      :ts-mode 'typespec-ts-mode
      :url "https://github.com/happenslol/tree-sitter-typespec/"
      :revision "main"
      :source-dir "src"
      :ext "\\.tsp\\'")
    ,(make-treesit-auto-recipe
      :lang 'typst
      :ts-mode 'typst-ts-mode
      :remap 'typst-mode
      :url "https://github.com/uben0/tree-sitter-typst"
      :revision "master"
      :source-dir "src"
      :ext "\\.typ\\'")
    ,(make-treesit-auto-recipe
      :lang 'verilog
      :ts-mode 'verilog-ts-mode
      :remap 'verilog-mode
      :url "https://github.com/gmlarumbe/tree-sitter-systemverilog"
      :ext "\\.s?vh?\\'")
    ,(make-treesit-auto-recipe
      :lang 'vhdl
      :ts-mode 'vhdl-ts-mode
      :remap 'vhdl-mode
      :url "https://github.com/alemuller/tree-sitter-vhdl"
      :ext "\\.vhdl?\\'")
    ,(make-treesit-auto-recipe
      :lang 'vue
      :ts-mode 'vue-ts-mode
      :remap 'vue-mode
      :url "https://github.com/tree-sitter-grammars/tree-sitter-vue"
      :ext "\\.vue\\'")
    ,(make-treesit-auto-recipe
      :lang 'wast
      :ts-mode 'wat-ts-wast-mode
      :remap 'wat-mode
      :url "https://github.com/wasm-lsp/tree-sitter-wasm"
      :source-dir "wast/src"
      :ext "\\.wast\\'")
    ,(make-treesit-auto-recipe
      :lang 'wat
      :ts-mode 'wat-ts-mode
      :remap 'wat-mode
      :url "https://github.com/wasm-lsp/tree-sitter-wasm"
      :source-dir "wat/src"
      :ext "\\.wat\\'")
    ,(make-treesit-auto-recipe
      :lang 'zig
      :ts-mode 'zig-ts-mode
      :remap 'zig-mode
      :url "https://github.com/maxxnino/tree-sitter-zig"
      :ext "\\.zig\\'")
    ,(make-treesit-auto-recipe
      :lang 'wgsl
      :ts-mode 'wgsl-ts-mode
      :remap 'wgsl-mode
      :url "https://github.com/mehmetoguzderin/tree-sitter-wgsl"
      :ext "\\.wgsl\\'")
    ,(make-treesit-auto-recipe
      :lang 'yaml
      :ts-mode 'yaml-ts-mode
      :remap 'yaml-mode
      :url "https://github.com/tree-sitter-grammars/tree-sitter-yaml"
      :abi14-revision "v0.7.2"
      :ext "\\.ya?ml\\'"))
  "Map each tree-sitter lang to Emacs metadata.")

(defcustom treesit-auto-langs (seq-map #'treesit-auto-recipe-lang treesit-auto-recipe-list)
  "Language symbols that should be automatically installed.

Setting this to a list of grammar symbols will modify the
behavior of `treesit-auto-install-all' and the
automatic/prompting behavior when visiting a buffer that has a
tree-sitter mode available.  For example, when set to \\='(python
rust go), then `treesit-auto-install-all' will only check and
install those three grammars.  Likewise, we will only get
automatic installation (or prompting, based on the value of
`treesit-auto-install') when visiting a Python, Go, or Rust file."
  :type '(repeat symbol)
  :group 'treesit)

(defun treesit-auto--maybe-install-grammar ()
  "Try to install the grammar matching the current file extension.

If the tree-sitter grammar is missing for the current file type, this will
silently fail, automatically install the grammar, or prompt the user about
automatic installation, depending on the value of `treesit-auto-install'.  If
installation of the grammar is successful, activate the tree-sitter major mode."
  (when-let* ((recipe (treesit-auto--get-mode-recipe))
              (ts-mode (treesit-auto-recipe-ts-mode recipe))
              (not-ready (not (treesit-auto--ready-p ts-mode)))
              (ts-mode-exists (fboundp ts-mode))
              (lang (treesit-auto-recipe-lang recipe))
              (treesit-language-source-alist (treesit-auto--build-treesit-source-alist))
              (treesit-auto-langs (remove lang treesit-auto-langs)))
    (dolist (req-lang (ensure-list (treesit-auto-recipe-requires recipe)))
      (treesit-auto--prompt-to-install-package req-lang))
    (treesit-auto--prompt-to-install-package lang)
    (if (and (stringp buffer-file-name)
             (file-exists-p buffer-file-name))
        (revert-buffer nil t)
      (when (treesit-auto--ready-p lang)
        (funcall ts-mode)))))

(defun treesit-auto--ready-p (mode)
  "Determine if MODE is tree-sitter ready.

MODE can be either the tree-sitter enhanced version or one of the
fallback modes."
  (when-let* ((recipe (treesit-auto--get-mode-recipe mode))
              (lang (treesit-auto-recipe-lang recipe))
              (ts-mode (treesit-auto-recipe-ts-mode recipe)))
    (and (treesit-ready-p lang t)
         (fboundp mode)
         (fboundp ts-mode))))

(defun treesit-auto--prompt-to-install-package (lang)
  "Ask the user if they want to install a tree-sitter grammar for `LANG'.

Non-nil only if installation completed without any errors."
  (when (cond ((eq t treesit-auto-install) t)
              ((eq 'prompt treesit-auto-install)
               (y-or-n-p (format "Tree-sitter grammar for %s is missing.  Install it from %s? "
                                 (symbol-name lang)
                                 (car (alist-get lang treesit-language-source-alist))))))
    (message "Installing the tree-sitter grammar for %s" lang)
    ;; treesit-install-language-grammar will return nil if the
    ;; operation succeeded and 't if a warning was sent to the
    ;; warning buffer. I don't think this is by design but just
    ;; because of the way `display-warning' works, so this might not
    ;; work in the future.
    (not (treesit-install-language-grammar lang))))

(defun treesit-auto--get-mode-recipe (&optional mode)
  "Look up the recipe for MODE.  If MODE is nil, use the current `major-mode'."
  (let ((mode (or mode major-mode)))
    (cl-loop for recipe in (treesit-auto--selected-recipes)
             if (memq
                 mode
                 (cons (treesit-auto-recipe-ts-mode recipe)
                       (ensure-list (treesit-auto-recipe-remap recipe))))
             return recipe)))

(defun treesit-auto--get-buffer-recipe ()
  "Look up the recipe for the current buffer using its extension."
  (seq-find (lambda (r) (string-match (treesit-auto-recipe-ext r) (buffer-name)))
            (treesit-auto--selected-recipes)))

(defun treesit-auto--selected-recipes ()
  "Filter `treesit-auto-recipe-list' for members of `treesit-auto-langs'."
  (seq-filter
   (lambda (r) (memq (treesit-auto-recipe-lang r) treesit-auto-langs))
   treesit-auto-recipe-list))

(defun treesit-auto--build-major-mode-remap-alist ()
  "Construct `major-mode-remap-alist' using all known recipes."
  (append major-mode-remap-alist
          (let ((remap-alist '()))
            (cl-loop for recipe in (treesit-auto--selected-recipes)
                     for ts-mode = (treesit-auto-recipe-ts-mode recipe)
                     when (treesit-auto--ready-p ts-mode)
                     do (dolist (remap (ensure-list (treesit-auto-recipe-remap recipe)))
                          (push (cons remap ts-mode) remap-alist))
                     finally return remap-alist))))

(defun treesit-auto--build-treesit-source-alist ()
  "Construct the `treesit-language-source-alist' using all known recipes."
  (append treesit-language-source-alist
          (cl-loop for recipe in (treesit-auto--selected-recipes)
                   collect (cons (treesit-auto-recipe-lang recipe)
                                 `(,(treesit-auto-recipe-url recipe)
                                   ,(treesit-auto-get-revision recipe)
                                   ,(treesit-auto-recipe-source-dir recipe)
                                   ,(treesit-auto-recipe-cc recipe)
                                   ,(treesit-auto-recipe-c++ recipe))))))

(defun treesit-auto-get-revision (recipe)
  "Return the revision for recipe, potentially using abi14-revision."
  (let ((default-revision (treesit-auto-recipe-revision recipe)))
    (if (and (fboundp 'treesit-library-abi-version) (eq (treesit-library-abi-version) 14))
        (or (treesit-auto-recipe-abi14-revision recipe) default-revision)
      default-revision)))

(defun treesit-auto-install-all ()
  "Install every available, maintained grammar.

See `treesit-auto-langs' and `treesit-auto-install' for
how to modify the behavior of this function."
  (interactive)
  (when-let* ((treesit-language-source-alist (treesit-auto--build-treesit-source-alist))
              (to-install (seq-filter (lambda (lang) (not (treesit-ready-p lang t)))
                                      treesit-auto-langs))
              (prompt (format "The following tree-sitter grammars are/were missing:\n%s\n"
                              (mapconcat 'symbol-name to-install "\n"))))
    ;; TODO QOL - it would be nice if this messaged what was installed or at
    ;; least mentioned that nothing was installed if skipped.
    (with-output-to-temp-buffer "*Treesit-auto install candidates*"
      (princ prompt))
    (when (or (eq treesit-auto-install t) ; Quiet mode is off
              (y-or-n-p "Install missing grammars? "))
      (mapcar 'treesit-install-language-grammar to-install))))

(define-minor-mode treesit-auto-mode
  "Toggle `global-treesit-auto-mode'."
  :group 'treesit)

;; https://github.com/renzmann/treesit-auto/issues/47
(defvar global-treesit-auto-modes)

(define-globalized-minor-mode global-treesit-auto-mode treesit-auto-mode
  treesit-auto--on
  :group 'treesit
  :predicate
  ;; allow global mode to activate only on recipe modes,
  ;; but also allow to activate on remap and ts-modes
  ;; in case only the ts-mode is available.
  ;; non emacs core ts-modes might autoload and would be
  ;; nice to also prompt for grammar installation
  (let ((modes '()))
    (cl-loop for recipe in (treesit-auto--selected-recipes)
             do (push (treesit-auto-recipe-ts-mode recipe) modes)
             do (dolist (mode (ensure-list (treesit-auto-recipe-remap recipe)))
                  (push mode modes))
             finally return modes))
  (if global-treesit-auto-mode
      ;; adding advice to set-auto-mode-0 is potentially dangerous
      ;; but we need to temporary update major-mode-remap-alist
      ;; and not modify the user specified list which will allow
      ;; the user to be in control of existing remaps.
      (advice-add #'set-auto-mode-0 :before #'treesit-auto--set-major-remap)
    (advice-remove #'set-auto-mode-0 #'treesit-auto--set-major-remap)))

(defun treesit-auto--set-major-remap (&rest _)
  "Locally set `major-mode-remap-alist' with all known recipes."
  ;; even though major-mode-remap-alist is set as local here,
  ;; when a major-mode matches the mode will be added to the top of
  ;; auto-mode-alist so it can't be really "switched off" afterwards.
  ;; The user needs to restart emacs or somehow reset auto-mode-alist to
  ;; the original.
  ;; For this mode to keep a cached copy is dangerous as it will be a global
  ;; replacement and ignores all changes while this mode is active, so
  ;; don't think it is a valid option.
  (setq-local major-mode-remap-alist (treesit-auto--build-major-mode-remap-alist)))

(defun treesit-auto--on ()
  "Turn `treesit-auto-mode' on."
  (treesit-auto--maybe-install-grammar))

(defun treesit-auto--recipe-ready-p (recipe)
  "Non-nil if the language associated with RECIPE is tree-sitter ready."
  (treesit-auto--ready-p (treesit-auto-recipe-ts-mode recipe)))

(defun treesit-auto--filter-recipes-with-langs (langs recipes)
  "Filter RECIPES down to only those corresponding to LANGS."
  (seq-filter
   (lambda (r) (and (member (treesit-auto-recipe-lang r) langs)
                    (fboundp (treesit-auto-recipe-ts-mode r))))
   recipes))

(defun treesit-auto-add-to-auto-mode-alist (&optional langs)
  "Register tree-sitter modes in `auto-mode-alist'.

When the optional argument LANGS is nil (the default), then this
function adds every tree-sitter mode that satisfies
`treesit-ready-p' to `auto-mode-alist'.

If LANGS is `all', then every tree-sitter mode available to
Emacs will be added to `auto-mode-alist', regardless of whether
it satisfies `treesit-ready-p'.  For instance, Emacs 29.1 ships
with `rust-ts-mode', so \\=(treesit-auto-add-to-auto-mode-alist
\\='all\\=) would cause Emacs to load a Rust file in `rust-ts-mode',
rather than `fundamental-mode'.

If LANGS is a list, then only the listed languages will be added
to `auto-mode-alist'.  The symbols in LANGS should correspond to
the `:lang' recipe argument for `treesit-auto-recipe-list', such
as `python', `rust', `go', etc.  The `treesit-auto-langs'
variable takes priority over this argument.  If a language is
missing from `treesit-auto-langs', then it will not be added to
`auto-mode-alist', even if it is listed in LANGS."
  (let* ((selected-recipes (treesit-auto--selected-recipes))
         (recipes (cond ((eq langs 'all) (seq-filter (lambda (r) (fboundp (treesit-auto-recipe-ts-mode r)))
                                                     selected-recipes))
                        ;; See https://github.com/renzmann/treesit-auto/pull/67 for why we have (and langs (listp langs)) here
                        ((and langs (listp langs)) (treesit-auto--filter-recipes-with-langs langs selected-recipes))
                        (t (seq-filter #'treesit-auto--recipe-ready-p selected-recipes)))))
    (dolist (r recipes)
      (let ((ext (treesit-auto-recipe-ext r)))
        (unless (null ext)
          (add-to-list 'auto-mode-alist
                       (cons ext (treesit-auto-recipe-ts-mode r))))))))

(provide 'treesit-auto)
;;; treesit-auto.el ends here
