How the 99 Completion System Integrates with nvim-cmp: A Complete Technical Guide

The 99 plugin integrates with nvim-cmp by registering a custom completion source that delegates to internal Agents and Files providers, enabling context-aware completions for rule references (#) and file paths (@) within 99 prompt buffers.

ThePrimeagen's 99 repository extends Neovim with an intelligent completion system that seamlessly integrates with nvim-cmp, the popular autocompletion plugin. Understanding how the 99 completion system integrates with nvim-cmp reveals a well-architected bridge between the plugin's internal completion registry and nvim-cmp's source interface.

Understanding the nvim-cmp Source Interface

The integration centers on a Lua object called CmpSource defined in lua/99/extensions/cmp.lua. This object implements the standard nvim-cmp source interface, which requires specific methods that nvim-cmp calls during the completion lifecycle:

  • is_available() – Returns true when the source should be active
  • get_debug_name() – Returns "99" for logging purposes
  • get_keyword_pattern() – Provides the regex pattern for keyword matching
  • get_trigger_characters() – Returns the characters that initiate completion (# and @)
  • complete() – Generates completion items based on cursor context
  • resolve() – Adds documentation to selected items
  • execute() – Handles item confirmation

Step-by-Step Integration Architecture

Source Registration in cmp.lua

When the 99 plugin initializes, it registers itself as a completion source through nvim-cmp's public API. In lua/99/extensions/cmp.lua at lines 15-18, the plugin executes:

local source = CmpSource.new(_99)
cmp.register_source("99", source)

This registration makes the "99" source available to nvim-cmp globally, though it remains inactive until specifically configured for a buffer.

Buffer-Local Configuration

The integration restricts 99 completions to specific buffers through the init_for_buffer() function (lines 71-81 in cmp.lua). When a 99 prompt buffer opens, the system:

  1. Sets the buffer's filetype to 99prompt
  2. Configures nvim-cmp locally for that buffer using cmp.setup.buffer()
function M.init_for_buffer(bufnr)
  vim.api.nvim_buf_set_option(bufnr, "filetype", "99prompt")
  cmp.setup.buffer({
    sources = {
      { name = "99" }
    }
  })
end

This ensures the 99 source only activates within 99 prompt buffers, preventing interference with other filetypes.

Trigger Character Handling

The completion system responds to specific trigger characters that initiate different completion contexts. In lua/99/extensions/cmp.lua, the get_trigger_characters() method returns:

function CmpSource:get_trigger_characters()
  return { "#", "@" }

These characters map to two distinct completion providers:

  • # – Activates the Agents provider for rule-based completions
  • @ – Activates the Files provider for file-path completions

The Completion Flow

When a user types a trigger character, nvim-cmp invokes the complete() method (lines 34-53 in cmp.lua). This method implements the core integration logic:

  1. Context Analysis – Examines the text before the cursor to identify which trigger character was used
  2. Provider Selection – Matches the trigger to the appropriate completion provider
  3. Item Retrieval – Calls Completions.get_completions(trigger) to fetch items from the 99 completion registry
  4. Callback Execution – Returns items to nvim-cmp through the callback function
function CmpSource:complete(params, callback)
  local line = params.context.cursor_before_line
  -- Logic to detect trigger and fetch completions
  local trigger = detect_trigger(line) -- "#" or "@"
  local items = require("99.extensions.completions").get_completions(trigger)
  callback(items)
end

The resolve() and execute() methods handle documentation display and item confirmation, respectively, forwarding these operations back to nvim-cmp with the payload already prepared by the 99 providers.

The Provider System: Agents and Files

The 99 completion system delegates item generation to specialized providers registered in the completion registry at lua/99/extensions/completions.lua.

Rule-Based Completions with Agents

The Agents provider, defined in lua/99/extensions/agents/init.lua, handles completions triggered by #. It:

  • Scans custom rule directories for available rules
  • Creates completion items where the label displays the rule name and insertText inserts #<path>
  • Implements resolve() to load rule content and format it as markdown documentation

When a user types #, the completion menu displays available rules from the Agents provider, and selecting one inserts the full rule reference.

File Reference Completions

The Files provider in lua/99/extensions/files/init.lua manages @ trigger completions. It:

  • Scans the project tree while respecting exclude patterns from the 99 configuration
  • Generates completion items for every discovered file
  • Implements resolve() to read file contents (subject to size limits) and return them as fenced code blocks in the documentation window

This allows users to reference project files directly within 99 prompts using the @ syntax.

Provider Registration

During initialization (lines 84-88 in cmp.lua), the system registers both providers with the completions registry:

local completions = require("99.extensions.completions")
completions.register(require("99.extensions.agents"))
completions.register(require("99.extensions.files"))

This registration makes the providers available to the CmpSource when it calls get_completions().

Dynamic State Refresh

The integration supports dynamic updates when the 99 configuration changes. The refresh_state() function (lines 20-33 in cmp.lua) re-registers providers when new rule directories are loaded or configuration changes occur:

function CmpSource:refresh_state(_99)
  -- Clear and re-register providers with updated state
  local completions = require("99.extensions.completions")
  completions.clear()
  completions.register(require("99.extensions.agents"))
  completions.register(require("99.extensions.files"))
end

This ensures that newly added custom rules or changed file paths immediately appear in completion menus without restarting Neovim.

Code Examples

Minimal 99 and nvim-cmp Setup

Configure both plugins in your init.lua to enable the integration:

-- Initialize the 99 plugin
require("99").setup()

-- Configure nvim-cmp (the 99 source is added automatically to 99 buffers)
local cmp = require("cmp")
cmp.setup({
  snippet = {
    expand = function(args)
      vim.fn["vsnip#anonymous"](args.body)
    end,
  },
  mapping = cmp.mapping.preset.insert({
    ["<C-Space>"] = cmp.mapping.complete(),
    ["<CR>"] = cmp.mapping.confirm({ select = true }),
  }),
  sources = cmp.config.sources({
    { name = "nvim_lsp" },
    { name = "path" },
  })
})

Completion Trigger Flow

When typing within a 99 prompt buffer, the integration handles triggers as follows:

-- User types "#"
-- 1. nvim-cmp detects trigger character from CmpSource:get_trigger_characters()
-- 2. nvim-cmp calls CmpSource:complete()
-- 3. CmpSource detects "#" trigger and queries the Agents provider:
local items = require("99.extensions.completions").get_completions("#")
-- 4. Items contain rule names; selecting one inserts "#path/to/rule"

-- User types "@"
-- Same flow, but queries the Files provider:
local items = require("99.extensions.completions").get_completions("@")
-- Items contain file paths; selecting one inserts the file reference

Refreshing Completions After Configuration Changes

Update the completion list dynamically after adding new rule directories:

-- Add a new custom rule path
vim.g["99_custom_rules"] = { "/path/to/my/rules" }

-- Refresh the 99 state to update completions
require("99").refresh()
-- This internally calls CmpSource:refresh_state(_99), which re-registers
-- the Agents and Files providers with the updated configuration

Summary

  • The 99 plugin implements the nvim-cmp source interface through the CmpSource object in lua/99/extensions/cmp.lua, registering itself as a source named "99".
  • Buffer-local configuration ensures isolation by setting the filetype to 99prompt and configuring nvim-cmp sources specifically for 99 buffers via init_for_buffer().
  • Trigger characters # and @ route to specialized providers through the completion registry in lua/99/extensions/completions.lua, with Agents handling rules and Files handling project paths.
  • Dynamic refresh capability allows the completion system to update without restarting Neovim when rule directories or file structures change, using the refresh_state() method.

Frequently Asked Questions

What trigger characters activate the 99 completion system?

The 99 completion system responds to two specific trigger characters: # and @. When you type either character in a 99 prompt buffer, nvim-cmp invokes the complete() method in lua/99/extensions/cmp.lua, which routes the request to either the Agents provider (for # rules) or the Files provider (for @ paths).

How does 99 prevent its completions from appearing in regular file buffers?

The integration uses buffer-local configuration to restrict completions to 99-specific contexts. When a 99 prompt buffer opens, the init_for_buffer() function in cmp.lua sets the buffer's filetype to 99prompt and calls cmp.setup.buffer() to activate the "99" source only for that buffer. This ensures standard file buffers never see 99 completions.

Can I customize which files or rules appear in the completion menu?

Yes, the completion providers respect your 99 configuration. The Files provider in lua/99/extensions/files/init.lua scans the project tree while respecting exclude patterns from your 99 config. The Agents provider reads from directories specified in vim.g["99_custom_rules"]. After changing these configurations, call require("99").refresh() to update the completion registry via refresh_state().

What happens when I select a completion item from the 99 source?

When you confirm a completion, nvim-cmp calls the execute() method on the CmpSource object, which simply forwards the selected item since the payload (insert text, documentation) is already prepared. The resolve() method handles documentation display by loading rule content or file contents on demand, formatting them as markdown or fenced code blocks for the nvim-cmp documentation window.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →