~whynothugo/dotfiles

ref: 50e71ecf374d62b2d7efc6576ade0f213d05bf2c dotfiles/home/.config/nvim/lua/lsp.lua -rw-r--r-- 9.9 KiB
50e71ecf — Hugo Osvaldo Barrera nvim: Redo LSP sandboxing 2 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
--  _
-- | | __ _ _ __   __ _ _   _  __ _  __ _  ___       ___  ___ _ ____   _____ _ __ ___
-- | |/ _` | '_ \ / _` | | | |/ _` |/ _` |/ _ \_____/ __|/ _ \ '__\ \ / / _ \ '__/ __|
-- | | (_| | | | | (_| | |_| | (_| | (_| |  __/_____\__ \  __/ |   \ V /  __/ |  \__ \
-- |_|\__,_|_| |_|\__, |\__,_|\__,_|\__, |\___|     |___/\___|_|    \_/ \___|_|  |___/
--                |___/             |___/
--
-- Neovim uses LSPs under the hood. LSPs provide error checking (a.k.a.
-- "diagnostics"), formatting, autocompletion suggestions, jump-to-definition
-- and a few other language-specific features.
--
-- Each language has its own LSP implementation, and each one needs to be
-- configured separately. nvim-lspconfig is a plugin with configuration with
-- most common LSPs, and it's used for all the LSPs in this setup.
--
-- On top of that, some LSPs are configured to run inside containers (using
-- bubblewrap for most, and docker/podman for others).
--
-- Aside from regular LSPs, a few tools are set up via null-ls. It uses these
-- tools (e.g.: shellcheck, flake8, mypy) to generate diagnostics and injects
-- them using neovim's diagnostics API. They show up side-to-side with regular
-- LSP diagnostics.

local lspconfig = require("lspconfig")
local lsp_status = require("lsp-status")
local lsp_containers = require("lspcontainers")
local null_ls = require("null-ls")

-- Register this plugin that keeps track of progress messages.
-- This is used by the status line to show a spinner while the LSP is working.
lsp_status.register_progress()

-- Common options for all continerised LSPs.
local container_opts = {
  container_runtime = "podman",
}

-- Returns an `on_new_config` handler for an LSP.
--
---@param name string
---@param opts table
---@return fun(name: string, opts: table): function
local function on_new_config_brap(name, opts)
  opts = opts or {}

  local cmd = opts.cmd
    or require("lspconfig.server_configurations." .. name).default_config.cmd

  local extra_args = {}
  -- TODO: extra_args might need to be a function sometimes.
  if opts.extra_args ~= nil then
    vim.list_extend(extra_args, opts.extra_args)
  end

  if opts.network == true then
    vim.list_extend(extra_args, {
      -- Actually share network.
      "--share-net",
      -- Required for DNS.
      "--ro-bind",
      "/etc/resolv.conf",
      "/etc/resolv.conf",
      -- Required for TLS / HTTPS.
      "--ro-bind",
      "/etc/ssl/certs/ca-certificates.crt",
      "/etc/ssl/certs/ca-certificates.crt",
    })
  end

  -- This inner handler configures the sandbox for the LSP. It mounts only the
  -- project root inside the sandbox filesystem (and paths required to actually
  -- execute the LSP).
  --
  ---@param new_config table
  ---@param new_root_dir string
  ---@return table
  return function(new_config, new_root_dir)
    new_config.cmd = vim.tbl_flatten({
      { "bwrap" },
      extra_args or {},
      { "--ro-bind", new_root_dir, new_root_dir },
      { "--ro-bind", "/usr", "/usr" },
      { "--ro-bind", "/nix", "/nix" },
      { "--ro-bind", "/lib64", "/lib64" },
      { "--proc", "/proc" },
      { "--dev", "/dev" },
      { "--tmpfs", "/tmp" },
      { "--new-session" },
      { "--die-with-parent" },
      { "--" },
      cmd,
      -- XXX: do I need seccomp?
    })
  end
end

-- Define all enabled LSPs.
--
-- Extra per-LSP settings are defined here, and they're merged with
-- common_settings before configuring each LSP.
local servers = {
  clangd = {},
  jedi_language_server = {},
  rust_analyzer = {
    -- FIXME: This LSP requires network access OR mounting ~/.cache/cargo
    --        https://github.com/lspcontainers/dockerfiles/issues/55
    -- cmd = lsp_containers.command("rust_analyzer", container_opts),
    settings = {
      ["rust-analyzer"] = {
        checkOnSave = {
          command = "clippy",
        },
      },
    },
  },
  tsserver = {
    -- Note: tssserver won't attach to vue files by default. Volar handles
    -- typescript errors itself, so tsserver is not necessary for vue files.
    cmd = lsp_containers.command("tsserver", container_opts),
    on_attach = function(client, bufnr)
      lsp_status.on_attach(client, bufnr)

      -- Disable `tsservers`'s formatting capability so that null-ls
      -- is registered as the only compatible formatter.
      client.resolved_capabilities.document_formatting = false
    end,
  },
  volar = {
    -- FIXME: volar seems to require RW access to the source.
    cmd = lsp_containers.command("volar", {
      container_runtime = "podman",
      image = "volar",
    }),
    on_attach = function(client, bufnr)
      lsp_status.on_attach(client, bufnr)

      -- Disable `volar`'s formatting capability so that null-ls
      -- is registered as the only compatible formatter.
      client.resolved_capabilities.document_formatting = false
    end,
  },
  solargraph = { -- ruby
    -- Use a dedicated image for the single rails project I work on.
    -- It's the same one used for development, and created by docker-compose.
    cmd = lsp_containers.command("solargraph", {
      container_runtime = "docker",
      image = "procwise_puma",
    }),
  },
  bashls = {
    cmd = { "false" },
    on_new_config = on_new_config_brap("bashls"),
  },
  terraformls = {
    cmd = { "false" },
    on_new_config = on_new_config_brap("terraformls"),
  },
  dockerls = {
    cmd = lsp_containers.command("dockerls", container_opts),
  },
  gopls = {
    cmd = { "false" },
    on_new_config = on_new_config_brap("gopls", {
      network = true,
      extra_args = {
        -- Share GOPATH. This is a shared cache for modules.
        -- This is mounted in the same location as the host so code navigation
        -- into these can work too.
        "--bind",
        -- TODO: should host's read $GOPATH on both here:
        "/home/hugo/.cache/golang",
        "/home/hugo/.cache/golang",
      },
    }),
    settings = {
      gopls = {
        staticcheck = true,
      },
    },
  },
  sumneko_lua = {
    cmd = lsp_containers.command("sumneko_lua", container_opts),
    settings = {
      Lua = {
        runtime = {
          version = "LuaJIT",
          path = vim.split(package.path, ";"),
        },
        diagnostics = {
          globals = { "vim" },
        },
        workspace = {
          library = {
            [vim.fn.expand("$VIMRUNTIME/lua")] = true,
            [vim.fn.expand("$VIMRUNTIME/lua/vim/lsp")] = true,
          },
        },
      },
    },
  },
  rnix = {
    cmd = { "false" },
    on_new_config = on_new_config_brap("rnix"),
  },
  -- TODO: "sorbet",  -- type checker for ruby
  -- TODO: "sourcekit",  -- swift
  -- TODO: json: https://github.com/neovim/nvim-lspconfig/blob/master/CONFIG.md#jsonls
  --       with: https://github.com/b0o/SchemaStore.nvim
  -- yamlls = {
  --   cmd = lsp_containers.command("yamlls", container_opts),
  -- },
}

-- Helper to conditionally register eslint handlers only if eslint is
-- configured. If eslint is not configured for a project, it just fails.
local function has_eslint_configured(utils)
  return utils.root_has_file(".eslintrc.js")
end

null_ls.setup({
  sources = {
    null_ls.builtins.code_actions.eslint_d.with({ condition = has_eslint_configured }),
    null_ls.builtins.diagnostics.eslint_d.with({ condition = has_eslint_configured }),
    null_ls.builtins.diagnostics.flake8,
    null_ls.builtins.diagnostics.mypy,
    null_ls.builtins.diagnostics.shellcheck.with({ filetypes = { "sh", "zsh" } }),
    null_ls.builtins.formatting.black,
    null_ls.builtins.formatting.eslint_d.with({ condition = has_eslint_configured }),
    null_ls.builtins.formatting.prettier.with({
      -- Only register prettier if eslint_d is not running as a formatter. This
      -- can happen if it's not configured for this project, or if it can't
      -- handle the current filetype.
      condition = function()
        return #null_ls.get_source({ name = "eslint_d", method = null_ls.methods.FORMATTING }) == 0
      end,
    }),
    null_ls.builtins.formatting.isort.with({ command = "isort-wrapper" }),
    null_ls.builtins.formatting.stylua,
    null_ls.builtins.formatting.pg_format,
    null_ls.builtins.formatting.swiftformat,
  },
  diagnostics_format = "[#{c}] #{m} (#{s})",
})

local common_settings = {
  before_init = function(params)
    -- Some LSPs will exit if the parent dies or is not found. The parent is
    -- determined based on its pid, but because we use sandboxed LSPs, they
    -- don't share the PID namespace with neovim, and this fails.
    --
    -- This prevents the LSP from attempting to executing this check.
    --
    -- See https://github.com/lspcontainers/lspcontainers.nvim#process-id
    params.processId = vim.NIL
  end,
  on_attach = lsp_status.on_attach,
  capabilities = lsp_status.capabilities,
}

-- Register all the LSP servers.
for server, config in pairs(servers) do
  -- Set default client capabilities plus window/workDoneProgress
  config.capabilities = vim.tbl_extend(
    "keep",
    config.capabilities or {},
    lsp_status.capabilities
  )

  -- Merge per-LSP configs with the common settings, and use that:
  lspconfig[server].setup(vim.tbl_extend("keep", config, common_settings))
end

-- Logs to ~/.cache/nvim/lsp.log
-- vim.lsp.set_log_level("debug")

require("lsp_lines").register_lsp_virtual_lines()
-- `virtual_text` is redundant due to lsp_lines.
vim.diagnostic.config({
  virtual_text = false,
})

-- Lightbulb ==================================================================

-- Show a lightbulb icon when there are available LSP actions.
vim.api.nvim_create_autocmd({ "CursorHold", "CursorHoldI" }, {
  callback = function()
    -- TODO: Can these settings be configure just ONCE, instead of in every call?
    require("nvim-lightbulb").update_lightbulb({
      sign = { enabled = false },
      virtual_text = { enabled = true, text = "" },
    })
  end,
})

vim.fn.sign_define(
  "LightBulbSign",
  { text = "", texthl = "", linehl = "", numhl = "" }
)
-- TODO: Probably using the status bar is best.

-- ========== EOF =============================================================