How 99's Search Operation Differs from Visual Mode: A Technical Deep Dive

99's search operation performs read-only project-wide lookups that populate the quickfix list, while visual mode performs write-back buffer modifications that replace selected text with AI-generated code.

ThePrimeagen's 99 Neovim plugin provides two distinct AI-powered operations—search and visual mode—that handle LLM interactions differently. Understanding how 99's search operation differs from visual mode operationally is crucial for developers who want to leverage semantic code search versus AI-assisted code generation effectively.

Core Operational Differences

Purpose and Intent

Search (:lua require("99").search()) is designed for project-wide discovery. It asks the LLM to locate code elsewhere in the codebase using semantic or grep-like queries, returning file locations without modifying the current buffer.

Visual (:lua require("99").visual()) is designed for inline transformation. It captures the current visual selection, sends it to the LLM with context, and replaces the selected text with the AI-generated response.

Prompt Engineering

Both operations use different system prompts defined in the prompts module:

  • Search calls context._99.prompts.prompts.semantic_search(), which instructs the model to return results in a strict file:line:col, notes format.
  • Visual calls context._99.prompts.prompts.visual_selection(range), which includes the selected range data and instructs the model to generate replacement text.

Result Handling and Buffer Interaction

The divergence in operational logic becomes clear when examining how each mode handles the LLM response:

Search (lua/99/ops/search.lua):

  • Parses each response line using parse_line to extract filename, line number, column, and description
  • Builds a quickfix list using vim.fn.setqflist
  • Opens the quickfix window with vim.cmd("copen")
  • Performs zero buffer modifications—it is strictly read-only

Visual (lua/99/ops/over-range.lua):

  • Creates persistent marks using Mark.mark_above_range and Mark.mark_point to survive the asynchronous operation
  • Reconstructs the range using Range.from_marks after the LLM responds
  • Calls new_range:replace_text() to overwrite the selected text with the AI output
  • Modifies the current buffer directly

User Feedback Mechanisms

Search provides minimal feedback—a simple notification on success or failure with no live status indication.

Visual implements a sophisticated streaming UI:

  • Creates two RequestStatus objects (top_status and bottom_status)
  • The observer handles on_stdout to update the status bar in real-time as the LLM streams output
  • Displays "Implementing" status while processing

Implementation Deep Dive

Search Operation Flow

Located in lua/99/ops/search.lua, the search operation follows this sequence:

  1. Prompt Construction: make_prompt merges the semantic_search system prompt with any additional_prompt options
  2. Request Initialization: Request.new(context) creates the async job with a clean_up function that only cancels the LLM request
  3. Observer Pattern: Uses a simple callback function(status, response) that only processes the final status and response
  4. Result Parsing: create_search_locations iterates through response lines, parsing the file:line:col, notes format
  5. Quickfix Population: Calls vim.fn.setqflist with the parsed locations and opens the window

Visual Mode Flow

Located in lua/99/ops/over-range.lua, visual mode requires state management across asynchronous boundaries:

  1. Range Capture: Before sending the request, Mark.mark_above_range and Mark.mark_point create durable marks at the visual selection boundaries
  2. Prompt Building: make_prompt receives the visual_selection prompt with the current range data included
  3. Status Setup: Initializes two RequestStatus instances to display live progress
  4. Streaming Observer: Uses a structured observer with on_complete and on_stdout callbacks:
    • on_stdout updates the status displays in real-time
    • on_complete validates marks still exist, reconstructs the geo.Range via Range.from_marks, and executes replace_text
  5. Comprehensive Cleanup: The clean_up function stops status displays, clears the marks, and cancels the request, ensuring the editor returns to a clean state if aborted

Practical Usage Examples

Search (Quickfix List)

To perform a semantic search across your project:

require("99").search{
  additional_prompt = "Find all usages of `my_func`"
}

Behind the scenes in lua/99/ops/search.lua, this builds the prompt, sends the request, and populates the quickfix list with locations in file:line:col format.

Visual (Replace Selected Text)

Map a keybinding to invoke visual mode replacement:

" In visual mode, press <leader>i to invoke 99's visual AI replace
vnoremap <leader>i :lua require("99").visual()<CR>

When executed, lua/99/ops/over-range.lua captures the visual marks, streams AI output to a temporary status bar, and replaces the selected lines with the model's response.

Summary

  • Search is a read-only lookup operation that populates Neovim's quickfix list with code locations, using lua/99/ops/search.lua to parse file:line:col formatted responses.
  • Visual is a write-back transformation that replaces selected text with AI-generated code, using lua/99/ops/over-range.lua to manage marks, stream status updates, and modify buffer content.
  • Search uses a simple final-response observer, while visual implements a streaming observer with on_stdout for real-time feedback.
  • Cleanup differs significantly: search only cancels the request, while visual stops UI components and clears editor marks to prevent state corruption.

Frequently Asked Questions

Does search mode modify my current buffer?

No. Search mode is strictly read-only. According to the implementation in lua/99/ops/search.lua, it parses the LLM response into quickfix entries using parse_line and displays them via vim.fn.setqflist and vim.cmd("copen"). Your buffer content remains unchanged.

Why does visual mode need to create marks?

Visual mode creates durable marks using Mark.mark_above_range and Mark.mark_point because the operation is asynchronous. The user could move the cursor or change buffers while waiting for the LLM response. As implemented in lua/99/ops/over-range.lua, these marks survive the request duration, allowing the code to reconstruct the exact geo.Range via Range.from_marks and execute replace_text accurately.

Can I see live progress during a search operation?

No. The search operation in lua/99/ops/search.lua uses a simple observer callback that only handles the final status and response. In contrast, visual mode implements a streaming observer with on_stdout callbacks that update two RequestStatus objects in real-time, showing "Implementing" status as the LLM generates output.

What happens if I cancel a visual mode request?

If you cancel a visual mode request, the clean_up function in lua/99/ops/over-range.lua performs three critical actions: it stops the two RequestStatus UI displays, clears the durable marks created at the start of the operation, and cancels the underlying LLM request. This ensures the editor returns to a clean state without orphaned UI elements or stale marks.

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 →