Toggle nvim-cmp keybinding

2025-07-27

I use nvim-cmp 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" }, "<C-k>", 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
["<C-k>"] = 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" }, "<C-k>", 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
					["<C-k>"] = cmp.mapping(function() 
						toggle_cmp_buffer() 
					end, { "i", "s" }),

					-- <Enter> will insert return unless item selected
					-- Insert snippet if selected
					['<CR>'] = 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
						}),
					}),
					
					-- <Tab> will select next item in list
					["<Tab>"] = 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" }),
					
					-- <Shift-Tab> will select previous item in list
					["<S-Tab>"] = 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,
	},
}