Configure Neovim – Autocompletion with blink.cmp

Published on Mar 14, 2026

Configure Neovim – Autocompletion with blink.cmp

In the last post we set up the Language Server Protocol in Neovim. We installed Mason, wired up nvim-lspconfig, and got diagnostics, hover documentation, and go-to-definition all working. What we did not get was a completion menu. The LSP knows everything about our code — every function signature, every type, every available method — but there is no way to see that information as we type unless we add a completion plugin.

That is what we are doing in this post. We are going to install blink.cmp, which is the completion plugin the broader Neovim community has largely moved to. It is fast, it is batteries-included, and it requires very little configuration to feel great out of the box. If you have come across nvim-cmp in other guides, blink.cmp is it’s modern successor. Major Neovim configurations like LazyVim and kickstart.nvim have both switched to it, and it is the one worth starting with in 2026.


What blink.cmp Does

Before we write any code, it is worth understanding what a completion plugin actually does and why blink.cmp is a good choice.

When you type inside a buffer, the LSP running in the background is continuously tracking what is in scope — functions, variables, imported modules, method names, and more. A completion plugin takes that information and surfaces it in a popup menu as you type, so you can select a suggestion and have it inserted without typing the full name. Without a completion plugin, the LSP produces that information but has no way to show it to you while you are editing.

blink.cmp handles the entire pipeline: it queries completion sources, ranks the results using a fuzzy matcher, renders the popup menu, handles snippet expansion, and shows documentation alongside the menu. Unlike nvim-cmp, which required a separate plugin for every source (one for LSP, one for buffer words, one for file paths), blink.cmp ships with all of those sources built in. You do not need to install anything extra to get LSP completions, word completions from open buffers, file path completions, and snippet completions. They are all available from the moment you finish the setup below.


Prerequisites

Make sure you have the following already in place from earlier posts in this series:

  • Lazy.nvim set up as your plugin manager
  • The folder structure lua/config/ and lua/plugins/ inside your Neovim config directory
  • The LSP post completed — blink.cmp works alongside the LSP, so the language servers need to be installed first

You will also want a Nerd Font set in your terminal so that the completion kind icons — the small symbols that indicate whether a suggestion is a function, a variable, a class, and so on — render correctly.


Installing blink.cmp

Inside lua/plugins/, create a new file called completion.lua. We will build the configuration up step by step before arriving at the final version at the end of the post.

The minimal install looks like this:

-- lua/plugins/completion.lua

return {
  "saghen/blink.cmp",
  -- pin to a tagged release so Lazy.nvim downloads a pre-built binary
  -- building from source requires a nightly Rust toolchain
  version = "1.*",
  dependencies = {
    -- a large collection of pre-written snippets for many languages
    "rafamadriz/friendly-snippets",
  },
  opts = {
    keymap = { preset = "default" },
    appearance = {
      nerd_font_variant = "mono",
    },
    sources = {
      default = { "lsp", "path", "snippets", "buffer" },
    },
  },
}

Save the file and run :Lazy sync. Lazy.nvim will download blink.cmp’s pre-built binary for your platform. This is what the version = "1.*" line is for — it tells Lazy.nvim to use a tagged release rather than the latest commit, which means the binary that ships with that release is already compiled and ready to use. Without it, blink.cmp would try to compile the fuzzy-matching engine from Rust source, which requires a nightly Rust toolchain most people do not have installed.

Once the install completes, open any file that has a language server attached and start typing. A completion menu should appear automatically.


Understanding the Sources

The sources table controls where blink.cmp pulls it’s suggestions from. The four we have enabled are all built into blink.cmp — no extra plugins needed.

  • lsp is the most important one. It queries whatever language server is attached to the current buffer and returns completions based on what is in scope at the cursor position. Function names, method names, class properties, imported identifiers — all of this comes from the LSP source.

  • path completes file system paths. When you type a string that looks like a path — /home/, ./src/, ../ — blink.cmp will suggest files and directories from the file system. This is useful in any file where you reference paths, whether that is an import statement, a config value, or a string argument.

  • snippets expands the snippet collection from rafamadriz/friendly-snippets. This plugin ships with hundreds of pre-written snippets for dozens of languages. When you type a snippet trigger — for example, cl in a JavaScript file for console.log — the snippet source will offer it as a completion. Selecting it inserts the full snippet and positions the cursor at the first tab stop.

  • buffer completes words that appear in any open buffer. This source is useful for text that the LSP does not know about — variable names you have not yet committed to a declaration, strings, comments, and so on. It is the fallback that catches everything the other sources miss.

The order in the list matters. blink.cmp shows suggestions from sources listed earlier before suggestions from sources listed later, so LSP completions will always appear at the top of the menu.


Configuring the Keymaps

The preset = "default" keymap setting gives you a mapping scheme that matches Neovim’s built-in completion behaviour. The most important keys are:

KeyAction
<C-n>Select the next item
<C-p>Select the previous item
<C-y>Accept the selected item
<C-e>Close the menu without accepting
<C-space>Open the menu manually or show docs
<Tab>Jump to the next snippet tab stop
<S-Tab>Jump to the previous snippet tab stop

If you prefer a VS Code-like experience where <Tab> accepts the completion, you can change the preset to "super-tab":

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

Or, if you want full control over every key, set the preset to "none" and define each mapping yourself:

keymap = {
  preset = "none",
  ["<C-space>"] = { "show", "show_documentation", "hide_documentation" },
  ["<C-e>"]     = { "hide" },
  ["<C-y>"]     = { "select_and_accept" },
  ["<CR>"]      = { "select_and_accept", "fallback" },
  ["<C-p>"]     = { "select_prev", "fallback" },
  ["<C-n>"]     = { "select_next", "fallback" },
  ["<C-b>"]     = { "scroll_documentation_up", "fallback" },
  ["<C-f>"]     = { "scroll_documentation_down", "fallback" },
  ["<Tab>"]     = { "snippet_forward", "fallback" },
  ["<S-Tab>"]   = { "snippet_backward", "fallback" },
},

The "fallback" at the end of each mapping tells blink.cmp that if the action is not applicable in the current context — for example, pressing <Tab> when there is no active snippet tab stop — it should pass the key through to Neovim as normal. Without "fallback", pressing <Tab> in insert mode outside of a snippet would do nothing at all.


Configuring the Completion Menu

blink.cmp has a handful of options for controlling how the menu looks and behaves. The ones worth knowing about are in the completion table.

completion = {
  -- show the documentation window automatically when an item is selected
  documentation = {
    auto_show = true,
    auto_show_delay_ms = 200,
  },

  -- show a ghost text preview of the selected item in the buffer
  ghost_text = {
    enabled = true,
  },

  -- automatically insert matching closing brackets after accepting a function
  accept = {
    auto_brackets = {
      enabled = true,
    },
  },

  menu = {
    -- use treesitter to highlight the code inside the documentation window
    draw = {
      treesitter = { "lsp" },
    },
  },
},

documentation.auto_show = true is the setting that makes the documentation window appear beside the completion menu whenever you highlight an item. You will see the full docstring for a function as you scroll through the suggestions, which saves a trip to K to look it up manually. The auto_show_delay_ms value controls how long blink.cmp wait’s before showing the window — 200 milliseconds is enough to avoid it flashing on every keypress, while still feeling responsive.

ghost_text.enabled = true renders a faded preview of the top suggestion directly in the buffer as you type, similar to the inline suggestions you might have seen in GitHub Copilot. It is a nice way to confirm the top suggestion at a glance without the menu getting in the way.

auto_brackets.enabled = true inserts the opening and closing parentheses automatically after you accept a function completion. If you accept console.log, blink.cmp inserts console.log() and places the cursor between the brackets. This is one of those small things that adds up to a lot of time saved.


Signature Help

One feature that sets blink.cmp apart from a basic completion setup is signature help. When you type the opening parenthesis of a function call, signature help shows you the parameter list of that function in a floating window, highlighting the argument at the current cursor position as you move through the arguments.

To enable it, add the following to your opts:

signature = {
  enabled = true,
},

This is listed as experimental in blink.cmp’s documentation, but in practice it works reliably for most language servers. It is particularly useful when calling functions with multiple arguments where the order is not obvious.


Putting It All Together

Here is the complete lua/plugins/completion.lua file with everything from this post included:

-- lua/plugins/completion.lua

return {
  "saghen/blink.cmp",
  version = "1.*",
  dependencies = {
    "rafamadriz/friendly-snippets",
  },
  ---@module "blink.cmp"
  ---@type blink.cmp.Config
  opts = {
    keymap = {
      preset = "none",
      ["<C-space>"] = { "show", "show_documentation", "hide_documentation" },
      ["<C-e>"]     = { "hide" },
      ["<C-y>"]     = { "select_and_accept" },
      ["<CR>"]      = { "select_and_accept", "fallback" },
      ["<C-p>"]     = { "select_prev", "fallback" },
      ["<C-n>"]     = { "select_next", "fallback" },
      ["<C-b>"]     = { "scroll_documentation_up", "fallback" },
      ["<C-f>"]     = { "scroll_documentation_down", "fallback" },
      ["<Tab>"]     = { "snippet_forward", "fallback" },
      ["<S-Tab>"]   = { "snippet_backward", "fallback" },
    },

    appearance = {
      nerd_font_variant = "mono",
    },

    completion = {
      documentation = {
        auto_show = true,
        auto_show_delay_ms = 200,
      },
      ghost_text = {
        enabled = true,
      },
      accept = {
        auto_brackets = {
          enabled = true,
        },
      },
      menu = {
        draw = {
          treesitter = { "lsp" },
        },
      },
    },

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

    signature = {
      enabled = true,
    },

    fuzzy = {
      -- prefer the Rust implementation if available; falls back to Lua
      implementation = "prefer_rust_with_warning",
    },
  },
}

The two annotation comments above opts---@module and ---@type — are LuaLS type hints. If you have the Lua language server set up from the previous post, these tell it the exact type of the opts table, which means you get autocompletion and documentation for every blink.cmp option while you are editing this file. They are optional but worth including.


Verifying the Setup

Restart Neovim after saving the file. Open any file for a language with a server installed — a .lua file works well since we configured lua_ls in the previous post. Start typing the name of a Neovim API function such as vim.api.nvi and a completion menu should appear. Scroll through the items with <C-n> and <C-p> and press <C-y> to accept one.

To confirm signature help is working, type vim.api.nvim_create_autocmd( and pause. A floating window should appear showing the parameter list for that function with the first argument highlighted.

To confirm snippets are working, open a JavaScript or TypeScript file, type cl, and check whether the console.log snippet appears in the menu. Accepting it should insert the full snippet and place the cursor inside the parentheses.


Wrapping Up

With blink.cmp in place, our Neovim setup now has the full IDE experience. The LSP provides the intelligence, and blink.cmp surfaces it as we type. Snippets, documentation, signature help, and ghost text are all working out of a single, reasonably concise configuration file.

From here, the natural next step is formatting. We have an LSP that can format our code, but it is better to manage formatters through a dedicated plugin so we can use different formatters per filetype and run them automatically on save. We will cover that in the next post using conform.nvim.

To find the rest of my posts on Neovim, click here.

Author Information

aeon501
aeon501

Web Developer, Restless. My mind goes on epic voyages, then return back to reality. I write about things I have experienced in my coding journey.

View all posts
Advertisement
Ad placeholder
Sponsored
Ad 2