nvim/.config/nvim/lua/lsp.lua

-- nvim/.config/nvim/lua/lsp.lua
-- LSP setup for the languages I actually work in: rust, go, python, lua.
-- Anything else uses the defaults from nvim-lspconfig without my hands on it.
-- I swapped null-ls for conform.nvim + nvim-lint in commit ae44b17.

local lspconfig = require("lspconfig")
local ok_cmp, cmp_lsp = pcall(require, "cmp_nvim_lsp")

local capabilities = vim.lsp.protocol.make_client_capabilities()
if ok_cmp then
  capabilities = cmp_lsp.default_capabilities(capabilities)
end
capabilities.textDocument.completion.completionItem.snippetSupport = true

-- Shared on_attach: per-buffer keymaps + a gentle format-on-save hook.
local augroup_fmt = vim.api.nvim_create_augroup("user_lsp_fmt", { clear = true })

local function on_attach(client, bufnr)
  local bmap = function(mode, lhs, rhs, desc)
    vim.keymap.set(mode, lhs, rhs, { buffer = bufnr, silent = true, desc = desc })
  end

  bmap("n", "gd", vim.lsp.buf.definition,      "goto definition")
  bmap("n", "gD", vim.lsp.buf.declaration,     "goto declaration")
  bmap("n", "gi", vim.lsp.buf.implementation,  "goto implementation")
  bmap("n", "gr", vim.lsp.buf.references,      "references")
  bmap("n", "gt", vim.lsp.buf.type_definition, "type definition")
  bmap("n", "K",  vim.lsp.buf.hover,           "hover")
  bmap("n", "<leader>rn", vim.lsp.buf.rename,  "rename symbol")
  bmap({ "n", "v" }, "<leader>ca", vim.lsp.buf.code_action, "code action")
  bmap("n", "<leader>f", function() vim.lsp.buf.format({ async = true }) end, "format buffer")
  bmap("i", "<C-s>", vim.lsp.buf.signature_help, "signature help")

  if client.supports_method("textDocument/formatting") then
    vim.api.nvim_clear_autocmds({ group = augroup_fmt, buffer = bufnr })
    vim.api.nvim_create_autocmd("BufWritePre", {
      group = augroup_fmt,
      buffer = bufnr,
      callback = function()
        vim.lsp.buf.format({
          bufnr = bufnr,
          timeout_ms = 2000,
          filter = function(c) return c.name == client.name end,
        })
      end,
    })
  end

  -- Inlay hints are noisy on small screens; off by default, toggle with ,h.
  if client.supports_method("textDocument/inlayHint") then
    vim.keymap.set("n", "<localleader>h",
      function()
        vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled({ bufnr = bufnr }), { bufnr = bufnr })
      end,
      { buffer = bufnr, desc = "toggle inlay hints" })
  end
end

-- Diagnostics
vim.diagnostic.config({
  virtual_text = { prefix = "ยท", spacing = 2 },
  severity_sort = true,
  update_in_insert = false,
  float = { border = "rounded", source = "if_many" },
  signs = {
    text = {
      [vim.diagnostic.severity.ERROR] = "E",
      [vim.diagnostic.severity.WARN]  = "W",
      [vim.diagnostic.severity.INFO]  = "I",
      [vim.diagnostic.severity.HINT]  = "H",
    },
  },
})

-- Rust: use rustaceanvim if present, otherwise fall back to rust_analyzer.
if vim.fn.executable("rust-analyzer") == 1 then
  lspconfig.rust_analyzer.setup({
    capabilities = capabilities,
    on_attach = on_attach,
    settings = {
      ["rust-analyzer"] = {
        cargo = { allFeatures = true, buildScripts = { enable = true } },
        check = { command = "clippy", extraArgs = { "--no-deps" } },
        diagnostics = { disabled = { "inactive-code" } },
        inlayHints = {
          parameterHints = { enable = true },
          typeHints = { enable = true },
          chainingHints = { enable = true },
        },
        procMacro = { enable = true },
      },
    },
  })
end

-- Go: gopls. I keep build tags off by default, override per-project in
-- .nvim.lua with vim.lsp.buf.request('workspace/configuration').
if vim.fn.executable("gopls") == 1 then
  lspconfig.gopls.setup({
    capabilities = capabilities,
    on_attach = on_attach,
    cmd = { "gopls", "-remote=auto" },
    settings = {
      gopls = {
        gofumpt = true,
        usePlaceholders = false,
        staticcheck = true,
        analyses = {
          unusedparams = true,
          shadow = true,
          unusedvariable = true,
        },
        hints = {
          assignVariableTypes = true,
          compositeLiteralFields = true,
          parameterNames = false,
          rangeVariableTypes = true,
        },
      },
    },
  })
end

-- Python: pyright for types, ruff for lint/format (via ruff-lsp).
if vim.fn.executable("pyright-langserver") == 1 then
  lspconfig.pyright.setup({
    capabilities = capabilities,
    on_attach = function(client, bufnr)
      -- Defer formatting to ruff; pyright doesn't format.
      client.server_capabilities.documentFormattingProvider = false
      on_attach(client, bufnr)
    end,
    settings = {
      python = {
        analysis = {
          typeCheckingMode = "basic",
          diagnosticMode = "openFilesOnly",
          useLibraryCodeForTypes = true,
        },
      },
    },
  })
end

if vim.fn.executable("ruff-lsp") == 1 or vim.fn.executable("ruff") == 1 then
  lspconfig.ruff.setup({
    capabilities = capabilities,
    on_attach = on_attach,
    init_options = {
      settings = {
        args = { "--line-length=100" },
      },
    },
  })
end

-- Lua: lua_ls, tuned to know about the Neovim runtime only in ~/.config/nvim.
if vim.fn.executable("lua-language-server") == 1 then
  lspconfig.lua_ls.setup({
    capabilities = capabilities,
    on_attach = on_attach,
    on_init = function(client)
      local root = client.workspace_folders and client.workspace_folders[1] and client.workspace_folders[1].name or ""
      if root:find(vim.fn.stdpath("config"), 1, true) then
        client.config.settings.Lua = vim.tbl_deep_extend("force", client.config.settings.Lua, {
          runtime = { version = "LuaJIT" },
          workspace = {
            checkThirdParty = false,
            library = { vim.env.VIMRUNTIME },
          },
          diagnostics = { globals = { "vim" } },
        })
      end
    end,
    settings = {
      Lua = {
        telemetry = { enable = false },
        format = {
          enable = true,
          defaultConfig = { indent_style = "space", indent_size = "2" },
        },
      },
    },
  })
end

-- Handlers with rounded borders so hover/signatures look consistent.
vim.lsp.handlers["textDocument/hover"]          = vim.lsp.with(vim.lsp.handlers.hover,          { border = "rounded" })
vim.lsp.handlers["textDocument/signatureHelp"]  = vim.lsp.with(vim.lsp.handlers.signature_help, { border = "rounded" })

-- Escape hatch: :LspLog to inspect; :LspRestart to bounce all clients.
vim.api.nvim_create_user_command("LspRestart",
  function()
    for _, c in ipairs(vim.lsp.get_clients()) do
      vim.lsp.stop_client(c.id, true)
    end
    vim.defer_fn(function() vim.cmd.edit() end, 250)
  end,
  {})