Skip to content

External LSP API Reference

Mo Abualruz edited this page Dec 9, 2025 · 2 revisions

External LSP API Reference

Status: ✅ Complete

Phase: Phase 4 - Alpha Polishing

Last Updated: December 5, 2025


Overview

This document provides detailed API reference for the External LSP Integration feature. It covers the main interfaces, data structures, and usage examples for developers integrating external LSP servers into ricecoder.

Core Interfaces

LspServerRegistry

The LspServerRegistry manages all configured LSP servers and provides methods to query and manage them.

Structure

pub struct LspServerRegistry {
    /// Map of language to server configurations
    pub servers: HashMap<String, Vec<LspServerConfig>>,
    /// Global settings
    pub global: GlobalLspSettings,
}

Methods

load_from_file(path: &Path) -> Result<Self>

Load registry from a YAML configuration file.

Parameters:

  • path: Path to the configuration file

Returns: Result<LspServerRegistry, ConfigError>

Example:

let registry = LspServerRegistry::load_from_file(
    Path::new("~/.ricecoder/lsp-servers.yaml")
)?;
get_server_for_language(language: &str) -> Option<&LspServerConfig>

Get the first available server configuration for a language.

Parameters:

  • language: Language identifier (e.g., "rust", "typescript")

Returns: Option<&LspServerConfig> - First enabled server for the language, or None

Example:

if let Some(config) = registry.get_server_for_language("rust") {
    println!("Using: {}", config.executable);
}
get_all_servers_for_language(language: &str) -> Vec<&LspServerConfig>

Get all server configurations for a language.

Parameters:

  • language: Language identifier

Returns: Vec<&LspServerConfig> - All enabled servers for the language

Example:

let servers = registry.get_all_servers_for_language("typescript");
for server in servers {
    println!("Available: {}", server.executable);
}
get_language_for_extension(extension: &str) -> Option<&str>

Get the language for a file extension.

Parameters:

  • extension: File extension (e.g., ".rs", ".ts")

Returns: Option<&str> - Language identifier, or None

Example:

if let Some(language) = registry.get_language_for_extension(".rs") {
    println!("Language: {}", language);
}
reload(&mut self) -> Result<()>

Reload configuration from disk.

Returns: Result<(), ConfigError>

Example:

registry.reload()?;

ProcessManager

The ProcessManager manages the lifecycle of external LSP server processes.

Structure

pub struct ProcessManager {
    processes: HashMap<String, LspClient>,
    config: GlobalLspSettings,
}

Methods

new(config: GlobalLspSettings) -> Self

Create a new process manager.

Parameters:

  • config: Global LSP settings

Returns: ProcessManager

Example:

let config = GlobalLspSettings {
    max_processes: 5,
    default_timeout_ms: 5000,
    enable_fallback: true,
    health_check_interval_ms: 30000,
};
let manager = ProcessManager::new(config);
spawn_server(config: &LspServerConfig, workspace_root: &Path) -> Result<LspClient>

Spawn a new LSP server process.

Parameters:

  • config: Server configuration
  • workspace_root: Workspace root directory

Returns: Result<LspClient, ExternalLspError>

Example:

let client = manager.spawn_server(
    &server_config,
    Path::new("/path/to/project")
)?;
get_or_spawn_server(language: &str, workspace_root: &Path) -> Result<&mut LspClient>

Get existing server for language or spawn new one.

Parameters:

  • language: Language identifier
  • workspace_root: Workspace root directory

Returns: Result<&mut LspClient, ExternalLspError>

Example:

let client = manager.get_or_spawn_server("rust", workspace_root)?;
shutdown_server(language: &str) -> Result<()>

Shutdown a server for a language.

Parameters:

  • language: Language identifier

Returns: Result<(), ExternalLspError>

Example:

manager.shutdown_server("rust")?;
shutdown_all() -> Result<()>

Shutdown all managed servers.

Returns: Result<(), ExternalLspError>

Example:

manager.shutdown_all()?;
health_check_all() -> Result<HashMap<String, HealthStatus>>

Perform health checks on all servers.

Returns: Result<HashMap<String, HealthStatus>, ExternalLspError>

Example:

let health = manager.health_check_all()?;
for (language, status) in health {
    println!("{}: {:?}", language, status);
}

LspClient

The LspClient represents a connection to an external LSP server.

Structure

pub struct LspClient {
    /// Configuration for this client
    pub config: LspServerConfig,
    /// Current state
    pub state: ClientState,
    /// Child process handle
    pub process: Option<Child>,
    /// Stdin writer for sending requests
    pub stdin: Option<ChildStdin>,
    /// Pending requests awaiting response
    pub pending_requests: HashMap<RequestId, PendingRequest>,
    /// Server capabilities (after initialization)
    pub capabilities: Option<ServerCapabilities>,
    /// Workspace root
    pub workspace_root: PathBuf,
}

Methods

send_request(method: &str, params: serde_json::Value) -> Result<serde_json::Value>

Send a request to the LSP server.

Parameters:

  • method: LSP method name (e.g., "textDocument/completion")
  • params: Request parameters as JSON

Returns: Result<serde_json::Value, ExternalLspError>

Example:

let params = json!({
    "textDocument": { "uri": "file:///path/to/file.rs" },
    "position": { "line": 10, "character": 5 }
});
let result = client.send_request("textDocument/completion", params)?;
send_notification(method: &str, params: serde_json::Value) -> Result<()>

Send a notification to the LSP server (no response expected).

Parameters:

  • method: LSP method name
  • params: Notification parameters as JSON

Returns: Result<(), ExternalLspError>

Example:

let params = json!({
    "textDocument": { "uri": "file:///path/to/file.rs" },
    "contentChanges": [{ "text": "new content" }]
});
client.send_notification("textDocument/didChange", params)?;
initialize(workspace_root: &Path) -> Result<ServerCapabilities>

Initialize the LSP server connection.

Parameters:

  • workspace_root: Workspace root directory

Returns: Result<ServerCapabilities, ExternalLspError>

Example:

let capabilities = client.initialize(Path::new("/path/to/project"))?;
println!("Completion support: {}", capabilities.completion_provider.is_some());
shutdown() -> Result<()>

Shutdown the LSP server connection gracefully.

Returns: Result<(), ExternalLspError>

Example:

client.shutdown()?;
health_check() -> Result<HealthStatus>

Perform a health check on the server.

Returns: Result<HealthStatus, ExternalLspError>

Example:

let status = client.health_check()?;
match status {
    HealthStatus::Healthy { latency } => println!("Latency: {:?}", latency),
    HealthStatus::Unhealthy { reason } => println!("Error: {}", reason),
}

OutputMappingConfig

Configuration for transforming LSP server responses.

Structure

pub struct OutputMappingConfig {
    /// Mapping rules for completion items
    pub completion: Option<CompletionMappingRules>,
    /// Mapping rules for diagnostics
    pub diagnostics: Option<DiagnosticsMappingRules>,
    /// Mapping rules for hover information
    pub hover: Option<HoverMappingRules>,
    /// Custom transformation functions (by name)
    pub custom_transforms: Option<HashMap<String, String>>,
}

CompletionMappingRules

pub struct CompletionMappingRules {
    /// JSON path to completion items array
    pub items_path: String,
    /// Field mappings for each completion item
    pub field_mappings: HashMap<String, String>,
    /// Optional transformation function name
    pub transform: Option<String>,
}

Example:

let rules = CompletionMappingRules {
    items_path: "$.result.items".to_string(),
    field_mappings: [
        ("label".to_string(), "$.label".to_string()),
        ("detail".to_string(), "$.detail".to_string()),
    ].iter().cloned().collect(),
    transform: None,
};

DiagnosticsMappingRules

pub struct DiagnosticsMappingRules {
    /// JSON path to diagnostics array
    pub items_path: String,
    /// Field mappings for each diagnostic
    pub field_mappings: HashMap<String, String>,
    /// Optional transformation function name
    pub transform: Option<String>,
}

HoverMappingRules

pub struct HoverMappingRules {
    /// JSON path to hover content
    pub content_path: String,
    /// Field mappings for hover data
    pub field_mappings: HashMap<String, String>,
    /// Optional transformation function name
    pub transform: Option<String>,
}

Data Structures

LspServerConfig

Configuration for a single LSP server.

pub struct LspServerConfig {
    pub language: String,
    pub extensions: Vec<String>,
    pub executable: String,
    pub args: Vec<String>,
    pub env: HashMap<String, String>,
    pub init_options: Option<serde_json::Value>,
    pub enabled: bool,
    pub timeout_ms: u64,
    pub max_restarts: u32,
    pub idle_timeout_ms: u64,
    pub output_mapping: Option<OutputMappingConfig>,
}

GlobalLspSettings

Global settings for LSP server management.

pub struct GlobalLspSettings {
    pub max_processes: usize,
    pub default_timeout_ms: u64,
    pub enable_fallback: bool,
    pub health_check_interval_ms: u64,
}

ClientState

State of an LSP client connection.

pub enum ClientState {
    Stopped,
    Starting,
    Running,
    Unhealthy,
    ShuttingDown,
    Crashed { restart_count: u32 },
}

HealthStatus

Result of a health check.

pub enum HealthStatus {
    Healthy { latency: Duration },
    Unhealthy { reason: String },
}

ExternalLspError

Error types for external LSP operations.

pub enum ExternalLspError {
    ServerNotFound { executable: String },
    SpawnFailed(std::io::Error),
    ServerCrashed { reason: String },
    Timeout { timeout_ms: u64 },
    ProtocolError(String),
    InitializationFailed(String),
    ConfigError(String),
    NoServerForLanguage { language: String },
}

Usage Examples

Basic Setup

use ricecoder_external_lsp::{LspServerRegistry, ProcessManager};
use std::path::Path;

// Load configuration
let registry = LspServerRegistry::load_from_file(
    Path::new("~/.ricecoder/lsp-servers.yaml")
)?;

// Create process manager
let mut manager = ProcessManager::new(registry.global.clone());

// Get or spawn server for Rust
let client = manager.get_or_spawn_server("rust", Path::new("/path/to/project"))?;

// Send completion request
let params = serde_json::json!({
    "textDocument": { "uri": "file:///path/to/file.rs" },
    "position": { "line": 10, "character": 5 }
});
let completions = client.send_request("textDocument/completion", params)?;

// Shutdown
manager.shutdown_all()?;

Custom LSP Server with Output Mapping

use ricecoder_external_lsp::{
    LspServerConfig, OutputMappingConfig, CompletionMappingRules,
};
use std::collections::HashMap;

let config = LspServerConfig {
    language: "custom".to_string(),
    extensions: vec![".custom".to_string()],
    executable: "custom-lsp-server".to_string(),
    args: vec![],
    env: HashMap::new(),
    init_options: None,
    enabled: true,
    timeout_ms: 5000,
    max_restarts: 3,
    idle_timeout_ms: 300000,
    output_mapping: Some(OutputMappingConfig {
        completion: Some(CompletionMappingRules {
            items_path: "$.completions".to_string(),
            field_mappings: [
                ("label".to_string(), "$.name".to_string()),
                ("detail".to_string(), "$.description".to_string()),
            ].iter().cloned().collect(),
            transform: None,
        }),
        diagnostics: None,
        hover: None,
        custom_transforms: None,
    }),
};

Health Checking

// Check health of all servers
let health = manager.health_check_all()?;

for (language, status) in health {
    match status {
        HealthStatus::Healthy { latency } => {
            println!("{}: OK ({}ms)", language, latency.as_millis());
        }
        HealthStatus::Unhealthy { reason } => {
            println!("{}: FAILED ({})", language, reason);
        }
    }
}

Graceful Degradation

// Try external LSP, fall back to internal
let completions = match client.send_request("textDocument/completion", params) {
    Ok(result) => {
        // Use external LSP completions
        parse_lsp_completions(result)
    }
    Err(e) => {
        // Fall back to internal provider
        eprintln!("External LSP failed: {}", e);
        internal_completion_provider.get_completions(&context)
    }
};

Error Handling

All operations return Result<T, ExternalLspError>. Handle errors appropriately:

match client.send_request("textDocument/completion", params) {
    Ok(result) => {
        // Process result
    }
    Err(ExternalLspError::Timeout { timeout_ms }) => {
        eprintln!("Request timed out after {}ms", timeout_ms);
        // Fall back to internal provider
    }
    Err(ExternalLspError::ServerCrashed { reason }) => {
        eprintln!("Server crashed: {}", reason);
        // Attempt restart or fall back
    }
    Err(e) => {
        eprintln!("Error: {}", e);
        // Handle other errors
    }
}

Configuration Examples

Minimal Configuration

servers:
  rust:
    - language: rust
      extensions: [".rs"]
      executable: rust-analyzer

Complete Configuration

global:
  max_processes: 5
  default_timeout_ms: 5000
  enable_fallback: true
  health_check_interval_ms: 30000

servers:
  rust:
    - language: rust
      extensions: [".rs"]
      executable: rust-analyzer
      args: []
      env: {}
      init_options:
        checkOnSave:
          command: "clippy"
      enabled: true
      timeout_ms: 10000
      max_restarts: 3
      idle_timeout_ms: 300000

Custom LSP with Output Mapping

servers:
  custom:
    - language: custom
      extensions: [".custom"]
      executable: custom-lsp-server
      output_mapping:
        completion:
          items_path: "$.completions"
          field_mappings:
            label: "$.name"
            detail: "$.description"

See Also


Last updated: December 5, 2025

Clone this wiki locally