How the 99 Extension System Works: A Deep Dive into Extensions.init

The Extensions.init function in ThePrimeagen's 99 repository acts as a thin dispatcher that loads plugin sources based on the completion.source configuration, initializes them globally, and delegates buffer-specific and refresh operations to the selected source module.

The extension system in 99 keeps the core library agnostic of specific completion implementations while providing a standardized contract for plugins. When Extensions.init runs during startup, it detects which source to load—such as the built-in cmp source or custom agents—calls the source's global initialization, and sets up the workspace root for file discovery.

How Extensions.init Loads Plugin Sources

The initialization flow begins in lua/99/init.lua at line 85, where Extensions.init(_99_state) is invoked immediately after language initialization completes.

Step 1: Source Detection via get_source

Inside lua/99/extensions/init.lua (lines 10-19), the private helper get_source(completion) reads _99_state.completion.source to determine which module to load. If the source string equals "cmp", the function requires 99.extensions.cmp and returns that module table.

-- Simplified logic from lua/99/extensions/init.lua
local function get_source(completion)
    if completion.source == "cmp" then
        return require("99.extensions.cmp")
    end
    -- Additional sources can be added here
end

Step 2: Global Initialization

Once a source module is identified, Extensions.init executes source.init(_99_state) at lines 21-30 of lua/99/extensions/init.lua. For the built-in CMP source, this registration happens in lua/99/extensions/cmp.lua (lines 90-118), where the code creates a singleton CmpSource object, registers it with Neovim's CMP, and sets up file-search rules and agent-based completions.

Step 3: Project Root Capture

Immediately after source initialization, Extensions.capture_project_root() determines the workspace root by checking for a git repository or falling back to the current working directory. This value is stored in the Files module to anchor all subsequent file discovery operations (lines 31-35 of lua/99/extensions/init.lua).

Buffer-Level and Refresh Operations

The extension system delegates per-buffer configuration and dynamic updates to the selected source through two additional entry points.

Per-Buffer Setup with setup_buffer

When a new buffer opens, Extensions.setup_buffer(_99_state) is called (typically from the UI layer), which forwards to source.init_for_buffer(_99_state) at lines 37-44 of lua/99/extensions/init.lua. In the CMP implementation (lua/99/extensions/cmp.lua, lines 66-82), this sets the buffer's filetype to 99prompt and registers a buffer-local CMP source, ensuring completion contexts remain isolated per buffer.

Dynamic Configuration Refresh

Whenever the user modifies configuration—such as updating file exclude patterns—the Files.setup(new_config) function updates the internal state, followed by Extensions.refresh(_99_state) at lines 46-53 of lua/99/extensions/init.lua. This delegates to source.refresh_state(_99_state), which for the CMP source (lines 120-126 of lua/99/extensions/cmp.lua) re-registers file- and agent-based completions to reflect the new filtering rules without requiring a full editor restart.

The CMP Source Implementation

The built-in CMP source demonstrates the contract expected by Extensions.init. Located at lua/99/extensions/cmp.lua, it exports three functions:

  • init(_99): Registers the global CMP source and initializes file-search and agent subsystems (lines 90-118).
  • init_for_buffer(_99): Configures buffer-local completion contexts (lines 66-82).
  • refresh_state(_99): Updates completion providers when configuration changes (lines 120-126).

This implementation relies on lua/99/extensions/files/init.lua for file discovery and filtering, and can be extended to include agent-based sources via lua/99/extensions/agents/init.lua.

Creating Custom Extension Sources

To add a new extension source to 99:

  1. Create a module at lua/99/extensions/<name>.lua that returns a table implementing:

    • init(_99) for global setup,
    • init_for_buffer(_99) for per-buffer configuration,
    • refresh_state(_99) for dynamic updates.
  2. Expose the source name via the completion.source configuration option (e.g., "my_source").

  3. Register the dispatcher by updating the get_source function in lua/99/extensions/init.lua to require your new module when the source string matches.

Because Extensions only depends on these three function signatures, any plugin adhering to this contract can be hot-plugged without modifying the core 99 codebase.

Summary

  • Extensions.init serves as the central dispatcher that loads plugin sources based on the completion.source configuration value.
  • The system uses a three-phase lifecycle: global initialization (init), per-buffer setup (setup_buffer), and dynamic refresh (refresh).
  • Source detection occurs in get_source within lua/99/extensions/init.lua, which maps strings like "cmp" to concrete Lua modules.
  • The CMP source (lua/99/extensions/cmp.lua) demonstrates the expected contract, implementing all three lifecycle functions to register completion providers with Neovim's CMP.
  • Custom sources can be added by creating a module with the three required functions and registering it in the dispatcher, keeping the core library agnostic of specific implementations.

Frequently Asked Questions

What is the purpose of Extensions.init in the 99 plugin?

Extensions.init acts as the bootstrap mechanism for the 99 extension system. It detects which completion source the user has configured (such as "cmp" for the built-in CMP integration), loads the corresponding Lua module, and executes that module's global initialization function to set up completion providers, file search rules, and project root detection.

How does 99 handle per-buffer completion configuration?

After global initialization, 99 delegates buffer-local setup to Extensions.setup_buffer, which forwards to the selected source's init_for_buffer function. For the CMP source, this sets the buffer's filetype to 99prompt and registers a buffer-local CMP source, ensuring that completion contexts remain isolated and properly scoped to individual buffers.

Can I create a custom completion source for 99?

Yes, you can create a custom source by implementing a Lua module at lua/99/extensions/<your_source>.lua that exports three functions: init(_99) for global setup, init_for_buffer(_99) for per-buffer configuration, and refresh_state(_99) for handling configuration changes. You must then register your source name in the get_source function within lua/99/extensions/init.lua to map your source string to the new module.

What triggers a refresh in the 99 extension system?

A refresh is triggered when the user modifies configuration values that affect completion behavior, such as updating file exclude patterns or changing search directories. The Extensions.refresh function delegates to the active source's refresh_state method, which for the built-in CMP source re-registers file- and agent-based completions to reflect the new filtering rules without requiring a full editor restart.

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 →