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

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. 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 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:

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:

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:

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 Full LSP integration including request wrappers, export extraction, hover processing, and formatting utilities
lua/99/editor/treesitter.lua Provides Tree-sitter node handling used to obtain cursor positions for LSP calls
lua/99/geo.lua Geometry utilities (Point, Range) that convert between Neovim and LSP coordinate systems
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 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 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, 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.

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 →