AI completion in Neovim: Codeium, Gemini and nvim-cmp

AI completion in Neovim: Codeium, Gemini and nvim-cmp

·5 min read·Updated on January 23, 2026

Why three completion layers

LSP remains the completion standard, but generative AI multiplied the options — and cramming everything into one menu quickly becomes unreadable.

Hence three distinct layers, each solving a specific problem: ghost text for passive suggestions, an AI model in the cmp menu for elaborate cases, and classic nvim-cmp for LSP, snippets and the rest.

Layer 1: NeoCodeium (ghost text)

NeoCodeium is the Neovim client for Codeium: free and unlimited (no tokens to track), shown as greyed ghost text inline rather than a popup, and conflict-free with nvim-cmp.

Ghost text covers the obvious cases: closing a parenthesis, finishing a variable name, boilerplate. You accept with a shortcut, or keep typing and it disappears.

{
  "monkoose/neocodeium",
  event = "VeryLazy",
  config = function()
    local neocodeium = require("neocodeium")
    neocodeium.setup()

    -- Alt-f to accept the suggestion
    vim.keymap.set("i", "<A-f>", neocodeium.accept)
    -- Alt-n / Alt-p to cycle through suggestions
    vim.keymap.set("i", "<A-n>", neocodeium.cycle_or_complete)
    vim.keymap.set("i", "<A-p>", function()
      neocodeium.cycle_or_complete(-1)
    end)
    -- Alt-c to clear the suggestion
    vim.keymap.set("i", "<A-c>", neocodeium.clear)
  end,
}

The keybindings are chosen to avoid any conflict with nvim-cmp.

Layer 2: Minuet AI (Gemini in the cmp menu)

Minuet integrates Gemini into the cmp menu. Unlike passive ghost text, it's an on-demand source appearing in the same menu as LSP.

{
  "milanglacier/minuet-ai.nvim",
  config = function()
    require("minuet").setup({
      provider = "gemini",
      provider_options = {
        gemini = {
          model = "gemini-2.5-flash",
          api_key = "GEMINI_API_KEY",
        },
      },
      cmp = {
        enable_auto_complete = false,
      },
    })
  end,
}

The gemini-2.5-flash model strikes a good speed/quality balance, the API key comes from the GEMINI_API_KEY environment variable, and enable_auto_complete = false keeps Gemini from firing automatically — you invoke it with Alt-y inside the cmp menu.

That's deliberate: generative AI is useful for complicated suggestions (refactoring, complex patterns, doc generation), but shouldn't slow down the normal workflow.

Layer 3: nvim-cmp (the main engine)

nvim-cmp orchestrates every source: popup, navigation, confirmation.

{
  "hrsh7th/nvim-cmp",
  event = "InsertEnter",
  dependencies = {
    "hrsh7th/cmp-nvim-lsp",
    "hrsh7th/cmp-buffer",
    "hrsh7th/cmp-path",
    "saadparwaiz1/cmp_luasnip",
  },
  config = function()
    local cmp = require("cmp")
    local luasnip = require("luasnip")

    cmp.setup({
      snippet = {
        expand = function(args)
          luasnip.lsp_expand(args.body)
        end,
      },
      completion = { completeopt = "menu,menuone,noinsert" },
      window = {
        completion = cmp.config.window.bordered(),
        documentation = cmp.config.window.bordered(),
      },
      performance = {
        fetching_timeout = 2000,
      },
      mapping = cmp.mapping.preset.insert({
        ["<C-n>"] = cmp.mapping.select_next_item(),
        ["<C-p>"] = cmp.mapping.select_prev_item(),
        ["<C-Space>"] = cmp.mapping.complete(),
        ["<C-e>"] = cmp.mapping.abort(),
        ["<CR>"] = cmp.mapping.confirm({ select = true }),
        ["<A-y>"] = require("minuet").make_cmp_map(),
        ["<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 = "minuet" },
        { name = "nvim_lsp" },
        { name = "luasnip" },
        { name = "path" },
      }, {
        { name = "buffer" },
      }),
    })
  end,
}

Four structural choices: priority-ordered sources (Minuet, then LSP, LuaSnip, path, with buffer as fallback), bordered windows for readability, a 2-second timeout (fetching_timeout = 2000) so a slow AI source can't block the workflow, and smart Tab/S-Tab that navigate the cmp menu when open and jump between snippet positions otherwise.

LuaSnip: the snippet engine

LuaSnip handles snippets, with the friendly-snippets pack providing VSCode-style snippets for every common language.

{
  "L3MON4D3/LuaSnip",
  build = "make install_jsregexp",
  dependencies = {
    {
      "rafamadriz/friendly-snippets",
      config = function()
        require("luasnip.loaders.from_vscode").lazy_load()
      end,
    },
  },
}

Snippets trigger through nvim-cmp, and navigation between positions uses Tab/S-Tab thanks to the mapping above.

How it all coexists

The three layers don't get in each other's way: NeoCodeium shows ghost text in the buffer without touching the cmp menu, nvim-cmp handles the classic popup (LSP, snippets, path), and Minuet plugs in as a cmp source but only fires manually.

In practice, while coding:

  1. Codeium ghost text appears greyed out — Alt-f to accept.
  2. For LSP results, the cmp menu opens automatically or via C-Space.
  3. For a more elaborate AI suggestion, Alt-y queries Gemini without leaving the menu.

The 2-second timeout matters: if Gemini lags, completion continues with the other sources, with no freeze.

Conclusion

This three-layer system combines AI for smart suggestions and LSP for precision, each layer with its own interaction mode. You always know where a suggestion came from and how to accept it — a setup that runs daily on TypeScript and Python projects with no slowdown.

ShareLinkedInXBluesky

Related articles