web-mode + eglot + Vetur + Vue.js = happy
Recently, I’ve been writing code for a work project that involves a Vue.js front end, written in a mix of JavaScript and TypeScript, along with a backend API that’s completely TypeScript. My emacs config gave me great tooling when I was working on the API, but support for the front end code felt really lacking in comparison. Yesterday, thanks to a lucky discovery, I solved this. I’m really happy about it!
(If you just want the config change TL;DR bit without all the narrative, it’s at the bottom of the post.)
The lucky discovery came when I contemplated switching over to
VS Code for working on the front end
code. When I opened one of the Vue single file components in VS Code
for the first time, it prompted me to install an extension called
Vetur. That’s how I learned that
Vetur is an extension that provides a Vue-specific
LSP server
called vls
,
which provides all the tooling support for working with Vue files in
VS Code. (If you’re not familiar with LSP, or Language Server
Protocol, there’s a good overview
on Wikipedia.)
In my Emacs config, I use
eglot
for LSP integration.
It’s great and doesn’t require much setup. The way eglot
decides
which LSP engine to use for a particular file depends on the major
mode of that file and a variable called eglot-server-programs
, which
provides a mapping between major modes and engines. Since I use
web-mode
to edit Vue code, my initial
attempt looked like this:
(add-to-list 'eglot-server-programs '(web-mode "vls"))
After I installed vls
(by running npm install -g vls
), I opened up
a Vue file and ran M-x eglot
, which starts up the configured server
in the background and starts using it manage stuff in the buffer. It
worked! This was great!
Since I didn’t want to have to remember to manually start up Eglot every time I started editing a Vue file, I next added this bit of configuration:
(add-hook web-mode-hook #'eglot-ensure)
This makes sure that whenever a file using web-mode
is opened,
there’s an Eglot process either already running. If not, one gets
started up. (Eglot uses one server instance per project to cut down on
resource usage.)
So I added that bit of config, restarted Emacs, opened a Vue file, and
everything worked! Awesome! But then I tried doing something else with
a normal HTML file in web-mode
, and …dear readers, things got
unpleasant. The Eglot server started up, it expected a Vue file, found
an HTML one, and then it threw all the errors and warnings.
I was so bummed at this point! It felt like I was sooooo close to
solving this problem, only to get derailed at the last instant because
I use web-mode
for more than just Vue files. I started thinking
about how I could have a major mode, like web-mode
, but dedicated
just to Vue …and then I remembered
define-derived-mode
!
If you haven’t run across it before, define-derived-mode
lets you
make a new major mode that’s basically a copy of an existing one. All
the configuration — variables, hooks, etc — that are associated
with the existing mode apply to the new one, and you can add
additional configuration that’s specific to the new mode too. (In OOP
terms, the new mode is essentially a child of the parent mode.)
I added some code to make a new mode, called genehack-vue-mode
. Then
I configured things so that Vue files open in that mode and that mode
is associted with the vls
server on the Eglot level. Finally, I
added the eglot-ensure
call to genehack-vue-mode-hook
. After
that, problem solved!
tl;dr
If you just want something to copy and paste, here you go:
(require 'eglot)
(require 'web-mode)
(define-derived-mode genehack-vue-mode web-mode "ghVue"
"A major mode derived from web-mode, for editing .vue files with LSP support.")
(add-to-list 'auto-mode-alist '("\\.vue\\'" . genehack-vue-mode))
(add-hook 'genehack-vue-mode-hook #'eglot-ensure)
(add-to-list 'eglot-server-programs '(genehack-vue-mode "vls"))
Make sure you’ve got eglot
and web-mode
installed, naturally
…and don’t forget to run install the vls
command via NPM. Make
sure the location it is installed to is in exec-path
so Emacs can
find it, or use the full path when adding it to the
eglot-server-programs
alist.
Hopefully, writing this up saves somebody else the time of figuring out how to do it…