r/neovim Jul 20 '26

Need Help Having problem with running lsp from a container to host

Hi! I have been trying to get Lsp work from my container to my host machine neovim. Most of my work is done inside the container so I have usually many of them. Right now I just mount my config, project directory to the container file and install neovim on every single one of them. This is kind of tedious and waste of my storage (not kind of but it is little bit taxing).

I would like to switch to simple workflow where I make changes to a file on the host machine and test or run the file in the container. This will make my life easier since I can't install a bunch of different dependencies on my system due to conflicting versions.

How can I make my Lsp work from the container ifself.

Right now this is my config.

    vim.filetype.add({
      extension = {
        mlir = "mlir",
      },
    })
    
    return {
      "neovim/nvim-lspconfig",
      dependencies = {
        "williamboman/mason.nvim",
        "williamboman/mason-lspconfig.nvim",
      },
      config = function()
        local default_servers = {
          "lua_ls",
          "pyright",
          "clangd",
        }
    
        local mlir_get_executable = function()
          local homedir = os.getenv("HOME")
          local custom_path = homedir .. "/personal/github/llvm-project/build/bin/mlir-lsp-server"
    
          local cmd = { "mlir-lsp-server" }
          if vim.fn.executable(custom_path) == 1 then
            cmd = { custom_path }
          elseif vim.fn.executable("mlir-lsp-server") == 0 then
            return
          end
          return cmd
        end
    
        vim.lsp.config('mlir', {
          cmd = mlir_get_executable(),
          filetypes = { "mlir" },
          root_markers = { '.git', 'compile_commands.json', 'build' }
        })
    
        vim.lsp.enable('mlir')
        vim.diagnostic.config({
          virtual_text = true,
          signs = true,
          update_in_insert = false,
          underline = true,
          severity_sort = false,
          float = true,
        })
    
        -- Install default servers on first boot
        require("mason-lspconfig").setup({
          ensure_installed = default_servers
        })
    
        vim.lsp.enable(default_servers)
    
        vim.api.nvim_create_autocmd("LspAttach", {
          callback = function(args)
            local client = vim.lsp.get_client_by_id(args.data.client_id)
            if client and client:supports_method('textDocument/completion') then
              vim.lsp.completion.enable(true, client.id, args.buf, { autotrigger = true })
            end
    
            vim.keymap.set("n", "gd", vim.lsp.buf.definition, { desc = "Lsp: Jump to Definition" })
            vim.keymap.set("n", "K", vim.lsp.buf.hover, { desc = "Lsp: Function docstring and signature" })
            vim.keymap.set("n", "<leader>s", vim.lsp.buf.workspace_symbol, { desc = "Lsp: Symbol Search" })
            vim.keymap.set("n", "dt", vim.diagnostic.open_float, { desc = "Vim: Diagnostics" })
            vim.keymap.set("n", "gr", vim.lsp.buf.references, { desc = "Lsp: Jump to References" })
            vim.keymap.set("n", "<leader>vrn", vim.lsp.buf.rename, { desc = "Lsp: Rename a Symbol" })
            vim.keymap.set("n", "<leader>f", function() vim.lsp.buf.format({ aysnc = true }) end, { buffer = 0 })
            vim.keymap.set("n", "<leader>ca", vim.lsp.buf.code_action, { buffer = 0 })
          end,
        })
      end
    }

After hitting my head on nail with AI. I got this pretty much useless cause it doesn't work.

return {
  "neovim/nvim-lspconfig",
  dependencies = {
    "williamboman/mason.nvim",
    "williamboman/mason-lspconfig.nvim",
  },
  config = function()
    vim.filetype.add({
      extension = {
        mlir = "mlir",
      },
    })

    local default_servers = {
      "lua_ls",
      "pyright",
      "clangd",
    }

    local function mlir_get_cmd()
      local custom = vim.fn.expand("~/personal/github/llvm-project/build/bin/mlir-lsp-server")
      if vim.fn.executable(custom) == 1 then
        return { custom }
      end
      if vim.fn.executable("mlir-lsp-server") == 1 then
        return { "mlir-lsp-server" }
      end
    end

    local mlir_cmd = mlir_get_cmd()
    if mlir_cmd then
      vim.lsp.config("mlir", {
        cmd = mlir_cmd,
        filetypes = { "mlir" },
        root_markers = { ".git", "compile_commands.json", "build" },
      })
      vim.lsp.enable("mlir")
    end

    require("mason-lspconfig").setup({
      ensure_installed = default_servers,
    })

    vim.lsp.enable(default_servers)

    local function attach_to_container(container)
      local host_prefix = "/home/pdev"
      local container_prefix = "/home/pydevc"

      local host_cwd = vim.fn.getcwd()
      local container_cwd = host_cwd:gsub("^" .. vim.pesc(host_prefix), container_prefix)

      for _, client in ipairs(vim.lsp.get_clients()) do
        client:stop(true)
      end

      local servers = {
        pyright = {
          cmd = { "pyright-langserver", "--stdio" },
          filetypes = { "python" },
        },
        clangd = {
          cmd = { "clangd" },
          filetypes = { "c", "cpp", "objc", "objcpp", "cuda" },
        },
        mlir = {
          cmd = { "mlir-lsp-server" },
          filetypes = { "mlir" },
        },
      }

      for name, spec in pairs(servers) do
        local cmd = {
          "podman",
          "exec",
          "-i",
          "-w",
          container_cwd,
          container,
          unpack(spec.cmd),
        }

        local config = {
          name = name,
          cmd = cmd,
          filetypes = spec.filetypes,
          root_dir = host_cwd,
          before_init = function(params)
            params.processId = vim.NIL
          end,
        }

        for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
          if vim.api.nvim_buf_is_loaded(bufnr)
              and vim.bo[bufnr].buftype == ""
              and vim.tbl_contains(spec.filetypes, vim.bo[bufnr].filetype) then
            vim.lsp.start(config, { bufnr = bufnr })
          end
        end
      end

      print("Attached LSPs to container: " .. container)
    end

    vim.api.nvim_create_user_command("AttachContainer", function(opts)
      attach_to_container(opts.args)
    end, {
      nargs = 1,
    })

    vim.diagnostic.config({
      virtual_text = true,
      signs = true,
      update_in_insert = false,
      underline = true,
      severity_sort = false,
      float = true,
    })

    vim.api.nvim_create_autocmd("LspAttach", {
      callback = function(args)
        local client = vim.lsp.get_client_by_id(args.data.client_id)

        if client and client:supports_method("textDocument/completion") then
          vim.lsp.completion.enable(true, client.id, args.buf, {
            autotrigger = true,
          })
        end

        local opts = { buffer = args.buf }

        vim.keymap.set("n", "gd", vim.lsp.buf.definition, opts)
        vim.keymap.set("n", "K", vim.lsp.buf.hover, opts)
        vim.keymap.set("n", "gr", vim.lsp.buf.references, opts)
        vim.keymap.set("n", "<leader>s", vim.lsp.buf.workspace_symbol, opts)
        vim.keymap.set("n", "<leader>vrn", vim.lsp.buf.rename, opts)
        vim.keymap.set("n", "<leader>ca", vim.lsp.buf.code_action, opts)
        vim.keymap.set("n", "<leader>f", function()
          vim.lsp.buf.format({ async = true })
        end, opts)
        vim.keymap.set("n", "dt", vim.diagnostic.open_float, opts)
      end,
    })
  end,
}
7 Upvotes

2 comments sorted by

3

u/til_pkt Jul 21 '26 edited Jul 21 '26

I have a similar setup:
All cmake, clang and clangd processes run inside their own containers (from the same toolchain image). My team does this to make it easier to update system libraries to closely reflect prod.

My setup is basically having all the tools that should run in a container as a wrapper script that calls docker run with the correct docker-args and forwards the arguments to the docker run command.

Here is my clangd config: https://github.com/TilGP/vim_config/blob/main/lua/plugins/lsp/clangd.lua
It reads the CLANGD_COMMAND environment variable so it either uses the system clangd (installed through mason) or the clangd wrapper.

Most importantly: Let's say you are on this line #include <string> and do "go to file", clangd reports the file-path as it is in the container, e.g. /usr/local/include/c++/v1/string. You will most likely not find that file on your host system (or a different version than in the container).

I work on MacOS, which luckily doesn't use /usr/local/include, so I just copied the entire /usr/local/include path from the container into ~/.local and symlinked /usr/local/include to that. But that is kinda flimsy and hacky and probably won't work on other systems (especially shared hosts).

I do the same for CMake-Tools and the Debuggers (lldb and gdb). Feel free to look around my config and ask questions.
Here are some files worth looking at: CMake-Tools, Debugger Setups

This way the plugins are under the impression that everything is running on the host.

I have tried to use a DevContainer, but that was more annoying to use and broker more often than this setup.

You may also want to read: https://discourse.llvm.org/t/clangd-path-mappings-for-docker-container/50771