Skip to main content

lspz/
config.rs

1//! 运行时配置。
2//!
3//! 支持环境变量覆盖和 TOML 文件的构建器模式配置。
4
5use std::path::Path;
6use std::str::FromStr;
7
8use serde::Deserialize;
9
10use crate::error::LspzError;
11use crate::metrics::MetricsConfig;
12
13/// 代理的输出格式。
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum OutputFormat {
17    /// 紧凑 JSON(非默认;默认见 [`OutputFormat::Toon`])。
18    Json,
19    /// TOON(Token-Oriented Object Notation)。默认输出格式。
20    Toon,
21    /// 标准 LSP JSON 透传(输出中不压缩)。
22    Passthrough,
23}
24
25impl FromStr for OutputFormat {
26    type Err = LspzError;
27
28    fn from_str(s: &str) -> Result<Self, Self::Err> {
29        match s.trim().to_lowercase().as_str() {
30            "json" => Ok(Self::Json),
31            "toon" => Ok(Self::Toon),
32            "passthrough" => Ok(Self::Passthrough),
33            _ => Err(LspzError::Config(format!("unknown output format: {s}"))),
34        }
35    }
36}
37
38/// LSP 服务器响应的各类型截断限制。
39///
40/// 值为 0 表示无限制(该类型的截断禁用)。
41#[derive(Debug, Clone, Default, Deserialize)]
42pub struct CappingConfig {
43    /// 保留的最大诊断数量(0 = 无限制)。
44    pub max_diags: usize,
45    /// 保留的最大完成项数量(0 = 无限制)。
46    pub max_completions: usize,
47    /// 保留的最大文档符号数量(0 = 无限制)。
48    pub max_symbols: usize,
49}
50
51impl CappingConfig {
52    /// 如果设置了任何截断限制,返回 `true`。
53    pub fn any_enabled(&self) -> bool {
54        self.max_diags > 0 || self.max_completions > 0 || self.max_symbols > 0
55    }
56}
57
58/// lspz 代理的配置。
59#[derive(Debug, Clone, Deserialize)]
60pub struct Config {
61    /// 用于启动后端 LSP 服务器的命令。
62    pub backend_cmd: String,
63    /// 各类型响应截断限制。
64    #[serde(default)]
65    pub capping: CappingConfig,
66    /// 是否启用诊断压缩。
67    #[serde(default = "default_true")]
68    pub enable_diag_compress: bool,
69    /// 是否启用补全压缩(默认:true)。
70    #[serde(default = "default_true")]
71    pub enable_completion_compress: bool,
72    /// 是否启用悬停压缩(默认:true)。
73    #[serde(default = "default_true")]
74    pub enable_hover_compress: bool,
75    /// 是否启用文档符号压缩(默认:true)。
76    #[serde(default = "default_true")]
77    pub enable_document_symbol_compress: bool,
78    /// 是否启用位置压缩(默认:true)。
79    #[serde(default = "default_true")]
80    pub enable_location_compress: bool,
81    /// 是否启用工作区符号压缩(默认:true)。
82    #[serde(default = "default_true")]
83    pub enable_workspace_symbol_compress: bool,
84    /// 是否启用工作区诊断压缩(默认:true)。
85    #[serde(default = "default_true")]
86    pub enable_workspace_diag_compress: bool,
87    /// 拦截消息的输出格式(json、toon、passthrough)。
88    #[serde(default = "default_output_format")]
89    pub output_format: OutputFormat,
90    /// 日志级别(trace、debug、info、warn、error)。
91    #[serde(default = "default_log_level")]
92    pub log_level: String,
93    /// 运行时指标配置。
94    #[serde(default)]
95    pub metrics: MetricsConfig,
96}
97
98fn default_true() -> bool {
99    true
100}
101
102fn default_output_format() -> OutputFormat {
103    OutputFormat::Toon
104}
105
106fn default_log_level() -> String {
107    "info".into()
108}
109
110impl Default for Config {
111    fn default() -> Self {
112        Self {
113            backend_cmd: String::new(),
114            capping: CappingConfig::default(),
115            enable_diag_compress: true,
116            enable_completion_compress: true,
117            enable_hover_compress: true,
118            enable_document_symbol_compress: true,
119            enable_location_compress: true,
120            enable_workspace_symbol_compress: true,
121            enable_workspace_diag_compress: true,
122            output_format: default_output_format(),
123            log_level: "info".into(),
124            metrics: MetricsConfig::default(),
125        }
126    }
127}
128
129impl Config {
130    /// Create a new [`ConfigBuilder`].
131    pub fn builder() -> ConfigBuilder {
132        ConfigBuilder::default()
133    }
134
135    /// Load config from a TOML file.
136    ///
137    /// Missing fields use their default values (same as `Config::default()`).
138    pub fn from_file(path: impl AsRef<Path>) -> Result<Self, LspzError> {
139        let content = std::fs::read_to_string(path.as_ref())
140            .map_err(|e| LspzError::Config(format!("cannot read config file: {e}")))?;
141        toml::from_str(&content).map_err(|e| LspzError::Config(format!("invalid config file: {e}")))
142    }
143
144    /// Returns `true` if the named interceptor is enabled in this config.
145    ///
146    /// Used by [`InterceptorChain`](crate::interceptors::InterceptorChain) at runtime
147    /// to skip disabled interceptors without removing them from the chain.
148    pub fn is_interceptor_enabled(&self, name: &str) -> bool {
149        match name {
150            "capping" => self.capping.any_enabled(),
151            "diagnostics_compressor" => self.enable_diag_compress,
152            "completion_compressor" => self.enable_completion_compress,
153            "hover_compressor" => self.enable_hover_compress,
154            "document_symbol_compressor" => self.enable_document_symbol_compress,
155            "location_compressor" => self.enable_location_compress,
156            "workspace_symbol_compressor" => self.enable_workspace_symbol_compress,
157            "workspace_diagnostic_compressor" => self.enable_workspace_diag_compress,
158            _ => true,
159        }
160    }
161}
162
163/// Builder for [`Config`].
164#[derive(Debug, Default)]
165pub struct ConfigBuilder {
166    backend_cmd: Option<String>,
167    capping: Option<CappingConfig>,
168    enable_diag_compress: Option<bool>,
169    enable_completion_compress: Option<bool>,
170    enable_hover_compress: Option<bool>,
171    enable_document_symbol_compress: Option<bool>,
172    enable_location_compress: Option<bool>,
173    enable_workspace_symbol_compress: Option<bool>,
174    enable_workspace_diag_compress: Option<bool>,
175    output_format: Option<OutputFormat>,
176    log_level: Option<String>,
177    metrics: Option<MetricsConfig>,
178}
179
180impl ConfigBuilder {
181    /// Set the backend LSP server command.
182    pub fn backend_cmd(mut self, cmd: impl Into<String>) -> Self {
183        self.backend_cmd = Some(cmd.into());
184        self
185    }
186
187    /// Enable or disable diagnostic compression.
188    pub fn enable_diag_compress(mut self, enable: bool) -> Self {
189        self.enable_diag_compress = Some(enable);
190        self
191    }
192
193    /// Enable or disable completion compression.
194    pub fn enable_completion_compress(mut self, enable: bool) -> Self {
195        self.enable_completion_compress = Some(enable);
196        self
197    }
198
199    /// Enable or disable hover compression.
200    pub fn enable_hover_compress(mut self, enable: bool) -> Self {
201        self.enable_hover_compress = Some(enable);
202        self
203    }
204
205    /// Enable or disable document symbol compression.
206    pub fn enable_document_symbol_compress(mut self, enable: bool) -> Self {
207        self.enable_document_symbol_compress = Some(enable);
208        self
209    }
210
211    /// Enable or disable location compression.
212    pub fn enable_location_compress(mut self, enable: bool) -> Self {
213        self.enable_location_compress = Some(enable);
214        self
215    }
216
217    /// Enable or disable workspace symbol compression.
218    pub fn enable_workspace_symbol_compress(mut self, enable: bool) -> Self {
219        self.enable_workspace_symbol_compress = Some(enable);
220        self
221    }
222
223    /// Enable or disable workspace diagnostic compression.
224    pub fn enable_workspace_diag_compress(mut self, enable: bool) -> Self {
225        self.enable_workspace_diag_compress = Some(enable);
226        self
227    }
228
229    /// Set the output format (json, toon, passthrough).
230    pub fn output_format(mut self, fmt: OutputFormat) -> Self {
231        self.output_format = Some(fmt);
232        self
233    }
234
235    /// Set the log level.
236    pub fn log_level(mut self, level: impl Into<String>) -> Self {
237        self.log_level = Some(level.into());
238        self
239    }
240
241    /// Set the response capping limits.
242    pub fn capping(mut self, capping: CappingConfig) -> Self {
243        self.capping = Some(capping);
244        self
245    }
246
247    /// Set the runtime metrics configuration.
248    pub fn metrics(mut self, metrics: MetricsConfig) -> Self {
249        self.metrics = Some(metrics);
250        self
251    }
252
253    /// Build the [`Config`], validating required fields.
254    pub fn build(self) -> Result<Config, LspzError> {
255        let backend_cmd = self
256            .backend_cmd
257            .or_else(|| std::env::var("LSPZ_BACKEND_CMD").ok())
258            .ok_or_else(|| LspzError::Config("backend_cmd is required".into()))?;
259
260        let enable_diag_compress =
261            resolve_bool_flag(self.enable_diag_compress, "LSPZ_ENABLE_DIAG_COMPRESS", true);
262        let enable_completion_compress = resolve_bool_flag(
263            self.enable_completion_compress,
264            "LSPZ_ENABLE_COMPLETION_COMPRESS",
265            true,
266        );
267        let enable_hover_compress = resolve_bool_flag(
268            self.enable_hover_compress,
269            "LSPZ_ENABLE_HOVER_COMPRESS",
270            true,
271        );
272        let enable_document_symbol_compress = resolve_bool_flag(
273            self.enable_document_symbol_compress,
274            "LSPZ_ENABLE_DOCUMENT_SYMBOL_COMPRESS",
275            true,
276        );
277        let enable_location_compress = resolve_bool_flag(
278            self.enable_location_compress,
279            "LSPZ_ENABLE_LOCATION_COMPRESS",
280            true,
281        );
282        let enable_workspace_symbol_compress = resolve_bool_flag(
283            self.enable_workspace_symbol_compress,
284            "LSPZ_ENABLE_WORKSPACE_SYMBOL_COMPRESS",
285            true,
286        );
287        let enable_workspace_diag_compress = resolve_bool_flag(
288            self.enable_workspace_diag_compress,
289            "LSPZ_ENABLE_WORKSPACE_DIAG_COMPRESS",
290            true,
291        );
292
293        let log_level = self
294            .log_level
295            .or_else(|| std::env::var("LSPZ_LOG_LEVEL").ok())
296            .unwrap_or_else(|| "info".into());
297
298        let output_format = self
299            .output_format
300            .or_else(|| {
301                std::env::var("LSPZ_OUTPUT_FORMAT")
302                    .ok()
303                    .and_then(|v| OutputFormat::from_str(&v).ok())
304            })
305            .unwrap_or(OutputFormat::Toon);
306
307        // Read metrics config from env vars or use builder value
308        let metrics = self.metrics.unwrap_or_else(|| MetricsConfig {
309            enabled: std::env::var("LSPZ_METRICS_ENABLED")
310                .ok()
311                .and_then(|v| v.parse().ok())
312                .unwrap_or(false),
313            report_interval_secs: std::env::var("LSPZ_METRICS_INTERVAL")
314                .ok()
315                .and_then(|v| v.parse().ok())
316                .unwrap_or(0),
317        });
318
319        // Read capping from env vars or use builder value
320        let capping = self.capping.unwrap_or_else(|| CappingConfig {
321            max_diags: std::env::var("LSPZ_MAX_DIAGS")
322                .ok()
323                .and_then(|v| v.parse().ok())
324                .unwrap_or(0),
325            max_completions: std::env::var("LSPZ_MAX_COMPLETIONS")
326                .ok()
327                .and_then(|v| v.parse().ok())
328                .unwrap_or(0),
329            max_symbols: std::env::var("LSPZ_MAX_SYMBOLS")
330                .ok()
331                .and_then(|v| v.parse().ok())
332                .unwrap_or(0),
333        });
334
335        Ok(Config {
336            backend_cmd,
337            capping,
338            enable_diag_compress,
339            enable_completion_compress,
340            enable_hover_compress,
341            enable_document_symbol_compress,
342            enable_location_compress,
343            enable_workspace_symbol_compress,
344            enable_workspace_diag_compress,
345            output_format,
346            log_level,
347            metrics,
348        })
349    }
350}
351
352/// Resolve a boolean flag from builder value, env var, or default.
353fn resolve_bool_flag(builder_val: Option<bool>, env_var: &str, default: bool) -> bool {
354    builder_val
355        .or_else(|| std::env::var(env_var).ok().and_then(|v| v.parse().ok()))
356        .unwrap_or(default)
357}