493 lines
19 KiB
EmacsLisp
493 lines
19 KiB
EmacsLisp
;;; zsh-ts-mode.el --- TreeSitter based mode for editing Zsh sripts -*- lexical-binding: t; -*-
|
|
;;; Commentary:
|
|
;;; Code:
|
|
|
|
(require 'cl-lib)
|
|
(require 'eieio)
|
|
(require 'sh-script)
|
|
(require 'treesit)
|
|
(require 'rx)
|
|
|
|
;;; #################
|
|
;;; # Fontification #
|
|
;;; #################
|
|
(defvar zsh-ts-mode--previous-let-arg-query
|
|
(treesit-query-compile 'zsh '((word) @arg))
|
|
"Query used in `zsh-ts-mode--fontify-let-arg'.")
|
|
|
|
(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)
|
|
(cond
|
|
((re-search-forward (rx (or "[" "=")) ne t)
|
|
(put-text-property ns (1- (point)) 'face
|
|
'font-lock-variable-name-face)
|
|
(unless (eql (char-before) ?\[)
|
|
(ignore-errors
|
|
(with-restriction (point) ne
|
|
(when (looking-at (rx (? "-") (+ (any (?0 . ?9)))))
|
|
(put-text-property (point-min) (point-max) 'face
|
|
'font-lock-number-face))))))
|
|
;; the parser is broken and parses:
|
|
;; let ans=${?}
|
|
;; as
|
|
;; command:let word:ans= word:${}
|
|
((null (treesit-query-capture (treesit-node-parent node)
|
|
zsh-ts-mode--previous-let-arg-query
|
|
(1- ns) ns))
|
|
(font-lock-append-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 nil nil t))
|
|
(let ((start (treesit-node-start child))
|
|
(end (treesit-node-end 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.")
|
|
|
|
(defvar zsh-ts-mode--treesit-keywords
|
|
(append sh-mode--treesit-keywords '("until"))
|
|
"List of keywords defined in the grammar.")
|
|
|
|
;; 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_pattern (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
|
|
[ ,@zsh-ts-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)))
|
|
|
|
;;; #########
|
|
;;; # IMenu #
|
|
;;; #########
|
|
(defvar zsh-ts-mode--defun-name-query
|
|
(treesit-query-compile 'zsh '((function_definition
|
|
name: ((word) @name))))
|
|
"Query used by `zsh-ts-mode--defun-name'.")
|
|
|
|
(defun zsh-ts-mode--defun-name (node)
|
|
"If NODE is a defun node, return its name.
|
|
This behaves according to `treesit-defun-name-function'."
|
|
(when-let ((node)
|
|
(res (treesit-query-capture node zsh-ts-mode--defun-name-query
|
|
nil nil t)))
|
|
(treesit-node-text (car res) t)))
|
|
|
|
(defun zsh-ts-mode--imenu-predicate (node)
|
|
"Predicate function for `zsh-ts-mode--simple-imenu-settings'.
|
|
Return non-nil if NODE is not an anonymous function."
|
|
(zsh-ts-mode--defun-name node))
|
|
|
|
(defvar zsh-ts-mode--simple-imenu-settings
|
|
'(("Function" "function_definition" zsh-ts-mode--imenu-predicate nil))
|
|
"`zsh-ts-mode' settings for `treesit-simple-imenu-settings'.")
|
|
|
|
;;; ########
|
|
;;; # xref #
|
|
;;; ########
|
|
(defun zsh-ts-mode--xref-backend ()
|
|
"Return `zsh-ts'."
|
|
'zsh-ts)
|
|
|
|
(defun zsh-ts-mode--not-let-predicate (node)
|
|
"Return nil if NODE is a let node, non-nil otherwise."
|
|
(not (equal (treesit-node-text node) "let")))
|
|
|
|
(defvar zsh-ts-mode--identifier-query
|
|
(treesit-query-compile
|
|
'zsh `((expansion
|
|
name: ([(simple_variable_name)
|
|
(special_variable_name)]
|
|
@var))
|
|
(expansion_with_modifier
|
|
name: ([(simple_variable_name)
|
|
(special_variable_name)]
|
|
@var))
|
|
(variable_ref ([(simple_variable_name)
|
|
(special_variable_name)]
|
|
@var))
|
|
(expansion_pattern
|
|
name: ([(simple_variable_name)
|
|
(special_variable_name)]
|
|
@var))
|
|
((word) @func
|
|
(:pred zsh-ts-mode--not-let-predicate @func))))
|
|
"Query for `zsh-ts-mode' `xref-backend-identifier-at-point'.")
|
|
|
|
(cl-defmethod xref-backend-identifier-at-point ((_backend (eql zsh-ts)))
|
|
"`zsh-ts-mode' implementation for xref's identifier at point function."
|
|
|
|
(or (when (memq 'font-lock-variable-name-face
|
|
(ensure-list (get-text-property (point) 'face)))
|
|
(let ((start (previous-single-property-change (point) 'face))
|
|
(end (next-single-property-change (point) 'face)))
|
|
(propertize
|
|
(buffer-substring-no-properties (if start (1+ start) (point-min))
|
|
(or end (point-max)))
|
|
'type 'variable)))
|
|
(when-let* ((nodes (treesit-query-capture (treesit-buffer-root-node)
|
|
zsh-ts-mode--identifier-query
|
|
(point) (1+ (point)))))
|
|
(if-let* ((var (alist-get 'var nodes)))
|
|
(propertize (treesit-node-text var t) 'type 'variable)
|
|
(when-let* ((func (alist-get 'func nodes)))
|
|
(propertize (treesit-node-text func t) 'type 'function))))))
|
|
|
|
(cl-defmethod xref-backend-identifier-completion-table ((_backend (eql zsh-ts)))
|
|
"Return nil."
|
|
nil)
|
|
|
|
(cl-defmethod xref-backend-identifier-completion-ignore-case
|
|
((_backend (eql zsh-ts)))
|
|
"Return `completion-ignore-case'."
|
|
completion-ignore-case)
|
|
|
|
(defun zsh-ts-mode--treesit-node-summary (node)
|
|
"Return the contents of the line of NODE."
|
|
(save-excursion
|
|
(goto-char (treesit-node-start node))
|
|
(buffer-substring (pos-bol) (pos-eol))))
|
|
|
|
(cl-defstruct zsh-ts-mode--location
|
|
"Xref locaton object for tree-sitter nodes."
|
|
node)
|
|
|
|
(cl-defmethod xref-location-line ((location zsh-ts-mode--location))
|
|
"Return the line number of LOCATION."
|
|
(line-number-at-pos (treesit-node-start
|
|
(zsh-ts-mode--location-node location))))
|
|
|
|
(cl-defmethod xref-location-group ((location zsh-ts-mode--location))
|
|
"Return the file name of the buffer of LOCATION."
|
|
(with-slots (node) location
|
|
(or (buffer-file-name (treesit-node-buffer node))
|
|
"")))
|
|
|
|
(cl-defmethod xref-location-marker ((location zsh-ts-mode--location))
|
|
"Return a marker for LOCATION."
|
|
(let ((node (zsh-ts-mode--location-node location))
|
|
(marker (make-marker)))
|
|
(set-marker marker (treesit-node-start node)
|
|
(treesit-node-buffer node))
|
|
marker))
|
|
|
|
(defun zsh-ts-mode--make-xref-for-node (node)
|
|
"Create an xref for a tree-sitter node NODE."
|
|
(xref-make (zsh-ts-mode--treesit-node-summary node)
|
|
(make-zsh-ts-mode--location :node node)))
|
|
|
|
(defvar zsh-ts-mode--xref-function-definitions-query
|
|
(treesit-query-compile 'zsh '((function_definition
|
|
name: (word) @name)))
|
|
"Function query for `zsh-ts-mode--xref-function-definitions'.")
|
|
|
|
(defun zsh-ts-mode--xref-function-definitions (identifier)
|
|
"Find all definitions of function IDENTIFIER in the current buffer."
|
|
(cl-loop for node in (treesit-query-capture
|
|
(treesit-buffer-root-node)
|
|
zsh-ts-mode--xref-function-definitions-query
|
|
nil nil t)
|
|
when (equal identifier (treesit-node-text node))
|
|
collect (zsh-ts-mode--make-xref-for-node node)))
|
|
|
|
(defvar zsh-ts-mode--xref-variable-definitions-query
|
|
(treesit-query-compile
|
|
'zsh `((variable_assignment name: (variable_name) @non-let)
|
|
(declaration_command argument: (word) @non-let)
|
|
(command name: ((command_name) @let.name
|
|
(:equal @let.name "let"))
|
|
argument: ((word) @let-arg
|
|
(:match ,(rx-to-string '(seq bos (not "-")))
|
|
@let-arg)))))
|
|
"Variable query for `zsh-ts-mode--xref-variable-definitions'.")
|
|
|
|
(defun zsh-ts-mode--xref-variable-definitions (identifier)
|
|
"Find all definitions of variable IDENTIFIER in the current buffer."
|
|
(let (out)
|
|
(dolist (ent (treesit-query-capture
|
|
(treesit-buffer-root-node)
|
|
zsh-ts-mode--xref-variable-definitions-query))
|
|
(cl-destructuring-bind (tag . node) ent
|
|
(cl-case tag
|
|
(non-let
|
|
(when (equal (treesit-node-text node) identifier)
|
|
(push (zsh-ts-mode--make-xref-for-node node) out)))
|
|
(let-arg
|
|
(when (string-match-p (rx bos (literal identifier) (or eos "="))
|
|
(treesit-node-text node))
|
|
(push (zsh-ts-mode--make-xref-for-node node) out))))))
|
|
out))
|
|
|
|
(cl-defmethod xref-backend-definitions ((_backend (eql zsh-ts)) identifier)
|
|
"`zsh-ts-mode' implementation for finding xref definitions for IDENTIFIER."
|
|
(when identifier
|
|
(let ((type (get-text-property 0 'type identifier)))
|
|
(append
|
|
(when (memq type '(nil function))
|
|
(zsh-ts-mode--xref-function-definitions identifier))
|
|
(when (memq type '(nil variable))
|
|
(zsh-ts-mode--xref-variable-definitions identifier))))))
|
|
|
|
(defvar zsh-ts-mode--function-references-query
|
|
(treesit-query-compile
|
|
'zsh '((command name: (command_name (word) @name))))
|
|
"Query used by `zsh-ts-mode--xref-function-references'.")
|
|
|
|
(defun zsh-ts-mode--xref-function-references (identifier)
|
|
"Find all references for the function IDENTIFIER in the current buffer."
|
|
(let ((nodes (treesit-query-capture
|
|
(treesit-buffer-root-node)
|
|
zsh-ts-mode--function-references-query
|
|
nil nil t))
|
|
(ident-copy (copy-sequence identifier))
|
|
(quote-ident (regexp-quote identifier)))
|
|
(set-text-properties 0 (length ident-copy) () ident-copy)
|
|
(cl-loop for node in nodes
|
|
when (save-excursion
|
|
(goto-char (treesit-node-start node))
|
|
(looking-at-p quote-ident))
|
|
collect (zsh-ts-mode--make-xref-for-node node))))
|
|
|
|
(defvar zsh-ts-mode--variable-references-query
|
|
(treesit-query-compile
|
|
'zsh '(([(simple_variable_name) (special_variable_name)] @var)))
|
|
"Query used by `zsh-ts-mode--xref-variable-references'.")
|
|
|
|
(defun zsh-ts-mode--xref-variable-references (identifier)
|
|
"Find all references for the variable IDENTIFIER in the current buffer."
|
|
(let ((ident-copy (copy-sequence identifier))
|
|
(quote-ident (regexp-quote identifier)))
|
|
(set-text-properties 0 (length ident-copy) () ident-copy)
|
|
(mapcan
|
|
#'(lambda (node)
|
|
(when (save-excursion
|
|
(goto-char (treesit-node-start node))
|
|
(looking-at-p quote-ident))
|
|
(list (zsh-ts-mode--make-xref-for-node node))))
|
|
(treesit-query-capture (treesit-buffer-root-node)
|
|
zsh-ts-mode--variable-references-query
|
|
nil nil t))))
|
|
|
|
(cl-defmethod xref-backend-references ((_backend (eql zsh-ts)) identifier)
|
|
"`zsh-ts-mode' implementation for finding xref references for IDENTIFIER."
|
|
(let ((type (get-text-property 0 'type identifier)))
|
|
(append
|
|
;; definitions are references
|
|
(ensure-list (xref-backend-definitions 'zsh-ts identifier))
|
|
(when (memq type '(nil function))
|
|
(zsh-ts-mode--xref-function-references identifier))
|
|
(when (memq type '(nil variable))
|
|
(zsh-ts-mode--xref-variable-references identifier)))))
|
|
|
|
(defvar zsh-ts-mode--xref-apropos-query
|
|
(treesit-query-compile
|
|
'zsh '(((simple_variable_name) @ident)
|
|
((special_variable_name) @ident)
|
|
((variable_name) @ident)
|
|
(command name: (command_name ( (word) @cname (:equal @cname "let")))
|
|
argument: (word) @ident)
|
|
(declaration_command argument: (word) @ident)
|
|
((variable_name) @ident)
|
|
(function_definition name: (word) @ident)))
|
|
"Query used by `xref-backend-apropos'.")
|
|
|
|
(cl-defmethod xref-backend-apropos ((_backend (eql zsh-ts)) pattern)
|
|
"Search though the buffer for PATTERN."
|
|
(let (out)
|
|
(dolist (ent (treesit-query-capture (treesit-buffer-root-node)
|
|
zsh-ts-mode--xref-apropos-query))
|
|
(cl-destructuring-bind (type . node) ent
|
|
(when (and (eq type 'ident)
|
|
(string-match-p pattern (treesit-node-text node)))
|
|
(push (zsh-ts-mode--make-xref-for-node node) out))))
|
|
out))
|
|
|
|
;;;###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-settings zsh-ts-mode--treesit-settings
|
|
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-simple-imenu-settings zsh-ts-mode--simple-imenu-settings
|
|
treesit-defun-name-function #'zsh-ts-mode--defun-name
|
|
imenu-create-index-function #'treesit-simple-imenu)
|
|
(add-hook 'xref-backend-functions #'zsh-ts-mode--xref-backend nil t)
|
|
(treesit-major-mode-setup)))
|
|
|
|
(provide 'zsh-ts-mode)
|
|
;;; zsh-ts-mode.el ends here
|