diff options
| author | Vincenzo Pupillo | 2025-02-14 18:38:51 +0100 |
|---|---|---|
| committer | Juri Linkov | 2025-02-17 09:22:22 +0200 |
| commit | 05a96fd39809f11a3820e2164b23ebf9df192b13 (patch) | |
| tree | 2078ff791d548605d552aeca32dc002f982c01a3 | |
| parent | 0e4d08f3dc7c76008da9cd912933a931c6e3e8d9 (diff) | |
| download | emacs-05a96fd39809f11a3820e2164b23ebf9df192b13.tar.gz emacs-05a96fd39809f11a3820e2164b23ebf9df192b13.zip | |
Add mhtml-ts-mode.
New major-mode alternative to mhtml-mode, based on treesitter, for
editing files containing html, javascript and css.
* etc/NEWS: Mention the new mode and new functions.
* lisp/textmodes/mhtml-ts-mode.el: New file.
* lisp/progmodes/js.el
(js--treesit-thing-settings): New variable.
(js--treesit-font-lock-feature-list); New variable.
(js--treesit-simple-imenu-settings): New variable.
(js--treesit-defun-type-regexp): New variable.
(js--treesit-jsdoc-comment-regexp): New variable.
(js-ts-mode): Use of new variables instead of direct assignment of
values.
* lisp/textmodes/css-mode.el
(css-mode--menu): New variable.
(css-mode-map): Use new variable.
(css--treesit-font-lock-feature-list): New variable.
(css--treesit-simple-imenu-settings): New variable.
(css--treesit-defun-type-regexp): New variable.
(cs-ts-mode): Use of new variables instead of direct assignment of
values.
* lisp/textmodes/html-ts-mode.el
(html-ts-mode--treesit-things-settings): New variable.
(html-ts-mode--treesit-font-lock-feature-list): New variable.
(html-ts-mode--treesit-simple-imenu-settings): New variable.
(html-ts-mode--treesit-defun-type-regexp): New variable.
(html-ts-mode): Use of new variables instead of direct assignment of
values.
* lisp/treesit.el
(treesit-merge-font-lock-feature-list): New fuction.
(treesit-replace-font-lock-feature-settings): New fuction.
(treesit-modify-indent-rules): New function.
| -rw-r--r-- | etc/NEWS | 29 | ||||
| -rw-r--r-- | lisp/progmodes/js.el | 72 | ||||
| -rw-r--r-- | lisp/textmodes/css-mode.el | 47 | ||||
| -rw-r--r-- | lisp/textmodes/html-ts-mode.el | 54 | ||||
| -rw-r--r-- | lisp/textmodes/mhtml-ts-mode.el | 594 | ||||
| -rw-r--r-- | lisp/treesit.el | 68 |
6 files changed, 801 insertions, 63 deletions
| @@ -1217,6 +1217,12 @@ runs its body, and removes the current buffer from | |||
| 1217 | 1217 | ||
| 1218 | 1218 | ||
| 1219 | * New Modes and Packages in Emacs 31.1 | 1219 | * New Modes and Packages in Emacs 31.1 |
| 1220 | ** New major modes based on the tree-sitter library | ||
| 1221 | |||
| 1222 | *** New major mode 'mhtml-ts-mode'. | ||
| 1223 | An optional major mode based on the tree-sitter library for editing html | ||
| 1224 | files. This mode handles indentation, fontification, and commenting for | ||
| 1225 | embedded JavaScript and CSS. | ||
| 1220 | 1226 | ||
| 1221 | 1227 | ||
| 1222 | * Incompatible Lisp Changes in Emacs 31.1 | 1228 | * Incompatible Lisp Changes in Emacs 31.1 |
| @@ -1371,6 +1377,29 @@ variable 'treesit-language-display-name-alist' holds the translations of | |||
| 1371 | language symbols where that translation is not trivial. | 1377 | language symbols where that translation is not trivial. |
| 1372 | 1378 | ||
| 1373 | +++ | 1379 | +++ |
| 1380 | ++++ | ||
| 1381 | *** New function 'treesit-merge-font-lock-feature-list'. | ||
| 1382 | This function the merge two tree-sitter font lock feature lists. | ||
| 1383 | Returns a new font lock feature list with no duplicates in the same level. | ||
| 1384 | It can be used to merge font lock feature lists in a multi-language major mode. | ||
| 1385 | |||
| 1386 | +++ | ||
| 1387 | *** New function 'treesit-replace-font-lock-feature-settings'. | ||
| 1388 | Given two treesit-font-lock-settings replaces the feature in the second | ||
| 1389 | font-lock-settings with the same feature in the first | ||
| 1390 | font-lock-settings. In a multi-linguage major mode it is sometimes | ||
| 1391 | necessary to replace features from one of the major modes, with others | ||
| 1392 | that are better suited to the new multilingual context. | ||
| 1393 | |||
| 1394 | +++ | ||
| 1395 | *** New function 'treesit-modify-indent-rules'. | ||
| 1396 | Given two treesit ident rules, it replaces, adds, or prepends the new | ||
| 1397 | rules to the old ones, then returns a new treesit indent rules. | ||
| 1398 | In a multi-linguage major mode it is sometimes necessary to modify rules | ||
| 1399 | from one of the major modes, with others that are better suited to the | ||
| 1400 | new multilingual context. | ||
| 1401 | |||
| 1402 | +++ | ||
| 1374 | *** New command 'treesit-explore'. | 1403 | *** New command 'treesit-explore'. |
| 1375 | This command replaces 'treesit-explore-mode'. It turns on | 1404 | This command replaces 'treesit-explore-mode'. It turns on |
| 1376 | 'treesit-explore-mode' if it’s not on, and pops up the explorer buffer | 1405 | 'treesit-explore-mode' if it’s not on, and pops up the explorer buffer |
diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el index 3168395acf1..f2acf9f40d6 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el | |||
| @@ -3920,6 +3920,44 @@ See `treesit-thing-settings' for more information.") | |||
| 3920 | (defvar js--treesit-jsdoc-beginning-regexp (rx bos "/**") | 3920 | (defvar js--treesit-jsdoc-beginning-regexp (rx bos "/**") |
| 3921 | "Regular expression matching the beginning of a jsdoc block comment.") | 3921 | "Regular expression matching the beginning of a jsdoc block comment.") |
| 3922 | 3922 | ||
| 3923 | (defvar js--treesit-thing-settings | ||
| 3924 | `((javascript | ||
| 3925 | (sexp ,(js--regexp-opt-symbol js--treesit-sexp-nodes)) | ||
| 3926 | (list ,(js--regexp-opt-symbol js--treesit-list-nodes)) | ||
| 3927 | (sentence ,(js--regexp-opt-symbol js--treesit-sentence-nodes)) | ||
| 3928 | (text ,(js--regexp-opt-symbol '("comment" | ||
| 3929 | "string_fragment"))))) | ||
| 3930 | "Settings for `treesit-thing-settings'.") | ||
| 3931 | |||
| 3932 | (defvar js--treesit-font-lock-feature-list | ||
| 3933 | '(( comment document definition) | ||
| 3934 | ( keyword string) | ||
| 3935 | ( assignment constant escape-sequence jsx number | ||
| 3936 | pattern string-interpolation) | ||
| 3937 | ( bracket delimiter function operator property)) | ||
| 3938 | "Settings for `treesit-font-lock-feature-list'.") | ||
| 3939 | |||
| 3940 | (defvar js--treesit-simple-imenu-settings | ||
| 3941 | `(("Function" "\\`function_declaration\\'" nil nil) | ||
| 3942 | ("Variable" "\\`lexical_declaration\\'" | ||
| 3943 | js--treesit-valid-imenu-entry nil) | ||
| 3944 | ("Class" ,(rx bos (or "class_declaration" | ||
| 3945 | "method_definition") | ||
| 3946 | eos) | ||
| 3947 | nil nil)) | ||
| 3948 | "Settings for `treesit-simple-imenu'.") | ||
| 3949 | |||
| 3950 | (defvar js--treesit-defun-type-regexp | ||
| 3951 | (rx (or "class_declaration" | ||
| 3952 | "method_definition" | ||
| 3953 | "function_declaration" | ||
| 3954 | "lexical_declaration")) | ||
| 3955 | "Settings for `treesit-defun-type-regexp'.") | ||
| 3956 | |||
| 3957 | (defvar js--treesit-jsdoc-comment-regexp | ||
| 3958 | (rx (or "comment" "line_comment" "block_comment" "description")) | ||
| 3959 | "Regexp for `c-ts-common--comment-regexp'.") | ||
| 3960 | |||
| 3923 | ;;;###autoload | 3961 | ;;;###autoload |
| 3924 | (define-derived-mode js-ts-mode js-base-mode "JavaScript" | 3962 | (define-derived-mode js-ts-mode js-base-mode "JavaScript" |
| 3925 | "Major mode for editing JavaScript. | 3963 | "Major mode for editing JavaScript. |
| @@ -3951,29 +3989,15 @@ See `treesit-thing-settings' for more information.") | |||
| 3951 | ;; Indent. | 3989 | ;; Indent. |
| 3952 | (setq-local treesit-simple-indent-rules js--treesit-indent-rules) | 3990 | (setq-local treesit-simple-indent-rules js--treesit-indent-rules) |
| 3953 | ;; Navigation. | 3991 | ;; Navigation. |
| 3954 | (setq-local treesit-defun-type-regexp | 3992 | (setq-local treesit-defun-type-regexp js--treesit-defun-type-regexp) |
| 3955 | (rx (or "class_declaration" | 3993 | |
| 3956 | "method_definition" | ||
| 3957 | "function_declaration" | ||
| 3958 | "lexical_declaration"))) | ||
| 3959 | (setq-local treesit-defun-name-function #'js--treesit-defun-name) | 3994 | (setq-local treesit-defun-name-function #'js--treesit-defun-name) |
| 3960 | 3995 | ||
| 3961 | (setq-local treesit-thing-settings | 3996 | (setq-local treesit-thing-settings js--treesit-thing-settings) |
| 3962 | `((javascript | ||
| 3963 | (sexp ,(js--regexp-opt-symbol js--treesit-sexp-nodes)) | ||
| 3964 | (list ,(js--regexp-opt-symbol js--treesit-list-nodes)) | ||
| 3965 | (sentence ,(js--regexp-opt-symbol js--treesit-sentence-nodes)) | ||
| 3966 | (text ,(js--regexp-opt-symbol '("comment" | ||
| 3967 | "string_fragment")))))) | ||
| 3968 | 3997 | ||
| 3969 | ;; Fontification. | 3998 | ;; Fontification. |
| 3970 | (setq-local treesit-font-lock-settings js--treesit-font-lock-settings) | 3999 | (setq-local treesit-font-lock-settings js--treesit-font-lock-settings) |
| 3971 | (setq-local treesit-font-lock-feature-list | 4000 | (setq-local treesit-font-lock-feature-list js--treesit-font-lock-feature-list) |
| 3972 | '(( comment document definition) | ||
| 3973 | ( keyword string) | ||
| 3974 | ( assignment constant escape-sequence jsx number | ||
| 3975 | pattern string-interpolation) | ||
| 3976 | ( bracket delimiter function operator property))) | ||
| 3977 | 4001 | ||
| 3978 | (when (treesit-ready-p 'jsdoc t) | 4002 | (when (treesit-ready-p 'jsdoc t) |
| 3979 | (setq-local treesit-range-settings | 4003 | (setq-local treesit-range-settings |
| @@ -3983,17 +4007,11 @@ See `treesit-thing-settings' for more information.") | |||
| 3983 | :local t | 4007 | :local t |
| 3984 | `(((comment) @capture (:match ,js--treesit-jsdoc-beginning-regexp @capture))))) | 4008 | `(((comment) @capture (:match ,js--treesit-jsdoc-beginning-regexp @capture))))) |
| 3985 | 4009 | ||
| 3986 | (setq c-ts-common--comment-regexp (rx (or "comment" "line_comment" "block_comment" "description")))) | 4010 | (setq c-ts-common--comment-regexp js--treesit-jsdoc-comment-regexp)) |
| 3987 | 4011 | ||
| 3988 | ;; Imenu | 4012 | ;; Imenu |
| 3989 | (setq-local treesit-simple-imenu-settings | 4013 | (setq-local treesit-simple-imenu-settings js--treesit-simple-imenu-settings) |
| 3990 | `(("Function" "\\`function_declaration\\'" nil nil) | 4014 | |
| 3991 | ("Variable" "\\`lexical_declaration\\'" | ||
| 3992 | js--treesit-valid-imenu-entry nil) | ||
| 3993 | ("Class" ,(rx bos (or "class_declaration" | ||
| 3994 | "method_definition") | ||
| 3995 | eos) | ||
| 3996 | nil nil))) | ||
| 3997 | (treesit-major-mode-setup) | 4015 | (treesit-major-mode-setup) |
| 3998 | 4016 | ||
| 3999 | (add-to-list 'auto-mode-alist | 4017 | (add-to-list 'auto-mode-alist |
diff --git a/lisp/textmodes/css-mode.el b/lisp/textmodes/css-mode.el index 53340195386..35c61e4f66d 100644 --- a/lisp/textmodes/css-mode.el +++ b/lisp/textmodes/css-mode.el | |||
| @@ -893,13 +893,7 @@ cannot be completed sensibly: `custom-ident', | |||
| 893 | (modify-syntax-entry ?? "." st) | 893 | (modify-syntax-entry ?? "." st) |
| 894 | st)) | 894 | st)) |
| 895 | 895 | ||
| 896 | (defvar-keymap css-mode-map | 896 | (defvar css-mode--menu |
| 897 | :doc "Keymap used in `css-mode'." | ||
| 898 | "<remap> <info-lookup-symbol>" #'css-lookup-symbol | ||
| 899 | ;; `info-complete-symbol' is not used. | ||
| 900 | "<remap> <complete-symbol>" #'completion-at-point | ||
| 901 | "C-c C-f" #'css-cycle-color-format | ||
| 902 | :menu | ||
| 903 | '("CSS" | 897 | '("CSS" |
| 904 | :help "CSS-specific features" | 898 | :help "CSS-specific features" |
| 905 | ["Reformat block" fill-paragraph | 899 | ["Reformat block" fill-paragraph |
| @@ -910,7 +904,17 @@ cannot be completed sensibly: `custom-ident', | |||
| 910 | ["Describe symbol" css-lookup-symbol | 904 | ["Describe symbol" css-lookup-symbol |
| 911 | :help "Display documentation for a CSS symbol"] | 905 | :help "Display documentation for a CSS symbol"] |
| 912 | ["Complete symbol" completion-at-point | 906 | ["Complete symbol" completion-at-point |
| 913 | :help "Complete symbol before point"])) | 907 | :help "Complete symbol before point"]) |
| 908 | "Menu bar for `css-mode'") | ||
| 909 | |||
| 910 | (defvar-keymap css-mode-map | ||
| 911 | :doc "Keymap used in `css-mode'." | ||
| 912 | "<remap> <info-lookup-symbol>" #'css-lookup-symbol | ||
| 913 | ;; `info-complete-symbol' is not used. | ||
| 914 | "<remap> <complete-symbol>" #'completion-at-point | ||
| 915 | "C-c C-f" #'css-cycle-color-format | ||
| 916 | :menu | ||
| 917 | css-mode--menu) | ||
| 914 | 918 | ||
| 915 | (eval-and-compile | 919 | (eval-and-compile |
| 916 | (defconst css--uri-re | 920 | (defconst css--uri-re |
| @@ -1771,6 +1775,21 @@ rgb()/rgba()." | |||
| 1771 | (replace-regexp-in-string "[\n ]+" " " s))) | 1775 | (replace-regexp-in-string "[\n ]+" " " s))) |
| 1772 | res))))))) | 1776 | res))))))) |
| 1773 | 1777 | ||
| 1778 | (defvar css--treesit-font-lock-feature-list | ||
| 1779 | '((selector comment query keyword) | ||
| 1780 | (property constant string) | ||
| 1781 | (error variable function operator bracket)) | ||
| 1782 | "Settings for `treesit-font-lock-feature-list'.") | ||
| 1783 | |||
| 1784 | (defvar css--treesit-simple-imenu-settings | ||
| 1785 | `(( nil ,(rx bos (or "rule_set" "media_statement") eos) | ||
| 1786 | nil nil)) | ||
| 1787 | "Settings for `treesit-simple-imenu'.") | ||
| 1788 | |||
| 1789 | (defvar css--treesit-defun-type-regexp | ||
| 1790 | "rule_set" | ||
| 1791 | "Settings for `treesit-defun-type-regexp'.") | ||
| 1792 | |||
| 1774 | (define-derived-mode css-base-mode prog-mode "CSS" | 1793 | (define-derived-mode css-base-mode prog-mode "CSS" |
| 1775 | "Generic mode to edit Cascading Style Sheets (CSS). | 1794 | "Generic mode to edit Cascading Style Sheets (CSS). |
| 1776 | 1795 | ||
| @@ -1825,16 +1844,12 @@ can also be used to fill comments. | |||
| 1825 | ;; Tree-sitter specific setup. | 1844 | ;; Tree-sitter specific setup. |
| 1826 | (setq treesit-primary-parser (treesit-parser-create 'css)) | 1845 | (setq treesit-primary-parser (treesit-parser-create 'css)) |
| 1827 | (setq-local treesit-simple-indent-rules css--treesit-indent-rules) | 1846 | (setq-local treesit-simple-indent-rules css--treesit-indent-rules) |
| 1828 | (setq-local treesit-defun-type-regexp "rule_set") | 1847 | (setq-local treesit-defun-type-regexp css--treesit-defun-type-regexp) |
| 1829 | (setq-local treesit-defun-name-function #'css--treesit-defun-name) | 1848 | (setq-local treesit-defun-name-function #'css--treesit-defun-name) |
| 1830 | (setq-local treesit-font-lock-settings css--treesit-settings) | 1849 | (setq-local treesit-font-lock-settings css--treesit-settings) |
| 1831 | (setq-local treesit-font-lock-feature-list | 1850 | (setq-local treesit-font-lock-feature-list css--treesit-font-lock-feature-list) |
| 1832 | '((selector comment query keyword) | 1851 | (setq-local treesit-simple-imenu-settings css--treesit-simple-imenu-settings) |
| 1833 | (property constant string) | 1852 | |
| 1834 | (error variable function operator bracket))) | ||
| 1835 | (setq-local treesit-simple-imenu-settings | ||
| 1836 | `(( nil ,(rx bos (or "rule_set" "media_statement") eos) | ||
| 1837 | nil nil))) | ||
| 1838 | (treesit-major-mode-setup) | 1853 | (treesit-major-mode-setup) |
| 1839 | 1854 | ||
| 1840 | (add-to-list 'auto-mode-alist '("\\.css\\'" . css-ts-mode)))) | 1855 | (add-to-list 'auto-mode-alist '("\\.css\\'" . css-ts-mode)))) |
diff --git a/lisp/textmodes/html-ts-mode.el b/lisp/textmodes/html-ts-mode.el index 0f07fbedeed..26efe1be726 100644 --- a/lisp/textmodes/html-ts-mode.el +++ b/lisp/textmodes/html-ts-mode.el | |||
| @@ -88,6 +88,35 @@ | |||
| 88 | `((attribute_name) @font-lock-variable-name-face)) | 88 | `((attribute_name) @font-lock-variable-name-face)) |
| 89 | "Tree-sitter font-lock settings for `html-ts-mode'.") | 89 | "Tree-sitter font-lock settings for `html-ts-mode'.") |
| 90 | 90 | ||
| 91 | (defvar html-ts-mode--treesit-things-settings | ||
| 92 | `((html | ||
| 93 | (sexp ,(regexp-opt '("element" | ||
| 94 | "text" | ||
| 95 | "attribute" | ||
| 96 | "value"))) | ||
| 97 | (list ,(rx (or | ||
| 98 | ;; Also match script_element and style_element | ||
| 99 | "element" | ||
| 100 | ;; HTML comments have the element syntax | ||
| 101 | "comment"))) | ||
| 102 | (sentence ,(rx (and bos (or "tag_name" "attribute") eos))) | ||
| 103 | (text ,(regexp-opt '("comment" "text"))))) | ||
| 104 | "Settings for `treesit-thing-settings'.") | ||
| 105 | |||
| 106 | (defvar html-ts-mode--treesit-font-lock-feature-list | ||
| 107 | '((comment keyword definition) | ||
| 108 | (property string) | ||
| 109 | () ()) | ||
| 110 | "Settings for `treesit-font-lock-feature-list'.") | ||
| 111 | |||
| 112 | (defvar html-ts-mode--treesit-simple-imenu-settings | ||
| 113 | '((nil "element" nil nil)) | ||
| 114 | "Settings for `treesit-simple-imenu'.") | ||
| 115 | |||
| 116 | (defvar html-ts-mode--treesit-defun-type-regexp | ||
| 117 | "element" | ||
| 118 | "Settings for `treesit-defun-type-regexp'.") | ||
| 119 | |||
| 91 | (defun html-ts-mode--defun-name (node) | 120 | (defun html-ts-mode--defun-name (node) |
| 92 | "Return the defun name of NODE. | 121 | "Return the defun name of NODE. |
| 93 | Return nil if there is no name or if NODE is not a defun node." | 122 | Return nil if there is no name or if NODE is not a defun node." |
| @@ -120,33 +149,18 @@ Return nil if there is no name or if NODE is not a defun node." | |||
| 120 | (setq-local treesit-simple-indent-rules html-ts-mode--indent-rules) | 149 | (setq-local treesit-simple-indent-rules html-ts-mode--indent-rules) |
| 121 | 150 | ||
| 122 | ;; Navigation. | 151 | ;; Navigation. |
| 123 | (setq-local treesit-defun-type-regexp "element") | 152 | (setq-local treesit-defun-type-regexp html-ts-mode--treesit-defun-type-regexp) |
| 153 | |||
| 124 | (setq-local treesit-defun-name-function #'html-ts-mode--defun-name) | 154 | (setq-local treesit-defun-name-function #'html-ts-mode--defun-name) |
| 125 | 155 | ||
| 126 | (setq-local treesit-thing-settings | 156 | (setq-local treesit-thing-settings html-ts-mode--treesit-things-settings) |
| 127 | `((html | ||
| 128 | (sexp ,(regexp-opt '("element" | ||
| 129 | "text" | ||
| 130 | "attribute" | ||
| 131 | "value"))) | ||
| 132 | (list ,(rx (or | ||
| 133 | ;; Also match script_element and style_element | ||
| 134 | "element" | ||
| 135 | ;; HTML comments have the element syntax | ||
| 136 | "comment"))) | ||
| 137 | (sentence ,(rx (and bos (or "tag_name" "attribute") eos))) | ||
| 138 | (text ,(regexp-opt '("comment" "text")))))) | ||
| 139 | 157 | ||
| 140 | ;; Font-lock. | 158 | ;; Font-lock. |
| 141 | (setq-local treesit-font-lock-settings html-ts-mode--font-lock-settings) | 159 | (setq-local treesit-font-lock-settings html-ts-mode--font-lock-settings) |
| 142 | (setq-local treesit-font-lock-feature-list | 160 | (setq-local treesit-font-lock-feature-list html-ts-mode--treesit-font-lock-feature-list) |
| 143 | '((comment keyword definition) | ||
| 144 | (property string) | ||
| 145 | () ())) | ||
| 146 | 161 | ||
| 147 | ;; Imenu. | 162 | ;; Imenu. |
| 148 | (setq-local treesit-simple-imenu-settings | 163 | (setq-local treesit-simple-imenu-settings html-ts-mode--treesit-simple-imenu-settings) |
| 149 | '((nil "element" nil nil))) | ||
| 150 | 164 | ||
| 151 | ;; Outline minor mode. | 165 | ;; Outline minor mode. |
| 152 | (setq-local treesit-outline-predicate #'html-ts-mode--outline-predicate) | 166 | (setq-local treesit-outline-predicate #'html-ts-mode--outline-predicate) |
diff --git a/lisp/textmodes/mhtml-ts-mode.el b/lisp/textmodes/mhtml-ts-mode.el new file mode 100644 index 00000000000..9be1a14c257 --- /dev/null +++ b/lisp/textmodes/mhtml-ts-mode.el | |||
| @@ -0,0 +1,594 @@ | |||
| 1 | ;;; mhtml-ts-mode.el --- Major mode for HTML using tree-sitter -*- lexical-binding: t; -*- | ||
| 2 | |||
| 3 | ;; Copyright (C) 2024 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | ;; Author: Vincenzo Pupillo <v.pupillo@gmail.com> | ||
| 6 | ;; Maintainer: Vincenzo Pupillo <v.pupillo@gmail.com> | ||
| 7 | ;; Created: Nov 2024 | ||
| 8 | ;; Keywords: HTML languages hypermedia tree-sitter | ||
| 9 | |||
| 10 | ;; This file is part of GNU Emacs. | ||
| 11 | |||
| 12 | ;; GNU Emacs is free software: you can redistribute it and/or modify | ||
| 13 | ;; it under the terms of the GNU General Public License as published by | ||
| 14 | ;; the Free Software Foundation, either version 3 of the License, or | ||
| 15 | ;; (at your option) any later version. | ||
| 16 | |||
| 17 | ;; GNU Emacs is distributed in the hope that it will be useful, | ||
| 18 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 19 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 20 | ;; GNU General Public License for more details. | ||
| 21 | |||
| 22 | ;; You should have received a copy of the GNU General Public License | ||
| 23 | ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. | ||
| 24 | |||
| 25 | ;;; Commentary: | ||
| 26 | ;; | ||
| 27 | ;; This package provides `mhtml-ts-mode' which is a major mode | ||
| 28 | ;; for editing HTML files with embedded JavaScript and CSS. | ||
| 29 | ;; Tree Sitter is used to parse each of these languages. | ||
| 30 | ;; | ||
| 31 | ;; Please note that this package requires `html-ts-mode', which | ||
| 32 | ;; registers itself as the major mode for editing HTML. | ||
| 33 | ;; | ||
| 34 | ;; This package is compatible and has been tested with the following | ||
| 35 | ;; tree-sitter grammars: | ||
| 36 | ;; * https://github.com/tree-sitter/tree-sitter-html | ||
| 37 | ;; * https://github.com/tree-sitter/tree-sitter-javascript | ||
| 38 | ;; * https://github.com/tree-sitter/tree-sitter-jsdoc | ||
| 39 | ;; * https://github.com/tree-sitter/tree-sitter-css | ||
| 40 | ;; | ||
| 41 | ;; Features | ||
| 42 | ;; | ||
| 43 | ;; * Indent | ||
| 44 | ;; * Flymake | ||
| 45 | ;; * IMenu | ||
| 46 | ;; * Navigation | ||
| 47 | ;; * Which-function | ||
| 48 | ;; * Tree-sitter parser installation helper | ||
| 49 | |||
| 50 | ;;; Code: | ||
| 51 | |||
| 52 | (require 'treesit) | ||
| 53 | (require 'html-ts-mode) | ||
| 54 | (require 'css-mode) ;; for embed css into html | ||
| 55 | (require 'js) ;; for embed javascript into html | ||
| 56 | |||
| 57 | (eval-when-compile | ||
| 58 | (require 'rx)) | ||
| 59 | |||
| 60 | ;; This tells the byte-compiler where the functions are defined. | ||
| 61 | ;; Is only needed when a file needs to be able to byte-compile | ||
| 62 | ;; in a Emacs not built with tree-sitter library. | ||
| 63 | (treesit-declare-unavailable-functions) | ||
| 64 | |||
| 65 | ;; In a multi-language major mode can be useful to have an "installer" to | ||
| 66 | ;; simplify the installation of the grammars supported by the major-mode. | ||
| 67 | (defvar mhtml-ts-mode--language-source-alist | ||
| 68 | '((html . ("https://github.com/tree-sitter/tree-sitter-html" "v0.23.2")) | ||
| 69 | (javascript . ("https://github.com/tree-sitter/tree-sitter-javascript" "v0.23.1")) | ||
| 70 | (jsdoc . ("https://github.com/tree-sitter/tree-sitter-jsdoc" "v0.23.2")) | ||
| 71 | (css . ("https://github.com/tree-sitter/tree-sitter-css" "v0.23.1"))) | ||
| 72 | "Treesitter language parsers required by `mhtml-ts-mode'. | ||
| 73 | You can customize this variable if you want to stick to a specific | ||
| 74 | commit and/or use different parsers.") | ||
| 75 | |||
| 76 | (defun mhtml-ts-mode-install-parsers () | ||
| 77 | "Install all the required treesitter parsers. | ||
| 78 | `mhtml-ts-mode--language-source-alist' defines which parsers to install." | ||
| 79 | (interactive) | ||
| 80 | (let ((treesit-language-source-alist mhtml-ts-mode--language-source-alist)) | ||
| 81 | (dolist (item mhtml-ts-mode--language-source-alist) | ||
| 82 | (treesit-install-language-grammar (car item))))) | ||
| 83 | |||
| 84 | ;;; Custom variables | ||
| 85 | |||
| 86 | (defgroup mhtml-ts-mode nil | ||
| 87 | "Major mode for editing HTML files, based on `html-ts-mode'. | ||
| 88 | Works with JS and CSS and for that use `js-ts-mode' and `css-ts-mode'." | ||
| 89 | :prefix "mhtml-ts-mode-" | ||
| 90 | ;; :group 'languages | ||
| 91 | :group 'html) | ||
| 92 | |||
| 93 | (defcustom mhtml-ts-mode-js-css-indent-offset 2 | ||
| 94 | "JavaScript and CSS indent spaces related to the <script> and <style> HTML tags. | ||
| 95 | By default should have same value as `html-ts-mode-indent-offset'." | ||
| 96 | :tag "HTML javascript or css indent offset" | ||
| 97 | :version "31.1" | ||
| 98 | :type 'integer | ||
| 99 | :safe 'integerp) | ||
| 100 | |||
| 101 | (defcustom mhtml-ts-mode-pretty-print-command | ||
| 102 | ;; prefer tidy because it's used by sgml-mode | ||
| 103 | (let ((executable nil)) | ||
| 104 | (cond ((setq executable (executable-find "tidy")) | ||
| 105 | (format | ||
| 106 | "%s --gnu-emacs yes --wrap 0 --indent-spaces %s -q -i -" | ||
| 107 | executable html-ts-mode-indent-offset)) | ||
| 108 | ((setq executable (executable-find "xmllint")) | ||
| 109 | (format "%s --html --quiet --format -" executable)) | ||
| 110 | (t "Install tidy, ore some other HTML pretty print tool, and set `mhtml-ts-mode-pretty-print-command'."))) | ||
| 111 | "The command to pretty print the current HTML buffer." | ||
| 112 | :type 'string | ||
| 113 | :version "31.1") | ||
| 114 | |||
| 115 | (defvar mhtml-ts-mode--js-css-indent-offset | ||
| 116 | mhtml-ts-mode-js-css-indent-offset | ||
| 117 | "Internal copy of `mhtml-ts-mode-js-css-indent-offset'. | ||
| 118 | The value changes, by `mhtml-ts-mode--tag-relative-indent-offset' according to | ||
| 119 | the value of `mhtml-ts-mode-tag-relative-indent'.") | ||
| 120 | |||
| 121 | (defun mhtml-ts-mode--tag-relative-indent-offset (sym val) | ||
| 122 | "Custom setter for `mhtml-ts-mode-tag-relative-indent'. | ||
| 123 | Apart from setting the default value of SYM to VAL, also change the | ||
| 124 | value of SYM in `mhtml-ts-mode' buffers to VAL. SYM should be | ||
| 125 | `mhtml-ts-mode-tag-relative-indent', and VAL should be t, nil or | ||
| 126 | `ignore'. When sym is `mhtml-ts-mode-tag-relative-indent' set the | ||
| 127 | value of `mhtml-ts-mode--js-css-indent-offset' to 0 if VAL is t, | ||
| 128 | otherwise to `mhtml-ts-mode-js-css-indent-offset'." | ||
| 129 | (set-default sym val) | ||
| 130 | (when (eq sym 'mhtml-ts-mode-tag-relative-indent) | ||
| 131 | (setq | ||
| 132 | mhtml-ts-mode--js-css-indent-offset | ||
| 133 | (if (eq val t) | ||
| 134 | mhtml-ts-mode-js-css-indent-offset | ||
| 135 | 0)))) | ||
| 136 | |||
| 137 | (defcustom mhtml-ts-mode-tag-relative-indent t | ||
| 138 | "How <script> and <style> bodies are indented relative to the tag. | ||
| 139 | |||
| 140 | When t, indentation looks like: | ||
| 141 | |||
| 142 | <script> | ||
| 143 | code(); | ||
| 144 | </script> | ||
| 145 | |||
| 146 | When nil, indentation of the tag body starts just below the | ||
| 147 | tag, like: | ||
| 148 | |||
| 149 | <script> | ||
| 150 | code(); | ||
| 151 | </script> | ||
| 152 | |||
| 153 | When `ignore', the tag body starts in the first column, like: | ||
| 154 | |||
| 155 | <script> | ||
| 156 | code(); | ||
| 157 | </script>" | ||
| 158 | :type '(choice (const nil) (const t) (const ignore)) | ||
| 159 | :safe 'symbolp | ||
| 160 | :set #'mhtml-ts-mode--tag-relative-indent-offset | ||
| 161 | :version "31.1") | ||
| 162 | |||
| 163 | (defcustom mhtml-ts-mode-css-fontify-colors t | ||
| 164 | "Whether CSS colors should be fontified using the color as the background. | ||
| 165 | If non-nil, text representing a CSS color will be fontified | ||
| 166 | such that its background is the color itself. | ||
| 167 | Works like `css--fontify-region'." | ||
| 168 | :tag "HTML colors the CSS properties values." | ||
| 169 | :version "31.1" | ||
| 170 | :type 'boolean | ||
| 171 | :safe 'booleanp) | ||
| 172 | |||
| 173 | (defvar mhtml-ts-mode-saved-pretty-print-command nil | ||
| 174 | "The command last used to pretty print in this buffer.") | ||
| 175 | |||
| 176 | (defun mhtml-ts-mode-pretty-print (command) | ||
| 177 | "Prettify the current buffer. | ||
| 178 | Argument COMMAND The command to use." | ||
| 179 | (interactive | ||
| 180 | (list (read-string | ||
| 181 | "Prettify command: " | ||
| 182 | (or mhtml-ts-mode-saved-pretty-print-command | ||
| 183 | (concat mhtml-ts-mode-pretty-print-command " "))))) | ||
| 184 | (setq mhtml-ts-mode-saved-pretty-print-command command) | ||
| 185 | (save-excursion | ||
| 186 | (shell-command-on-region | ||
| 187 | (point-min) (point-max) | ||
| 188 | command (buffer-name) t | ||
| 189 | "*mhtml-ts-mode-pretty-pretty-print-errors*" t))) | ||
| 190 | |||
| 191 | (defun mhtml-ts-mode--switch-fill-defun (&rest arguments) | ||
| 192 | "Switch between `fill-paragraph' and `prog-fill-reindent-defun'. | ||
| 193 | In an HTML region it calls `fill-paragraph' as does `html-ts-mode', | ||
| 194 | otherwise it calls `prog-fill-reindent-defun'. | ||
| 195 | Optional ARGUMENTS to to be passed to it." | ||
| 196 | (interactive) | ||
| 197 | (if (eq (treesit-language-at (point)) 'html) | ||
| 198 | (funcall-interactively #'fill-paragraph arguments) | ||
| 199 | (funcall-interactively #'prog-fill-reindent-defun arguments))) | ||
| 200 | |||
| 201 | (defvar-keymap mhtml-ts-mode-map | ||
| 202 | :doc "Keymap for `mhtml-ts-mode' buffers." | ||
| 203 | :parent html-mode-map | ||
| 204 | ;; `mhtml-ts-mode' derive from `html-ts-mode' so the keymap is the | ||
| 205 | ;; same, we need to add some mapping from others languages. | ||
| 206 | "C-c C-f" #'css-cycle-color-format | ||
| 207 | "M-q" #'mhtml-ts-mode--switch-fill-defun) | ||
| 208 | |||
| 209 | ;; Place the CSS menu in the menu bar as well. | ||
| 210 | (easy-menu-define mhtml-ts-mode-menu mhtml-ts-mode-map | ||
| 211 | "Menu bar for `mhtml-ts-mode'." | ||
| 212 | css-mode--menu) | ||
| 213 | |||
| 214 | ;; To enable some basic treesiter functionality, you should define | ||
| 215 | ;; a function that recognizes which grammar is used at-point. | ||
| 216 | ;; This function should be assigned to `treesit-language-at-point-function' | ||
| 217 | (defun mhtml-ts-mode--language-at-point (point) | ||
| 218 | "Return the language at POINT assuming the point is within a HTML buffer." | ||
| 219 | (let* ((node (treesit-node-at point 'html)) | ||
| 220 | (parent (treesit-node-parent node)) | ||
| 221 | (node-query (format "(%s (%s))" | ||
| 222 | (treesit-node-type parent) | ||
| 223 | (treesit-node-type node)))) | ||
| 224 | (cond | ||
| 225 | ((equal "(script_element (raw_text))" node-query) (js--treesit-language-at-point point)) | ||
| 226 | ((equal "(style_element (raw_text))" node-query) 'css) | ||
| 227 | (t 'html)))) | ||
| 228 | |||
| 229 | ;; Custom font-lock function that's used to apply color to css color | ||
| 230 | ;; The signature of the function should be conforming to signature | ||
| 231 | ;; QUERY-SPEC required by `treesit-font-lock-rules'. | ||
| 232 | (defun mhtml-ts-mode--colorize-css-value (node override start end &rest _) | ||
| 233 | "Colorize CSS property value like `css--fontify-region'. | ||
| 234 | For NODE, OVERRIDE, START, and END, see `treesit-font-lock-rules'." | ||
| 235 | (if (and mhtml-ts-mode-css-fontify-colors | ||
| 236 | (string-equal "plain_value" (treesit-node-type node))) | ||
| 237 | (let ((color (css--compute-color start (treesit-node-text node t)))) | ||
| 238 | (when color | ||
| 239 | (with-silent-modifications | ||
| 240 | (add-text-properties | ||
| 241 | (treesit-node-start node) (treesit-node-end node) | ||
| 242 | (list 'face (list :background color | ||
| 243 | :foreground (readable-foreground-color | ||
| 244 | color) | ||
| 245 | :box '(:line-width -1))))))) | ||
| 246 | (treesit-fontify-with-override | ||
| 247 | (treesit-node-start node) (treesit-node-end node) | ||
| 248 | 'font-lock-variable-name-face | ||
| 249 | override start end))) | ||
| 250 | |||
| 251 | ;; Embedded languages should be indented according to the language | ||
| 252 | ;; that embeds them. | ||
| 253 | ;; This function signature complies with `treesit-simple-indent-rules' | ||
| 254 | ;; ANCHOR. | ||
| 255 | (defun mhtml-ts-mode--js-css-tag-bol (_node _parent &rest _) | ||
| 256 | "Find the first non-space characters of html tags <script> or <style>. | ||
| 257 | Return `line-beginning-position' when `treesit-node-at' is html, or | ||
| 258 | `mhtml-ts-mode-tag-relative-indent' is equal to ignore. | ||
| 259 | NODE and PARENT are ignored." | ||
| 260 | (if (or (eq (treesit-language-at (point)) 'html) | ||
| 261 | (eq mhtml-ts-mode-tag-relative-indent 'ignore)) | ||
| 262 | (line-beginning-position) | ||
| 263 | ;; Ok, we are in js or css block. | ||
| 264 | (save-excursion | ||
| 265 | (re-search-backward "<script.*>\\|<style.*>" nil t)))) | ||
| 266 | |||
| 267 | ;; Treesit supports 4 level of decoration, `treesit-font-lock-level' | ||
| 268 | ;; define which level to use. Major modes categorize their fontification | ||
| 269 | ;; features, these categories are defined by `treesit-font-lock-rules' of | ||
| 270 | ;; each major-mode using :feature keyword. | ||
| 271 | ;; In a multiple language Major mode it's a good idea to provide, for each | ||
| 272 | ;; level, the union of the :feature of the same level. | ||
| 273 | ;; TODO: Since the feature-list is not defined per "parser" (like, for | ||
| 274 | ;; example, the thing-settings), the same feature can appear in | ||
| 275 | ;; different levels, so the appearance of a multiple main mode can be | ||
| 276 | ;; different from the main mode used. For e.g the feature "function" is | ||
| 277 | ;; at level 4 for Javascript while it is at level 3 for CSS. | ||
| 278 | (defvar mhtml-ts-mode--treesit-font-lock-feature-list | ||
| 279 | (treesit-merge-font-lock-feature-list | ||
| 280 | html-ts-mode--treesit-font-lock-feature-list | ||
| 281 | (treesit-merge-font-lock-feature-list | ||
| 282 | js--treesit-font-lock-feature-list | ||
| 283 | css--treesit-font-lock-feature-list)) | ||
| 284 | "Settings for `treesit-font-lock-feature-list'.") | ||
| 285 | |||
| 286 | (defvar mhtml-ts-mode--treesit-font-lock-settings | ||
| 287 | (append html-ts-mode--font-lock-settings | ||
| 288 | js--treesit-font-lock-settings | ||
| 289 | ;; Let's replace a css rule with a new one that adds color to | ||
| 290 | ;; the css value. | ||
| 291 | (treesit-replace-font-lock-feature-settings | ||
| 292 | (treesit-font-lock-rules | ||
| 293 | :language 'css | ||
| 294 | :override t | ||
| 295 | :feature 'variable | ||
| 296 | '((plain_value) @font-lock-variable-name-face | ||
| 297 | (plain_value) @mhtml-ts-mode--colorize-css-value)) | ||
| 298 | css--treesit-settings)) | ||
| 299 | "Settings for `treesit-font-lock-settings'.") | ||
| 300 | |||
| 301 | (defvar mhtml-ts-mode--treesit-thing-settings | ||
| 302 | ;; In addition to putting together the various definitions, we need to | ||
| 303 | ;; add 'defun' which is used to support `imenu' and 'which-function'. | ||
| 304 | (list | ||
| 305 | ;; HTML thing settings | ||
| 306 | (append | ||
| 307 | (car html-ts-mode--treesit-things-settings) | ||
| 308 | `((defun ,(regexp-opt (list html-ts-mode--treesit-defun-type-regexp))))) | ||
| 309 | ;; Javascript thing settings | ||
| 310 | (append | ||
| 311 | (car js--treesit-thing-settings) | ||
| 312 | `((defun ,js--treesit-defun-type-regexp))) | ||
| 313 | ;; CSS thing settings | ||
| 314 | `(css | ||
| 315 | (defun ,(regexp-opt (list css--treesit-defun-type-regexp))))) | ||
| 316 | "Settings for `treesit-thing-settings'.") | ||
| 317 | |||
| 318 | (defvar mhtml-ts-mode--treesit-indent-rules | ||
| 319 | (treesit--indent-rules-optimize | ||
| 320 | (append html-ts-mode--indent-rules | ||
| 321 | ;; Extended rules for js and css, to | ||
| 322 | ;; indent appropriately when injected | ||
| 323 | ;; into html | ||
| 324 | (treesit-modify-indent-rules | ||
| 325 | 'javascript | ||
| 326 | `((javascript ((parent-is "program") | ||
| 327 | mhtml-ts-mode--js-css-tag-bol | ||
| 328 | mhtml-ts-mode--js-css-indent-offset))) | ||
| 329 | js--treesit-indent-rules | ||
| 330 | :replace) | ||
| 331 | (treesit-modify-indent-rules | ||
| 332 | 'css | ||
| 333 | `((css ((parent-is "stylesheet") | ||
| 334 | mhtml-ts-mode--js-css-tag-bol | ||
| 335 | mhtml-ts-mode--js-css-indent-offset))) | ||
| 336 | css--treesit-indent-rules 'prepend) | ||
| 337 | :replace)) | ||
| 338 | "Settings for `treesit-simple-indent-rules'.") | ||
| 339 | |||
| 340 | (defvar mhtml-ts-mode--treesit-aggregated-simple-imenu-settings | ||
| 341 | `((html ,@html-ts-mode--treesit-simple-imenu-settings) | ||
| 342 | (javascript ,@js--treesit-simple-imenu-settings) | ||
| 343 | (css ,@css--treesit-simple-imenu-settings)) | ||
| 344 | "Settings for `treesit-simple-imenu'.") | ||
| 345 | |||
| 346 | ;; TODO: treesit-defun-type-regexp should have an aggregated version, | ||
| 347 | ;; like treesit-aggregated-simple-imenu-settings. Otherwise we can't | ||
| 348 | ;; reuse the regex defined in the major mode we use. | ||
| 349 | (defvar mhtml-ts-mode--treesit-defun-type-regexp | ||
| 350 | (regexp-opt '("class_declaration" | ||
| 351 | "method_definition" | ||
| 352 | "function_declaration" | ||
| 353 | "lexical_declaration" | ||
| 354 | "element" | ||
| 355 | "rule_set")) | ||
| 356 | "Settings for `treesit-defun-type-regexp'.") | ||
| 357 | |||
| 358 | ;; In order to support `prettify-symbols-mode', just `append' the prettify | ||
| 359 | ;; alist of all the languages. In our case only javascript defined this alist. | ||
| 360 | (defvar mhtml-ts-mode--prettify-symbols-alist js--prettify-symbols-alist | ||
| 361 | "Alist of symbol prettifications for various supported languages.") | ||
| 362 | |||
| 363 | (defun mhtml-ts-mode--html-defun-name (node) | ||
| 364 | "Return the defun name of NODE. | ||
| 365 | Return nil if there is no name or if NODE is not a defun node." | ||
| 366 | (when (string-match-p "element" (treesit-node-type node)) | ||
| 367 | (treesit-node-text | ||
| 368 | node | ||
| 369 | ;; (treesit-search-subtree node "\\`tag_name\\'" nil nil 2) | ||
| 370 | t))) | ||
| 371 | |||
| 372 | ;; In order to support `which-fuction-mode' we should define | ||
| 373 | ;; a function that return the defun name. | ||
| 374 | ;; In a multilingual treesit mode, this can be implemented simply by | ||
| 375 | ;; calling language-specific functions. | ||
| 376 | (defun mhtml-ts-mode--defun-name (node) | ||
| 377 | "Return the defun name of NODE. | ||
| 378 | Return nil if there is no name or if NODE is not a defun node." | ||
| 379 | (let ((html-name (html-ts-mode--defun-name node)) | ||
| 380 | (js-name (js--treesit-defun-name node)) | ||
| 381 | (css-name (css--treesit-defun-name node))) | ||
| 382 | (cond | ||
| 383 | (html-name html-name) | ||
| 384 | (js-name js-name) | ||
| 385 | (css-name css-name)))) | ||
| 386 | |||
| 387 | ;;; Flymake integration | ||
| 388 | |||
| 389 | (defvar-local mhtml-ts-mode--flymake-process nil | ||
| 390 | "Store the Flymake process.") | ||
| 391 | |||
| 392 | (defun mhtml-ts-mode-flymake-mhtml (report-fn &rest _args) | ||
| 393 | "MHTML backend for Flymake. | ||
| 394 | Calls REPORT-FN directly. Requires tidy." | ||
| 395 | (when (process-live-p mhtml-ts-mode--flymake-process) | ||
| 396 | (kill-process mhtml-ts-mode--flymake-process)) | ||
| 397 | (let ((tidy (executable-find "tidy")) | ||
| 398 | (source (current-buffer)) | ||
| 399 | (diagnostics-pattern (eval-when-compile | ||
| 400 | (rx bol | ||
| 401 | "line " (group (+ num)) ;; :1 line | ||
| 402 | " column " (group (+ num)) ;; :2 column | ||
| 403 | " - " (group (+? nonl)) ;; :3 type | ||
| 404 | ": " (group (+? nonl)) ;; :4 msg | ||
| 405 | eol)))) | ||
| 406 | (if (not tidy) | ||
| 407 | (error "Unable to find tidy command") | ||
| 408 | (save-restriction | ||
| 409 | (widen) | ||
| 410 | (setq mhtml-ts-mode--flymake-process | ||
| 411 | (make-process | ||
| 412 | :name "mhtml-ts-mode-flymake" | ||
| 413 | :noquery t | ||
| 414 | :connection-type 'pipe | ||
| 415 | :buffer (generate-new-buffer "*mhtml-ts-mode-flymake*") | ||
| 416 | :command `(,tidy "--gnu-emacs" "yes" "-e" "-q") | ||
| 417 | :sentinel | ||
| 418 | (lambda (proc _event) | ||
| 419 | (when (eq 'exit (process-status proc)) | ||
| 420 | (unwind-protect | ||
| 421 | (if (with-current-buffer source | ||
| 422 | (eq proc mhtml-ts-mode--flymake-process)) | ||
| 423 | (with-current-buffer (process-buffer proc) | ||
| 424 | (goto-char (point-min)) | ||
| 425 | (let (diags) | ||
| 426 | (while (search-forward-regexp diagnostics-pattern nil t) | ||
| 427 | (let* ((pos | ||
| 428 | (flymake-diag-region | ||
| 429 | source | ||
| 430 | (string-to-number (match-string 1)) | ||
| 431 | (string-to-number (match-string 2)))) ;; line and column | ||
| 432 | (type (cond ((equal (match-string 3) "Warning") :warning) | ||
| 433 | ((equal (match-string 3) "Error") :error))) ;; type of message | ||
| 434 | (msg (match-string 4))) ;; message | ||
| 435 | (push (flymake-make-diagnostic source (car pos) (cdr pos) type msg) | ||
| 436 | diags))) | ||
| 437 | (funcall report-fn diags))) | ||
| 438 | (flymake-log :warning "Canceling obsolete check %s" proc)) | ||
| 439 | (kill-buffer (process-buffer proc))))))) | ||
| 440 | (process-send-region mhtml-ts-mode--flymake-process (point-min) (point-max)) | ||
| 441 | (process-send-eof mhtml-ts-mode--flymake-process))))) | ||
| 442 | |||
| 443 | (define-derived-mode mhtml-ts-mode html-ts-mode | ||
| 444 | '("HTML+" (:eval (let ((lang (mhtml-ts-mode--language-at-point (point)))) | ||
| 445 | (cond ((eq lang 'html) "") | ||
| 446 | ((eq lang 'javascript) "JS") | ||
| 447 | ((eq lang 'css) "CSS"))))) | ||
| 448 | "Major mode for editing HTML with embedded JavaScript and CSS. | ||
| 449 | Powered by tree-sitter." | ||
| 450 | (if (not (and | ||
| 451 | (treesit-ready-p 'html) | ||
| 452 | (treesit-ready-p 'javascript) | ||
| 453 | (treesit-ready-p 'css))) | ||
| 454 | (error "Tree-sitter parsers for HTML isn't available. You can | ||
| 455 | install the parsers with M-x `mhtml-ts-mode-install-parsers'") | ||
| 456 | |||
| 457 | ;; When an language is embedded, you should initialize some variable | ||
| 458 | ;; just like it's done in the original mode. | ||
| 459 | |||
| 460 | ;; Comment. | ||
| 461 | ;; indenting settings for js-ts-mode. | ||
| 462 | (c-ts-common-comment-setup) | ||
| 463 | (setq-local comment-multi-line t) | ||
| 464 | |||
| 465 | ;; Font-lock. | ||
| 466 | |||
| 467 | ;; There are two ways to handle embedded code: | ||
| 468 | ;; 1. Use a single parser for all the embedded code in the buffer. In | ||
| 469 | ;; this case, the embedded code blocks are concatenated together and are | ||
| 470 | ;; seen as a single continuous document to the parser. | ||
| 471 | ;; 2. Each embedded code block gets its own parser. Each parser only sees | ||
| 472 | ;; that particular code block. | ||
| 473 | |||
| 474 | ;; If you go with 2 for a language, the local parsers are created and | ||
| 475 | ;; destroyed automatically by Emacs. So don't create a global parser for | ||
| 476 | ;; that embedded language here. | ||
| 477 | |||
| 478 | ;; Create the parsers, only the global ones. | ||
| 479 | ;; jsdoc is a local parser, don't create a parser for it. | ||
| 480 | (treesit-parser-create 'css) | ||
| 481 | (treesit-parser-create 'javascript) | ||
| 482 | |||
| 483 | ;; Multi-language modes must set the primary parser. | ||
| 484 | (setq-local treesit-primary-parser (treesit-parser-create 'html)) | ||
| 485 | |||
| 486 | (setq-local treesit-range-settings | ||
| 487 | (treesit-range-rules | ||
| 488 | :embed 'javascript | ||
| 489 | :host 'html | ||
| 490 | '((script_element | ||
| 491 | (start_tag (tag_name)) | ||
| 492 | (raw_text) @cap)) | ||
| 493 | |||
| 494 | ;; Another rule could be added that when it matches an | ||
| 495 | ;; attribute_value that has as its parent an | ||
| 496 | ;; attribute_name "style" it captures it and then | ||
| 497 | ;; passes it to the css parser. | ||
| 498 | :embed 'css | ||
| 499 | :host 'html | ||
| 500 | '((style_element | ||
| 501 | (start_tag (tag_name)) | ||
| 502 | (raw_text) @cap)))) | ||
| 503 | |||
| 504 | ;; jsdoc is not mandatory for js-ts-mode, so we respect this by | ||
| 505 | ;; adding jsdoc range rules only when jsdoc is available. | ||
| 506 | (when (treesit-ready-p 'jsdoc t) | ||
| 507 | (setq-local treesit-range-settings | ||
| 508 | (append treesit-range-settings | ||
| 509 | (treesit-range-rules | ||
| 510 | :embed 'jsdoc | ||
| 511 | :host 'javascript | ||
| 512 | :local t | ||
| 513 | `(((comment) @cap | ||
| 514 | (:match ,js--treesit-jsdoc-beginning-regexp @cap)))))) | ||
| 515 | (setq-local c-ts-common--comment-regexp | ||
| 516 | js--treesit-jsdoc-comment-regexp)) | ||
| 517 | |||
| 518 | |||
| 519 | ;; Many treesit fuctions need to know the language at-point. | ||
| 520 | ;; So you should define such a function. | ||
| 521 | (setq-local treesit-language-at-point-function #'mhtml-ts-mode--language-at-point) | ||
| 522 | (setq-local prettify-symbols-alist mhtml-ts-mode--prettify-symbols-alist) | ||
| 523 | |||
| 524 | ;; Indent. | ||
| 525 | |||
| 526 | ;; Since `mhtml-ts-mode' inherits indentation rules from `html-ts-mode', `js-ts-mode' | ||
| 527 | ;; and `css-ts-mode', if you want to change the offset you have to act on the | ||
| 528 | ;; *-offset variables defined for those languages. | ||
| 529 | |||
| 530 | ;; JavaScript and CSS must be indented relative to their code block. | ||
| 531 | ;; This is done by inserting a special rule before the normal | ||
| 532 | ;; indentation rules of these languages. | ||
| 533 | ;; The value of `mhtml-ts-mode--js-css-indent-offset' changes based on | ||
| 534 | ;; `mhtml-ts-mode-tag-relative-indent' and can be used to indent | ||
| 535 | ;; JavaScript and CSS code relative to the HTML that contains them, | ||
| 536 | ;; just like in mhtml-mode. | ||
| 537 | (setq-local treesit-simple-indent-rules mhtml-ts-mode--treesit-indent-rules) | ||
| 538 | |||
| 539 | ;; Navigation. | ||
| 540 | |||
| 541 | ;; This is for which-function-mode. | ||
| 542 | ;; Since mhtml-ts-mode is derived from html-ts-mode, which sets | ||
| 543 | ;; the value of `treesit-defun-type-regexp', you have to reset it to nil | ||
| 544 | ;; otherwise `imenu' and `which-function-mode' will not work. | ||
| 545 | (setq-local treesit-defun-type-regexp nil) | ||
| 546 | |||
| 547 | ;; This is for finding defun name, it's used by IMenu as default | ||
| 548 | ;; function no specific functions are defined. | ||
| 549 | (setq-local treesit-defun-name-function #'mhtml-ts-mode--defun-name) | ||
| 550 | |||
| 551 | ;; Define what are 'thing' for treesit. | ||
| 552 | ;; 'Thing' is a symbol representing the thing, like `defun', `sexp', or | ||
| 553 | ;; `sentence'. | ||
| 554 | ;; As an alternative, if you want just defun, you can define a `treesit-defun-type-regexp'. | ||
| 555 | (setq-local treesit-thing-settings mhtml-ts-mode--treesit-thing-settings) | ||
| 556 | |||
| 557 | ;; Font-lock. | ||
| 558 | |||
| 559 | ;; In a multi-language scenario, font lock settings are usually a | ||
| 560 | ;; concatenation of language rules. As you can see, it is possible | ||
| 561 | ;; to extend/modify the default rule or use a different set of | ||
| 562 | ;; rules. See `php-ts-mode--custom-html-font-lock-settings' for more | ||
| 563 | ;; advanced usage. | ||
| 564 | (setq-local treesit-font-lock-settings mhtml-ts-mode--treesit-font-lock-settings) | ||
| 565 | |||
| 566 | ;; Tells treesit the list of features to fontify. | ||
| 567 | (setq-local treesit-font-lock-feature-list mhtml-ts-mode--treesit-font-lock-feature-list) | ||
| 568 | |||
| 569 | ;; Imenu | ||
| 570 | |||
| 571 | ;; Setup Imenu: if no function is specified, try to find an object | ||
| 572 | ;; using `treesit-defun-name-function'. | ||
| 573 | (setq-local treesit-aggregated-simple-imenu-settings | ||
| 574 | mhtml-ts-mode--treesit-aggregated-simple-imenu-settings) | ||
| 575 | |||
| 576 | ;; (setq-local treesit-outline-predicate nil) | ||
| 577 | |||
| 578 | (treesit-major-mode-setup) | ||
| 579 | |||
| 580 | ;; This is sort of a prog-mode as well as a text mode. | ||
| 581 | (run-mode-hooks 'prog-mode-hook) | ||
| 582 | |||
| 583 | ;; Flymake | ||
| 584 | (add-hook 'flymake-diagnostic-functions #'mhtml-ts-mode-flymake-mhtml nil 'local))) | ||
| 585 | |||
| 586 | ;; Add nome extra parents. | ||
| 587 | (derived-mode-add-parents 'mhtml-ts-mode '(css-mode js-mode)) | ||
| 588 | |||
| 589 | (when (and (treesit-ready-p 'html) (treesit-ready-p 'javascript) (treesit-ready-p 'css)) | ||
| 590 | (add-to-list | ||
| 591 | 'auto-mode-alist '("\\.[sx]?html?\\(\\.[a-zA-Z_]+\\)?\\'" . mhtml-ts-mode))) | ||
| 592 | |||
| 593 | (provide 'mhtml-ts-mode) | ||
| 594 | ;;; mhtml-ts-mode.el ends here | ||
diff --git a/lisp/treesit.el b/lisp/treesit.el index b923545d50c..30fe3798bd9 100644 --- a/lisp/treesit.el +++ b/lisp/treesit.el | |||
| @@ -1317,6 +1317,40 @@ and leave settings for other languages unchanged." | |||
| 1317 | ((memq feature remove-list) nil) | 1317 | ((memq feature remove-list) nil) |
| 1318 | (t current-value)))))) | 1318 | (t current-value)))))) |
| 1319 | 1319 | ||
| 1320 | (defun treesit-merge-font-lock-feature-list (features-list-1 features-list-2) | ||
| 1321 | "Merge two tree-sitter font lock feature lists. | ||
| 1322 | Returns a new font lock feature list with no duplicates in the same level. | ||
| 1323 | It can be used to merge font lock feature lists in a multi-language major mode. | ||
| 1324 | FEATURES-LIST-1 and FEATURES-LIST-2 are list of lists of feature symbols." | ||
| 1325 | (let ((result nil) | ||
| 1326 | (features-1 (car features-list-1)) | ||
| 1327 | (features-2 (car features-list-2))) | ||
| 1328 | (while (or features-1 features-2) | ||
| 1329 | (cond | ||
| 1330 | ((and features-1 (not features-2)) (push features-1 result)) | ||
| 1331 | ((and (not features-1) features-2) (push features-2 result)) | ||
| 1332 | ((and features-1 features-2) (push (cl-union features-1 features-2) result))) | ||
| 1333 | (setq features-list-1 (cdr features-list-1) | ||
| 1334 | features-list-2 (cdr features-list-2) | ||
| 1335 | features-1 (car features-list-1) | ||
| 1336 | features-2 (car features-list-2))) | ||
| 1337 | (nreverse result))) | ||
| 1338 | |||
| 1339 | (defun treesit-replace-font-lock-feature-settings (new-settings settings) | ||
| 1340 | "Replaces :feature in SETTINGS with :feature from NEW-SETTINGS. | ||
| 1341 | Both SETTINGS and NEW-SETTINGS must be a value suitable for | ||
| 1342 | `treesit-font-lock-settings'. | ||
| 1343 | Return a value suitable for `treesit-font-lock-settings'" | ||
| 1344 | (let ((result nil)) | ||
| 1345 | (dolist (new-setting new-settings) | ||
| 1346 | (let ((new-feature (treesit-font-lock-setting-feature new-setting))) | ||
| 1347 | (dolist (setting settings) | ||
| 1348 | (let ((feature (treesit-font-lock-setting-feature setting))) | ||
| 1349 | (if (eq new-feature feature) | ||
| 1350 | (push new-setting result) | ||
| 1351 | (push setting result)))))) | ||
| 1352 | (nreverse result))) | ||
| 1353 | |||
| 1320 | (defun treesit-add-font-lock-rules (rules &optional how feature) | 1354 | (defun treesit-add-font-lock-rules (rules &optional how feature) |
| 1321 | "Add font-lock RULES to the current buffer. | 1355 | "Add font-lock RULES to the current buffer. |
| 1322 | 1356 | ||
| @@ -2506,6 +2540,40 @@ end of existing rules." | |||
| 2506 | (append rules existing-rules))))) | 2540 | (append rules existing-rules))))) |
| 2507 | (setf (alist-get language treesit-simple-indent-rules) new-rules))) | 2541 | (setf (alist-get language treesit-simple-indent-rules) new-rules))) |
| 2508 | 2542 | ||
| 2543 | (defun treesit-modify-indent-rules (lang new-rules rules &optional how) | ||
| 2544 | "Modify a copy of RULES using NEW-RULES. | ||
| 2545 | As default replace rules with the same anchor. | ||
| 2546 | When HOW is :prepend NEW-RULES are prepend to RULES, when | ||
| 2547 | :append NEW-RULES are appended to RULES, when :replace (the default) | ||
| 2548 | NEW-RULES replace rule in RULES which the same anchor." | ||
| 2549 | (cond | ||
| 2550 | ((not (alist-get lang rules)) | ||
| 2551 | (error "No rules for language %s in RULES" lang)) | ||
| 2552 | ((not (alist-get lang new-rules)) | ||
| 2553 | (error "No rules for language %s in NEW-RULES" lang)) | ||
| 2554 | (t (let* ((copy-of-rules (copy-tree js--treesit-indent-rules)) | ||
| 2555 | (lang-rules (alist-get lang copy-of-rules)) | ||
| 2556 | (lang-new-rules (alist-get lang new-rules))) | ||
| 2557 | (cond | ||
| 2558 | ((eq how :prepend) | ||
| 2559 | (setf (alist-get lang copy-of-rules) | ||
| 2560 | (append lang-new-rules lang-rules))) | ||
| 2561 | ((eq how :append) | ||
| 2562 | (setf (alist-get lang copy-of-rules) | ||
| 2563 | (append lang-rules lang-new-rules))) | ||
| 2564 | ((or (eq how :replace) t) | ||
| 2565 | (let ((tail-new-rules lang-new-rules) | ||
| 2566 | (tail-rules lang-rules) | ||
| 2567 | (new-rule nil) | ||
| 2568 | (rule nil)) | ||
| 2569 | (while (setq new-rule (car tail-new-rules)) | ||
| 2570 | (while (setq rule (car tail-rules)) | ||
| 2571 | (when (equal (nth 0 new-rule) (nth 0 rule)) | ||
| 2572 | (setf (car tail-rules) new-rule)) | ||
| 2573 | (setq tail-rules (cdr tail-rules))) | ||
| 2574 | (setq tail-new-rules (cdr tail-new-rules)))))) | ||
| 2575 | copy-of-rules)))) | ||
| 2576 | |||
| 2509 | ;;; Search | 2577 | ;;; Search |
| 2510 | 2578 | ||
| 2511 | (defun treesit-search-forward-goto | 2579 | (defun treesit-search-forward-goto |