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

> Explore how the 99 completion system integrates with nvim-cmp. Learn about custom sources, prompt buffers, and context-aware completions for rule references and file paths.

- Repository: [ThePrimeagen/99](https://github.com/theprimeagen/99)
- Tags: deep-dive
- Published: 2026-02-16

---

**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`](https://github.com/ThePrimeagen/99/blob/main/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`](https://github.com/ThePrimeagen/99/blob/main/lua/99/extensions/cmp.lua) at lines 15-18, the plugin executes:

```lua
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`](https://github.com/ThePrimeagen/99/blob/main/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()`

```lua
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`](https://github.com/ThePrimeagen/99/blob/main/lua/99/extensions/cmp.lua), the `get_trigger_characters()` method returns:

```lua
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`](https://github.com/ThePrimeagen/99/blob/main/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

```lua
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`](https://github.com/ThePrimeagen/99/blob/main/lua/99/extensions/completions.lua).

### Rule-Based Completions with Agents

The **Agents** provider, defined in [`lua/99/extensions/agents/init.lua`](https://github.com/ThePrimeagen/99/blob/main/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`](https://github.com/ThePrimeagen/99/blob/main/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`](https://github.com/ThePrimeagen/99/blob/main/cmp.lua)), the system registers both providers with the completions registry:

```lua
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`](https://github.com/ThePrimeagen/99/blob/main/cmp.lua)) re-registers providers when new rule directories are loaded or configuration changes occur:

```lua
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`](https://github.com/ThePrimeagen/99/blob/main/init.lua) to enable the integration:

```lua
-- 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:

```lua
-- 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:

```lua
-- 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`](https://github.com/ThePrimeagen/99/blob/main/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`](https://github.com/ThePrimeagen/99/blob/main/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`](https://github.com/ThePrimeagen/99/blob/main/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`](https://github.com/ThePrimeagen/99/blob/main/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`](https://github.com/ThePrimeagen/99/blob/main/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.