学习 Tree-sitter 的过程中感觉可以开发一些辅助工具提升编辑效率,尤其是针对前端 jsx 开发,于是便有了以下尝试。小有所成,忍不住要跟大家分享一下, 抛砖引玉也希望能得到更多灵感继续完善。
初学时以勇哥 TreeSit API 详解 这篇文章启蒙,得到了很多帮助。
以下功能都是基于 Emacs 29 自带的 tsx-ts-mode
开发:
首先从一个 helper 开始
(defun jsx/kill-region-and-goto-start (start end)
"Kill the region between START and END, and move the point to START."
(kill-region start end)
(goto-char start))
这段代码会用在很多地方。
(defun jsx/empty-element ()
"Empty the content of the JSX element containing the point."
(interactive)
(when-let* ((node (treesit-node-at (point)))
(element (treesit-parent-until node (lambda (n)
(string= (treesit-node-type n) "jsx_element"))))
(opening-node (treesit-node-child element 0))
(closing-node (treesit-node-child element -1))
(start (treesit-node-end opening-node))
(end (treesit-node-start closing-node)))
(jsx/kill-region-and-goto-start start end)))
清空 tag,类似 Vim 的 cit
指令。
(defun jsx/raise-element ()
"Raise the JSX element containing the point."
(interactive)
(when-let* ((node (treesit-node-at (point)))
(element (treesit-parent-until node (lambda (n)
(member (treesit-node-type n)
'("jsx_element"
"jsx_self_closing_element")))))
(element-text (treesit-node-text element t))
(element-parent (treesit-parent-until element (lambda (n)
(string= (treesit-node-type n) "jsx_element"))))
(start (treesit-node-start element-parent))
(end (treesit-node-end element-parent)))
(delete-region start end)
(insert element-text)
(indent-region start (point))))
提升 tag,类似 lispy 的 raises
功能
(defun jsx/delete-until ()
"Delete up to the end of the parent closing."
(interactive)
(when-let* ((node (treesit-node-at (point)))
(parent (treesit-parent-until node (lambda (n)
(member (treesit-node-type n)
'("array"
"string"
"arguments"
"named_imports"
"object_pattern"
"formal_parameters"
"jsx_expression"
"jsx_opening_element")))))
(end (1- (treesit-node-end parent))))
(delete-region (point) end)))
同样借鉴了 Vim 的 ct
的指令,删除从光标处到 "
, )
, ]
, }
, or >
根据 treesitter 节点判断, 就不用指明是哪个结尾符号了
(defun jsx/kill-attribute-value ()
"Kill the value of the JSX attribute containing the point."
(interactive)
(when-let* ((node (treesit-node-at (point)))
(attribute (treesit-parent-until node (lambda (n)
(string= (treesit-node-type n) "jsx_attribute"))))
(value (treesit-node-child attribute -1)))
(let ((start (1+ (treesit-node-start value)))
(end (1- (treesit-node-end value))))
(jsx/kill-region-and-goto-start start end))))
快速删除 attriubte value,适合修改组件的 props
(defun jsx/declaration-to-if-statement ()
"Convert the variable declaration at point to an if statement."
(interactive)
(when-let* ((node (treesit-node-at (point)))
(parent (treesit-parent-until node (lambda (n)
(string= (treesit-node-type n) "lexical_declaration"))))
(value (treesit-search-subtree parent (lambda (n)
(string= (treesit-node-type n) "call_expression"))))
(value-text (treesit-node-text value t))
(start (treesit-node-start parent))
(end (treesit-node-end parent)))
(delete-region start end)
(insert (format "if (%s) {\n\n}" value-text))
(indent-region start (point))
(forward-line -1)
(indent-for-tab-command)))
偶尔要用到的功能,把变量声明编变成 if 声明,变量值作为判断条件
(defun jsx/kill-by-node-type ()
"[Experimental] Kill the node or region based on the node type at point."
(interactive)
(let* ((node (treesit-node-at (point)))
(node-text (treesit-node-text node t)))
(pcase node-text
((or "." ":" ";" "<" "</" ">" "(" ")" "[" "]" "{" "}")
(call-interactively 'backward-kill-word))
((or "'" "\"" "`")
(let* ((parent-node (treesit-node-parent node))
(start (1+ (treesit-node-start parent-node)))
(end (1- (treesit-node-end parent-node))))
(jsx/kill-region-and-goto-start start end)))
(","
(when-let* ((prev-node (treesit-node-prev-sibling node))
(start (treesit-node-start prev-node))
(end (treesit-node-end node))
(space-prefix (string= (buffer-substring-no-properties (1- start) start) " ")))
(jsx/kill-region-and-goto-start (if space-prefix (1- start) start) end)))
(_ (kill-region (treesit-node-start node) (treesit-node-end node))))))
快速删除光标所在的语法节点,使用的过程中发现遇到标点符号后的表现不如预期,所以标点符号继续用 backward-kill-word
符合直觉。
以上只是一部分函数,还有很多其他函数,下面是在用的按键绑定。整体还在不断完善修改中,具体代码在 emacs.d/lisp/init-web.el at master · P233/emacs.d · GitHub
(add-hook 'tsx-ts-mode-hook (lambda ()
(define-key tsx-ts-mode-map (kbd "C-<backspace>") 'jsx/kill-by-node-type)
(define-key tsx-ts-mode-map (kbd "C-c C-k") 'jsx/kill-block)
(define-key tsx-ts-mode-map (kbd "C-c C-w") 'jsx/copy-block)
(define-key tsx-ts-mode-map (kbd "C-c C-x") 'jsx/duplicate-block)
(define-key tsx-ts-mode-map (kbd "C-c C-SPC") 'jsx/select-block)
(define-key tsx-ts-mode-map (kbd "C-c C-u") 'jsx/delete-until)
(define-key tsx-ts-mode-map (kbd "C-c C-;") 'jsx/comment-uncomment-block)
(define-key tsx-ts-mode-map (kbd "C-c C-t C-e") 'jsx/empty-element)
(define-key tsx-ts-mode-map (kbd "C-c C-t C-r") 'jsx/raise-element)
(define-key tsx-ts-mode-map (kbd "C-c C-t C-p") 'jsx/move-to-opening-tag)
(define-key tsx-ts-mode-map (kbd "C-c C-t C-n") 'jsx/move-to-closing-tag)
(define-key tsx-ts-mode-map (kbd "C-c C-a C-k") 'jsx/kill-attribute)
(define-key tsx-ts-mode-map (kbd "C-c C-a C-w") 'jsx/copy-attribute)
(define-key tsx-ts-mode-map (kbd "C-c C-a C-v") 'jsx/kill-attribute-value)
(define-key tsx-ts-mode-map (kbd "C-c C-a C-p") 'jsx/move-to-prev-attribute)
(define-key tsx-ts-mode-map (kbd "C-c C-a C-n") 'jsx/move-to-next-attribute)
(define-key tsx-ts-mode-map (kbd "C-c C-s") 'my/open-or-create-associated-scss-file)))
抛砖引玉,请多指教