# How 99's @file Completion System Discovers and Indexes Project Files

> Discover how 99s @file completion system efficiently indexes project files using Neovims libuv API for fast fuzzy matching. Learn about its caching and retrieval methods.

- Repository: [ThePrimeagen/99](https://github.com/theprimeagen/99)
- Tags: internals
- Published: 2026-02-19

---

**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`](https://github.com/ThePrimeagen/99/blob/main/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`](https://github.com/ThePrimeagen/99/blob/main/lua/99/extensions/files/init.lua) (lines 19-35). This table defines the operational boundaries:

- `enabled`: Boolean toggle for the feature
- `max_file_size`: Maximum bytes per file to prevent memory issues
- `max_files`: Hard limit on indexed files for performance
- `exclude`: Pattern list including `node_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:

1. **Exclusion Filtering**: The `matches_exclude_pattern()` helper checks each entry against the configured `exclude` list, skipping matches immediately
2. **Resource Limits**: The scanner tracks cumulative file count and individual file sizes, aborting when `max_files` or `max_file_size` thresholds are reached
3. **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`](https://github.com/ThePrimeagen/99/blob/main/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:

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

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

```lua
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()` in [`lua/99/extensions/files/init.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/extensions/files/init.lua), utilizing `vim.uv.fs_scandir` for asynchronous filesystem traversal
- **Indexing** stores results in the module-level `cache.files` table as alphabetically sorted relative paths
- **Configuration** controls scanning behavior via `max_files`, `max_file_size`, and `exclude` patterns set through `M.setup()`
- **Completion** triggers on `@` and uses character-order fuzzy matching via `M.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`](https://github.com/ThePrimeagen/99/blob/main/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`.