写了一组针对 jsx 的 tree-sitter 辅助方法

学习 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)))

抛砖引玉,请多指教 :handshake:

7 个赞

改进了一下 jsx/empty-element 函数,现在不止清空 jsx element, 可以以清空 () [] {} 等其中的内容,而且通过 treesitter 节点判断,不用指明要清除那个括号内的内容。(节点列表会随着使用调整)

同时过滤了 jsx attribute 的情况,有 jsx/kill-attribute-value 这个函数处理,优先清空 element

emacs 真的太好玩了, 哈哈哈

1 个赞

很棒啊,我曾经写过类似部分功能的command来选择tag,不过是基于meow的:

;;;以下两个方程返回配对tag的四个位置 < > < >

;;; SEE https://github.com/llemaitre19/jtsx 一个增强jsx体验的mode
;;; jtsx-jsx-mode
(defun jtsx-jsx-element-pos ()
  (when-let* (((jtsx-jsx-context-p))
              (node (jtsx-enclosing-jsx-element-at-point t))
              (open (treesit-node-child-by-field-name node "open_tag"))
              (close (treesit-node-child-by-field-name node "close_tag")))
    (list (treesit-node-start open)
          (treesit-node-end open)
          (treesit-node-start close)
          (treesit-node-end close))))

;;; web-mode
(defun web-mode-element-pos ()
  (when-let* ((ele-begin (web-mode-element-beginning-position))
              (ele-end (web-mode-element-end-position)))
    (list ele-begin
          (1+ (web-mode-tag-end-position ele-begin))
          (web-mode-tag-beginning-position ele-end)
          (1+ ele-end))))

;;; 定义 meow inner of tag
(defun meow--inner-of-tag ()
  (-let [(_ beg end _)
         (cond ((memq major-mode '(jtsx-jsx-mode jtsx-tsx-mode))
                (jtsx-jsx-element-pos))
               ((eq major-mode 'web-mode)
                (web-mode-element-pos)))]
    (and beg end (cons beg end))))


;;; 定义 meow bounds of tag
(defun meow--bounds-of-tag ()
  (-let [(beg _ _ end)
         (cond ((memq major-mode '(jtsx-jsx-mode jtsx-tsx-mode))
                (jtsx-jsx-element-pos))
               ((eq major-mode 'web-mode)
                (web-mode-element-pos)))]
    (and beg end (cons beg end))))

;;; 注册 tag 并绑定
(meow-thing-register 'tag #'meow--inner-of-tag #'meow--bounds-of-tag)
(push (cons ?t 'tag) meow-char-thing-table)
2 个赞

感谢,又学到一招 treesit-node-child-by-field-name :grin:

现在每天都会优化一点点, 说不定也能形成一套独特的工作流