Files
doomemacs/modules/tools/tree-sitter/config.el
Henrik Lissner 1dae2bf916 fix(tree-sitter): remap to ts-mode w/o base mode
If the base major mode doesn't exist, let's assume we want no
fallthrough for this major mode (in the event the grammar isn't ready)
and simply push forward anyway, even if a missing grammar results in a
broken state.

This will particularly affect major modes like `typescript-mode` (which
simply won't be installed if +tree-sitter is enabled) and
`go-{mod,work}-ts-mode` (for which no base major modes exist).

The difference (generally) between most base and ts-modes is
performance, not features, so "gracefully failing back to the base mode"
makes sense UX-wise, but `typescript-mode` notably inferior to
`typescript-ts-mode` (particularly in its TSX support). Beginners may
misidentify its shortcomings as bugs, so it's better to simply error out
early so the user can be made aware of the problem sooner and less
ambiguously.
2025-08-31 16:20:14 +02:00

177 lines
9.2 KiB
EmacsLisp

;;; tools/tree-sitter/config.el -*- lexical-binding: t; -*-
;;
;;; Packages
(use-package! treesit
:when (fboundp 'treesit-available-p)
:when (treesit-available-p)
:defer t
:preface
(setq treesit-enabled-modes t)
;; HACK: The *-ts-mode major modes are inconsistent about how they treat
;; missing language grammars (some error out, some respect
;; `treesit-auto-install-grammar', some fall back to `fundamental-mode').
;; I'd like to address this poor UX using `major-mode-remap-alist' entries
;; created by `set-tree-sitter!' (which will fall back to the non-treesit
;; modes), but most *-ts-mode's clobber `auto-mode-alist' and/or
;; `interpreter-mode-alist' each time the major mode is activated, so those
;; must be undone too so they don't overwrite user config.
;; TODO: Handle this during the 'doom sync' process instead.
(save-match-data
(dolist (sym '(auto-mode-alist interpreter-mode-alist))
(set
sym (cl-loop for (src . fn) in (symbol-value sym)
unless (and (functionp fn)
(string-match "-ts-mode\\(?:-maybe\\)?$" (symbol-name fn)))
collect (cons src fn)))))
;; HACK: These *-ts-modes change `auto-mode-alist' and/or
;; `interpreter-mode-alist' every time they are activated, running the risk
;; of overwriting user (or Doom) config.
;; REVIEW: Should be addressed upstream.
(dolist (mode '(csharp-ts-mode
python-ts-mode))
(advice-add mode :around #'+tree-sitter-ts-mode-inhibit-side-effects-a))
;; HACK: Intercept all ts-mode major mode remappings so grammars can be
;; dynamically checked and `treesit-auto-install-grammar' can be
;; consistently respected (which isn't currently the case with the majority
;; of ts-modes, even the built-in ones).
(defadvice! +tree-sitter--maybe-remap-major-mode-a (fn mode)
:around #'major-mode-remap
(let ((mode (funcall fn mode)))
(if-let* ((ts (get mode '+tree-sitter))
(fallback-mode (car ts)))
(cond ((not (fboundp mode))
(message "Couldn't find `%S', falling back to `%S'" mode fallback-mode)
fallback-mode)
((and (or (eq treesit-enabled-modes t)
(memq fallback-mode treesit-enabled-modes))
;; Lazily load autoloaded `treesit-language-source-alist'
;; entries.
(let ((fn (symbol-function mode))
;; Silence "can't find grammar" warning popups from
;; `treesit-ready-p' calls in Emacs <=30.1. We'll
;; log it to *Messages* instead.
(warning-suppress-types
(cons '(treesit) warning-suppress-types)))
(or (not (autoloadp fn))
(autoload-do-load fn mode)))
;; Only prompt once, and log other times.
(or (null (cdr ts)) ; no grammars, no problem!
;; If the base/fallback mode doesn't exist, let's
;; assume we want no fallthrough for this major mode
;; and push forward anyway, even if a missing grammar
;; results in a broken state.
(not (fboundp fallback-mode))
(cl-every (if (get mode '+tree-sitter-ensured)
(doom-rpartial #'treesit-ready-p 'message)
#'treesit-ensure-installed)
(cdr ts))))
(put mode '+tree-sitter-ensured t)
mode)
(fallback-mode))
mode)))
:config
;; HACK: Keep $EMACSDIR clean by installing grammars to the active profile.
(add-to-list 'treesit-extra-load-path (concat doom-profile-data-dir "tree-sitter"))
(defadvice! +tree-sitter--install-grammar-to-local-dir-a (fn &rest args)
"Write grammars to `doom-profile-data-dir'."
:around #'treesit-install-language-grammar
:around #'treesit--build-grammar
(let ((user-emacs-directory doom-profile-data-dir))
(apply fn args)))
;; TODO: Move most of these out to modules
(dolist (map '((awk "https://github.com/Beaglefoot/tree-sitter-awk" nil nil nil nil)
(bibtex "https://github.com/latex-lsp/tree-sitter-bibtex" nil nil nil nil)
(blueprint "https://github.com/huanie/tree-sitter-blueprint" nil nil nil nil)
(commonlisp "https://github.com/tree-sitter-grammars/tree-sitter-commonlisp" nil nil nil nil)
(latex "https://github.com/latex-lsp/tree-sitter-latex" nil nil nil nil)
(make "https://github.com/tree-sitter-grammars/tree-sitter-make" nil nil nil nil)
(nu "https://github.com/nushell/tree-sitter-nu" nil nil nil nil)
(org "https://github.com/milisims/tree-sitter-org" nil nil nil nil)
(perl "https://github.com/ganezdragon/tree-sitter-perl" nil nil nil nil)
(proto "https://github.com/mitchellh/tree-sitter-proto" nil nil nil nil)
(r "https://github.com/r-lib/tree-sitter-r" nil nil nil nil)
(sql "https://github.com/DerekStride/tree-sitter-sql" "gh-pages" nil nil nil)
(surface "https://github.com/connorlay/tree-sitter-surface" nil nil nil nil)
(toml "https://github.com/tree-sitter/tree-sitter-toml" nil nil nil nil)
(typst "https://github.com/uben0/tree-sitter-typst" "master" "src" nil nil)
(verilog "https://github.com/gmlarumbe/tree-sitter-verilog" nil nil nil nil)
(vhdl "https://github.com/alemuller/tree-sitter-vhdl" nil nil nil nil)
(vue "https://github.com/tree-sitter-grammars/tree-sitter-vue" nil nil nil nil)
(wast "https://github.com/wasm-lsp/tree-sitter-wasm" nil "wast/src" nil nil)
(wat "https://github.com/wasm-lsp/tree-sitter-wasm" nil "wat/src" nil nil)
(wgsl "https://github.com/mehmetoguzderin/tree-sitter-wgsl" nil nil nil nil)))
(cl-pushnew map treesit-language-source-alist :test #'eq :key #'car)))
;; TODO: combobulate or evil-textobj-tree-sitter
;; (use-package! combobulate
;; :commands combobulate-query-builder
;; :hook (prog-mode . combobulate-mode))
;; (use-package! evil-textobj-tree-sitter
;; :when (modulep! :editor evil +everywhere)
;; :defer t
;; :init (after! tree-sitter (require 'evil-textobj-tree-sitter))
;; :after-call doom-first-input-hook
;; :config
;; (defvar +tree-sitter-inner-text-objects-map (make-sparse-keymap))
;; (defvar +tree-sitter-outer-text-objects-map (make-sparse-keymap))
;; (defvar +tree-sitter-goto-previous-map (make-sparse-keymap))
;; (defvar +tree-sitter-goto-next-map (make-sparse-keymap))
;; (evil-define-key '(visual operator) 'tree-sitter-mode
;; "i" +tree-sitter-inner-text-objects-map
;; "a" +tree-sitter-outer-text-objects-map)
;; (evil-define-key 'normal 'tree-sitter-mode
;; "[g" +tree-sitter-goto-previous-map
;; "]g" +tree-sitter-goto-next-map)
;; (map! (:map +tree-sitter-inner-text-objects-map
;; "A" (+tree-sitter-get-textobj '("parameter.inner" "call.inner"))
;; "f" (+tree-sitter-get-textobj "function.inner")
;; "F" (+tree-sitter-get-textobj "call.inner")
;; "C" (+tree-sitter-get-textobj "class.inner")
;; "v" (+tree-sitter-get-textobj "conditional.inner")
;; "l" (+tree-sitter-get-textobj "loop.inner"))
;; (:map +tree-sitter-outer-text-objects-map
;; "A" (+tree-sitter-get-textobj '("parameter.outer" "call.outer"))
;; "f" (+tree-sitter-get-textobj "function.outer")
;; "F" (+tree-sitter-get-textobj "call.outer")
;; "C" (+tree-sitter-get-textobj "class.outer")
;; "c" (+tree-sitter-get-textobj "comment.outer")
;; "v" (+tree-sitter-get-textobj "conditional.outer")
;; "l" (+tree-sitter-get-textobj "loop.outer"))
;; (:map +tree-sitter-goto-previous-map
;; "a" (+tree-sitter-goto-textobj "parameter.outer" t)
;; "f" (+tree-sitter-goto-textobj "function.outer" t)
;; "F" (+tree-sitter-goto-textobj "call.outer" t)
;; "C" (+tree-sitter-goto-textobj "class.outer" t)
;; "c" (+tree-sitter-goto-textobj "comment.outer" t)
;; "v" (+tree-sitter-goto-textobj "conditional.outer" t)
;; "l" (+tree-sitter-goto-textobj "loop.outer" t))
;; (:map +tree-sitter-goto-next-map
;; "a" (+tree-sitter-goto-textobj "parameter.outer")
;; "f" (+tree-sitter-goto-textobj "function.outer")
;; "F" (+tree-sitter-goto-textobj "call.outer")
;; "C" (+tree-sitter-goto-textobj "class.outer")
;; "c" (+tree-sitter-goto-textobj "comment.outer")
;; "v" (+tree-sitter-goto-textobj "conditional.outer")
;; "l" (+tree-sitter-goto-textobj "loop.outer")))
;; (after! which-key
;; (setq which-key-allow-multiple-replacements t)
;; (pushnew!
;; which-key-replacement-alist
;; '(("" . "\\`+?evil-textobj-tree-sitter-function--\\(.*\\)\\(?:.inner\\|.outer\\)") . (nil . "\\1")))))