# How 99's Visual Selection Mode Integrates with AI Code Generation in Neovim

> Discover how 99's visual selection mode in Neovim seamlessly integrates with AI code generation. Highlight code, get AI suggestions, and update instantly.

- Repository: [ThePrimeagen/99](https://github.com/theprimeagen/99)
- Tags: how-to-guide
- Published: 2026-02-16

---

**99's visual selection mode converts highlighted code into structured LLM prompts and automatically replaces the selection with AI-generated output using internal Range and Request APIs.**

ThePrimeagen's `99` plugin transforms Neovim's visual selection mode into a seamless AI code generation interface. By leveraging internal Lua APIs, the plugin captures your highlighted code, constructs context-rich prompts, and streams LLM responses directly back into your buffer. This workflow eliminates the need for external scripts while maintaining full editor responsiveness.

## How Visual Selection Mode Works in 99

The integration follows a strict pipeline from selection capture to text replacement, orchestrated through several specialized modules.

### Entry Point: The visual API

When you invoke `:lua require('99').visual()` or its mapped keybinding, the function `_99.visual` in [`lua/99/init.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/init.lua) initiates the workflow. This function prepares the request context using `get_context("visual")`, then immediately calls `set_selection_marks()` followed by `Range.from_visual_selection()` to capture the selected region.

### Capturing the Selection Range

The `Range.from_visual_selection` function in [`lua/99/geo.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/geo.lua) reads Neovim's `'<` and `'>` marks to determine the exact boundaries of your selection. It normalizes line-end handling and returns a `Range` object containing the buffer number, start/end points, and helper methods including `:to_string()`, `:to_text()`, and `:replace_text()`.

### Processing the AI Request

The captured range is passed to `ops.over_range` in [`lua/99/ops/over-range.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/ops/over-range.lua), which orchestrates the core logic:

- **Mark Preservation**: Creates temporary top and bottom marks using `Mark.mark_above_range` and `Mark.mark_point` to ensure the original selection can be restored after the request completes.
- **Prompt Construction**: Builds the visual-selection prompt by calling `context._99.prompts.prompts.visual_selection(range)`, which references the template in [`lua/99/prompt-settings.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/prompt-settings.lua). This template embeds the range location, current selection text, and full file contents.
- **Request Initialization**: Creates a `Request` object via `make_prompt` (in [`lua/99/ops/make-prompt.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/ops/make-prompt.lua)) that handles completions, agent rules, and streams the AI output while displaying live status updates through `RequestStatus`.

### Replacing Text with Generated Code

When the request finishes with status `"success"`, `over_range` validates that the original marks remain valid (accounting for potential user edits during generation). It reconstructs the target range using `Range.from_marks` and executes `new_range:replace_text(lines)`, where `lines` contains the AI-generated content split on newlines. The function injects a leading empty line to maintain visual line alignment before writing the output back into the buffer.

Cleanup occurs regardless of success or failure via `clean_up`, which stops visual status indicators, clears temporary marks, and cancels any pending requests.

## Key Components of the Visual Selection Workflow

The integration relies on these specific modules:

- **[`lua/99/init.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/init.lua)** – Public API entry point; defines `_99.visual` that captures selections and invokes core operations.
- **[`lua/99/geo.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/geo.lua)** – Implements `Range.from_visual_selection` for converting Neovim marks into structured range objects.
- **[`lua/99/ops/over-range.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/ops/over-range.lua)** – Core workflow engine handling mark preservation, prompt building, request execution, and text replacement.
- **[`lua/99/prompt-settings.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/prompt-settings.lua)** – Contains the `visual_selection` prompt template that structures context for the LLM.
- **[`lua/99/ops/make-prompt.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/ops/make-prompt.lua)** – Combines visual prompts with user input and extracts completion references.
- **[`lua/99/request/init.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/request/init.lua)** – Manages AI request lifecycle including streaming and cancellation.
- **[`lua/99/ops/request_status.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/ops/request_status.lua)** – Provides live status updates during LLM generation.
- **[`lua/99/ops/marks.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/ops/marks.lua)** – Utility for creating and managing temporary marks that survive asynchronous operations.

## Practical Examples

### Triggering Visual Selection Mode

Visually select the code you want to replace, then execute the command (typically mapped to `<Leader>v`):

```lua
-- Visually select code, then run:
vim.cmd('lua require("99").visual()')

```

### Understanding the Internal Flow

The plugin processes your selection through several internal stages:

```lua
-- _99.visual (lua/99/init.lua)
local range = Range.from_visual_selection()   -- Captures '< and '> marks
ops.over_range(context, range, opts)          -- Hands to core logic

```

The LLM receives a structured prompt generated from [`lua/99/prompt-settings.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/prompt-settings.lua):

```lua
-- visual_selection prompt template structure
-- You receive a selection in neovim that you need to replace with new code.
-- <SELECTION_LOCATION>
-- range(1:5,1:12)
-- </SELECTION_LOCATION>
-- <SELECTION_CONTENT>
-- old code…
-- </SELECTION_CONTENT>
-- <FILE_CONTAINING_SELECTION>
-- path/to/file.lua
-- </FILE_CONTAINING_SELECTION>

```

After generation, the plugin replaces the original text:

```lua
-- Replacing selection (lua/99/ops/over-range.lua)
local new_range = Range.from_marks(top_mark, bottom_mark)
local lines = vim.split(response, "\n")
table.insert(lines, 1, "")            -- Maintains line alignment
new_range:replace_text(lines)          -- Writes AI output to buffer

```

### Simulating Requests in Tests

You can test the workflow programmatically:

```lua
-- In lua/99/test/visual_spec.lua (excerpt)
local range = Range:new(buf, Point.from_0_based(0,0), Point.from_0_based(2,0))
local context = fake_context()
ops.over_range(context, range, {})
-- Expect the buffer to contain the AI-generated content after the call

```

## Summary

- **99's visual selection mode** transforms highlighted Neovim code into AI generation requests through the `_99.visual` API in [`lua/99/init.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/init.lua).
- The **Range system** in [`lua/99/geo.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/geo.lua) converts Neovim's visual marks into structured objects with text replacement capabilities.
- **Prompt construction** combines selection context, file contents, and user input via templates in [`lua/99/prompt-settings.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/prompt-settings.lua) and [`lua/99/ops/make-prompt.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/ops/make-prompt.lua).
- **Asynchronous handling** preserves editor state using temporary marks in [`lua/99/ops/marks.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/ops/marks.lua) while streaming LLM responses through [`lua/99/request/init.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/request/init.lua).
- **Automatic replacement** swaps the original selection with generated code using `Range:replace_text()` after validating mark integrity.

## Frequently Asked Questions

### How do I trigger 99's visual selection mode?

Visually select the code you want to modify using Neovim's visual mode (character-wise, line-wise, or block-wise), then execute `:lua require('99').visual()` or use the default keybinding mapped to `<Leader>v`. The plugin immediately captures the selection boundaries and initiates the AI request workflow.

### What happens to my original code while waiting for AI generation?

The plugin creates temporary marks above and below your selection using `Mark.mark_above_range` and `Mark.mark_point` in [`lua/99/ops/marks.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/ops/marks.lua) to preserve the exact location. While the LLM generates code, your original text remains visible and editable. If you modify the buffer during generation, the plugin validates mark integrity before replacement and aborts if the context has changed significantly.

### Can I customize the prompt sent to the LLM in visual selection mode?

Yes, the visual selection prompt template is defined in [`lua/99/prompt-settings.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/prompt-settings.lua) under `prompts.visual_selection`. This template automatically injects the selection location, content, and file path. You can provide additional context through the `opts` parameter in `_99.visual()` or by configuring the global prompt settings to include specific coding standards, language preferences, or agent rules that get combined via `make_prompt` in [`lua/99/ops/make-prompt.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/ops/make-prompt.lua).

### Does 99's visual selection mode work with multiple cursors or block selections?

The current implementation in [`lua/99/geo.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/geo.lua) uses Neovim's `'<` and `'>` marks via `Range.from_visual_selection()`, which captures the last visual selection area. While this supports character-wise (`v`), line-wise (`V`), and block-wise (`<C-v>`) visual modes, it processes the selection as a single continuous range. Multiple cursor support would require extending the `Range` system to handle disjointed selections, which is not currently implemented in the `ops.over_range` workflow.