Start work on zsh-ts-mode

This commit is contained in:
2026-04-12 15:07:38 -07:00
parent e689d7dc87
commit a32f88aabc
2 changed files with 216 additions and 1 deletions

214
elisp/zsh-ts-mode.el Normal file
View File

@@ -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