I need help setting up LSP for Neovim using lspconfig & language server binaries

Hello,

I am new to NixOS, have used it in the past and now come back. I have been trying to install LSP for my neovim setup for a few days now and have very minimal success. After a lot of research and other people nixos configuration files (which are very helpful), I gained some understanding of nvim LSP setup & nix in general but still unable to do it. I must have been missing something.

I assume with nixpkgs packaging language server binaries & nixpkgs.nvim-lspconfig, it should be possible.

Here is the part of my configuration.nix that matters. I am setting up TailwindCSS language server without success.

  home-manager.users.vee = { nixpkgs, pkgs, ... }: {
    nixpkgs.config.allowUnfree = true;
    home.packages = [
      pkgs.google-chrome
      pkgs.tailwindcss_3
      pkgs.nodejs_20
      pkgs.tailwindcss-language-server
    ];
    programs.bash.enable = true;
    programs.git = {
      enable = true;
      userName = "Vee";
      userEmail = "vee@gmail.com";
      extraConfig = {
        init.defaultBranch = "main";
      };
    };

    # Neovim
    programs.neovim = {
      enable = true;
      defaultEditor = true;
      viAlias = true;
      vimAlias = true;

      plugins = with pkgs.vimPlugins; [
        telescope-nvim
        telescope-fzf-native-nvim
        nvim-treesitter.withAllGrammars
	harpoon
	vim-fugitive

	{
	  plugin = nvim-lspconfig;
	  type = "lua";
	  config = "require('lspconfig').tailwindcss.setup{}";
	}
	# use coc-nvim to have LSP
	# coc-nvim
	# coc-css
	# coc-solargraph
      ];

      extraPackages = with pkgs; [
	# nodejs_20 # required for coc-nvim to work
	# rubyPackages_3_4.solargraph # required for coc-solargraph to work	
      ];
    };

    # The state version is required and should stay at the version you
    # originally installed.
    home.stateVersion = "25.05";
  };

I rebuild the configuration file without errors. Boot nvim without errors but here are :LspInfo reports

LspInfo

From my terminal I have

[vee@nixos:~/Test]$ tailwindcss-language-server --stdio
Content-Length: 99

{"jsonrpc":"2.0","method":"window/logMessage","params":{"type":4,"message":"Setting up server…"}}Content-Length: 104

{"jsonrpc":"2.0","method":"window/logMessage","params":{"type":4,"message":"Listening for messages…"}}

[vee@nixos:~/Test]$ node -v 
v20.19.2

[vee@nixos:~/Test]$ npm -v 
10.8.2

[vee@nixos:~/Test]$ ls 
main.css  node_modules  package.json  package-lock.json  postcss.config.js  tailwind.config.js  test.css  test.go  test.html  test.rb

[vee@nixos:~/Test]$ tailwindcss version 

tailwindcss v3.4.17

Invalid command: version

Usage:
   tailwindcss <command> [options]

Commands:
   init [options]

Options:
   -h, --help               Display usage information

Let me now if I am doing something wrong. Thanks!

what’s wrong: looks fine to me?

It looks fine but TailwindCSS LSP doesn’t give any suggestions when I writes CSS classes in my test.html file so I guess something is broken.

On my other machine running Pop!_OS, it should look like this

sry I dont know about this specific LSP server. Maybe it needs some additional configuration to work in html files (vs pure css files).

1 Like

I tried setting up another LSP gopls and it seems to work. There might be a problem with how I setup tailwindcss-language-server because it doesn’t respond to nvim LSP client at all.

LspInfo of gopls, gopls responded to the client with its version & other information.

Turns out, it’s not that it doesn’t work but you’d need to setup key bindings & configurations correctly for it to work. For regular setup on Ubuntu, plugins set those up for us but there might not be that plugin packaged for Nix. Fortunately, Neovim is very feature rich now, what LSP features we need previously configured by plugins can now be configured by Neovim instead.

lsp-zero has a great guide on this LSP config | lsp-zero.nvim

Well here it is, to answer my own question.

This is a demo with Ruby + solargraph and Go + gopls. Language with auto completion configured. This took me half a week, hopefully someone’d find it useful in the future, drop a reaction if you do.

(Sorry for inlining Lua configs, I am sure you can find a way to break it into multiple files)

  home-manager.users.vee = { nixpkgs, pkgs, ... }: {
    nixpkgs.config.allowUnfree = true;

    home.packages = [
      pkgs.google-chrome
      pkgs.xclip
    ];

    programs.bash.enable = true;

    programs.git = {
      enable = true;
      userName = "Vee";
      userEmail = "vee@gmail.com";
      extraConfig = {
        init.defaultBranch = "main";
      };
    };

    # Neovim
    programs.neovim = {
      enable = true;
      defaultEditor = true;
      viAlias = true;
      vimAlias = true;

      plugins = with pkgs.vimPlugins; [
        telescope-nvim
        telescope-fzf-native-nvim
        nvim-treesitter.withAllGrammars
        harpoon
        vim-fugitive
        nvim-ufo
        nvim-lspconfig
        nvim-cmp
        cmp-nvim-lsp
        cmp-buffer
        cmp-path
        cmp-cmdline
      ];

      extraPackages = with pkgs; [
        # Ruby
        ruby_3_4
        rubyPackages_3_4.solargraph

        # Go
        # go
        # gopls
      ];

      extraLuaConfig = ''
        -- Set
        vim.opt.guicursor = ""
        
        vim.opt.nu = true
        vim.opt.relativenumber = true
        
        vim.opt.tabstop = 2
        vim.opt.softtabstop = 2
        vim.opt.shiftwidth = 2
        vim.opt.expandtab = true
        vim.opt.autoindent = true
        vim.opt.smartindent = true
        -- vim.opt.tabstop = 4
        -- vim.opt.softtabstop = 4
        -- vim.opt.shiftwidth = 4
        -- vim.opt.expandtab = false
        -- vim.opt.autoindent = false
        -- vim.opt.smartindent = false

        -- vim.opt.wrap = false
        -- vim.opt.swapfile = false
        -- vim.opt.backup = false
        -- vim.opt.undodir = os.getenv("HOME") .. "/.vim/undodir"
        -- vim.opt.undofile = true
        -- vim.opt.hlsearch = false
        -- vim.opt.incsearch = true
        -- vim.opt.termguicolors = true
        vim.opt.scrolloff = 8
        vim.opt.signcolumn = "yes"
        vim.opt.isfname:append("@-@")
        vim.opt.updatetime = 50
        -- vim.opt.colorcolumn = "80"

        -- Remap
        vim.g.mapleader = " "
        
        -- Open diagnostic (errors) floating window
        vim.api.nvim_set_keymap('n', '<space>e', '<cmd>lua vim.diagnostic.open_float()<CR>',
          { noremap=true, silent=true }
        )
        
        -- Copy diagnostic (errors) into quickfix
        vim.api.nvim_set_keymap('n', '<space>ce', '<cmd>lua vim.diagnostic.setqflist()<CR>', {})
        
        -- Go back to the previous buffer
        vim.keymap.set("n", "<leader>pb", "<C-^>")
        
        -- Open the f*cking explorer
        vim.keymap.set("n", "<leader>pv", vim.cmd.Ex)
        
        -- Move the visually selected text down one line, reselect, and reindent
        vim.keymap.set("v", "J", ":m '>+1<CR>gv=gv")
        
        -- Move the visually selected text up two lines, reselect, and reindent
        vim.keymap.set("v", "K", ":m '<-2<CR>gv=gv")
        
        -- Move the current line down one position, reposition cursor, and reindent
        vim.keymap.set("n", "J", "mzJ`z")
        
        -- Scroll the view down by half the screen, keeping the cursor in the same position
        vim.keymap.set("n", "<C-d>", "<C-d>zz")
        
        -- Scroll the view up by half the screen, keeping the cursor in the same position
        vim.keymap.set("n", "<C-u>", "<C-u>zz")
        
        -- copy the selected text (visual mode)
        -- then paste it multiple times!
        vim.keymap.set("x", "<leader>p", [["_dP]])
        
        -- copy the selected text (visual mode) into system clipboard
        vim.keymap.set({"n", "v"}, "<leader>y", [["+y]])
        
        -- copy the whole line (normal mode) into system clipboard
        vim.keymap.set("n", "<leader>Y", [["+Y]])
        
        -- Cutting text without being able to paste haha
        vim.keymap.set({"n", "v"}, "<leader>d", [["_d]])
        
        -- This is going to get me cancelled
        vim.keymap.set("i", "<C-c>", "<Esc>")
        
        vim.keymap.set("n", "Q", "<nop>")
        vim.keymap.set("n", "<leader>f", vim.lsp.buf.format)
        
        -- Search and replace in the entire file for the word under the cursor, case-insensitive
        vim.keymap.set("n", "<leader>s", [[:%s/\<<C-r><C-w>\>/<C-r><C-w>/gI<Left><Left><Left>]])
        
        -- Run the shell command "chmod +x" on the current file, and automatically save the changes
        vim.keymap.set("n", "<leader>x", "<cmd>!chmod +x %<CR>", { silent = true })
        
        -- ufo configurations
        vim.o.foldcolumn = '1' -- '0' is not bad
        vim.o.foldlevel = 99 -- Using ufo provider need a large value, feel free to decrease the value
        vim.o.foldlevelstart = 99
        vim.o.foldenable = true
        
        -- Using ufo provider need remap `zR` and `zM`. If Neovim is 0.6.1, remap yourself
        vim.keymap.set('n', 'zR', require('ufo').openAllFolds)
        vim.keymap.set('n', 'zM', require('ufo').closeAllFolds)
        
        -- kj movements
        vim.keymap.set("n", "<C-k>", "<cmd>cnext<CR>zz")
        vim.keymap.set("n", "<C-j>", "<cmd>cprev<CR>zz")
        vim.keymap.set("n", "<leader>k", "<cmd>lnext<CR>zz")
        vim.keymap.set("n", "<leader>j", "<cmd>lprev<CR>zz")
        vim.keymap.set("n", "<leader><leader>", function()
          vim.cmd("so")
        end)

        -- LSP keymap
        vim.api.nvim_create_autocmd('LspAttach', {
          callback = function(args)
            local opts = {buffer = args.buf, remap = false}

            vim.keymap.set("n", "gd", function() vim.lsp.buf.definition() end, opts)
            vim.keymap.set("n", "K", function() vim.lsp.buf.hover() end, opts)
            vim.keymap.set("n", "<leader>vws", function() vim.lsp.buf.workspace_symbol() end, opts)
            vim.keymap.set("n", "<leader>vd", function() vim.diagnostic.open_float() end, opts)
            vim.keymap.set("n", "[d", function() vim.diagnostic.goto_next() end, opts)
            vim.keymap.set("n", "]d", function() vim.diagnostic.goto_prev() end, opts)
            vim.keymap.set("n", "<leader>vca", function() vim.lsp.buf.code_action() end, opts)
            vim.keymap.set("n", "<leader>vrr", function() vim.lsp.buf.references() end, opts)
            vim.keymap.set("n", "<leader>vrn", function() vim.lsp.buf.rename() end, opts)
            vim.keymap.set("i", "<C-h>", function() vim.lsp.buf.signature_help() end, opts)
            vim.keymap.set({'n', 'x'}, "<leader>f", function()
              vim.lsp.buf.format { async = true }
            end, opts)
          end
       })

       vim.diagnostic.config({
         virtual_text = true
       })

       -- Completion
       local cmp = require('cmp')
       local cmp_select = {behavior = cmp.SelectBehavior.Select}

       cmp.setup({
         mapping = cmp.mapping.preset.insert({
           ['<C-p>'] = cmp.mapping.select_prev_item(cmp_select),
           ['<C-n>'] = cmp.mapping.select_next_item(cmp_select),
           ['<C-y>'] = cmp.mapping.confirm({ select = true }),
           ["<C-Space>"] = cmp.mapping.complete(),
           ['<Tab>'] = nil,
           ['<S-Tab>'] = nil
         }),
         sources = cmp.config.sources({
           { name = 'nvim_lsp' },
           { name = 'buffer' }
         })
       })

       cmp.setup.cmdline(':', {
         mapping = cmp.mapping.preset.cmdline(),
         sources = cmp.config.sources({
           { name = 'path' },
           { name = 'cmdline' }
         }),
         matching = { disallow_symbol_nonprefix_matching = false }
       })

       -- LspConfig
       -- local capabilities = require('cmp_nvim_lsp').default_capabilities()
       local lspConfig = require('lspconfig')

       lspConfig['solargraph'].setup {
         capabilities = capabilities
       }

       -- lspConfig['gopls'].setup {
       --   capabilities = capabilities
       -- }
      '';
    };

    # The state version is required and should stay at the version you
    # originally installed.
    home.stateVersion = "25.05";
  };

1 Like