Skip to content

Recipes

General

Disable per filetype

lua
enabled = function() return not vim.tbl_contains({ "lua", "markdown" }, vim.bo.filetype) end,

Disable completion in only shell command mode

Windows when inside of git bash or WSL may cause a hang with shell commands. This disables cmdline completions only when running shell commands ( i.e. [ ':!' , ':%!' ] ), but still allows completion in other command modes ( i.e. [ ':' , ':help', '/' , '?' ] etc ).

lua
sources = {
  providers = {
    cmdline = {
      -- ignores cmdline completions when executing shell commands
      enabled = function()
        return vim.fn.getcmdtype() ~= ':' or not vim.fn.getcmdline():match("^[%%0-9,'<>%-]*!")
      end
    }
  }
}

Emacs behavior

Full discussion: https://github.com/Saghen/blink.cmp/issues/1367

Tab: If completion hasn't been triggered yet, insert the first suggestion; if it has, cycle to the next suggestion.

Shift-Tab: Navigate to the previous suggestion or cancel completion if currently on the first one.

lua
-- helper function to check if there's a word before the cursor.
local has_words_before = function()
  local line, col = unpack(vim.api.nvim_win_get_cursor(0))
  if col == 0 then
    return false
  end
  local text = vim.api.nvim_buf_get_lines(0, line - 1, line, true)[1]
  return text:sub(col, col):match("%s") == nil
end

-- in your blink configuration
keymap = {
  preset = 'none',

  ['<Tab>'] = {
    function(cmp)
      if has_words_before() then
        return cmp.insert_next()
      end
    end,
    'fallback',
  },
  ['<S-Tab>'] = { 'insert_prev' },
},
completion = {
  menu = { enabled = false },
  list = { selection = { preselect = false }, cycle = { from_top = false } },
}

Border

lua
completion = {
  menu = { border = 'single' },
  documentation = { window = { border = 'single' } },
},
signature = { window = { border = 'single' } },

Select Nth item from the list

Here's an example configuration that allows you to select the nth item from the list, based on #382:

lua
keymap = {
  preset = 'default',
  ['<A-1>'] = { function(cmp) cmp.accept({ index = 1 }) end },
  ['<A-2>'] = { function(cmp) cmp.accept({ index = 2 }) end },
  ['<A-3>'] = { function(cmp) cmp.accept({ index = 3 }) end },
  ['<A-4>'] = { function(cmp) cmp.accept({ index = 4 }) end },
  ['<A-5>'] = { function(cmp) cmp.accept({ index = 5 }) end },
  ['<A-6>'] = { function(cmp) cmp.accept({ index = 6 }) end },
  ['<A-7>'] = { function(cmp) cmp.accept({ index = 7 }) end },
  ['<A-8>'] = { function(cmp) cmp.accept({ index = 8 }) end },
  ['<A-9>'] = { function(cmp) cmp.accept({ index = 9 }) end },
  ['<A-0>'] = { function(cmp) cmp.accept({ index = 10 }) end },
},
completion = {
  menu = {
    draw = {
      columns = { { 'item_idx' }, { 'kind_icon' }, { 'label', 'label_description', gap = 1 } },
      components = {
        item_idx = {
          text = function(ctx) return ctx.idx == 10 and '0' or ctx.idx >= 10 and ' ' or tostring(ctx.idx) end,
          highlight = 'BlinkCmpItemIdx' -- optional, only if you want to change its color
        }
      }
    }
  }
}

Hide Copilot on suggestion

lua
vim.api.nvim_create_autocmd('User', {
  pattern = 'BlinkCmpMenuOpen',
  callback = function()
    require("copilot.suggestion").dismiss()
    vim.b.copilot_suggestion_hidden = true
  end,
})

vim.api.nvim_create_autocmd('User', {
  pattern = 'BlinkCmpMenuClose',
  callback = function()
    vim.b.copilot_suggestion_hidden = false
  end,
})

Show on newline, tab and space

WARNING

This may not be working as expected at the moment. Please see #836

Note that you may want to add the override to other sources as well, since if the LSP doesn't return any items, we won't show the menu if it was triggered by any of these three characters.

lua
-- by default, blink.cmp will block newline, tab and space trigger characters, disable that behavior
completion.trigger.show_on_blocked_trigger_characters = {}

-- add newline, tab and space to LSP source trigger characters
sources.providers.lsp.override.get_trigger_characters = function(self)
  local trigger_characters = self:get_trigger_characters()
  vim.list_extend(trigger_characters, { '\n', '\t', ' ' })
  return trigger_characters
end

Fuzzy (sorting/filtering)

Always prioritize exact matches

By default, the fuzzy matcher will give a bonus score of 4 to exact matches. If you want to ensure that exact matches are always prioritized, you may set

lua
fuzzy = {
  sorts = {
    'exact',
    -- defaults
    'score',
    'sort_text',
  },
}

Deprioritize specific LSP

You may use a custom sort function to deprioritize LSPs such as Emmet Language Server (emmet_ls)

lua
fuzzy = {
  sorts = {
    function(a, b)
      if (a.client_name == nil or b.client_name == nil) or (a.client_name == b.client_name) then
        return
      end
      return b.client_name == 'emmet_ls'
    end,
    -- default sorts
    'score',
    'sort_text',
}

Completion menu drawing

mini.icons

Original discussion

lua
completion = {
  menu = {
    draw = {
      components = {
        kind_icon = {
          ellipsis = false,
          text = function(ctx)
            local kind_icon, _, _ = require('mini.icons').get('lsp', ctx.kind)
            return kind_icon
          end,
          -- Optionally, you may also use the highlights from mini.icons
          highlight = function(ctx)
            local _, hl, _ = require('mini.icons').get('lsp', ctx.kind)
            return hl
          end,
        }
      }
    }
  }
}

nvim-web-devicons + lspkind

Original discussion

lua
completion = {
  menu = {
    draw = {
      components = {
        kind_icon = {
          ellipsis = false,
          text = function(ctx)
            local lspkind = require("lspkind")
            local icon = ctx.kind_icon
            if vim.tbl_contains({ "Path" }, ctx.source_name) then
                local dev_icon, _ = require("nvim-web-devicons").get_icon(ctx.label)
                if dev_icon then
                    icon = dev_icon
                end
            else
                icon = require("lspkind").symbolic(ctx.kind, {
                    mode = "symbol",
                })
            end

            return icon .. ctx.icon_gap
          end,

          -- Optionally, use the highlight groups from nvim-web-devicons
          -- You can also add the same function for `kind.highlight` if you want to
          -- keep the highlight groups in sync with the icons.
          highlight = function(ctx)
            local hl = ctx.kind_hl
            if vim.tbl_contains({ "Path" }, ctx.source_name) then
              local dev_icon, dev_hl = require("nvim-web-devicons").get_icon(ctx.label)
              if dev_icon then
                hl = dev_hl
              end
            end
            return hl
          end,
        }
      }
    }
  }
}

Sources

Buffer completion from all open buffers

The default behavior is to only show completions from visible "normal" buffers (i.e. it wouldn't include neo-tree). This will instead show completions from all buffers, even if they're not visible on screen. Note that the performance impact of this has not been tested.

lua
sources = {
  providers = {
    buffer = {
      opts = {
        -- get all buffers, even ones like neo-tree
        get_bufnrs = vim.api.nvim_list_bufs
        -- or (recommended) filter to only "normal" buffers
        get_bufnrs = function()
          return vim.tbl_filter(function(bufnr)
            return vim.bo[bufnr].buftype == ''
          end, vim.api.nvim_list_bufs())
        end
      }
    }
  }
}

Dynamically picking providers by treesitter node/filetype

lua
sources.default = function(ctx)
  local success, node = pcall(vim.treesitter.get_node)
  if success and node and vim.tbl_contains({ 'comment', 'line_comment', 'block_comment' }, node:type()) then
    return { 'buffer' }
  elseif vim.bo.filetype == 'lua' then
    return { 'lsp', 'path' }
  else
    return { 'lsp', 'path', 'snippets', 'buffer' }
  end
end

Hide snippets after trigger character

Trigger characters are defined by the sources. For example, for Lua, the trigger characters are ., ", '.

lua
sources.providers.snippets.should_show_items = function(ctx)
  return ctx.trigger.initial_kind ~= 'trigger_character'
end

Set source kind icon and name

lua
sources.providers.copilot.transform_items = function(ctx, items)
  for _, item in ipairs(items) do
    item.kind_icon = ''
    item.kind_name = 'Copilot'
  end
  return items
end

Disable all snippets

See the relevant section in the snippets documentation

Set minimum keyword length by filetype

lua
sources.min_keyword_length = function()
  return vim.bo.filetype == 'markdown' and 2 or 0
end

Path completion from cwd instead of current buffer's directory

It's common to run code from the root of your repository, in which case relative paths will start from that directory. In that case, you may want path completions to be relative to your current working directory rather than the default, which is the current buffer's parent directory.

lua
sources = {
  providers = {
    path = {
      opts = {
        get_cwd = function(_)
          return vim.fn.getcwd()
        end,
      },
    },
  },
},

This also makes it easy to :cwd to the desired base directory for path completion.

For writers

When writing prose, you may want significantly different behavior than typical LSP completions. If you find any interesting configurations, please open a PR adding it here!

Keep first letter capitalization on buffer source

lua
sources = {
  providers = {
    buffer = {
      -- keep case of first char
      transform_items = function (a, items)
        local keyword = a.get_keyword()
        local correct, case
        if keyword:match('^%l') then
            correct = '^%u%l+$'
            case = string.lower
        elseif keyword:match('^%u') then
            correct = '^%l+$'
            case = string.upper
        else
            return items
        end

        -- avoid duplicates from the corrections
        local seen = {}
        local out = {}
        for _, item in ipairs(items) do
            local raw = item.insertText
            if raw:match(correct) then
                local text = case(raw:sub(1,1)) .. raw:sub(2)
                item.insertText = text
                item.label = text
            end
            if not seen[item.insertText] then
                seen[item.insertText] = true
                table.insert(out, item)
            end
        end
        return out
      end
    }
  }
}