Fransys

Tech blog — Architecture, Cloud & DevOps

BlogServicesContactAbout

Follow me

githubGitHublinkedinLinkedinmailMail

© 2026 Fransys • Fransys

Fransys

Categories

  • All posts
  • Tags
  • productivity10
  • nas10
  • ai8
  • security7
  • self-hosting7
  • linux6
  • claude-code6
  • neovim5
  • docker5
  • editor4
  • networking4
  • mcp3
  • vpn3
  • lua2
  • terminal2
neovimaieditorproductivity

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

Published on
January 23, 2026·5 min read
Avatar François GUERLEZFrançois GUERLEZ

Why three completion layers?

Most people think completion means LSP. It's the standard. But generative AI exploded and suddenly you've got options. You can't just dump everything into one menu - that'd be a mess.

So I built a three-layer system. Not one. Three. Each one handles a specific job, has its own interaction model.

The idea: each layer solves a different problem. Ghost text for passive suggestions that just happen. An AI model inside the cmp menu for more involved cases. And regular nvim-cmp for LSP, snippets, everything else.

Layer 1: NeoCodeium (ghost text)

NeoCodeium. The Neovim client for Codeium. Why does it work?

  • Free and unlimited: no token counting. No subscription gates.
  • Ghost text: suggestions appear grayed-out, inline in the buffer. No popup stealing screen space.
  • Zero conflicts with nvim-cmp: you can run both and they don't interfere.

Ghost text is perfect for obvious stuff: closing a paren, finishing a variable name, basic boilerplate. Accept with a shortcut, or keep typing and it vanishes.

{
  "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,
}

Keybindings are chosen to avoid any conflict with nvim-cmp. Alt-f to accept? Muscle memory now. Fast, accessible, doesn't step on anything.

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

Minuet. Integrates Gemini directly into the nvim-cmp menu. Unlike ghost text (which is passive), this is an on-demand source that shows up 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,
}

A few important details:

  • The gemini-2.5-flash model balances speed and quality well
  • The API key comes from the GEMINI_API_KEY environment variable
  • enable_auto_complete = false: Gemini doesn't fire automatically. You trigger it with Alt-y in the cmp menu.

Intentional. Generative AI is useful for heavier stuff - refactoring, complex patterns, docs. You don't want it slowing down your normal typing. By making it on-demand, you stay in control.

Layer 3: nvim-cmp (the main engine)

At the heart is nvim-cmp. The completion engine. It orchestrates all the sources. Manages the popup, the navigation, the 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,
}

Several key choices here:

  • Sources by priority: Minuet (AI) first, then LSP, LuaSnip, path, buffer as fallback
  • Bordered windows: the cmp menu and doc popup both have borders. Cleaner.
  • 2-second timeout: fetching_timeout = 2000 ensures a sluggy AI source doesn't block your entire workflow
  • Smart Tab/S-Tab: navigate the cmp menu when it's open. Otherwise jump between LuaSnip positions

LuaSnip: the snippet engine

Alongside completion, LuaSnip handles snippets with the friendly-snippets pack, which provides VSCode-style snippets for all common languages.

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

Snippets are triggered through nvim-cmp, and you navigate between positions using Tab/S-Tab thanks to the mapping described above.

How it all coexists

The critical thing about this architecture is that all three layers stay out of each other's way:

  • NeoCodeium shows ghost text inline. Doesn't touch the cmp menu.
  • nvim-cmp handles the standard completion popup: LSP, snippets, path.
  • Minuet integrates as a cmp source. Only triggers manually with Alt-y.

In practice, when I'm coding:

  1. Codeium ghost text shows in gray. Good? Alt-f to accept.
  2. Want LSP results? The cmp menu opens auto or I hit C-Space.
  3. Need a fancier AI suggestion? Alt-y queries Gemini without leaving the cmp menu.

The 2-second timeout on cmp? Critical. If Gemini drags, completion keeps going with other sources. No freeze. No waiting. Constant flow.

The wrap-up

This three-layer system gives me the best of both worlds: AI for intelligent suggestions, LSP for precision. Each layer has its own interaction mode. No cognitive overload. You always know where a suggestion comes from and how to accept it. It's a setup that runs every day on my TypeScript and Python projects without missing a beat.


Neovim from scratch series — This article is part of a complete series on configuring Neovim from scratch.

Previous: Terminal, Git and global search integrated in Neovim

Previous post

← Firewall and Fail2ban: locking down NAS network access

Next post

Hardening your NAS Linux kernel to CIS Level 2 standards→
← Back to blog

Table of Contents

  • Why three completion layers?
  • Layer 1: NeoCodeium (ghost text)
  • Layer 2: Minuet AI (Gemini in the cmp menu)
  • Layer 3: nvim-cmp (the main engine)
  • LuaSnip: the snippet engine
  • How it all coexists
  • The wrap-up