luaneovim

Setting up formatters in Neovim with mason & lsp-zero


I am using lsp-zero & mason to install LSPs, formatters and linters. However, I am not sure how the formatters work and how can I configure them.

For example, the formatter for yml files seems not to be working even though that I've installed yamlls and yamlfmt. On the other hand, Golang's formatter seems to be working just fine, when I save a *.go file, it will be automatically formatted.

How would you setup the YAML formatter in this case? Here is a snippet of my configuration:

lsp.lua

local lsp = require("lsp-zero")

lsp.preset("recommended")

-- Fix Undefined global 'vim'
lsp.nvim_workspace()

local cmp = require('cmp')
local cmp_select = {behavior = cmp.SelectBehavior.Select}
local cmp_mappings = lsp.defaults.cmp_mappings({
  ['<C-p>'] = cmp.mapping.select_prev_item(cmp_select),
  ['<C-n>'] = cmp.mapping.select_next_item(cmp_select),
  ['<CR>'] = cmp.mapping.confirm({ select = true }),
  ["<C-Space>"] = cmp.mapping.complete(),
})

cmp_mappings['<Tab>'] = nil
cmp_mappings['<S-Tab>'] = nil

lsp.setup_nvim_cmp({
  mapping = cmp_mappings
})

lsp.set_preferences({
    suggest_lsp_servers = false,
    sign_icons = {
        error = 'E',
        warn = 'W',
        hint = 'H',
        info = 'I'
    }
})

lsp.on_attach(function(client, bufnr)
  local opts = {buffer = bufnr, 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)
end)

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

mason.lua

require('mason-tool-installer').setup {
  ensure_installed = {
    'golangci-lint',
    'bash-language-server',
    'lua-language-server',
    'vim-language-server',
    'gopls',
    'stylua',
    'shellcheck',
    'sqlfmt',
    'editorconfig-checker',
    'gofumpt',
    'golines',
    'gomodifytags',
    'gotests',
    'goimports',
    'impl',
    'json-to-struct',
    'jq',
    'misspell',
    'revive',
    'shellcheck',
    'shfmt',
    'staticcheck',
    'vint',
    'yamllint',
    'yamlfmt',
    'yamlls',
    'hadolint',
    'dockerls',
    'diagnosticls',
    'sqlls',
    'terraformls',
    'delve'
  }
}

Solution

  • Mason is a package manager that allows you to manage packages. Some packages will work out of the box, others require manual set up and/or calling the required functionality via commands---formatters are one example of this.

    Once you've installed the formatter via Mason, you can set up a Neovim auto-command that calls the formatter on particular files with desired arguments. For example, for yamlfmt, you can create a file (e.g. ~/.config/nvim/lua/autocmds.lua) and add the following code:

    -- Create group to assign commands
    -- "clear = true" must be set to prevent loading an
    -- auto-command repeatedly every time a file is resourced
    local autocmd_group = vim.api.nvim_create_augroup("Custom auto-commands", { clear = true })
    
    vim.api.nvim_create_autocmd({ "BufWritePost" }, {
        pattern = { "*.yaml", "*.yml" },
        desc = "Auto-format YAML files after saving",
        callback = function()
            local fileName = vim.api.nvim_buf_get_name(0)
            vim.cmd(":!yamlfmt " .. fileName)
        end,
        group = autocmd_group,
    })
    

    Then, ensure this file (autocmds.lua) is sourced every time you start Neovim by adding require("autocmds") to your ~/.config/nvim/init.lua file.

    Now, every time you write a buffer (BufWritePost) (i.e. save a file) ending with a .yaml or .yml extension, the callback will be made which gets the absolute path of the file loaded in the current buffer (nvim_buf_get_name(0)), and then executes a non-interactive terminal command that calls the YAML formatter on that file.

    To suppress the command's output, you add silent to the non-interactive terminal command:

    vim.cmd(":silent !yamlfmt " .. fileName)
    

    You can also add multiple terminal commands to the callback. For example, when editing my Python files, I call the following formatters, each of which handles a specific part of code:

    vim.api.nvim_create_autocmd({ "BufWritePost" }, {
        pattern = { "*.py" },
        desc = "Auto-format Python files after saving",
        callback = function()
            local fileName = vim.api.nvim_buf_get_name(0)
            vim.cmd(":silent !black --preview -q " .. fileName)
            vim.cmd(":silent !isort --profile black --float-to-top -q " .. fileName)
            vim.cmd(":silent !docformatter --in-place --black " .. fileName)
        end,
        group = autocmd_group,
    })
    

    Don't forget to add each auto-command to an auto-command group that has the clear flag set to avoid repeatedly loading a command every time a file is resourced. You can read more about auto-commands via the manual :h autocmd.