Part 6: Neovim auto-completion with blink.cmp

Nov 28, 2025
942 words - 5 min read

This is part 6 of my Neovim series. Today we setup auto-completion.

Auto-completion helps you code faster. Type a few letters, it suggests the rest. Hit Tab, it completes.

LSP (from part 3) knows your code. It knows functions, variables, types. Auto-completion uses this to suggest what you need.

I use blink.cmp. It supports LSP, cmdline, signature help, and snippets. Has fuzzy matching for typos. Fast and extensible.

Setup blink.cmp

Here's my complete setup:

File: lua/plugins/blink.lua

vim.pack.add({
	{
		src = "https://github.com/saghen/blink.cmp",
		version = vim.version.range("^1"),
	},
})

-- Lazy load on first insert mode entry (may not necessary)
local group = vim.api.nvim_create_augroup("BlinkCmpLazyLoad", { clear = true })

vim.api.nvim_create_autocmd("InsertEnter", {
	pattern = "*",
	group = group,
	once = true,
	callback = function()
		require("blink.cmp").setup({
			keymap = { preset = "super-tab" },
			appearance = {
				nerd_font_variant = "mono",
				use_nvim_cmp_as_default = true,
			},
			completion = {
				documentation = { auto_show = false },
			},
			sources = {
				default = { "lsp", "path", "snippets", "buffer" },
			},
			fuzzy = { implementation = "prefer_rust_with_warning" },
		})
	end,
})

Restart Neovim. Enter insert mode. Now you have completion.

Breaking down the setup

Let me explain each part:

Install the plugin

vim.pack.add({
	{
		src = "https://github.com/saghen/blink.cmp",
		version = vim.version.range("^1"),
	},
})

This installs blink.cmp using Neovim's native package manager. The version ensures you get v1.x updates but nothing breaking.

Lazy loading

local group = vim.api.nvim_create_augroup("BlinkCmpLazyLoad", { clear = true })

vim.api.nvim_create_autocmd("InsertEnter", {
	pattern = "*",
	group = group,
	once = true,
	callback = function()
		require("blink.cmp").setup({
			-- config here
		})
	end,
})

This loads blink only when you enter insert mode for the first time. Faster startup. The once = true means it only runs once.

Keymap preset

keymap = { preset = "super-tab" },

The super-tab preset makes Tab key work like VSCode:

  • Press Tab to show suggestions
  • Press Tab again to accept
  • Press Tab to jump through snippet placeholders

Simple and familiar.

Appearance

appearance = {
	nerd_font_variant = "mono",
	use_nvim_cmp_as_default = true,
},

nerd_font_variant = "mono" - Uses monospace nerd font icons in the menu. Works with most terminal setups.

use_nvim_cmp_as_default = true - Makes the menu look like nvim-cmp. Nice and clean.

Documentation

completion = {
	documentation = { auto_show = false },
},

This disables auto-showing documentation popup. You can still show it manually with Ctrl+Space. I disable it because it can be distracting. Enable it if you want docs to show automatically.

Sources

sources = {
	default = { "lsp", "path", "snippets", "buffer" },
},

blink.cmp gets suggestions from 4 places:

  • lsp - From your LSP server (functions, variables, types)
  • path - File paths when you type ./ or /
  • snippets - Code templates (we'll setup these next)
  • buffer - Words from your current file

The order matters. LSP comes first, so LSP suggestions appear at the top.

Fuzzy matcher

fuzzy = { implementation = "prefer_rust_with_warning" },

blink.cmp has two fuzzy matchers:

  • Rust implementation (fast, native)
  • Lua implementation (fallback)

This setting prefers Rust but falls back to Lua if Rust isn't available. Fuzzy matching lets you make typos and still find matches.

Setup snippets

Blink uses the vim.snippet API by default for expanding and navigating snippets. The built-in snippets source will load friendly-snippets, if available, and load any snippets found at ~/.config/nvim/snippets

So we don't need other plugin, we can setup snippets directly:

  • Create snippets folder
mkdir -p ~/.config/nvim/lua/snippets
  • Create package.json

blink.cmp uses VSCode snippet format. You need a package.json to define them:

File: lua/snippets/package.json

{
    "name": "custom-snippets",
    "contributes": {
        "snippets": [
            {
                "language": ["typescript", "typescriptreact", "javascriptreact", "javascript"],
                "path": "./typescript.json"
            }
        ]
    }
}

This tells blink where to find snippets. The language array lists which file types use these snippets. The path points to the snippet file.

  • Create snippet file

File: lua/snippets/typescript.json

{
    "Describe unit test": {
        "prefix": "descr",
        "body": ["describe('function', () => {", "", "})"],
        "description": "describe unit test"
    },
    "It unit test": {
        "prefix": "itt",
        "body": ["it('should return ', () => {", "", "})"],
        "description": "it unit test"
    },
    "Before unit test": {
        "prefix": "bf",
        "body": ["before(() => {", "", "})"],
        "description": "before unit test"
    },
    "After unit test": {
        "prefix": "af",
        "body": ["after(() => {", "    sandbox.restore()", "})"],
        "description": "after unit test"
    },
    "BeforeAll unit test": {
        "prefix": "bfa",
        "body": ["beforeAll(() => {", "", "})"],
        "description": "beforeAll unit test"
    },
    "AfterAll unit test": {
        "prefix": "afa",
        "body": ["afterAll(() => {", "    sandbox.restore()", "})"],
        "description": "afterAll unit test"
    },
    "AfterEach unit test": {
        "prefix": "afe",
        "body": ["afterEach(() => {", "    sandbox.reset()", "})"],
        "description": "afterEach unit test"
    },
    "ItParams mocha test": {
        "prefix": "itp",
        "body": ["itParam('should return when ${value.case}', data, (value) => {", "", "})"],
        "description": "itParam mocha test"
    }
}

Each snippet has:

  • prefix - What you type to trigger it
  • body - The code it expands to (array of lines)
  • description - Shows in completion menu

These snippets load automatically when you open TypeScript/JavaScript files.

How it looks

When you type in insert mode:

You type: desc

blink shows:

┌──────────────────────────────┐
│ describe                     │ (lsp)
│ description                  │ (lsp)
│ descr → describe('...', ...) │ (snippet)
└──────────────────────────────┘

Press Tab to accept. If you pick the snippet, it expands. If you pick LSP suggestion, it completes the word.

What's next

Now you have:

  • LSP (part 3)
  • Setup vim.pack with snacks.nvim (part 4)
  • Tree-sitter syntax highlighting (part 5)
  • Auto-completion (this article)

Next article, we will setup formatter with conform.nvim

Thanks for reading! If you notice anything outdated, feel free to comment or send me a message.