DEV Community

Cover image for Finally Modernized My Emacs Setup with LSP + Tree-sitter
Takashi Masuda
Takashi Masuda

Posted on • Originally published at masutaka.net

Finally Modernized My Emacs Setup with LSP + Tree-sitter

In recent years, I've been mainly writing LookML, SQL, HCL, and other languages, with fewer opportunities to write programming languages like Ruby. However, I've recently returned to Rails development this month.

So, I finally got around to setting up LSP (Language Server Protocol) in Emacs.

During this process, I also learned about Tree-sitter and configured it as well.

※ LSP is an advanced code assistance mechanism provided by Language Servers, while Tree-sitter is a parsing engine that quickly generates AST from code to enable highlighting and structural editing.

Emacs Version I'm Using

I'm using Emacs 29.1 instead of the latest 30.2 for the following reasons:

Introducing LSP

Adopted lsp-mode

I compared eglot, which is standard in Emacs 29, with the third-party lsp-mode.

I would have preferred to use the standard eglot, but I decided to adopt lsp-mode because there's information suggesting that Solargraph, which eglot requires as the Ruby Language Server, doesn't have good performance. lsp-mode requires ruby-lsp as the Language Server.

I haven't actually verified whether Solargraph really has poor performance 😅

lsp-mode Configuration Example

First, install the Language Server for each language in a PATH that Emacs recognizes (exec-path).

The correspondence between each language and Language Server is documented in Languages in the lsp-mode documentation.

Here are the Language Servers I installed this time:

Next, install lsp-mode from Melpa.

Finally, just add the lsp-deferred function to the hook of the major-mode you want to enable.

Here's a configuration example for ruby-mode:

(add-hook 'ruby-mode-hook #'lsp-deferred)
Enter fullscreen mode Exit fullscreen mode

With this, you can jump to definitions with M-. and return with M-,, and it also points out indentation issues. For other features, please see the lsp-mode documentation.

I also configured the following:

(setq lsp-keymap-prefix "C-c C-l")

;; Disable rubocop-ls so that ruby-lsp is prioritized even when rubocop is installed.
(setq lsp-disabled-clients '(rubocop-ls))

;; Format files on save for all buffers where lsp-mode is active.
(setq-default lsp-format-buffer-on-save t)
Enter fullscreen mode Exit fullscreen mode

Discovered xxx-ts-mode Has Been Added to Emacs

When trying to configure lsp-mode for TypeScript, I noticed that xxx-ts-mode exists, which wasn't in previous versions of Emacs.

$ find /Applications/Emacs.app -type f -name '*-ts-mode.elc' | sort
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/c-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/cmake-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/dockerfile-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/go-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/java-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/json-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/ruby-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/rust-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/typescript-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/textmodes/toml-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/textmodes/yaml-ts-mode.elc
Enter fullscreen mode Exit fullscreen mode

Unlike traditional xxx-mode which parses syntax using regular expressions, xxx-ts-mode parses syntax at the AST level using Tree-sitter. Therefore, syntax highlighting is more accurate, and indentation and structural editing are more stable.

I was about to skip it, but when I looked at typescript-mode's README.md, I learned that development has stopped:

Essentially all major development of typescript-mode has come to a halt.

So I decided to introduce xxx-ts-mode, starting with typescript-ts-mode. I also felt the benefit of being able to drop third-party xxx-mode packages.

Introducing typescript-ts-mode

You can install the Tree-sitter for TypeScript by adding the following to init.el:

(setq treesit-language-source-alist
      '((tsx "https://github.com/tree-sitter/tree-sitter-typescript" "v0.23.2" "tsx/src")
        (typescript "https://github.com/tree-sitter/tree-sitter-typescript" "v0.23.2" "typescript/src")))

(dolist (element treesit-language-source-alist)
  (let ((lang (car element)))
    (unless (treesit-language-available-p lang)
      (treesit-install-language-grammar lang))))
Enter fullscreen mode Exit fullscreen mode

When this is evaluated, the *.dylib files built from source code fetched from GitHub will be installed directly under ~/.emacs.d/tree-sitter/.

  • libtree-sitter-tsx.dylib
  • libtree-sitter-typescript.dylib

💡 If you don't specify a tag, the default branch will be referenced. Since commit hash couldn't be specified, I specified tags from the perspective of security and version pinning.

Then, simply associate *.ts and *.tsx with typescript-ts-mode and tsx-ts-mode respectively. Also enable lsp-mode.

※ tsx-ts-mode is also defined in typescript-ts-mode.el.

;; TypeScript

(add-to-list 'auto-mode-alist '("\\.ts\\'" . typescript-ts-mode))

(defun typescript-ts-mode-hook-func ()
  (lsp-deferred))
(add-hook 'typescript-ts-mode-hook #'typescript-ts-mode-hook-func)

;; TSX

(add-to-list 'auto-mode-alist '("\\.tsx\\'" . tsx-ts-mode))

(defun tsx-ts-mode-hook-func ()
  (lsp-deferred))
(add-hook 'tsx-ts-mode-hook #'tsx-ts-mode-hook-func)
Enter fullscreen mode Exit fullscreen mode

Supplement: Why I Didn't Adopt the tree-sitter-langs Package

The tree-sitter-langs package can also install *.dylib, but I decided not to adopt it for the following reasons:

  • It installs under ~/.emacs.d/elpa/tree-sitter-langs-20251019.1145/bin/ instead of ~/.emacs.d/tree-sitter/
    • This directory name will change with version updates of the tree-sitter-langs package
  • The library names are like typescript.dylib instead of libtree-sitter-typescript.dylib

It's good for checking the location of Tree-sitter parsers for each language:

Introducing Other xxx-ts-modes

Among the Emacs built-in xxx-ts-modes mentioned earlier, I introduced those with high usage frequency:

  • dockerfile-ts-mode
  • go-ts-mode
  • json-ts-mode
  • ruby-ts-mode

As an exception, I didn't introduce yaml-ts-mode. When saving, it formats with indent 4, and I couldn't figure out how to change it to 2.

Final Configuration for LSP mode, Tree-sitter, and xxx-ts-mode

Bonus: Migration from auto-complete to company-mode

There were usage points for company-mode in the lsp-mode code.

I had only heard the name company-mode, but apparently it's a completion framework that assists code input.

I migrated from auto-complete, which I've been using for years, to company-mode.

I saw some information suggesting that corfu, which is lighter than company-mode, might be better now, but I decided to go with company-mode this time as it seemed easier.

(setq company-minimum-prefix-length 2)
(setq company-show-quick-access t)

(add-hook 'after-init-hook #'global-company-mode)
Enter fullscreen mode Exit fullscreen mode

Issues

  • When lsp-format-buffer-on-save is enabled for all buffers where lsp-mode is active, code written by others gets heavily reformatted, which might be awkward. Especially YAML files
  • global-company-mode is a bit noisy, so it might be better to configure it for individual major-modes. I might change this later

Conclusion

Finally, I brought my Emacs setup closer to a modern configuration with LSP + Tree-sitter. However, since there are almost no Emacs users around me, I'm not entirely sure how close I got.

I was also able to reduce some third-party packages. I want to minimize dependencies as much as possible, so that's good.

  • Added:
    • company
    • lsp-mode
  • Removed:
    • auto-complete
    • dockerfile-mode
    • go-autocomplete
    • go-eldoc
    • go-mode

References

Top comments (0)