C.W.K.
Stream
Lesson 01 of 04 · published

Auto-Completion with nvim-cmp (or blink.cmp)

~14 min · neovim, completion, lsp

Level 0Trapped
0 XP0/35 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

Why a completion engine matters

Native LSP gives you the data: "these are the symbols available here, with these signatures." A completion engine takes that data and presents it as a usable popup — ranking suggestions, expanding snippets, integrating buffer words and file paths, handling tab behavior. Neovim has two strong options in 2026:

  • nvim-cmp — the long-standing default. Mature, huge plugin ecosystem, lots of source plugins.
  • blink.cmp — newer (2024+), Rust-backed, designed to be faster and simpler. Rapidly catching up on parity. Worth trying if startup-perf matters.

This lesson uses nvim-cmp — it's the safer choice if you're configuring once and forgetting. Migrating to blink.cmp later is straightforward; the spec shape is similar.

What you wire up

  • The completion engine itself.
  • Sources — LSP, buffer words, file paths, snippets.
  • A snippet expander (LuaSnip).
  • Key mappings inside the popup (Tab to navigate, Enter to confirm, Esc to dismiss).
  • The capabilities object that tells the LSP server what your client supports — this enables snippet completion, auto-imports, etc.

The mental model

Completion is a sorted, filtered, ranked merge of candidates from each source. nvim-cmp queries every source, takes their suggestions, applies your ranking rules, shows the popup. Each source is independent — you can disable buffer words for Markdown files, prioritize LSP in Python files, etc.

Trust the LSP source first. Buffer-word completion is great for prose but noisy in code; LSP-aware suggestions know types, scope, imports. In source files, rank LSP above buffer; in Markdown / commit messages, the reverse. nvim-cmp's per-filetype config is how you encode this.

Code

nvim-cmp full setup — paste into your lazy.nvim spec·lua
{
  "hrsh7th/nvim-cmp",
  event = "InsertEnter",
  dependencies = {
    "hrsh7th/cmp-nvim-lsp",     -- LSP source
    "hrsh7th/cmp-buffer",       -- buffer-word source
    "hrsh7th/cmp-path",         -- filesystem-path source
    "L3MON4D3/LuaSnip",         -- snippet engine
    "saadparwaiz1/cmp_luasnip", -- snippet source
    "rafamadriz/friendly-snippets", -- prebuilt snippets per language
  },
  config = function()
    local cmp = require("cmp")
    local luasnip = require("luasnip")
    require("luasnip.loaders.from_vscode").lazy_load()

    cmp.setup({
      snippet = {
        expand = function(args) luasnip.lsp_expand(args.body) end,
      },
      mapping = cmp.mapping.preset.insert({
        ["<C-b>"] = cmp.mapping.scroll_docs(-4),
        ["<C-f>"] = cmp.mapping.scroll_docs(4),
        ["<C-Space>"] = cmp.mapping.complete(),
        ["<C-e>"] = cmp.mapping.abort(),
        ["<CR>"]  = cmp.mapping.confirm({ 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 = cmp.config.sources({
        { name = "nvim_lsp", priority = 1000 },
        { name = "luasnip",  priority = 750 },
      }, {
        { name = "buffer", priority = 500 },
        { name = "path",   priority = 250 },
      }),
    })
  end,
}
Tell the LSP server what your client supports·lua
-- Paste once, reuse for every server config
local capabilities = require("cmp_nvim_lsp").default_capabilities()

vim.lsp.config("pyright", {
  cmd = { "pyright-langserver", "--stdio" },
  root_markers = { "pyproject.toml", "setup.py", ".git" },
  capabilities = capabilities,   -- enables snippet completion etc.
})
blink.cmp alternative (faster, simpler)·lua
-- If you want to try the modern alternative:
{
  "saghen/blink.cmp",
  event = "InsertEnter",
  version = "*",
  opts = {
    keymap = { preset = "default" },
    appearance = { use_nvim_cmp_as_default = true },
    sources = {
      default = { "lsp", "path", "snippets", "buffer" },
    },
  },
}

External links

Exercise

Add the nvim-cmp setup and the capabilities hook to your config. Restart Neovim. Open a Python (or your language) file in a project where the LSP server attaches (:LspInfo to confirm). Start typing a name; the popup should appear. Press Tab to navigate, Enter to confirm, Esc to dismiss. Trigger a snippet (e.g. type main in Python and expand) to verify LuaSnip works.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.