Autocompletion & Snippets

Good autocompletion does more than save keystrokes -- it helps you discover APIs, catch typos before they become bugs, and stay in a state of flow rather than constantly switching to documentation. Modern completion in Neovim draws from multiple sources at once: your language server, the current buffer, file paths, and snippet libraries, all presented in a unified menu that feels fast and intuitive.

Neovim's nvim-cmparrow-up-right provides a flexible, extensible completion engine with multiple sources.

What you'll learn in this chapter:

  • Install and configure nvim-cmp with LSP, buffer, path, and snippet sources

  • Navigate the completion menu efficiently using keyboard shortcuts

  • Create custom LuaSnip snippets with placeholders and jump points

  • Customize completion source priority and formatting to suit your workflow

nvim-cmp Setup

-- lua/plugins/completion.lua
return {
    'hrsh7th/nvim-cmp',
    event = 'InsertEnter',
    dependencies = {
        -- Snippet engine
        'L3MON4D3/LuaSnip',
        'saadparwaiz1/cmp_luasnip',

        -- Completion sources
        'hrsh7th/cmp-nvim-lsp',     -- LSP completions
        'hrsh7th/cmp-buffer',       -- Buffer words
        'hrsh7th/cmp-path',         -- File paths
        'hrsh7th/cmp-cmdline',      -- Command-line completions

        -- Snippet collection
        'rafamadriz/friendly-snippets',
    },
    config = function()
        local cmp = require('cmp')
        local luasnip = require('luasnip')

        -- Load friendly-snippets
        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 to cycle through completions
                ['<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' },    -- LSP (highest priority)
                { name = 'luasnip' },     -- Snippets
                { name = 'path' },        -- File paths
            }, {
                { name = 'buffer' },      -- Buffer words (fallback)
            }),
            formatting = {
                format = function(entry, vim_item)
                    -- Show source name
                    vim_item.menu = ({
                        nvim_lsp = '[LSP]',
                        luasnip = '[Snip]',
                        buffer = '[Buf]',
                        path = '[Path]',
                    })[entry.source.name]
                    return vim_item
                end,
            },
        })

        -- Completions for command-line
        cmp.setup.cmdline(':', {
            mapping = cmp.mapping.preset.cmdline(),
            sources = {
                { name = 'cmdline' },
                { name = 'path' },
            },
        })

        -- Completions for search
        cmp.setup.cmdline('/', {
            mapping = cmp.mapping.preset.cmdline(),
            sources = {
                { name = 'buffer' },
            },
        })
    end,
}

Key Bindings Reference

Key
Action

Ctrl+Space

Trigger completion manually

Tab

Select next item / expand snippet

Shift+Tab

Select previous item

Enter

Confirm selection

Ctrl+e

Close completion menu

Ctrl+b/f

Scroll documentation

LuaSnip: Snippets

LuaSnip is the most popular snippet engine for Neovim. Combined with friendly-snippets, you get hundreds of pre-made snippets for many languages.

Using Snippets

  1. Start typing a snippet trigger (e.g., fn in a Lua file)

  2. It appears in the completion menu with a [Snip] label

  3. Press Enter to expand

  4. Press Tab to jump to the next placeholder

  5. Press Shift+Tab to jump back

Creating Custom Snippets

This creates a fn snippet for Lua files that expands to:

Loading VS Code Snippets

If you have VS Code snippets (JSON format), load them with:

Completion Sources

The most powerful completion source is cmp-nvim-lsp, which draws suggestions from your language server (see LSP -- Language Server Protocol). Treesitter-aware plugins can also enhance completion context (see Treesitter).

Source
Plugin
Description

LSP

cmp-nvim-lsp

Language server completions

Buffer

cmp-buffer

Words from open buffers

Path

cmp-path

File system paths

Snippets

cmp_luasnip

LuaSnip snippets

Command-line

cmp-cmdline

Vim command completions

Emoji

cmp-emoji

Emoji completions

Calc

cmp-calc

Math calculations

Tip: Order matters in the sources table. Items from higher-priority sources appear first in the completion menu.

Note: nvim-cmp and its sources are installed through a plugin manager like lazy.nvim (see Plugin Management). The dependencies table in the setup above ensures all required plugins are installed automatically.

Summary

Autocompletion and snippets transform Neovim into a productive coding environment where API discovery, typo prevention, and boilerplate generation happen seamlessly as you type. By combining nvim-cmp with LSP, buffer, path, and snippet sources, you get a completion experience that rivals any modern IDE while remaining fully customizable.

Key takeaways:

  • nvim-cmp unifies multiple completion sources (LSP, buffers, paths, snippets) into a single, prioritized menu.

  • LuaSnip provides a powerful snippet engine with tab-stop jumping; pair it with friendly-snippets for hundreds of pre-made snippets.

  • The Tab/Shift-Tab mappings serve double duty: cycling through completion items and jumping between snippet placeholders.

  • Source ordering in the sources table controls priority -- LSP results appear before buffer words when both are available.

Exercises

  1. Trigger and navigate completion -- Open a code file with an active language server, start typing a known function name, and use the completion menu to select it.

  2. Expand and navigate a snippet -- Type a snippet trigger (such as fn in a Lua file) and expand it, then use Tab to jump through all placeholders.

  3. Create a custom snippet -- Write a custom LuaSnip snippet for your most-used language that generates a common code pattern (e.g., an error handling block, a test function, or a class definition).

  4. Configure command-line completion -- Verify that command-line completion is working by typing : and then a partial command, and observe the completion suggestions.

Last updated

Was this helpful?