From 7c5d4348330b206aff1f8e5bc4fd241d6a6dc0b5 Mon Sep 17 00:00:00 2001 From: Yuan Fu Date: Tue, 25 Oct 2022 13:54:12 -0700 Subject: [PATCH] New tree-sitter toggle scheme This version: central variable, everything controlled by treesit-settings. Major mode sets up tree-sitter/non-tree-sitter in a conditional branch, based on the setting. * lisp/treesit.el (treesit-settings): New option. (treesit-defun-type-regexp): Change docstring. (treesit-mode-supported) (treesit-required-languages) (treesit--local-variable-backup): Remove variables. (treesit--backup-local-variable) (treesit-mode) (global-treesit-mode--turn-on) (global-treesit-mode): Remove functions. (treesit--setting-for-mode): New function. (treesit-ready-p): New argument MODE, changed REPORT to QUIET, and LANGUAGEs to LANGUAGE (now it can be a single symbol or a list of them). (treesit-major-mode-setup): New function. Mostly comes from treesit-mode. * test/src/treesit-tests.el (treesit-misc): New test. * lisp/progmodes/python.el (python-mode): Move some setup code into the conditional branch at the end. * lisp/progmodes/js.el (js-json-mode) (js-mode): Move some setup code into the conditional branch at the end. * lisp/progmodes/ts-mode.el: Move tree-sitter setup into the conditional branch. --- lisp/progmodes/js.el | 71 +++++----- lisp/progmodes/python.el | 42 +++--- lisp/progmodes/ts-mode.el | 41 +++--- lisp/treesit.el | 269 ++++++++++++++++++++------------------ test/src/treesit-tests.el | 20 +++ 5 files changed, 239 insertions(+), 204 deletions(-) diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el index 52160fbb5ee..4ccdab0d928 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el @@ -3609,19 +3609,7 @@ This function can be used as a value in `which-func-functions'" :group 'js ;; Ensure all CC Mode "lang variables" are set to valid values. (c-init-language-vars js-mode) - (setq-local indent-line-function #'js-indent-line) - (setq-local beginning-of-defun-function #'js-beginning-of-defun) - (setq-local end-of-defun-function #'js-end-of-defun) (setq-local open-paren-in-column-0-is-defun-start nil) - (setq-local font-lock-defaults - (list js--font-lock-keywords nil nil nil nil - '(font-lock-syntactic-face-function - . js-font-lock-syntactic-face-function))) - (setq-local syntax-propertize-function #'js-syntax-propertize) - (add-hook 'syntax-propertize-extend-region-functions - #'syntax-propertize-multiline 'append 'local) - (add-hook 'syntax-propertize-extend-region-functions - #'js--syntax-propertize-extend-region 'append 'local) (setq-local prettify-symbols-alist js--prettify-symbols-alist) (setq-local parse-sexp-ignore-comments t) @@ -3634,9 +3622,6 @@ This function can be used as a value in `which-func-functions'" (setq-local fill-paragraph-function #'js-fill-paragraph) (setq-local normal-auto-fill-function #'js-do-auto-fill) - ;; Parse cache - (add-hook 'before-change-functions #'js--flush-caches t t) - ;; Frameworks (js--update-quick-match-re) @@ -3688,17 +3673,40 @@ This function can be used as a value in `which-func-functions'" ;; calls to syntax-propertize wherever it's really needed. ;;(syntax-propertize (point-max)) - ;; Tree-sitter support. - (setq-local treesit-mode-supported t) - (setq-local treesit-required-languages '(javascript)) - (setq-local treesit-simple-indent-rules js--treesit-indent-rules) - (setq-local treesit-defun-type-regexp - (rx (or "class_declaration" - "method_definition" - "function_declaration" - "lexical_declaration"))) - (setq-local treesit-font-lock-settings js--treesit-font-lock-settings) - (setq-local treesit-font-lock-feature-list '((minimal) (moderate) (full)))) + (cond + ;; Tree-sitter. + ((treesit-ready-p 'js-mode 'javascript) + ;; Indent. + (setq-local treesit-simple-indent-rules js--treesit-indent-rules) + ;; Navigation. + (setq-local treesit-defun-type-regexp + (rx (or "class_declaration" + "method_definition" + "function_declaration" + "lexical_declaration"))) + ;; Fontification. + (setq-local treesit-font-lock-settings js--treesit-font-lock-settings) + (setq-local treesit-font-lock-feature-list '((minimal) (moderate) (full))) + (treesit-major-mode-setup)) + ;; Elisp. + (t + ;; Ensure all CC Mode "lang variables" are set to valid values + ;; (continued). + (setq-local indent-line-function #'js-indent-line) + (setq-local beginning-of-defun-function #'js-beginning-of-defun) + (setq-local end-of-defun-function #'js-end-of-defun) + (setq-local font-lock-defaults + (list js--font-lock-keywords nil nil nil nil + '(font-lock-syntactic-face-function + . js-font-lock-syntactic-face-function))) + (setq-local syntax-propertize-function #'js-syntax-propertize) + (add-hook 'syntax-propertize-extend-region-functions + #'syntax-propertize-multiline 'append 'local) + (add-hook 'syntax-propertize-extend-region-functions + #'js--syntax-propertize-extend-region 'append 'local) + + ;; Parse cache + (add-hook 'before-change-functions #'js--flush-caches t t)))) (defvar js-json--treesit-font-lock-settings (treesit-font-lock-rules @@ -3737,11 +3745,12 @@ This function can be used as a value in `which-func-functions'" ;; regexp matchers nor #! thingies (and `js-enabled-frameworks' is nil). (setq-local syntax-propertize-function #'ignore) - ;; Tree-sitter support. - (setq-local treesit-mode-supported t) - (setq-local treesit-required-languages '(json)) - (setq-local treesit-simple-indent-rules js--json-treesit-indent-rules) - (setq-local treesit-font-lock-settings js-json--treesit-font-lock-settings)) + (cond + ;; Tree-sitter. + ((treesit-ready-p 'js-json-mode 'json) + (setq-local treesit-simple-indent-rules js--json-treesit-indent-rules) + (setq-local treesit-font-lock-settings js-json--treesit-font-lock-settings) + (treesit-major-mode-setup)))) ;; Since we made JSX support available and automatically-enabled in ;; the base `js-mode' (for ease of use), now `js-jsx-mode' simply diff --git a/lisp/progmodes/python.el b/lisp/progmodes/python.el index 4ac5f2e3b0a..2c0c35174c2 100644 --- a/lisp/progmodes/python.el +++ b/lisp/progmodes/python.el @@ -6391,15 +6391,6 @@ Add import for undefined name `%s' (empty to skip): " (setq-local forward-sexp-function python-forward-sexp-function) - (setq-local font-lock-defaults - `(,python-font-lock-keywords - nil nil nil nil - (font-lock-syntactic-face-function - . python-font-lock-syntactic-face-function))) - - (setq-local syntax-propertize-function - python-syntax-propertize-function) - (setq-local indent-line-function #'python-indent-line-function) (setq-local indent-region-function #'python-indent-region) ;; Because indentation is not redundant, we cannot safely reindent code. @@ -6424,14 +6415,9 @@ Add import for undefined name `%s' (empty to skip): " (add-hook 'post-self-insert-hook #'python-indent-post-self-insert-function 'append 'local) - (setq-local imenu-create-index-function - #'python-imenu-create-index) - (setq-local add-log-current-defun-function #'python-info-current-defun) - (add-hook 'which-func-functions #'python-info-current-defun nil t) - (setq-local skeleton-further-elements '((abbrev-mode nil) (< '(backward-delete-char-untabify (min python-indent-offset @@ -6479,13 +6465,27 @@ Add import for undefined name `%s' (empty to skip): " (add-hook 'flymake-diagnostic-functions #'python-flymake nil t) - (setq-local treesit-mode-supported t) - (setq-local treesit-required-languages '(python)) - (setq-local treesit-font-lock-feature-list - '((basic) (moderate) (elaborate))) - (setq-local treesit-font-lock-settings python--treesit-settings) - (setq-local treesit-imenu-function - #'python-imenu-treesit-create-index)) + (cond + ;; Tree-sitter. + ((treesit-ready-p 'python-mode 'python) + (setq-local treesit-font-lock-feature-list + '((basic) (moderate) (elaborate))) + (setq-local treesit-font-lock-settings python--treesit-settings) + (setq-local imenu-create-index-function + #'python-imenu-treesit-create-index) + (treesit-major-mode-setup)) + ;; Elisp. + (t + (setq-local font-lock-defaults + `(,python-font-lock-keywords + nil nil nil nil + (font-lock-syntactic-face-function + . python-font-lock-syntactic-face-function))) + (setq-local syntax-propertize-function + python-syntax-propertize-function) + (setq-local imenu-create-index-function + #'python-imenu-create-index) + (add-hook 'which-func-functions #'python-info-current-defun nil t)))) ;;; Completion predicates for M-x ;; Commands that only make sense when editing Python code diff --git a/lisp/progmodes/ts-mode.el b/lisp/progmodes/ts-mode.el index 4d57edf65b0..f2002f9ef72 100644 --- a/lisp/progmodes/ts-mode.el +++ b/lisp/progmodes/ts-mode.el @@ -260,29 +260,26 @@ :group 'typescript :syntax-table ts-mode--syntax-table - ;; Treesit-mode. - (setq-local treesit-mode-supported t) - (setq-local treesit-required-languages '(tsx)) - ;; Comments. - (setq-local comment-start "// ") - (setq-local comment-start-skip "\\(?://+\\|/\\*+\\)\\s *") - (setq-local comment-end "") - ;; Indent. - (setq-local treesit-simple-indent-rules ts-mode--indent-rules) - ;; Navigation. - (setq-local treesit-defun-type-regexp - (rx (or "class_declaration" - "method_definition" - "function_declaration" - "lexical_declaration"))) - ;; Font-lock. - (setq-local font-lock-defaults '(nil)) - (setq-local treesit-font-lock-settings ts-mode--font-lock-settings) - (setq-local treesit-font-lock-feature-list '((minimal) (moderate) (full))) - (cond - ((treesit-ready-p '(tsx)) - (treesit-mode)) + ((treesit-ready-p nil 'tsx) + ;; Tree-sitter. + ;; Comments. + (setq-local comment-start "// ") + (setq-local comment-start-skip "\\(?://+\\|/\\*+\\)\\s *") + (setq-local comment-end "") + ;; Indent. + (setq-local treesit-simple-indent-rules ts-mode--indent-rules) + ;; Navigation. + (setq-local treesit-defun-type-regexp + (rx (or "class_declaration" + "method_definition" + "function_declaration" + "lexical_declaration"))) + ;; Font-lock. + (setq-local treesit-font-lock-settings ts-mode--font-lock-settings) + (setq-local treesit-font-lock-feature-list '((minimal) (moderate) (full))) + (treesit-major-mode-setup)) + ;; Elisp. (t (js-mode) (message "Tree-sitter for TypeScript isn't available, falling back to `js-mode'")))) diff --git a/lisp/treesit.el b/lisp/treesit.el index a3bc1259b13..86cff52af24 100644 --- a/lisp/treesit.el +++ b/lisp/treesit.el @@ -50,6 +50,34 @@ indent, imenu, etc." (declare-function treesit-available-p "treesit.c") +(defcustom treesit-settings '((t nil t)) + "Tree-sitter toggle settings for major modes. + +A list of (MODE ACTIVATE INHERIT). MODE is a major mode, ACTIVATE +can be one of the following: + + demand => Demand the use of tree-sitter, warn if it can't activate + t => Enable if available + nil => Don't enable + +If INHERIT is t, the setting for MODE is inherited by all its +derived modes. For a derived mode, closer ancestor mode's +setting takes higher precedence. + +A special MODE, t, is considered the ancestor of every mode, and +its INHERIT flag is ignored." + :type '(repeat + (list :tag "Setting" + (symbol :tag "Mode") + (choice :tag "Activate" + (const :tag "No" nil) + (const :tag "Yes" t) + (const :tag "Demand" demand)) + (choice :tag "Inherit" + (const :tag "Yes" t) + (const :tag "No" nil)))) + :version "29.1") + ;;; Parser API supplement (defun treesit-parse-string (string language) @@ -891,9 +919,7 @@ BACKWARD and ALL are the same as in `treesit-search-forward'." "A regexp that matches the node type of defun nodes. For example, \"(function|class)_definition\". -This is used by `treesit-beginning-of-defun' and friends. Bind -it buffer-locally and `treesit-mode' will use it for navigating -defun's.") +This is used by `treesit-beginning-of-defun' and friends.") (defun treesit--find-top-level-match (node type) "Return the top-level parent of NODE matching TYPE. @@ -947,136 +973,119 @@ to `imenu-create-index-function'.") ;;; Activating tree-sitter -(defvar-local treesit-mode-supported nil - "Set this variable to t to indicate support for `treesit-mode'.") - -(defvar-local treesit-required-languages nil - "A list of languages this major mode need for tree-sitter.") - -(defun treesit-ready-p (languages &optional report) - "Check that tree-sitter is ready to be used. - -If tree-sitter is not ready and REPORT non-nil, emit a warning or -message. Emit a warning if REPORT is `warn', message if REPORT -is `message'. - -LANGUAGES is a list of languages we want check for availability." - (let (msg) +(defun treesit--setting-for-mode (mode settings) + "Get the setting for MODE in SETTINGS. +MODE is a major mode symbol. SETTINGS should be `treesit-settings'." + ;; A setting for exactly this MODE. The shape is (FLAG INHERIT). + (let ((self (alist-get mode settings)) + ;; Fallback setting, shape is (FLAG INHERIT). + (fallback (alist-get t settings)) + ;; Settings for ancestor modes of MODE. Its shape is + ;; ((MODE . FLAG)...) + (applicable (cl-loop for setting in settings + for m = (nth 0 setting) + for flag = (nth 1 setting) + for inherit = (nth 2 setting) + if (and (not (eq m t)) + (not (eq m mode)) + inherit + (provided-mode-derived-p mode m)) + collect (cons m flag)))) + (cond + (self (car self)) + ((null applicable) (car fallback)) + (t + ;; After sort, the most specific setting is at the top. + (setq applicable + (cl-sort applicable + (lambda (a b) + ;; Major mode inheritance has a total ordering + ;; right? + (provided-mode-derived-p (car a) (car b))))) + (cdar applicable))))) + +(defun treesit-ready-p (mode language &optional quiet) + "Check that tree-sitter is ready to be used for MODE. + +Checks the user setting in `treesit-settings', if user sets +`demand' for MODE, and tree-sitter is not ready, emit a warning +and return nil. If user chose to activate tree-sitter for MODE +and tree-sitter is ready, return non-nil. If QUIET is t, no +warning is emitted in any case, if quiet is `message', message +instead of emitting warning. + +If MODE is nil, don't check for user setting and assume the +setting is t. + +LANGUAGE is languages symbol we want check for availability. It +can also be a list of language symbols." + (let ((language-list (if (consp language) + language + (list language))) + (activate (if mode + (treesit--setting-for-mode mode treesit-settings) + t)) + msg) ;; Check for each condition and set MSG. - (catch 'term - (when (not treesit-mode-supported) - (setq msg "this major mode doesn't support it") - (throw 'term nil)) - (when (not (treesit-available-p)) - (setq msg "tree-sitter library is not built with Emacs") - (throw 'term nil)) - (when (> (buffer-size) treesit-max-buffer-size) - (setq msg "buffer larger than `treesit-max-buffer-size'") - (throw 'term nil)) - (dolist (lang languages) - (pcase-let ((`(,available . ,err) - (treesit-language-available-p lang t))) - (when (not available) - (setq msg (format "language definition for %s is unavailable (%s): %s" - lang (nth 0 err) - (string-join - (mapcar (lambda (x) (format "%s" x)) - (cdr err)) - " "))) - (throw 'term nil))))) - ;; Decide if all conditions met and whether emit a warning. - (if (not msg) - t - (setq msg (concat "Cannot activate tree-sitter, because " msg)) - (pcase report - ('warn (display-warning 'treesit msg)) - ('message (message "%s" msg))) - nil))) - -(defvar-local treesit--local-variable-backup nil - "An alist of (VAR . VALUE) that backs up pre-treesit-mode values.") - -(defun treesit--backup-local-variable (variable value &optional restore) - "Backup VARIABLE's value and set it to VALUE. -If RESTORE is non-nil, ignore VALUE and restore the backup (if -there is any)." - (if restore - (when-let ((value (alist-get variable - treesit--local-variable-backup))) - (set variable value)) - ;; Set. - (make-variable-buffer-local variable) - (setf (alist-get variable treesit--local-variable-backup) - (symbol-value variable)) - (set variable value))) - -;;;###autoload -(define-minor-mode treesit-mode + (if (null activate) + nil + (catch 'term + (when (not (treesit-available-p)) + (setq msg "tree-sitter library is not compiled with Emacs") + (throw 'term nil)) + (when (> (buffer-size) treesit-max-buffer-size) + (setq msg "buffer larger than `treesit-max-buffer-size'") + (throw 'term nil)) + (dolist (lang language-list) + (pcase-let ((`(,available . ,err) + (treesit-language-available-p lang t))) + (when (not available) + (setq msg (format "language definition for %s is unavailable (%s): %s" + lang (nth 0 err) + (string-join + (mapcar (lambda (x) (format "%s" x)) + (cdr err)) + " "))) + (throw 'term nil))))) + ;; Decide if all conditions met and whether emit a warning. + (if (not msg) + t + (when (eq activate 'demand) + (setq msg (concat "Cannot activate tree-sitter, because " msg)) + (pcase quiet + ('nil (display-warning 'treesit msg)) + ('message (message "%s" msg)))) + nil)))) + +(defun treesit-major-mode-setup () "Activate tree-sitter to power major-mode features. -This mode is merely a SUGGESTION of turning on tree-sitter, -actual activation of tree-sitter functionalities depends on -whether the major mode supports tree-sitter, availability of -specific tree-sitter language definition, etc." - :version "29.1" - :group 'treesit - (if treesit-mode - (when (treesit-ready-p treesit-required-languages 'warn) - ;; Font-lock. - (setq-local treesit--local-variable-backup nil) - ;; NOTE: If anyone other than the major mode added stuff to - ;; `font-lock-keywords', they would need to re-add them after - ;; `treesit-mode' turns on. - (when treesit-font-lock-settings - (treesit--backup-local-variable 'font-lock-keywords-only t) - (treesit--backup-local-variable 'font-lock-keywords nil) - (treesit--backup-local-variable - 'font-lock-fontify-region-function - #'treesit-font-lock-fontify-region) - (treesit-font-lock-recompute-features)) - ;; Indent. - (when treesit-simple-indent-rules - (treesit--backup-local-variable - 'indent-line-function #'treesit-indent)) - ;; Navigation. - (when treesit-defun-type-regexp - (treesit--backup-local-variable - 'beginning-of-defun-function #'treesit-beginning-of-defun) - (treesit--backup-local-variable - 'end-of-defun-function #'treesit-end-of-defun)) - ;; Imenu. - (when treesit-imenu-function - (treesit--backup-local-variable - 'imenu-create-index-function treesit-imenu-function))) - ;; Font-lock. - (treesit--backup-local-variable 'font-lock-keywords nil t) - (treesit--backup-local-variable 'font-lock-keywords-only nil t) - (treesit--backup-local-variable - 'font-lock-fontify-region-function nil t) - ;; Indent. - (treesit--backup-local-variable 'indent-line-function nil t) - ;; Navigation. - (treesit--backup-local-variable 'beginning-of-defun-function nil t) - (treesit--backup-local-variable 'end-of-defun-function nil t) - ;; Imenu. - (treesit--backup-local-variable 'imenu-create-index-function nil t) - )) - -(defun global-treesit-mode--turn-on () - "Function used to determine whether to turn on `treesit-mode'. -Called in every buffer if `global-treesit-mode' is on." - ;; Check tree-sitter readiness and don't emit warnings. - (when (and treesit-mode-supported - (treesit-ready-p treesit-required-languages)) - (treesit-mode))) - -;;;###autoload -(define-globalized-minor-mode global-treesit-mode treesit-mode - global-treesit-mode--turn-on - :version "29.1" - :group 'treesit - :predicate t - nil) +If `treesit-font-lock-settings' is non-nil, setup fontification and +enable `font-lock-mode'. + +If `treesit-simple-indent-rules' is non-nil, setup indentation. + +If `treesit-defun-type-regexp' is non-nil, setup +`beginning/end-of-defun' functions." + ;; Font-lock. + (when treesit-font-lock-settings + ;; `font-lock-mode' wouldn't setup properly if + ;; `font-lock-defaults' is nil, see `font-lock-specified-p'. + ;; And we disable syntax-table-based font-lock by setting the + ;; KEYWORD-ONLY flag to t, so syntax-table-based font-lock + ;; doesn't override tree-sitter's fontification. + (setq-local font-lock-defaults '(nil t)) + (setq-local font-lock-fontify-region-function + #'treesit-font-lock-fontify-region) + (font-lock-mode 1) + (treesit-font-lock-recompute-features)) + ;; Indent. + (when treesit-simple-indent-rules + (setq-local indent-line-function #'treesit-indent)) + ;; Navigation. + (when treesit-defun-type-regexp + (setq-local beginning-of-defun-function #'treesit-beginning-of-defun) + (setq-local end-of-defun-function #'treesit-end-of-defun))) ;;; Debugging diff --git a/test/src/treesit-tests.el b/test/src/treesit-tests.el index adc17bc079a..2e26df69258 100644 --- a/test/src/treesit-tests.el +++ b/test/src/treesit-tests.el @@ -448,6 +448,26 @@ visible_end.)" ;; `treesit-search-forward-goto' )) +(ert-deftest treesit-misc () + "Misc helper functions." + (let ((settings '((t 0 t) + (c-mode 1 t) + (text-mode 2 nil) + (prog-mode 3 t) + (fundamental-mode 4 t)))) + ;; `treesit--setting-for-mode'. + ;; Exact match. + (should (eq 1 (treesit--setting-for-mode 'c-mode settings))) + ;; Inherit from t. + (should (eq 0 (treesit--setting-for-mode 'non-exist settings))) + ;; Inherit from prog-mode rather than fundamental-mode. + (require 'elisp-mode) + (should (eq 3 (treesit--setting-for-mode 'emacs-lisp-mode settings))) + ;; Not inherit from text-mode. + (require 'outline) + (should (not (eq 2 (treesit--setting-for-mode 'outline-mode settings)))) + )) + ;; TODO ;; - Functions in treesit.el ;; - treesit-load-name-override-list -- 2.39.2