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 activeget_debug_name()– Returns "99" for logging purposesget_keyword_pattern()– Provides the regex pattern for keyword matchingget_trigger_characters()– Returns the characters that initiate completion (#and@)complete()– Generates completion items based on cursor contextresolve()– Adds documentation to selected itemsexecute()– 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:
- Sets the buffer's filetype to
99prompt - 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:
- Context Analysis – Examines the text before the cursor to identify which trigger character was used
- Provider Selection – Matches the trigger to the appropriate completion provider
- Item Retrieval – Calls
Completions.get_completions(trigger)to fetch items from the 99 completion registry - 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
labeldisplays the rule name andinsertTextinserts#<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
excludepatterns 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
CmpSourceobject inlua/99/extensions/cmp.lua, registering itself as a source named "99". - Buffer-local configuration ensures isolation by setting the filetype to
99promptand configuring nvim-cmp sources specifically for 99 buffers viainit_for_buffer(). - Trigger characters
#and@route to specialized providers through the completion registry inlua/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →