215 lines
7.4 KiB
EmacsLisp
215 lines
7.4 KiB
EmacsLisp
;;; 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
|