diff --git a/elisp/zsh-ts-mode.el b/elisp/zsh-ts-mode.el new file mode 100644 index 0000000..ecbe8e3 --- /dev/null +++ b/elisp/zsh-ts-mode.el @@ -0,0 +1,214 @@ +;;; zsh-ts-mode.el --- TreeSitter based mode for editing Zsh sripts -*- lexical-binding: t; -*- +;;; Commentary: +;;; Code: + +(require 'sh-script) +(require 'treesit) +(require 'rx) + +(defun zsh-ts-mode--fontify-let-arg (node _override _start _end &rest _r) + "Internal function used from `zsh-ts-mode--treesit-settings'. +Fontify NODE, an argument to a Zsh \"let\" statement." + (let ((ns (treesit-node-start node)) + (ne (treesit-node-end node))) + (save-excursion + (goto-char ns) + (if (re-search-forward (rx (or "=" "[")) ne t) + (put-text-property ns (1- (point)) 'face + 'font-lock-variable-name-face) + (put-text-property ns ne 'face 'font-lock-variable-name-face))))) + +(defvar zsh-ts-mode--arithmetic-variable-query + (treesit-query-compile 'zsh '((word) @name)) + "Query used in `zsh-ts-mode--fontify-arithmetic-variables'.") + +(defun zsh-ts-mode--fontify-arithmetic-variables + (node _override _start _end &rest _r) + "Fontify all arithmetic variables below NODE. +NODE should be an `arithmetic_expansion' node." + (dolist (child (treesit-query-capture + node zsh-ts-mode--arithmetic-variable-query)) + (let ((start (treesit-node-start (cdr child))) + (end (treesit-node-end (cdr child)))) + (font-lock-append-text-property start end 'face + 'font-lock-variable-use-face)))) + +(defun zsh-ts-mode--other-keywords () + "Return a list of `other' keywords for Zsh. +This just appends some more stuff to `sh-mode--treesit-other-keywords'." + (append (sh-mode--treesit-other-keywords) '("let"))) + +(defvar zsh-ts-mode--declaration-commands + '("typeset" "local" "integer" "float" "readonly" "export" "declare") + "Return a list of keywords that declare a parameter in Zsh. +Note that \"let\" isn't included here as the parser treats it as a normal +command.") + +;; from `bash-ts-mode' +(defvar zsh-ts-mode--treesit-settings + (treesit-font-lock-rules + :feature 'comment + :language 'zsh + '((comment) @font-lock-comment-face) + + :feature 'function + :language 'zsh + '((function_definition name: (word) @font-lock-function-name-face)) + + :feature 'string + :language 'zsh + '(((raw_string) @font-lock-string-face) + ((string :anchor ("\"" @font-lock-string-face) + [(string_content) + (arithmetic_expansion :anchor "((")] + :* @font-lock-string-face + ("\"" @font-lock-string-face) :anchor))) + + :feature 'string-interpolation + :language 'zsh + :override t + '((command_substitution :anchor ("$(" @sh-quoted-exec) + (")" @sh-quoted-exec) :anchor) + ;; work around parser error of "(( ))" (no $) being an arithmetic expansion + (string (arithmetic_expansion :anchor ("$((" @sh-quoted-exec) + ("))" @sh-quoted-exec) :anchor))) + + :feature 'variable-reference + :language 'zsh + :override t + '((expansion (simple_variable_name) @font-lock-variable-use-face) + (special_variable_name) @font-lock-variable-use-face + (expansion_with_modifier (simple_variable_name) + @font-lock-variable-use-face) + (expansion_substring (simple_variable_name) @font-lock-variable-use-face) + (variable_ref (simple_variable_name) @font-lock-variable-use-face) + ;; variables in math expansions + ((arithmetic_expansion :anchor "$((") + @zsh-ts-mode--fontify-arithmetic-variables)) + + :feature 'math-function-call + :language 'zsh + :override t + `((arithmetic_call name: (word) @font-lock-function-name-face)) + + :feature 'heredoc + :language 'zsh + '([(heredoc_start) (heredoc_body)] @sh-heredoc) + + :feature 'keyword + :language 'zsh + `(;; keywords + [ ,@sh-mode--treesit-keywords ] @font-lock-keyword-face + ;; reserved words + (command_name + ((word) @font-lock-keyword-face + (:match ,(rx-to-string + `(seq bos + (or ,@(zsh-ts-mode--other-keywords)) + eos)) + @font-lock-keyword-face)))) + + :feature 'command + :language 'zsh + `(;; builtin commands + (command_name + ((word) @font-lock-builtin-face + (:match ,(let ((builtins + (sh-feature sh-builtins))) + (rx-to-string + `(seq bos + (or ,@builtins) + eos))) + @font-lock-builtin-face))) + ;; function/non-builtin command calls + (command_name (word) @font-lock-function-name-face)) + + :feature 'declaration-command + :language 'zsh + ;; the parser doesn't treat "let" as a keyword + `([,@zsh-ts-mode--declaration-commands] @font-lock-keyword-face) + + ;; variable declarations in the form of a=b + ;; e.g. local x=2 + :feature 'decl-equal-variable + :language 'zsh + '((variable_name) @font-lock-variable-name-face) + + ;; variable declarations in the form of a + ;; e.g. local y z + :feature 'decl-solo-variable + :language 'zsh + :override 'prepend + `((declaration_command + argument: ((_) @font-lock-variable-name-face + (:match ,(rx-to-string '(seq bos (not "-"))) + @font-lock-variable-name-face)))) + + + ;; let variable declarations (handled differently by the parser) + :feature 'decl-let-variable + :language 'zsh + :override 'prepend + `((command + (command_name ((word) @ignore + (:equal "let" @ignore))) + argument: ((_) @zsh-ts-mode--fontify-let-arg + (:match ,(rx-to-string '(seq bos (not "-"))) + @zsh-ts-mode--fontify-let-arg)))) + + :feature 'for-variable + :language 'zsh + `("for" variable: ((simple_variable_name) @font-lock-variable-name-face)) + + :feature 'constant + :language 'zsh + '((case_item value: (word) @font-lock-constant-face) + (file_descriptor) @font-lock-constant-face) + + :feature 'operator + :language 'zsh + `([,@sh-mode--treesit-operators] @font-lock-operator-face) + + :feature 'number + :language 'zsh + `(((word) @font-lock-number-face + (:match "\\`[0-9]+\\'" @font-lock-number-face)) + ((number) @font-lock-number-face)) + + :feature 'bracket + :language 'zsh + '((["(" ")" "((" "))" "[" "]" "[[" "]]" "{" "}"]) @font-lock-bracket-face) + + :feature 'delimiter + :language 'zsh + '(([";" ";;"]) @font-lock-delimiter-face) + + :feature 'misc-punctuation + :language 'zsh + '((["$"]) @font-lock-misc-punctuation-face))) + +;;;###autoload +(define-derived-mode zsh-ts-mode sh-base-mode "Zsh" + ;; This function is based mostly on `bash-ts-mode'. + "Major mode for editing Zsh shell scripts. +This mode automatically falls back to `sh-mode' if the buffer is +not written in Zsh." + :syntax-table sh-mode-syntax-table + (when (treesit-ready-p 'zsh) + (sh-set-shell "zsh") + (add-hook 'hack-local-variables-hook + #'sh-after-hack-local-variables nil t) + (treesit-parser-create 'zsh) + (setq-local treesit-font-lock-feature-list + '((comment function) + (command declaration-command keyword string math-function-call) + (constant heredoc number string-interpolation + decl-equal-variable decl-solo-variable decl-let-variable + for-variable variable-reference) + (bracket delimiter misc-punctuation operator))) + (setq-local treesit-font-lock-settings + zsh-ts-mode--treesit-settings) + (treesit-major-mode-setup))) + +(provide 'zsh-ts-mode) +;;; zsh-ts-mode.el ends here diff --git a/init.el b/init.el index eedfff0..b31cbdb 100644 --- a/init.el +++ b/init.el @@ -286,7 +286,8 @@ This is a single element list with that element being the file's trusted status. (blueprint "https://github.com/huanie/tree-sitter-blueprint") (kdl "https://github.com/tree-sitter-grammars/tree-sitter-kdl") (php "https://github.com/tree-sitter/tree-sitter-php") - (haskell "https://github.com/tree-sitter/tree-sitter-haskell"))) + (haskell "https://github.com/tree-sitter/tree-sitter-haskell") + (zsh "https://github.com/georgeharker/tree-sitter-zsh.git"))) ;; Tree sitter major mode conversions (dolist (ent '((c-mode . c-ts-mode) (c++-mode . c++-ts-mode)