Toggle nvim-cmp keybinding

DATE: 2025-07-27

AUTHOR: John L. Godlee

I use nvim-cmp[1] in neovim for auto-completion. It's great for programming, but if I'm writing prose I only rarely want completion, for example if there's a difficult to spell species name that I want to repeat. I added to my nvim-cmp lua config a function and keybinding which toggles completion in any buffer.

These are the relevant bits of the config. Full config at the end:

This function goes in the config = function() part of the config, and defines the function to toggle completion in the local buffer, with a notification showing the status of the toggle. Below this is a keybinding (<C-k>) which runs the function in normal and visual modes.

-- Define custom function to toggle cmp
local function toggle_cmp_buffer() 
    local buf_enabled = cmp.get_config().enabled 
    cmp.setup.buffer({ enabled = not buf_enabled })
    vim.notify("nvim-cmp " .. (not buf_enabled and "enabled" or "disabled") .. " in buffer", vim.log.levels.INFO)
end
vim.keymap.set({ "n", "v" }, "", toggle_cmp_buffer, { 
    noremap = true, silent = true 
})

This next part goes in the cmp.setup({ ... mapping = { section alongside other cmp keybindings. It simply runs the function using the same keybinding as above, but in insert and select modes, since nvim-cmp is only active in these modes.

-- Toggle cmp in insert and select modes
[""] = cmp.mapping(function() 
    toggle_cmp_buffer() 
end, { "i", "s" }),

Full nvim-cmp config:

-- Completion
return {
    {
    	"hrsh7th/nvim-cmp",
    	event = { "InsertEnter", "CmdlineEnter" },
    	dependencies = {
    		"hrsh7th/cmp-buffer",
    		"hrsh7th/cmp-path",
    		"hrsh7th/cmp-cmdline",
    		{
    			"L3MON4D3/LuaSnip",
    			version = "v2.*",
    		},
    		"saadparwaiz1/cmp_luasnip",
    		"onsails/lspkind.nvim",
    		"R-nvim/cmp-r",
    	},
    	config = function()
    		local cmp = require("cmp")
    		local luasnip = require("luasnip")
    		local lspkind = require("lspkind")
    		-- Load SnipMate style snippets
    		require("luasnip.loaders.from_snipmate").lazy_load()

    		-- Define custom function to only expand if preceded
    		local has_words_before = function()
    			unpack = unpack or table.unpack
    			local line, col = unpack(vim.api.nvim_win_get_cursor(0))
    			return col ~= 0 and vim.api.nvim_buf_get_lines(0, line - 1, line, true)[1]:sub(col, col):match("%s") == nil
    		end

    		-- Define custom function to toggle cmp
    		local function toggle_cmp_buffer() 
    			local buf_enabled = cmp.get_config().enabled 
    			cmp.setup.buffer({ enabled = not buf_enabled })
    			vim.notify("nvim-cmp " .. (not buf_enabled and "enabled" or "disabled") .. " in buffer", vim.log.levels.INFO)
    		end
    		vim.keymap.set({ "n", "v" }, "", toggle_cmp_buffer, { 
    			noremap = true, silent = true 
    		})

    		cmp.setup({ 
    			completion = {
    				completeopt = "menu,menuone,preview,noselect",
    			},

    			snippet = {
    				expand = function(args)
    					luasnip.lsp_expand(args.body)
    				end,
    			},

    			mapping = {
    				-- Toggle cmp in insert and select modes
    				[""] = cmp.mapping(function() 
    					toggle_cmp_buffer() 
    				end, { "i", "s" }),

    				--  will insert return unless item selected
    				-- Insert snippet if selected
    				[''] = cmp.mapping({
    					i = function(fallback)
    						if cmp.visible() and cmp.get_active_entry() then
    							if luasnip.expandable() then
    								luasnip.expand()
    							else
    								cmp.confirm({
    									select = true,
    								})
    							end
    						else 
    							fallback()
    						end
    					end,
    					s = cmp.mapping.confirm({ select = false }),
    					c = cmp.mapping.confirm({ 
    						behavior = cmp.ConfirmBehavior.Replace,
    						select = false
    					}),
    				}),

    				--  will select next item in list
    				[""] = cmp.mapping(function(fallback)
    					if cmp.visible() then
    						cmp.select_next_item()
    					elseif luasnip.locally_jumpable(1) then
    						luasnip.jump(1) 
    					elseif has_words_before() then
    						cmp.complete()
    						if #cmp.get_entries() == 1 then
    							cmp.confirm({ select = true })
    						end
    					else
    						fallback()
    					end
    				end, { "i", "s" }),

    				--  will select previous item in list
    				[""] = cmp.mapping(function(fallback)
    				  if cmp.visible() then
    					cmp.select_prev_item()
    				  elseif luasnip.locally_jumpable(-1) then
    					luasnip.jump(-1)
    				  else
    					fallback()
    				  end
    				end, { "i", "s" }),
    			},
    			sources = cmp.config.sources({
    				{ name = "luasnip" },
    				{ name = "buffer" },
    				{ name = "path" },
    				{ name = "cmp_r" },
    			}),
    			formatting = {
    				format = function(entry, vim_item)
    				if vim.tbl_contains({ 'path' }, entry.source.name) then
    					local icon, hl_group = require('nvim-web-devicons').get_icon(entry:get_completion_item().label)
    					if icon then
    						vim_item.kind = icon
    						vim_item.kind_hl_group = hl_group
    						return vim_item
    					end
    				end
    				return require('lspkind').cmp_format({ with_text = true })(entry, vim_item)
    				end
    			},
    		})

    		-- Use buffer source for `/` and `?` 
    		cmp.setup.cmdline({ '/', '?' }, {
    			mapping = cmp.mapping.preset.cmdline(),
    			sources = {
    				{ name = 'buffer' }
    			}
    		})

    		-- Use cmdline & path source for ':'
    		cmp.setup.cmdline(':', {
    			mapping = cmp.mapping.preset.cmdline(),
    			sources = cmp.config.sources({
    				{ name = 'path' }
    			}, {
    				{ name = 'cmdline' }
    			}),
    			matching = { disallow_symbol_nonprefix_matching = false }
    		})
    	end,
    },
}