How to set up Neovim for tailwind intelisense in Phoenix files?

Shoutout to nvim users.

I’m trying to make tailwind work in phoenix files (heex, ex, etc).
No matter what config I have, the cmp doesn’t help me with tailwind intelisense in phoenix files.
It works in other envs (HTML, React, etc).

I suspect it has something to do with the fact the config file is under ./assets/tailwind.config.js.

my lsp config for tailwind -

tailwindcss = {
          root_dir = function(fname)
            return require('lspconfig.util').root_pattern(
              'assets/tailwind.config.ts',
              'assets/tailwind.config.js',
              'assets/tailwind.config.cjs',
              'assets/package.json',
              '.git'
            )(fname) or vim.fn.getcwd()
          end,
          settings = {
            tailwindCSS = {
              experimental = {
                classRegex = {
                  -- For Phoenix `class="..."` syntax
                  { 'class[:]\\s*"([^"]*)"', 1 },
                  -- For Phoenix `~H` and `HEEx` tags
                  { '~H"([^"]*)"', 1 },
                },
              },
              includeLanguages = {
                elixir = 'html-eex',
                eelixir = 'html-eex',
                heex = 'html-eex',
              },
            },
            -- tailwindCSS = {
            --   experimental = {
            --     classRegex = {
            --       'class[:]\\s*"([^"]*)"',
            --     },
            --   },
            -- },
          },
        }

LspInfo output:

CmpStatus output:

From poking around, this fixes it.

config = {
  tailwindcss = {
    settings = {
      tailwindCSS = {
        -- docs imply it should be html, js or css, but you can also specify
        -- another "known" language so `heex = "phoenix-heex"` or
        -- `heex = "HTML (EEx)"` also works. I think they just all get
        -- treated as "html" by tailwind (effecting if it looks for class="" for completion triggers)
        -- but could not say for certain.
        includeLanguages = {heex = "html"}
      }
    }
  }
}

My assets/tailwind.config.js should be pretty normal, I point my content like so:

module.exports = {
  content: [
    '../lib/**/*.ex',
    '../lib/**/*.leex',
    '../lib/**/*.heex',
    '../lib/**/*.exs',
    '../lib/**/*.eex',
    './js/**/*.js'
  ],
  // ...
}

You can verify your settings are correctly merged with
:=require'lspconfig'.tailwindcss.manager.config.settings (probably only after opening a file).

Considering tailwind lists HTML (EEx), HTML (Eex) and html-eex in its htmlLanguages list, I think it’s just matching any free form string that the LSP client sends as the file/language type, so possibly adding heex would be a good PR. I guess vs-code reports it as phoenix-heex? I assume all those types behave the same. (Or maybe I have configured my neovim to call it heex at some point, you can check your own configs name with :set ft? with a file open).

Note there are two npm packages, @tailwindcss/language-server and tailwindcss-language-server (defunct). @tailwindcss/language-server is what I got working.

References:

Default nvim-lspconfig tailwind settings: nvim-lspconfig/lua/lspconfig/configs/tailwindcss.lua at master · neovim/nvim-lspconfig · GitHub

Tailwind LSP settings: GitHub - tailwindlabs/tailwindcss-intellisense: Intelligent Tailwind CSS tooling for Visual Studio Code

Tailwind LSP known languages: tailwindcss-intellisense/packages/tailwindcss-language-service/src/util/languages.ts at 434c7db38d0eaf1b854678e5e1d7db9e7322ceef · tailwindlabs/tailwindcss-intellisense · GitHub

2 Likes

Hm, I just noticed you have heex = "html-eex" in your config, so I am not sure why yours is not working beyond maybe its not getting merged correctly. Maybe check it with :=require'lspconfig'.tailwindcss.manager.config.settings.

I can 100% confirm that mine did not function until I added that line. I only installed the LSP and set it up for this thread.

1 Like

Thanks for you input.
Didn’t know :=require'lspconfig'... is a thing, thanks for that.

The output of this command it -

{
  editor = {
    tabSize = 2
  },
  tailwindCSS = {
    classAttributes = { "class", "className", "class:list", "classList", "ngClass" },
    experimental = {
      classRegex = { { 'class[:]\\s*"([^"]*)"', 1 }, { '~H"([^"]*)"', 1 } }
    },
    includeLanguages = {
      eelixir = "html-eex",
      elixir = "html-eex",
      eruby = "erb",
      heex = "html-eex",
      htmlangular = "html",
      templ = "html"
    },
    lint = {
      cssConflict = "warning",
      invalidApply = "error",
      invalidConfigPath = "error",
      invalidScreen = "error",
      invalidTailwindDirective = "error",
      invalidVariant = "error",
      recommendedVariantOrder = "warning"
    },
    validate = true
  }
}

which seems fine.

Not clear what’s off

  • Does it function if you create a regular test.html file as a sibling to a layout heex file? eg: is it functioning in the project, but not for the file type or not functioning for the project at all?
  • Does :LspLog show anything interesting?
  • Try disabling all other LSP clients in case they’re some how conflicting?
  • What version of Neovim? I am using 0.10.2 and nvim-cmp#3403e2e, nvim-lspconfig#0ef6459.
  • If you run tailwindcss-language-server in a project dir, do you see any output? Mine logs “setting up” & “listening” json messages. I have mine installed via npm install -g @tailwindcss/language-server, in a mise controlled install of node@22.9.0, I wonder what mason installs.

For reference this is my output, perhaps try removing the experimental class regex?

{
  editor = {
    tabSize = 2
  },
  tailwindCSS = {
    classAttributes = { "class", "className", "class:list", "classList", "ngClass" },
    includeLanguages = {
      eelixir = "html-eex",
      eruby = "erb",
      heex = "html",
      htmlangular = "html",
      templ = "html"
    },
    lint = {
      cssConflict = "warning",
      invalidApply = "error",
      invalidConfigPath = "error",
      invalidScreen = "error",
      invalidTailwindDirective = "error",
      invalidVariant = "error",
      recommendedVariantOrder = "warning"
    },
    validate = true
  }
}
1 Like

Thank you so much for your efforts :slight_smile:

My current config output looks identical to yours.
Now checking your other suggestions.

Thanks again.

Could it be that it something with the fact that the config is under assets/tailwind.config.js?
From what I’ve seen online, it shouldn’t be a problem.
I’ve added a new HTML file like you proposed, doesn’t work there.

I’ve opened another terminal with an astro (framework) project, there everything works as expected.
Which kinda answers the rest of the questions you asked regarding collisions and versions.

arghhhhh

Before adding the includeLanguages field, I tried

  • setting the root_dir to <project>/assets
  • symlinking project/tailwind.config.js -> assets/tailwind.config.js
  • just opening a file with the cwd in project/assets
  • adjusting the tailwind content to include ./lib/... in addition to the normal ../lib/... paths

None of it had any (positive) effect.

The project I am testing with has tailwind installed via npm, so there is a package.json with tailwindcss in it. I wonder if you’re using the stand alone tailwind binary?

I think the LSP server has some “fall back to standalone mode” in it (judging by some issues I saw), but it may not function correctly if the project config is using npm packages (postcss?) that it cant load? Or maybe vice versa?

The :LspLog says nothing?

Since we last discussed it, I haven’t touched my config.
Suddenly, now, I’ve opened another phoenix project I got and tailwind works in an html.heex file.

Sooo weiiird…
I’ll look into it and will write back once I have some more info.

Are you ready? :drum: :drum: :drum: :drum:

In this specific project, my tailwind file was an ESM one.
In the working project, where intelisense was fine, it was CJS.

Once I’ve converted to CJS, everything works fine.
Hmmm… :thinking:

Once I find some time I’ll see why and how can one use ESM and still have the cmp work.

Interesting yes, mine is a CJS format with modules.exports = {...}.

The LSP server is supposed to support all formats, but perhaps you need to name it tailwind.config.mjs for it to load it correctly. The default created by tailwind init is a CJS file, so maybe it defaults to assuming that style.

I tried converting my config to ESM style (export default {...}), with the name tailwind.config.js and it still functions…

Interesting :thinking:

I’ll find some time to dig into it.

Regardless, thank you so much for your help, much appreciated!

Sorry to bother. I’m having issues with the tailwind lsp when running a completely new phoenix project (latest version).

A completely fresh project works properly for you? And your lsp settings are the same as you posted above? Thanks for any help

I’m a Neovim newb, but it seems to be working ok in AstroNvim. I think a poster on this forum actually recommend it to someone. I had run through LazyNvim, Kickstart, and NVChad, but I hadn’t tried AstroNvim for whatever reason and it looks like I’ll stick with it for a while as it was fairly easy to get it running with elixir and phoenix, and I haven’t had any issues with updating so far either. :crossed_fingers:

The configs are in the community repo in case it helps.

Thanks will give this a go

This is my config, and note the filetypes_include = { "heex" } part

That was what got it working for me as well, and nothing else worked.

{
    "neovim/nvim-lspconfig",
    opts = {
      servers = {
        tailwindcss = {
          -- exclude a filetype from the default_config
          filetypes_exclude = { "markdown" },
          -- add additional filetypes to the default_config
          filetypes_include = { "heex" },
          -- to fully override the default_config, change the below
          -- filetypes = {}
        },
      },
      setup = {
        tailwindcss = function(_, opts)
          local tw = LazyVim.lsp.get_raw_config("tailwindcss")
          opts.filetypes = opts.filetypes or {}

          -- Add default filetypes
          vim.list_extend(opts.filetypes, tw.default_config.filetypes)

          -- Remove excluded filetypes
          --- @param ft string
          opts.filetypes = vim.tbl_filter(function(ft)
            return not vim.tbl_contains(opts.filetypes_exclude or {}, ft)
          end, opts.filetypes)

          -- Additional settings for Phoenix projects
          opts.settings = {
            tailwindCSS = {
              includeLanguages = {
                elixir = "html-eex",
                eelixir = "html-eex",
                heex = "html-eex",
              },
            },
          }

          -- Add additional filetypes
          vim.list_extend(opts.filetypes, opts.filetypes_include or {})
        end,
      },
    },
  },
2 Likes