You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让nvim-cmp自动补全按Tab时选中建议项?

关于nvim-cmp Tab键选中补全建议的配置调整

现有配置

return {
  'hrsh7th/nvim-cmp',
  event = 'InsertEnter',
  dependencies = {
    'hrsh7th/cmp-buffer',
    'hrsh7th/cmp-path',
    'hrsh7th/cmp-nvim-lsp',
    {
      'L3MON4D3/LuaSnip',
      version = 'v2.*',
      build = 'make install_jsregexp',
    },
    'saadparwaiz1/cmp_luasnip',
    'rafamadriz/friendly-snippets',
    'onsails/lspkind.nvim',
  },
  config = function()
    local cmp = require('cmp')
    local luasnip = require('luasnip')
    local lspkind = require('lspkind')
    require('luasnip.loaders.from_vscode').lazy_load()
    -- Disable preselection at LSP capabilities level
    local capabilities = require('cmp_nvim_lsp').default_capabilities()
    capabilities.completion.completionItem.preselectSupported = false
    cmp.setup({
      preselect = cmp.PreselectMode.None,
      completion = { completeopt = 'menu,menuone,preview,noselect,noinsert' },
      snippet = {
        expand = function(args)
          luasnip.lsp_expand(args.body)
        end,
      },
      mapping = {
        ['<C-n>'] = cmp.mapping.select_next_item(
          { behavior = cmp.SelectBehavior.Select },
          { desc = 'Select next item' }
        ),
        ['<C-p>'] = cmp.mapping.select_prev_item(
          { behavior = cmp.SelectBehavior.Select },
          { desc = 'Select previous item' }
        ),
        ['<C-b>'] = cmp.mapping.scroll_docs(-4, { desc = 'Scroll docs up' }),
        ['<C-f>'] = cmp.mapping.scroll_docs(4, { desc = 'Scroll docs down' }),
        ['<C-Space>'] = cmp.mapping.complete({ desc = 'Trigger completion' }),
        ['<C-e>'] = cmp.mapping.abort({ desc = 'Abort completion' }),
        ['<CR>'] = cmp.mapping(function(fallback)
          if cmp.visible() and cmp.get_selected_entry() then
            cmp.confirm({ select = false })
          else
            fallback()
          end
        end, { 'i', 's', desc = 'Confirm selection' }),
        ['<Tab>'] = cmp.mapping(function(fallback)
          if cmp.visible() then
            cmp.select_next_item()
          else
            fallback()
          end
        end, { 'i', 's', desc = 'Next item or indent' }),
        ['<S-Tab>'] = cmp.mapping(function(fallback)
          if cmp.visible() then
            cmp.select_prev_item()
          else
            fallback()
          end
        end, { 'i', 's', desc = 'Previous item or dedent' }),
      },
      sources = cmp.config.sources({
        { name = 'nvim_lsp' },
        { name = 'luasnip' },
        { name = 'buffer' },
        { name = 'path' },
      }),
      formatting = {
        format = lspkind.cmp_format({
          maxwidth = 50,
          ellipsis_char = '...',
        }),
      },
    })
    -- Automatically deselect first item when menu opens
    cmp.event:on('menu_opened', function()
      vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('<Up>', true, true, true), 'n', true)
    end)
    -- Pass capabilities to your LSP servers (add this to your lspconfig setup)
    -- Example: require("lspconfig").lua_ls.setup({ capabilities = capabilities })
  end,
}

已尝试操作

  • 在completion配置中添加noinsert参数
  • 自定义回车键映射规则
  • 绑定menu_opened事件,自动按<Up>取消初始预选

需求

实现自动补全菜单弹出时,按Tab(或其他按键)选中补全建议项,支持切换选择。

调整方案

方案1:保留无预选初始状态,Tab优先选中第一个项

修改<Tab>和<S-Tab>的映射逻辑,当补全菜单可见但无选中项时,直接选中第一个/最后一个项,同时兼容LuaSnip的展开/跳转:

['<Tab>'] = cmp.mapping(function(fallback)
  if cmp.visible() then
    -- 无选中项时先选中第一个,否则切换到下一个
    if not cmp.get_selected_entry() then
      cmp.select_next_item({ behavior = cmp.SelectBehavior.Select })
    else
      cmp.select_next_item({ behavior = cmp.SelectBehavior.Select })
    end
  elseif luasnip.expand_or_jumpable() then
    luasnip.expand_or_jump()
  else
    fallback()
  end
end, { 'i', 's', desc = 'Select next item or expand snippet' }),
['<S-Tab>'] = cmp.mapping(function(fallback)
  if cmp.visible() then
    -- 无选中项时先选中最后一个,否则切换到上一个
    if not cmp.get_selected_entry() then
      cmp.select_prev_item({ behavior = cmp.SelectBehavior.Select })
    else
      cmp.select_prev_item({ behavior = cmp.SelectBehavior.Select })
    end
  elseif luasnip.jumpable(-1) then
    luasnip.jump(-1)
  else
    fallback()
  end
end, { 'i', 's', desc = 'Select previous item or jump back snippet' }),

方案2:移除自动取消预选逻辑,Tab直接从首个项切换

如果不需要初始无预选的状态,可删除menu_opened事件的绑定代码:

-- 删除这段代码
-- cmp.event:on('menu_opened', function()
--   vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('<Up>', true, true, true), 'n', true)
-- end)

此时补全菜单弹出时默认选中第一个项,按Tab直接切换到下一个,<S-Tab>切换到上一个。

额外优化:选中时自动插入文本

若希望选中补全项时直接将内容插入缓冲区,可将cmp.SelectBehavior.Select改为cmp.SelectBehavior.Insert:

cmp.select_next_item({ behavior = cmp.SelectBehavior.Insert })

最终完整配置(方案1示例)

return {
  'hrsh7th/nvim-cmp',
  event = 'InsertEnter',
  dependencies = {
    'hrsh7th/cmp-buffer',
    'hrsh7th/cmp-path',
    'hrsh7th/cmp-nvim-lsp',
    {
      'L3MON4D3/LuaSnip',
      version = 'v2.*',
      build = 'make install_jsregexp',
    },
    'saadparwaiz1/cmp_luasnip',
    'rafamadriz/friendly-snippets',
    'onsails/lspkind.nvim',
  },
  config = function()
    local cmp = require('cmp')
    local luasnip = require('luasnip')
    local lspkind = require('lspkind')
    require('luasnip.loaders.from_vscode').lazy_load()
    -- Disable preselection at LSP capabilities level
    local capabilities = require('cmp_nvim_lsp').default_capabilities()
    capabilities.completion.completionItem.preselectSupported = false
    cmp.setup({
      preselect = cmp.PreselectMode.None,
      completion = { completeopt = 'menu,menuone,preview,noselect,noinsert' },
      snippet = {
        expand = function(args)
          luasnip.lsp_expand(args.body)
        end,
      },
      mapping = {
        ['<C-n>'] = cmp.mapping.select_next_item(
          { behavior = cmp.SelectBehavior.Select },
          { desc = 'Select next item' }
        ),
        ['<C-p>'] = cmp.mapping.select_prev_item(
          { behavior = cmp.SelectBehavior.Select },
          { desc = 'Select previous item' }
        ),
        ['<C-b>'] = cmp.mapping.scroll_docs(-4, { desc = 'Scroll docs up' }),
        ['<C-f>'] = cmp.mapping.scroll_docs(4, { desc = 'Scroll docs down' }),
        ['<C-Space>'] = cmp.mapping.complete({ desc = 'Trigger completion' }),
        ['<C-e>'] = cmp.mapping.abort({ desc = 'Abort completion' }),
        ['<CR>'] = cmp.mapping(function(fallback)
          if cmp.visible() and cmp.get_selected_entry() then
            cmp.confirm({ select = false })
          else
            fallback()
          end
        end, { 'i', 's', desc = 'Confirm selection' }),
        ['<Tab>'] = cmp.mapping(function(fallback)
          if cmp.visible() then
            if not cmp.get_selected_entry() then
              cmp.select_next_item({ behavior = cmp.SelectBehavior.Select })
            else
              cmp.select_next_item({ behavior = cmp.SelectBehavior.Select })
            end
          elseif luasnip.expand_or_jumpable() then
            luasnip.expand_or_jump()
          else
            fallback()
          end
        end, { 'i', 's', desc = 'Select next item or expand snippet' }),
        ['<S-Tab>'] = cmp.mapping(function(fallback)
          if cmp.visible() then
            if not cmp.get_selected_entry() then
              cmp.select_prev_item({ behavior = cmp.SelectBehavior.Select })
            else
              cmp.select_prev_item({ behavior = cmp.SelectBehavior.Select })
            end
          elseif luasnip.jumpable(-1) then
            luasnip.jump(-1)
          else
            fallback()
          end
        end, { 'i', 's', desc = 'Select previous item or jump back snippet' }),
      },
      sources = cmp.config.sources({
        { name = 'nvim_lsp' },
        { name = 'luasnip' },
        { name = 'buffer' },
        { name = 'path' },
      }),
      formatting = {
        format = lspkind.cmp_format({
          maxwidth = 50,
          ellipsis_char = '...',
        }),
      },
    })
    -- Automatically deselect first item when menu opens
    cmp.event:on('menu_opened', function()
      vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('<Up>', true, true, true), 'n', true)
    end)
    -- Pass capabilities to your LSP servers (add this to your lspconfig setup)
    -- Example: require("lspconfig").lua_ls.setup({ capabilities = capabilities })
  end,
}

内容的提问来源于stack exchange,提问作者Bruce

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 04:04:54