stevearc / conform.nvim

Lightweight yet powerful formatter plugin for Neovim
MIT License
3.14k stars 161 forks source link

bug: conform format_on_save autocmds do not work #561

Closed nnathan closed 3 days ago

nnathan commented 4 days ago

Neovim version (nvim -v)

v0.10.0

Operating system/version

MacOS 15.1

Read debugging tips

Add the debug logs

Log file

N/A

Describe the bug

Conform format_on_save isn't working with my neovim kickstart setup. If I modify the conform init.lua such that I comment out the group = aug in the BufWritePre autocommand, it is working fine.

My nvim init.lua works fine on Ubuntu 22.04/WSL2, in that Conform does the right thing. It just doesn't work on my mac.

Manually creating the autocmd as an alternative to format_on_save as described in the README works, and it's the workaround I'm using.

I couldn't reproduce this issue with repro.lua. I'm hoping to get some assistance.

What is the severity of this bug?

tolerable (can work around it)

Steps To Reproduce

I've provided my init.lua in Minimal init.lua.

Expected Behavior

Using the format_on_save should

Minimal example file

foo.go:

    package main // gofumpt will remove the initial whitespace

Minimal init.lua

-- Install packer
local install_path = vim.fn.stdpath("data") .. "/site/pack/packer/start/packer.nvim"
local is_bootstrap = false
if vim.fn.empty(vim.fn.glob(install_path)) > 0 then
    is_bootstrap = true
    vim.fn.system({ "git", "clone", "--depth", "1", "https://github.com/wbthomason/packer.nvim", install_path })
    vim.cmd([[packadd packer.nvim]])
end

require("packer").startup(function(use)
    -- Package manager
    use("wbthomason/packer.nvim")

    use({ -- LSP Configuration & Plugins
        "neovim/nvim-lspconfig",
        requires = {
            -- Automatically install LSPs to stdpath for neovim
            "williamboman/mason.nvim",
            "williamboman/mason-lspconfig.nvim",

            -- Useful status updates for LSP
            -- NOTE: `opts = {}` is the same as calling `require('fidget').setup({})`
            { "j-hui/fidget.nvim", tag = "legacy", opts = {} },

            -- Additional lua configuration, makes nvim stuff amazing
            "folke/neodev.nvim",
        },
    })

    use({ -- Autocompletion
        "hrsh7th/nvim-cmp",
        requires = { "hrsh7th/cmp-nvim-lsp", "L3MON4D3/LuaSnip", "saadparwaiz1/cmp_luasnip" },
    })

    use({ -- Highlight, edit, and navigate code
        "nvim-treesitter/nvim-treesitter",
        run = function()
            pcall(require("nvim-treesitter.install").update({ with_sync = true }))
        end,
    })

    use({ -- Additional text objects via treesitter
        "nvim-treesitter/nvim-treesitter-textobjects",
        after = "nvim-treesitter",
    })

    -- Autoformatting
    use({
        "stevearc/conform.nvim",
        config = function()
            require("conform").setup()
        end,
    })

    -- Git related plugins
    use("tpope/vim-fugitive")
    use("lewis6991/gitsigns.nvim")

    use("nnathan/desertrocks")
    use("nvim-lualine/lualine.nvim") -- Fancier statusline
    use("numToStr/Comment.nvim") -- "gc" to comment visual regions/lines
    use("tpope/vim-sleuth") -- Detect tabstop and shiftwidth automatically

    -- Fuzzy Finder (files, lsp, etc)
    use({ "nvim-telescope/telescope.nvim", branch = "0.1.x", requires = { "nvim-lua/plenary.nvim" } })

    -- Fuzzy Finder Algorithm which requires local dependencies to be built. Only load if `make` is available
    use({ "nvim-telescope/telescope-fzf-native.nvim", run = "make", cond = vim.fn.executable("make") == 1 })

    -- Add custom plugins to packer from ~/.config/nvim/lua/custom/plugins.lua
    local has_plugins, plugins = pcall(require, "custom.plugins")
    if has_plugins then
        plugins(use)
    end

    if is_bootstrap then
        require("packer").sync()
    end
end)

-- When we are bootstrapping a configuration, it doesn't
-- make sense to execute the rest of the init.lua.
--
-- You'll need to restart nvim, and then it will work.
if is_bootstrap then
    print("==================================")
    print("    Plugins are being installed")
    print("    Wait until Packer completes,")
    print("       then restart nvim")
    print("==================================")
    return
end

-- Automatically source and re-compile packer whenever you save this init.lua
local packer_group = vim.api.nvim_create_augroup("Packer", { clear = true })
vim.api.nvim_create_autocmd("BufWritePost", {
    command = "source <afile> | silent! LspStop | silent! LspStart | PackerCompile",
    group = packer_group,
    pattern = vim.fn.expand("$MYVIMRC"),
})

-- [[ Setting options ]]
-- See `:help vim.o`

-- Set highlight on search
vim.o.hlsearch = true

-- Make line numbers default
vim.wo.number = false

-- Enable mouse mode
vim.o.mouse = ""

-- Enable break indent
vim.o.breakindent = false

-- Save undo history
vim.o.undofile = true

-- Case insensitive searching UNLESS /C or capital in search
vim.o.ignorecase = true
vim.o.smartcase = true

-- Decrease update time
vim.o.updatetime = 250
vim.wo.signcolumn = "auto"

-- Set colorscheme
vim.o.termguicolors = true

-- pmenu colours

vim.api.nvim_create_autocmd("ColorScheme", {
    pattern = "*",
    command = "highlight Pmenu ctermbg=60 ctermfg=81 guibg=MediumPurple4 guifg=SteelBlue1",
})
vim.api.nvim_create_autocmd("ColorScheme", {
    pattern = "*",
    command = "highlight PmenuSel ctermbg=60 ctermfg=50 guibg=MediumPurple4 guifg=Cyan2",
})
vim.api.nvim_create_autocmd("ColorScheme", {
    pattern = "*",
    command = "highlight Search ctermbg=24 ctermfg=49 guibg=DeepSkyBlue4 guifg=MediumSpringGreen",
})

-- tabmenu colours

vim.api.nvim_create_autocmd("ColorScheme", {
    pattern = "*",
    command = "highlight TabLineFill ctermbg=DarkGreen ctermfg=LightGreen guibg=SeaGreen3 guifg=DarkGreen",
})

vim.api.nvim_create_autocmd("ColorScheme", {
    pattern = "*",
    command = "highlight TabLine ctermbg=DarkGreen ctermfg=LightGreen guibg=Grey23 guifg=DarkSeaGreen",
})

vim.api.nvim_create_autocmd("ColorScheme", {
    pattern = "*",
    command = "highlight TabLineSel ctermbg=DarkGreen ctermfg=LightGreen guibg=DeepSkyBlue4 guifg=Yellow3",
})

vim.g.desertrocks_show_whitespace = 1
vim.cmd([[colorscheme desertrocks]])
if vim.g.desertrocks_show_whitespace then
    local ag = vim.api.nvim_create_augroup("desertrocks_show_whitespace", { clear = true })
    vim.api.nvim_create_autocmd(
        "VimEnter",
        { -- using VimEnter instead of Syntax because nvim sets syntax on and the event never triggers/it deletes the existing autocommands
            pattern = "*",
            command = [[syntax match Tab /\v\t/ containedin=ALL | syntax match TrailingWS /\v\s\ze\s*$/ containedin=ALL]],
            group = ag,
        }
    )
    vim.cmd([[highlight Tab ctermbg=240 guibg=Grey50]])
    vim.cmd([[highlight TrailingWS ctermbg=203 guibg=IndianRed1]])
end

-- Set completeopt to have a better completion experience
vim.o.completeopt = "menuone,noselect"

-- [[ Basic Keymaps ]]
-- Set \ as the leader key
-- See `:help mapleader`
--  NOTE: Must happen before plugins are required (otherwise wrong leader will be used)
vim.g.mapleader = "\\"
vim.g.maplocalleader = "\\"

-- Keymaps for better default experience
-- See `:help vim.keymap.set()`
-- this seems to matter when <Space> is leader key
-- vim.keymap.set({ 'n', 'v' }, '<Space>', '<Nop>', { silent = true })

-- Remap for dealing with word wrap (so that j/k default to going up/down a word-wrapped line)
-- vim.keymap.set('n', 'k', "v:count == 0 ? 'gk' : 'k'", { expr = true, silent = true })
-- vim.keymap.set('n', 'j', "v:count == 0 ? 'gj' : 'j'", { expr = true, silent = true })

-- useful maps
vim.keymap.set("n", "\\l", ":IndentBlanklineToggle<CR>")
vim.keymap.set("n", "\\c", ":close<CR>")
-- close all windows except active - won't autowrite as long as you don't fuck
-- with hidden and autowrite, always use the vim-sensible
vim.keymap.set("n", "\\o", ":only<CR>")
vim.keymap.set("n", "\\s", ":tab split<CR>")

-- useful to rearrange tabs
-- i wanted to use \n and \p for next and prev
-- but i went with (p)rev and undo (P)rev
-- so i can keep \n for numbers
vim.keymap.set("n", "\\p", ":-tabmove<CR>")
vim.keymap.set("n", "\\P", ":+tabmove<CR>")
vim.keymap.set("n", "\\[", ":-tabmove<CR>")
vim.keymap.set("n", "\\]", ":+tabmove<CR>")

-- easy tab switching
vim.keymap.set("n", "\\1", ":tabn 1<CR>")
vim.keymap.set("n", "\\2", ":tabn 2<CR>")
vim.keymap.set("n", "\\3", ":tabn 3<CR>")
vim.keymap.set("n", "\\4", ":tabn 4<CR>")
vim.keymap.set("n", "\\5", ":tabn 5<CR>")
vim.keymap.set("n", "\\6", ":tabn 6<CR>")
vim.keymap.set("n", "\\7", ":tabn 7<CR>")
vim.keymap.set("n", "\\8", ":tabn 8<CR>")
vim.keymap.set("n", "\\9", ":tabn 9<CR>")
vim.keymap.set("n", "\\0", ":tabn 0<CR>")

-- faster quit
vim.keymap.set("n", "\\q", ":q<CR>")

-- courtesy of JoshTriplett:
--   https://news.ycombinator.com/item?id=40839361
vim.keymap.set("n", "\\.", ":Texplore<CR>")
vim.keymap.set("n", "\\v", ":Vexplore<CR>")

-- convenient to open a new tab instead of :tabnew
vim.keymap.set("n", "\\t", ":tabnew<CR>")

-- toggle line numbers
vim.keymap.set("n", "\\n", ":set nu!<CR>")

-- toggle gutter/signcolumn
vim.keymap.set("n", "\\g", function()
    vim.wo.signcolumn = vim.wo.signcolumn == "no" and "yes" or "no"
end)

-- lsp format
vim.keymap.set("n", "\\f", ":lua require('conform').format()<CR>")

-- [[ Highlight on yank ]]
-- See `:help vim.highlight.on_yank()`
local highlight_group = vim.api.nvim_create_augroup("YankHighlight", { clear = true })
vim.api.nvim_create_autocmd("TextYankPost", {
    callback = function()
        vim.highlight.on_yank()
    end,
    group = highlight_group,
    pattern = "*",
})

-- Set lualine as statusline
-- See `:help lualine.txt`
require("lualine").setup({
    options = {
        icons_enabled = false,
        theme = "onedark",
        component_separators = "|",
        section_separators = "",
    },
})

-- Enable Comment.nvim
require("Comment").setup()

-- Gitsigns
-- See `:help gitsigns.txt`
require("gitsigns").setup({
    signs = {
        add = { text = "+" },
        change = { text = "~" },
        delete = { text = "_" },
        topdelete = { text = "‾" },
        changedelete = { text = "~" },
    },
})

-- [[ Configure Telescope ]]
-- See `:help telescope` and `:help telescope.setup()`
require("telescope").setup({
    defaults = {
        mappings = {
            i = {
                ["<C-u>"] = false,
                ["<C-d>"] = false,
            },
        },
    },
})

-- Enable telescope fzf native, if installed
pcall(require("telescope").load_extension, "fzf")

-- See `:help telescope.builtin`
vim.keymap.set("n", "<leader>?", require("telescope.builtin").oldfiles, { desc = "[?] Find recently opened files" })
vim.keymap.set("n", "<leader><space>", require("telescope.builtin").buffers, { desc = "[ ] Find existing buffers" })
vim.keymap.set("n", "<leader>/", function()
    -- You can pass additional configuration to telescope to change theme, layout, etc.
    require("telescope.builtin").current_buffer_fuzzy_find(require("telescope.themes").get_dropdown({
        winblend = 10,
        previewer = false,
    }))
end, { desc = "[/] Fuzzily search in current buffer]" })

vim.keymap.set("n", "<leader>ff", require("telescope.builtin").find_files, { desc = "[S]earch [F]iles" })
vim.keymap.set("n", "<leader>sh", require("telescope.builtin").help_tags, { desc = "[S]earch [H]elp" })
vim.keymap.set("n", "<leader>gs", require("telescope.builtin").grep_string, { desc = "[S]earch current [W]ord" })
vim.keymap.set("n", "<leader>lg", require("telescope.builtin").live_grep, { desc = "[S]earch by [G]rep" })
vim.keymap.set("n", "<leader>sd", require("telescope.builtin").diagnostics, { desc = "[S]earch [D]iagnostics" })

-- [[ Configure Treesitter ]]
-- See `:help nvim-treesitter`
require("nvim-treesitter.configs").setup({
    -- Add languages to be installed here that you want installed for treesitter
    ensure_installed = { "c", "cpp", "go", "lua", "python", "rust", "tsx", "typescript", "vimdoc", "vim" },

    highlight = { enable = true },
    indent = { enable = true, disable = { "python" } },
    incremental_selection = {
        enable = true,
        keymaps = {
            init_selection = "<c-space>",
            node_incremental = "<c-space>",
            scope_incremental = "<c-s>",
            node_decremental = "<c-backspace>",
        },
    },
    textobjects = {
        select = {
            enable = true,
            lookahead = true, -- Automatically jump forward to textobj, similar to targets.vim
            keymaps = {
                -- You can use the capture groups defined in textobjects.scm
                ["aa"] = "@parameter.outer",
                ["ia"] = "@parameter.inner",
                ["af"] = "@function.outer",
                ["if"] = "@function.inner",
                ["ac"] = "@class.outer",
                ["ic"] = "@class.inner",
            },
        },
        move = {
            enable = true,
            set_jumps = true, -- whether to set jumps in the jumplist
            goto_next_start = {
                ["]m"] = "@function.outer",
                ["]]"] = "@class.outer",
            },
            goto_next_end = {
                ["]M"] = "@function.outer",
                ["]["] = "@class.outer",
            },
            goto_previous_start = {
                ["[m"] = "@function.outer",
                ["[["] = "@class.outer",
            },
            goto_previous_end = {
                ["[M"] = "@function.outer",
                ["[]"] = "@class.outer",
            },
        },
        swap = {
            enable = true,
            swap_next = {
                ["<leader>a"] = "@parameter.inner",
            },
            swap_previous = {
                ["<leader>A"] = "@parameter.inner",
            },
        },
    },
})

-- Diagnostic keymaps
vim.keymap.set("n", "[d", vim.diagnostic.goto_prev)
vim.keymap.set("n", "]d", vim.diagnostic.goto_next)
vim.keymap.set("n", "<leader>e", vim.diagnostic.open_float)
vim.keymap.set("n", "<leader>q", vim.diagnostic.setloclist)

-- LSP settings.
--  This function gets run when an LSP connects to a particular buffer.
local on_attach = function(_, bufnr)
    -- NOTE: Remember that lua is a real programming language, and as such it is possible
    -- to define small helper and utility functions so you don't have to repeat yourself
    -- many times.
    --
    -- In this case, we create a function that lets us more easily define mappings specific
    -- for LSP related items. It sets the mode, buffer and description for us each time.
    local nmap = function(keys, func, desc)
        if desc then
            desc = "LSP: " .. desc
        end

        vim.keymap.set("n", keys, func, { buffer = bufnr, desc = desc })
    end

    nmap("<leader>rn", vim.lsp.buf.rename, "[R]e[n]ame")
    nmap("<leader>ca", vim.lsp.buf.code_action, "[C]ode [A]ction")

    nmap("gd", vim.lsp.buf.definition, "[G]oto [D]efinition")
    nmap("gr", require("telescope.builtin").lsp_references, "[G]oto [R]eferences")
    nmap("gI", vim.lsp.buf.implementation, "[G]oto [I]mplementation")
    nmap("<leader>D", vim.lsp.buf.type_definition, "Type [D]efinition")
    nmap("<leader>ds", require("telescope.builtin").lsp_document_symbols, "[D]ocument [S]ymbols")
    nmap("<leader>ws", require("telescope.builtin").lsp_dynamic_workspace_symbols, "[W]orkspace [S]ymbols")

    -- See `:help K` for why this keymap
    nmap("K", vim.lsp.buf.hover, "Hover Documentation")
    nmap("<C-k>", vim.lsp.buf.signature_help, "Signature Documentation")

    -- Lesser used LSP functionality
    nmap("gD", vim.lsp.buf.declaration, "[G]oto [D]eclaration")
    nmap("<leader>wa", vim.lsp.buf.add_workspace_folder, "[W]orkspace [A]dd Folder")
    nmap("<leader>wr", vim.lsp.buf.remove_workspace_folder, "[W]orkspace [R]emove Folder")
    nmap("<leader>wl", function()
        print(vim.inspect(vim.lsp.buf.list_workspace_folders()))
    end, "[W]orkspace [L]ist Folders")

    -- Create a command `:Format` local to the LSP buffer
    vim.api.nvim_buf_create_user_command(bufnr, "Format", function(_)
        require("conform").format()
    end, { desc = "Format current buffer with LSP" })
end

-- Enable the following language servers
--  Feel free to add/remove any LSPs that you want here. They will automatically be installed.
--
--  Add any additional override configuration in the following tables. They will be passed to
--  the `settings` field of the server config. You must look up that documentation yourself.
local servers = {
    clangd = {},
    gopls = {},
    rust_analyzer = {},
    pylsp = {},

    lua_ls = {
        Lua = {
            workspace = { checkThirdParty = false },
            telemetry = { enable = false },
        },
    },
}

-- Setup neovim lua configuration
require("neodev").setup()
--
-- nvim-cmp supports additional completion capabilities, so broadcast that to servers
local capabilities = vim.lsp.protocol.make_client_capabilities()
capabilities = require("cmp_nvim_lsp").default_capabilities(capabilities)

-- Setup mason so it can manage external tooling
require("mason").setup()

-- Ensure the servers above are installed
local mason_lspconfig = require("mason-lspconfig")

mason_lspconfig.setup({
    ensure_installed = vim.tbl_keys(servers),
})

mason_lspconfig.setup_handlers({
    function(server_name)
        require("lspconfig")[server_name].setup({
            capabilities = capabilities,
            on_attach = on_attach,
            settings = servers[server_name],
        })
    end,
})

-- nvim-cmp setup
local cmp = require("cmp")
local luasnip = require("luasnip")

cmp.setup({
    snippet = {
        expand = function(args)
            luasnip.lsp_expand(args.body)
        end,
    },
    mapping = cmp.mapping.preset.insert({
        ["<C-d>"] = cmp.mapping.scroll_docs(-4),
        ["<C-f>"] = cmp.mapping.scroll_docs(4),
        ["<C-Space>"] = cmp.mapping.complete(),
        ["<CR>"] = cmp.mapping.confirm({
            behavior = cmp.ConfirmBehavior.Replace,
            select = true,
        }),
        ["<Tab>"] = cmp.mapping(function(fallback)
            if cmp.visible() then
                cmp.select_next_item()
            elseif luasnip.expand_or_jumpable() then
                luasnip.expand_or_jump()
            else
                fallback()
            end
        end, { "i", "s" }),
        ["<S-Tab>"] = cmp.mapping(function(fallback)
            if cmp.visible() then
                cmp.select_prev_item()
            elseif luasnip.jumpable(-1) then
                luasnip.jump(-1)
            else
                fallback()
            end
        end, { "i", "s" }),
    }),
    sources = {
        { name = "nvim_lsp" },
        { name = "luasnip" },
    },
})

vim.g.diagnostics_active = true

function _G.toggle_diagnostics()
    if vim.g.diagnostics_active then
        vim.g.diagnostics_active = false
        vim.diagnostic.disable()
    else
        vim.g.diagnostics_active = true
        vim.diagnostic.enable()
    end
end

vim.api.nvim_set_keymap("n", "\\d", ":call v:lua.toggle_diagnostics()<CR>", { noremap = true, silent = true })

require("conform").setup({
    formatters_by_ft = {
        lua = { "stylua" },
        -- Conform will run multiple formatters sequentially
        python = { "isort", "black" },
        -- You can customize some of the format options for the filetype (:help conform.format)
        rust = { "rustfmt", lsp_format = "fallback" },
        -- Conform will run the first available formatter
        javascript = { "prettierd", "prettier", stop_after_first = true },
        -- Conform for gopls
        go = { "gofumpt" },
    },
})

-- setup format on save
-- !!!
--     This is the bit that doesn't work.
--     If I change conform/init.lua such that `group = aug` is commented
--     in the autocmds that are created, then everything works correctly.
-- !!!
require("conform").setup({
    format_on_save = {
        -- These options will be passed to conform.format()
        -- timeout for 10s for mac where first exec of binary
        -- takes awhile
        timeout_ms = 10000,
        lsp_format = "fallback",
    },
})

--au FileType go set shiftwidth=4
vim.api.nvim_create_autocmd("FileType", {
    pattern = "go",
    callback = function()
        vim.opt_local.tabstop = 4
    end,
})

vim.api.nvim_create_autocmd("FileType", {
    pattern = "lua",
    callback = function()
        vim.opt_local.tabstop = 2
    end,
})

Additional context

No response

stevearc commented 3 days ago

You are calling conform.setup() three times. Each time you call it, we will clear any autocmds created previously (and if configured, create new ones). What is likely happening is you're doing this:

require("conform").setup({
    formatters_by_ft = {
        lua = { "stylua" },
        -- Conform will run multiple formatters sequentially
        python = { "isort", "black" },
        -- You can customize some of the format options for the filetype (:help conform.format)
        rust = { "rustfmt", lsp_format = "fallback" },
        -- Conform will run the first available formatter
        javascript = { "prettierd", "prettier", stop_after_first = true },
        -- Conform for gopls
        go = { "gofumpt" },
    },
})

Autocmds were cleared, none created. No-op.

require("conform").setup({
    format_on_save = {
        -- These options will be passed to conform.format()
        -- timeout for 10s for mac where first exec of binary
        -- takes awhile
        timeout_ms = 10000,
        lsp_format = "fallback",
    },
})

Autocmds created now

config = function()
  require("conform").setup()
end,

Autocmds cleared again

I would recommend only calling setup() once with all of your config options.