Logo

dev-resources.site

for different kinds of informations.

Mastering JSX Editing in Emacs with Tree-sitter

Published at
5/16/2024
Categories
emacs
jsx
treesitter
Author
p233
Categories
3 categories in total
emacs
open
jsx
open
treesitter
open
Author
4 person written this
p233
open
Mastering JSX Editing in Emacs with Tree-sitter

Emacs 29 introduced built-in support for Tree-sitter, a powerful tool that revolutionizes syntax highlighting and editing. Tree-sitter constructs a concrete syntax tree for source code and efficiently updates it as modifications are made, offering significant improvements in performance and accuracy compared to traditional regular expression parsing.

While exploring Tree-sitter, I discovered its vast potential for enhancing the editing experience. I started by focusing on the tsx-ts-mode, which I use most frequently, and created a collection of helpful functions to boost productivity. In this post, I'll share these functions and invite you to contribute your own insights and improvements.

To better understand Tree-sitter, refer to the Emacs Tree-sitter documentation. Chinese readers can also check out γ€ŠTreeSit API 详解》. Additionally, you can explore the Tree-sitter structure interactively using M-x treesit-explore-mode and M-x treesit-inspect-mode.

To help you quickly grasp the essentials, here are some of the most commonly used Tree-sitter APIs covered in this post:

  • treesit-node-at: Get the syntax node at the current point.
  • treesit-node-type: Get the node's type as a string.
  • treesit-node-text: Get the node's text content as a string.
  • treesit-parent-until: Traverse up the tree to find a parent node matching a condition.
  • treesit-search-subtree: Search the node's subtree for a descendant matching a condition.
  • treesit-node-child: Get a node's child at a specific index.
  • treesit-node-prev-sibling: Get the node's previous sibling.
  • treesit-node-next-sibling: Get the node's next sibling.
  • treesit-node-start: Get the node's starting buffer position.
  • treesit-node-end: Get the node's ending buffer position.

Now, let's dive into some practical editing functions implemented using Tree-sitter. Keep in mind that all the functions are specific to the tree-sitter-typescript parser and only work in the Emacs 29 built-in tsx-ts-mode.

(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))
Enter fullscreen mode Exit fullscreen mode

This utility function deletes a specified region and moves the cursor to the start of that region. It serves as a helper function used in other examples.


(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)))
Enter fullscreen mode Exit fullscreen mode

Inspired by the Vim cit operation, this function finds the JSX element (tag) enclosing the current point, removes its content while preserving the opening and closing tags.


(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))))
Enter fullscreen mode Exit fullscreen mode

Drawing inspiration from the Lispy raises function, this function moves the current JSX tag one level up in the tree hierarchy and removes the original parent tag.


(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)))
Enter fullscreen mode Exit fullscreen mode

Akin to the Vim ct command, this function automatically deletes from the current point up to the ending character, such as ", ), ], }, or >. Unlike Vim, you don't need to manually specify the matching end character.


(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))))
Enter fullscreen mode Exit fullscreen mode

This convenient function clears the value of the JSX attribute containing the point.


(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)))
Enter fullscreen mode Exit fullscreen mode

Although rarely used, this function can be very handy in specific scenarios. It converts a variable declaration into an if statement, using the variable's value as the condition.


(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))))))
Enter fullscreen mode Exit fullscreen mode

This experimental function aggressively deletes the current Tree-sitter node under the point. However, for punctuation characters, it employs backward-kill-word to maintain consistent deletion behavior.

Finally, let's assign these functions to some handy keybindings. The example functions presented here are just a fraction of the available functions. Please note that all functions are still under active development. You can find the most up-to-date code in my .emacs.d repository. If you have any ideas for improvement or suggestions, I'd be delighted to hear from you!

(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)))
Enter fullscreen mode Exit fullscreen mode

By leveraging the power of Tree-sitter and these custom functions, you can significantly enhance your TypeScript and JSX editing experience in Emacs. Experiment with these functions, adapt them to your workflow, and share your own innovations with the community. Happy coding!

emacs Article's
30 articles in total
Favicon
Emacs 2024 Changes
Favicon
emacs error Failed to verify signature archive-contents.sig
Favicon
Mastering Parentheses in Emacs: Essential Commands
Favicon
C Development with GNU Emacs
Favicon
Mastering Golang Debugging in Emacs
Favicon
Emacs for Python and Poetry Using `basedpyright-langserver`
Favicon
Explorando org-babel en emacs
Favicon
(Game)Dev with Emacs - Because it's not Already Hard Enough Without it
Favicon
The Power of Tries, Data Structure Optimization in Emacs
Favicon
Creating an Emacs major mode - because why not?
Favicon
Learning to Like Neovim
Favicon
Emacs, a simple tour
Favicon
Learning Lisp
Favicon
Managing multiple terminals in Emacs
Favicon
Vim-style repeatable key bindings for navigating windows in Emacs
Favicon
Mermaid preview using xwidget browser
Favicon
Setting up Doom Emacs for Astro Development
Favicon
Moving to Emacs Tree Sitter Modes
Favicon
Mastering JSX Editing in Emacs with Tree-sitter
Favicon
Exploring Syntax Trees in Emacs with Tree-sitter
Favicon
Chinese Zodiac Time for Emacs
Favicon
Using Jmespath in Emacs
Favicon
From Doom to Vanilla Emacs
Favicon
Tell runtime/cgo to not thread warnings as errors
Favicon
Creating dynamic task runners for your npm scripts in Emacs
Favicon
5 ways to get text from an Emacs buffer
Favicon
Using age with org-journal
Favicon
Once again I wanted to have Emacs be my XML Notepad 2006
Favicon
Dia 9
Favicon
Emacs is More Like a Terminal Than an Editor

Featured ones: