# How 99 Integrates with LSP for Code Intelligence: A Deep Dive into the Neovim Plugin

> Discover how the 99 plugin integrates with LSP for code intelligence in Neovim. Learn about its asynchronous API for definitions, hover info, symbols, and export extraction.

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

---

**The 99 plugin integrates with LSP for code intelligence by providing a thin wrapper around Neovim's built-in LSP client that handles definitions, hover information, document symbols, and export extraction through an asynchronous, non-blocking API.**

The **99** Neovim plugin delivers rich code intelligence capabilities by leveraging the Language Server Protocol (LSP) through a custom abstraction layer. Rather than requiring users to interact directly with raw LSP commands, 99 provides a streamlined interface that orchestrates multiple LSP requests to deliver comprehensive symbol information, type definitions, and export summaries. This integration enables features like "show exports" functionality that aggregates definition locations, document symbols, and hover information into human-readable output.

## Core LSP Architecture in 99

The LSP integration in 99 centers around a dedicated module that manages all server interactions while providing higher-level abstractions for export extraction and formatting.

### The Lsp Class: Central Entry Point

At the heart of 99's LSP integration sits the **`Lsp`** class defined in [`lua/99/editor/lsp.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/editor/lsp.lua). This class stores user configuration and exposes the primary public API for code intelligence operations. Key methods include:

- **`new(config)`** – Initializes a new Lsp instance with optional configuration
- **`stringify_definition_exports(bufnr, pos, cb)`** – Retrieves and formats export information for the symbol at the given position
- **`stringify_definition_exports_from_node(node, cb)`** – Tree-sitter node variant that converts AST nodes to points before processing
- **`get_exports(uri, cb)`** – Retrieves all exports for an entire module via its URI

### LSP Request Wrappers and Buffer Management

The integration provides thin wrappers around Neovim's `vim.lsp.buf_request` to normalize request payloads and callback signatures. These helper functions handle the low-level LSP protocol details:

- **`get_lsp_definitions(bufnr, pos, cb)`** – Wraps `textDocument/definition` requests (lines 19-40)
- **`get_lsp_hover(bufnr, pos, cb)`** – Wraps `textDocument/hover` requests (lines 65-88)
- **`get_lsp_document_symbols(bufnr, cb)`** – Wraps `textDocument/documentSymbol` requests (lines 93-112)

Before issuing any request, 99 ensures the target buffer has an active LSP client attached through **`ensure_buffer_with_lsp`** (lines 41-63) and **`with_target_buffer_from_uri`** (lines 223-233). These functions guarantee that the language server is ready before attempting code intelligence queries.

### Export Extraction and Symbol Processing

Once document symbols are retrieved, 99 processes them to identify exported functions, classes, and members:

- **`document_symbols_to_exports`** (lines 124-210) – Converts LSP DocumentSymbol results into export metadata, with fallback to a simple Lua parser when needed
- **`find_export_keys`** (lines 212-240) – Identifies which symbols are actually exported from the module
- **`find_class_member_positions`** (lines 260-280) – Locates class members for type extraction

Hover information processing occurs through **`get_exports_hover_info`** and **`get_class_members_hover`**, which transform raw LSP hover payloads into readable type signatures while stripping markdown fences and collapsing complex table structures.

## How 99 Processes LSP Requests for Code Intelligence

The integration follows a sophisticated multi-stage pipeline to transform raw LSP responses into actionable code intelligence.

### The Complete Request Flow

When a user triggers a "show exports" command, 99 executes the following asynchronous sequence:

1. **Definition Resolution** – `resolve_definition_location` sends a `textDocument/definition` request via `get_lsp_definitions` to locate the target symbol's source file
2. **Buffer Preparation** – `with_target_buffer_from_uri` ensures the target URI is loaded and has an active LSP client attached
3. **Symbol Extraction** – `get_lsp_document_symbols` retrieves all symbols from the target file, which `document_symbols_to_exports` parses to locate exported functions, classes, and members
4. **Type Information Gathering** – For each export, `get_exports_hover_info` performs `textDocument/hover` requests to retrieve type signatures
5. **Class Member Resolution** – If a symbol is identified as a class (LSP kind 5/11/23 or hover contains `__index`), `find_class_member_positions` discovers members and `get_class_members_hover` gathers their signatures
6. **Formatting and Output** – `build_export_definitions` combines all data, which `_format_exports` or `stringify_export_definition` transforms into the final human-readable string displayed to the user

### Asynchronous Architecture

Every LSP request in 99 is **completely asynchronous**. The integration wraps callbacks in `vim.schedule` calls or uses Neovim's native LSP callback mechanisms to ensure the UI remains responsive during long-running language server queries. This design prevents blocking the editor while waiting for `textDocument/definition` or `textDocument/hover` responses from slow language servers.

## Integration with Neovim's Built-in LSP Client

Rather than implementing a custom LSP client, 99 leverages Neovim's native `vim.lsp` API to handle protocol communication.

### Direct LSP API Usage

The integration in [`lua/99/editor/lsp.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/editor/lsp.lua) calls `vim.lsp.buf_request` directly for all operations. This approach ensures compatibility with any language server that conforms to the LSP specification, including `lua-language-server`, `typescript-language-server`, `rust-analyzer`, and others.

The wrapper functions normalize the callback signatures across different request types. For example, `get_lsp_definitions` handles the variance between single Location results and arrays of LocationLink objects, presenting a consistent interface to the higher-level export extraction logic.

### Coordination with Tree-sitter and Geometry Utilities

While the LSP module handles server communication, it integrates closely with other 99 components for position management:

- **`99.editor.treesitter`** supplies AST nodes that convert to `99.Point` objects via `Geo.Range:from_ts_node`, which `Lsp.stringify_definition_exports_from_node` consumes to initiate LSP queries from tree-sitter contexts
- **`99.geo`** provides the `Point` and `Range` utilities that translate between Neovim's 1-indexed line/column coordinates and LSP's 0-indexed Position objects

This separation of concerns allows the LSP module to remain focused on protocol handling while delegating buffer geometry and syntax tree operations to specialized subsystems.

## Practical Examples: Using 99's LSP Features

The following examples demonstrate how to initialize the LSP wrapper and invoke its code intelligence capabilities in your own Neovim configuration or plugin development.

### Initializing the LSP Wrapper

Typically performed once during plugin setup:

```lua
local Lsp = require("99.editor.lsp").Lsp
local lsp = Lsp.new({})   -- configuration table can be passed here

```

### Displaying Exports for the Symbol Under Cursor

This example retrieves and displays formatted export information for the symbol at the current cursor position:

```lua
local bufnr = vim.api.nvim_get_current_buf()
local pos   = require("99.geo").Point:from_lsp(vim.api.nvim_win_get_cursor(0))

lsp.stringify_definition_exports(bufnr, pos, function(result, err)
  if err then
    vim.notify("LSP error: " .. err, vim.log.levels.ERROR)
    return
  end
  
  -- Display result in a floating window
  local win = vim.api.nvim_open_win(vim.api.nvim_create_buf(false, true), true, {
    relative = "cursor",
    row = 1,
    col = 0,
    width = math.min(80, vim.o.columns),
    height = math.min(20, vim.o.lines),
    style = "minimal",
    border = "single",
  })
  
  vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.split(result, "\n"))
end)

```

### Retrieving Exports for an Entire Module

To fetch all exports from a specific file URI:

```lua
local uri = vim.uri_from_fname("/path/to/module.lua")

lsp.get_exports(uri, function(result, err)
  if err then
    vim.err_writeln("Failed to fetch exports: " .. err)
    return
  end
  
  print(result)   -- Nicely formatted export list with type signatures
end)

```

## Key Source Files for LSP Integration

The following files contain the core implementation of 99's LSP integration:

| File | Primary Purpose |
|------|----------------|
| [`lua/99/editor/lsp.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/editor/lsp.lua) | Full LSP integration including request wrappers, export extraction, hover processing, and formatting utilities |
| [`lua/99/editor/treesitter.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/editor/treesitter.lua) | Provides Tree-sitter node handling used to obtain cursor positions for LSP calls |
| [`lua/99/geo.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/geo.lua) | Geometry utilities (`Point`, `Range`) that convert between Neovim and LSP coordinate systems |
| [`lua/99/editor/init.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/editor/init.lua) | Exposes editor-related sub-modules including the LSP wrapper to the rest of the plugin |
| [`lua/99/request/init.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/request/init.lua) | High-level request handling that forwards user actions to the LSP wrapper |

## Summary

- **99 integrates with LSP for code intelligence** through a thin wrapper around Neovim's built-in `vim.lsp` client, providing asynchronous access to definitions, hover information, and document symbols.

- The **`Lsp`** class in [`lua/99/editor/lsp.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/editor/lsp.lua) serves as the central hub, exposing high-level methods like `stringify_definition_exports` and `get_exports` while handling low-level protocol details internally.

- The integration follows a **multi-stage pipeline**: resolving definitions, loading target buffers, extracting document symbols, gathering hover type information, resolving class members, and formatting the results into human-readable output.

- **Asynchronous architecture** ensures the UI remains responsive by wrapping all LSP requests in proper callback chains and `vim.schedule` calls.

- The system integrates with **Tree-sitter** and **geometry utilities** to convert between AST nodes, Neovim coordinates, and LSP positions.

## Frequently Asked Questions

### How does 99 handle asynchronous LSP requests?

99 wraps all LSP interactions in asynchronous callbacks using Neovim's `vim.lsp.buf_request` API. Each request—whether for definitions, hover information, or document symbols—accepts a callback function that processes the result without blocking the main UI thread. The implementation uses `vim.schedule` to ensure callbacks execute safely within Neovim's event loop, keeping the editor responsive even when querying slow language servers.

### What LSP methods does 99 support for code intelligence?

According to the source code in [`lua/99/editor/lsp.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/editor/lsp.lua), 99 primarily utilizes three core LSP methods: **`textDocument/definition`** for locating symbol declarations, **`textDocument/hover`** for retrieving type signatures and documentation, and **`textDocument/documentSymbol`** for extracting module exports and class structures. These methods combine to power 99's export extraction, type inspection, and definition navigation features.

### How does 99 extract export information from LSP document symbols?

The plugin processes `textDocument/documentSymbol` responses through the `document_symbols_to_exports` function, which parses the hierarchical symbol tree to identify exported functions, classes, and members. For each identified symbol, 99 performs subsequent `textDocument/hover` requests via `get_exports_hover_info` to capture type signatures. If the symbol represents a class (identified by LSP kind values 5, 11, or 23, or hover text containing `__index`), the system additionally resolves member positions and gathers their type information through `find_class_member_positions` and `get_class_members_hover`.

### Can 99 work with any language server?

Yes, 99's LSP integration is designed to work with any language server that conforms to the Language Server Protocol specification. Because it leverages Neovim's built-in `vim.lsp` client rather than implementing custom protocol handlers, 99 automatically supports popular servers like `lua-language-server`, `typescript-language-server`, `rust-analyzer`, and others. The only requirement is that the language server must implement the standard `textDocument/definition`, `textDocument/hover`, and `textDocument/documentSymbol` methods that 99 relies upon for its code intelligence features.