How 99's @file Completion System Discovers and Indexes Project Files
The @file completion system in ThePrimeagen's 99 plugin scans the project tree using Neovim's libuv API, caches the results in a sorted table, and exposes them through a fuzzy-matching completion provider triggered by the @ symbol.
The @file completion feature allows users to reference project files directly within 99's prompt buffers, automatically inserting fenced code blocks with the file's contents. According to the source code in lua/99/extensions/files/init.lua, this functionality relies on a self-contained module that handles recursive filesystem traversal, intelligent caching, and character-order fuzzy matching.
Configuration and Enable-Switch
The system initializes with a default config table located in lua/99/extensions/files/init.lua (lines 19-35). This table defines the operational boundaries:
enabled: Boolean toggle for the featuremax_file_size: Maximum bytes per file to prevent memory issuesmax_files: Hard limit on indexed files for performanceexclude: Pattern list includingnode_modules,.git, and*.log
Users override these defaults through M.setup(), which merges custom options into the internal configuration (lines 38-45). This design ensures that scanning behavior respects project-specific constraints before any filesystem operations begin.
Project Root Registration and Caching Strategy
When a project opens, M.set_project_root(root) (lines 68-73) stores the absolute root path and invalidates any existing file cache. This function serves as the entry point for session management, ensuring that subsequent file discoveries operate within the correct directory scope.
The module maintains an internal cache table where cache.files stores the alphabetically sorted index. M.get_files() (lines 132-148) returns this cached list immediately if available, triggering M.discover_files() only when the cache is empty. This approach minimizes filesystem I/O by performing depth-first scans once per session unless manually invalidated.
Recursive Directory Scanning Implementation
The core discovery logic resides in M.discover_files() (lines 79-130), which implements a recursive walker using Neovim's vim.uv.fs_scandir API. The algorithm processes directory entries through three validation layers:
- Exclusion Filtering: The
matches_exclude_pattern()helper checks each entry against the configuredexcludelist, skipping matches immediately - Resource Limits: The scanner tracks cumulative file count and individual file sizes, aborting when
max_filesormax_file_sizethresholds are reached - Path Normalization: The
get_relative_path(full, root)function (lines 54-66) strips the project root from absolute paths, storing only relative paths in the final index
Each valid file generates a structured table entry: {path = rel_path, name = name, absolute_path = full_path}. After traversal completes, table.sort alphabetizes the collection before writing to cache.files.
Fuzzy Matching Algorithm
Completion matching occurs through M.find_matches(query) (lines 150-178), which implements character-order fuzzy search on the cached index. The algorithm concatenates file.name and file.path, converts to lowercase, and verifies that each character of the query appears in sequence.
This method tolerates partial matches and typos while maintaining performance by operating entirely in memory against the pre-computed cache. Matching files return immediately to the completion engine for display.
Completion Provider Integration
The @ trigger registers through M.completion_provider() (lines 57-96), which implements the provider interface expected by the generic completion engine in lua/99/extensions/completions.lua (lines 46-63).
The provider's get_items() method calls M.find_matches("") to retrieve all indexed files as LSP-style completion items. When a user selects an item, the is_valid and resolve callbacks verify the file exists, read its contents (respecting max_file_size), and return a markdown-fenced code block representation for insertion into the prompt buffer.
Usage Examples
Basic @file Completion
Open any 99 prompt buffer and type @ to trigger the completion menu:
-- Type:
@src/utils/strings.lua
-- 99 expands this to:
-- ```lua
-- -- src/utils/strings.lua
-- <file contents here>
-- ```
Customizing Scan Behavior
Configure exclusion patterns and limits during setup:
require('99').setup({
files = {
exclude = { '.env', 'node_modules', 'dist', '.mycustomignore' },
max_files = 10000,
max_file_size = 200 * 1024, -- 200 KB
},
})
Manually Refreshing the Index
Force a rescan when files change outside of Neovim:
local files = require('99.extensions.files')
files.set_project_root(vim.fn.getcwd()) -- Update root if changed
files.discover_files() -- Force fresh scan
Summary
- File discovery occurs through
M.discover_files()inlua/99/extensions/files/init.lua, utilizingvim.uv.fs_scandirfor asynchronous filesystem traversal - Indexing stores results in the module-level
cache.filestable as alphabetically sorted relative paths - Configuration controls scanning behavior via
max_files,max_file_size, andexcludepatterns set throughM.setup() - Completion triggers on
@and uses character-order fuzzy matching viaM.find_matches()to filter the cached index - Content resolution reads validated files and inserts fenced code blocks through the provider interface defined in
lua/99/extensions/completions.lua
Frequently Asked Questions
How does 99 handle large projects with thousands of files?
The system enforces hard limits through the max_files configuration parameter (defaulting to a conservative threshold). When M.discover_files() reaches this limit during the recursive scan (lines 79-130), it terminates early and returns the collected subset. Additionally, max_file_size prevents memory exhaustion by skipping files exceeding the byte threshold, ensuring the completion system remains responsive in monorepos.
Can I use @file completion for files outside the project root?
No. The M.set_project_root() function (lines 68-73) establishes a strict boundary for the file index. M.discover_files() begins traversal exclusively from this cached root, and get_relative_path() (lines 54-66) normalizes all paths relative to this location. Files outside the registered project directory are excluded from both indexing and completion results.
How do I exclude specific directories from the file index?
Pass custom patterns to the exclude table in your setup configuration. The matches_exclude_pattern() function (referenced in lines 79-130) checks each filesystem entry against these patterns before inclusion. Standard entries like node_modules and .git are excluded by default, but you can append project-specific directories such as build, dist, or .local to prevent indexing generated artifacts.
What happens if a file is deleted after indexing?
The completion provider implements validation through the is_valid callback (lines 57-96). When resolving a selected completion item, the system verifies the file exists on disk before reading. If the file has been deleted or moved since the last scan, the provider returns nil, preventing insertion of stale paths into your prompt buffer. To update the index after deletions, manually invoke M.discover_files() to refresh cache.files.
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 →