lspz 0.11.7

AI-friendly LSP compression proxy
Documentation
//! 运行时配置。
//!
//! 支持环境变量覆盖和 TOML 文件的构建器模式配置。

use std::path::Path;
use std::str::FromStr;

use serde::Deserialize;

use crate::error::LspzError;
use crate::metrics::MetricsConfig;

/// 代理的输出格式。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OutputFormat {
    /// 紧凑 JSON(非默认;默认见 [`OutputFormat::Toon`])。
    Json,
    /// TOON(Token-Oriented Object Notation)。默认输出格式。
    Toon,
    /// 标准 LSP JSON 透传(输出中不压缩)。
    Passthrough,
}

impl FromStr for OutputFormat {
    type Err = LspzError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.trim().to_lowercase().as_str() {
            "json" => Ok(Self::Json),
            "toon" => Ok(Self::Toon),
            "passthrough" => Ok(Self::Passthrough),
            _ => Err(LspzError::Config(format!("unknown output format: {s}"))),
        }
    }
}

/// LSP 服务器响应的各类型截断限制。
///
/// 值为 0 表示无限制(该类型的截断禁用)。
#[derive(Debug, Clone, Default, Deserialize)]
pub struct CappingConfig {
    /// 保留的最大诊断数量(0 = 无限制)。
    pub max_diags: usize,
    /// 保留的最大完成项数量(0 = 无限制)。
    pub max_completions: usize,
    /// 保留的最大文档符号数量(0 = 无限制)。
    pub max_symbols: usize,
}

impl CappingConfig {
    /// 如果设置了任何截断限制,返回 `true`。
    pub fn any_enabled(&self) -> bool {
        self.max_diags > 0 || self.max_completions > 0 || self.max_symbols > 0
    }
}

/// lspz 代理的配置。
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
    /// 用于启动后端 LSP 服务器的命令。
    pub backend_cmd: String,
    /// 各类型响应截断限制。
    #[serde(default)]
    pub capping: CappingConfig,
    /// 是否启用诊断压缩。
    #[serde(default = "default_true")]
    pub enable_diag_compress: bool,
    /// 是否启用补全压缩(默认:true)。
    #[serde(default = "default_true")]
    pub enable_completion_compress: bool,
    /// 是否启用悬停压缩(默认:true)。
    #[serde(default = "default_true")]
    pub enable_hover_compress: bool,
    /// 是否启用文档符号压缩(默认:true)。
    #[serde(default = "default_true")]
    pub enable_document_symbol_compress: bool,
    /// 是否启用位置压缩(默认:true)。
    #[serde(default = "default_true")]
    pub enable_location_compress: bool,
    /// 是否启用工作区符号压缩(默认:true)。
    #[serde(default = "default_true")]
    pub enable_workspace_symbol_compress: bool,
    /// 是否启用工作区诊断压缩(默认:true)。
    #[serde(default = "default_true")]
    pub enable_workspace_diag_compress: bool,
    /// 拦截消息的输出格式(json、toon、passthrough)。
    #[serde(default = "default_output_format")]
    pub output_format: OutputFormat,
    /// 日志级别(trace、debug、info、warn、error)。
    #[serde(default = "default_log_level")]
    pub log_level: String,
    /// 运行时指标配置。
    #[serde(default)]
    pub metrics: MetricsConfig,
}

fn default_true() -> bool {
    true
}

fn default_output_format() -> OutputFormat {
    OutputFormat::Toon
}

fn default_log_level() -> String {
    "info".into()
}

impl Default for Config {
    fn default() -> Self {
        Self {
            backend_cmd: String::new(),
            capping: CappingConfig::default(),
            enable_diag_compress: true,
            enable_completion_compress: true,
            enable_hover_compress: true,
            enable_document_symbol_compress: true,
            enable_location_compress: true,
            enable_workspace_symbol_compress: true,
            enable_workspace_diag_compress: true,
            output_format: default_output_format(),
            log_level: "info".into(),
            metrics: MetricsConfig::default(),
        }
    }
}

impl Config {
    /// Create a new [`ConfigBuilder`].
    pub fn builder() -> ConfigBuilder {
        ConfigBuilder::default()
    }

    /// Load config from a TOML file.
    ///
    /// Missing fields use their default values (same as `Config::default()`).
    pub fn from_file(path: impl AsRef<Path>) -> Result<Self, LspzError> {
        let content = std::fs::read_to_string(path.as_ref())
            .map_err(|e| LspzError::Config(format!("cannot read config file: {e}")))?;
        toml::from_str(&content).map_err(|e| LspzError::Config(format!("invalid config file: {e}")))
    }

    /// Returns `true` if the named interceptor is enabled in this config.
    ///
    /// Used by [`InterceptorChain`](crate::interceptors::InterceptorChain) at runtime
    /// to skip disabled interceptors without removing them from the chain.
    pub fn is_interceptor_enabled(&self, name: &str) -> bool {
        match name {
            "capping" => self.capping.any_enabled(),
            "diagnostics_compressor" => self.enable_diag_compress,
            "completion_compressor" => self.enable_completion_compress,
            "hover_compressor" => self.enable_hover_compress,
            "document_symbol_compressor" => self.enable_document_symbol_compress,
            "location_compressor" => self.enable_location_compress,
            "workspace_symbol_compressor" => self.enable_workspace_symbol_compress,
            "workspace_diagnostic_compressor" => self.enable_workspace_diag_compress,
            _ => true,
        }
    }
}

/// Builder for [`Config`].
#[derive(Debug, Default)]
pub struct ConfigBuilder {
    backend_cmd: Option<String>,
    capping: Option<CappingConfig>,
    enable_diag_compress: Option<bool>,
    enable_completion_compress: Option<bool>,
    enable_hover_compress: Option<bool>,
    enable_document_symbol_compress: Option<bool>,
    enable_location_compress: Option<bool>,
    enable_workspace_symbol_compress: Option<bool>,
    enable_workspace_diag_compress: Option<bool>,
    output_format: Option<OutputFormat>,
    log_level: Option<String>,
    metrics: Option<MetricsConfig>,
}

impl ConfigBuilder {
    /// Set the backend LSP server command.
    pub fn backend_cmd(mut self, cmd: impl Into<String>) -> Self {
        self.backend_cmd = Some(cmd.into());
        self
    }

    /// Enable or disable diagnostic compression.
    pub fn enable_diag_compress(mut self, enable: bool) -> Self {
        self.enable_diag_compress = Some(enable);
        self
    }

    /// Enable or disable completion compression.
    pub fn enable_completion_compress(mut self, enable: bool) -> Self {
        self.enable_completion_compress = Some(enable);
        self
    }

    /// Enable or disable hover compression.
    pub fn enable_hover_compress(mut self, enable: bool) -> Self {
        self.enable_hover_compress = Some(enable);
        self
    }

    /// Enable or disable document symbol compression.
    pub fn enable_document_symbol_compress(mut self, enable: bool) -> Self {
        self.enable_document_symbol_compress = Some(enable);
        self
    }

    /// Enable or disable location compression.
    pub fn enable_location_compress(mut self, enable: bool) -> Self {
        self.enable_location_compress = Some(enable);
        self
    }

    /// Enable or disable workspace symbol compression.
    pub fn enable_workspace_symbol_compress(mut self, enable: bool) -> Self {
        self.enable_workspace_symbol_compress = Some(enable);
        self
    }

    /// Enable or disable workspace diagnostic compression.
    pub fn enable_workspace_diag_compress(mut self, enable: bool) -> Self {
        self.enable_workspace_diag_compress = Some(enable);
        self
    }

    /// Set the output format (json, toon, passthrough).
    pub fn output_format(mut self, fmt: OutputFormat) -> Self {
        self.output_format = Some(fmt);
        self
    }

    /// Set the log level.
    pub fn log_level(mut self, level: impl Into<String>) -> Self {
        self.log_level = Some(level.into());
        self
    }

    /// Set the response capping limits.
    pub fn capping(mut self, capping: CappingConfig) -> Self {
        self.capping = Some(capping);
        self
    }

    /// Set the runtime metrics configuration.
    pub fn metrics(mut self, metrics: MetricsConfig) -> Self {
        self.metrics = Some(metrics);
        self
    }

    /// Build the [`Config`], validating required fields.
    pub fn build(self) -> Result<Config, LspzError> {
        let backend_cmd = self
            .backend_cmd
            .or_else(|| std::env::var("LSPZ_BACKEND_CMD").ok())
            .ok_or_else(|| LspzError::Config("backend_cmd is required".into()))?;

        let enable_diag_compress =
            resolve_bool_flag(self.enable_diag_compress, "LSPZ_ENABLE_DIAG_COMPRESS", true);
        let enable_completion_compress = resolve_bool_flag(
            self.enable_completion_compress,
            "LSPZ_ENABLE_COMPLETION_COMPRESS",
            true,
        );
        let enable_hover_compress = resolve_bool_flag(
            self.enable_hover_compress,
            "LSPZ_ENABLE_HOVER_COMPRESS",
            true,
        );
        let enable_document_symbol_compress = resolve_bool_flag(
            self.enable_document_symbol_compress,
            "LSPZ_ENABLE_DOCUMENT_SYMBOL_COMPRESS",
            true,
        );
        let enable_location_compress = resolve_bool_flag(
            self.enable_location_compress,
            "LSPZ_ENABLE_LOCATION_COMPRESS",
            true,
        );
        let enable_workspace_symbol_compress = resolve_bool_flag(
            self.enable_workspace_symbol_compress,
            "LSPZ_ENABLE_WORKSPACE_SYMBOL_COMPRESS",
            true,
        );
        let enable_workspace_diag_compress = resolve_bool_flag(
            self.enable_workspace_diag_compress,
            "LSPZ_ENABLE_WORKSPACE_DIAG_COMPRESS",
            true,
        );

        let log_level = self
            .log_level
            .or_else(|| std::env::var("LSPZ_LOG_LEVEL").ok())
            .unwrap_or_else(|| "info".into());

        let output_format = self
            .output_format
            .or_else(|| {
                std::env::var("LSPZ_OUTPUT_FORMAT")
                    .ok()
                    .and_then(|v| OutputFormat::from_str(&v).ok())
            })
            .unwrap_or(OutputFormat::Toon);

        // Read metrics config from env vars or use builder value
        let metrics = self.metrics.unwrap_or_else(|| MetricsConfig {
            enabled: std::env::var("LSPZ_METRICS_ENABLED")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or(false),
            report_interval_secs: std::env::var("LSPZ_METRICS_INTERVAL")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or(0),
        });

        // Read capping from env vars or use builder value
        let capping = self.capping.unwrap_or_else(|| CappingConfig {
            max_diags: std::env::var("LSPZ_MAX_DIAGS")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or(0),
            max_completions: std::env::var("LSPZ_MAX_COMPLETIONS")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or(0),
            max_symbols: std::env::var("LSPZ_MAX_SYMBOLS")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or(0),
        });

        Ok(Config {
            backend_cmd,
            capping,
            enable_diag_compress,
            enable_completion_compress,
            enable_hover_compress,
            enable_document_symbol_compress,
            enable_location_compress,
            enable_workspace_symbol_compress,
            enable_workspace_diag_compress,
            output_format,
            log_level,
            metrics,
        })
    }
}

/// Resolve a boolean flag from builder value, env var, or default.
fn resolve_bool_flag(builder_val: Option<bool>, env_var: &str, default: bool) -> bool {
    builder_val
        .or_else(|| std::env::var(env_var).ok().and_then(|v| v.parse().ok()))
        .unwrap_or(default)
}