🪲

Debugging Tailwind's Language Server Protocol

I use Neovim as my code editing driver. At work, our tech stack revolves around the Phoenix framework on top of Elixir

Recently we’ve made the move to incorporate TailwindCSS into the mix - hardly a controversial decision since TW is basically bundled into Phoenix at this point!

Anyway, I’ve previously had the Tailwind LSP set up to good effect in other hobby projects, but in my work repo nothing seems to work correctly.

Installing the language server

The go to place for install instructions for any new language server is the LSPConfig repo’s server_configurations.md file.

Here’s the snippet relating to Tailwind to save you a click:

Tailwind CSS Language Server can be installed via npm:

# heads up, a better way coming up below
npm install -g @tailwindcss/language-server

Snippet to enable the language server:

require'lspconfig'.tailwindcss.setup{}

An alternative to installing globally

The npm command above is the default option for installing the language server, but it has the downside of requiring your npm to be configured correctly, and the global installs directory to be on your path so that you can run tailwindcss-language-server as a shell command.

There is a better way.

Enter Mason.

Mason will install whatever language servers/linting tooling/basically any third party binary, in a single deterministic place on your Neovim path.

What kinds of binaries you install is split into sub packages, the one we’re interested in for this use case is the mason-lspconfig package.

Lets install both packages using our Neovim package manager (I use Packer but there’s also the new kid on the block Lazy which has a slightly different configuration pattern)

  use "williamboman/mason.nvim"
  use {
    "williamboman/mason-lspconfig.nvim",
    requires = {
      "williamboman/mason.nvim",
      "neovim/nvim-lspconfig",
    },
    config = function()
      require("mason-lspconfig").setup {
        ensure_installed = {
          "sumneko_lua",
          "tailwindcss",
          -- any other language servers you want to explicitly install
        },
      }
    end,
  }

When you run a :PackerSync the explicitly defined language servers will be installed.

But where doth they go you ask?

print(vim.fn.stdpath("data") .. "/mason/bin/")
-- ~/.local/share/nvim/mason/bin

Setting up the tailwind language server

The LSP needs to be told to start the tailwind language server when you enter a buffer of a file type which Tailwind would be interested in.

-- same as above 
require'lspconfig'.tailwindcss.setup{
  -- tell vim lsp where the binary lives
  cmd = { vim.fn.stdpath "data" .. "/mason/bin/tailwindcss-language-server" },
}

First failed attempt

At this point, I thought all was golden and I would get that sweet sweet LSP goodness in my heex templates.

I thought wrong.

I hop into a heex template, and trigger my keybinding for vim.lsp.buf.hover() (in my case its mapped to “K”), nothing. No feedback whatsoever.

A handy tool when debugging why a language server isn’t working is the :LspLog command. This will open a new vim tab with the default log for LSP servers to dump their logs. Sometimes you’ll find a key piece of info, like missing binaries etc.

In this instance though, it was of no help. No errors. Nada.

Another handy one is :LspInfo. Here you’ll see a popup with whether each server has been loaded and the workspace path. Again, for this issue all seemed fine there.

My first thought was whether the fact that the tailwind.config.js being in a nested directory (assets/) was causing the issue. The TW language server states that this file is a requirement for it to function.

A quick test to disprove this theory was to run tailwind --init at the root of the project and fire up nvim again.

Nope.

Manually changing the file type

To cut a long and confusing story short, I was able to find out what the cause of the issue was by manually changing the file type of the buffer.

:set filetype=html

Now try your hover command. It works!

So the issue is the heex filetype not trigger Tailwind to start.

Luckily there is an option which can be passed to the LSP, init_options. This is a standard option from lspconfig which does what it says on the tin.

For our issue, we need to change the userLanguages TW option, like so:

require'lspconfig'.tailwindcss.setup{
  -- other options
  init_options = {
    userLanguages = {
      elixir = "phoenix-heex",
      eruby = "erb",
      heex = "phoenix-heex",
      svelte = "html",
    },
  },
  filetypes = {
    "css",
    "scss",
    "sass",
    "html",
    "heex",
    "elixir",
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact",
    "svelte",
  },
}

The filetypes option also tells the vim lsp when to attempt loading of this server. So basically, and heex filetypes will trigger the tailwindcss lsp client, and that filetype translates to phoenix-heex - which is already supported internally in the language server.

Conclusion

There we have it. I hope this helps some other lowly vim adventurers.

I found this area to be very sparsely documented, so I hope stumbling across this post helps others with their setting of the ultimate webdev editing experience!

References

https://github.com/tailwindlabs/tailwindcss-intellisense/blob/master/packages/tailwindcss-language-service/src/util/languages.ts

https://elixirforum.com/t/how-to-use-phoenix-heex-templates-in-vscode/42461/25

https://pragmaticstudio.com/tutorials/formatting-heex-templates-in-vscode