Skip to main content

perl_lsp_config/
lib.rs

1//! Configuration models for perl-lsp server runtime state.
2//!
3//! This microcrate isolates configuration parsing and defaults from the main
4//! server crate so they can evolve independently and be reused by tooling.
5
6use std::path::PathBuf;
7#[cfg(not(target_arch = "wasm32"))]
8use std::process::Command;
9
10/// Server configuration
11///
12/// Runtime configuration for the LSP server features including inlay hints
13/// and test runner integration. Updated dynamically via `didChangeConfiguration`.
14#[derive(Debug, Clone)]
15pub struct ServerConfig {
16    /// Whether inlay hints are globally enabled.
17    pub inlay_hints_enabled: bool,
18    /// Show parameter name hints at call sites.
19    pub inlay_hints_parameter_hints: bool,
20    /// Show inferred type hints for variables.
21    pub inlay_hints_type_hints: bool,
22    /// Show hints for method chains.
23    pub inlay_hints_chained_hints: bool,
24    /// Maximum character length for hint labels before truncation.
25    pub inlay_hints_max_length: usize,
26
27    /// Whether the integrated test runner is enabled.
28    pub test_runner_enabled: bool,
29    /// Command to execute tests (e.g., "perl", "prove").
30    pub test_runner_command: String,
31    /// Additional arguments passed to the test command.
32    pub test_runner_args: Vec<String>,
33    /// Test execution timeout in milliseconds.
34    pub test_runner_timeout: u64,
35
36    /// Whether telemetry events are enabled.
37    pub telemetry_enabled: bool,
38}
39
40impl Default for ServerConfig {
41    fn default() -> Self {
42        Self {
43            inlay_hints_enabled: true,
44            inlay_hints_parameter_hints: true,
45            inlay_hints_type_hints: true,
46            inlay_hints_chained_hints: false,
47            inlay_hints_max_length: 30,
48            test_runner_enabled: true,
49            test_runner_command: "perl".to_string(),
50            test_runner_args: vec![],
51            test_runner_timeout: 60000,
52            telemetry_enabled: false,
53        }
54    }
55}
56
57impl ServerConfig {
58    /// Update configuration from LSP settings
59    pub fn update_from_value(&mut self, settings: &serde_json::Value) {
60        if let Some(inlay) = settings.get("inlayHints") {
61            if let Some(enabled) = inlay.get("enabled").and_then(|v| v.as_bool()) {
62                self.inlay_hints_enabled = enabled;
63            }
64            if let Some(param) = inlay.get("parameterHints").and_then(|v| v.as_bool()) {
65                self.inlay_hints_parameter_hints = param;
66            }
67            if let Some(type_hints) = inlay.get("typeHints").and_then(|v| v.as_bool()) {
68                self.inlay_hints_type_hints = type_hints;
69            }
70            if let Some(chained) = inlay.get("chainedHints").and_then(|v| v.as_bool()) {
71                self.inlay_hints_chained_hints = chained;
72            }
73            if let Some(max_len) = inlay.get("maxLength").and_then(|v| v.as_u64()) {
74                self.inlay_hints_max_length = max_len as usize;
75            }
76        }
77
78        if let Some(test) = settings.get("testRunner") {
79            if let Some(enabled) = test.get("enabled").and_then(|v| v.as_bool()) {
80                self.test_runner_enabled = enabled;
81            }
82            if let Some(cmd) = test.get("command").and_then(|v| v.as_str()) {
83                self.test_runner_command = cmd.to_string();
84            }
85            if let Some(args) = test.get("args").and_then(|v| v.as_array()) {
86                self.test_runner_args =
87                    args.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect();
88            }
89            if let Some(timeout) = test.get("timeout").and_then(|v| v.as_u64()) {
90                self.test_runner_timeout = timeout;
91            }
92        }
93
94        if let Some(telemetry) = settings.get("telemetry")
95            && let Some(enabled) = telemetry.get("enabled").and_then(|v| v.as_bool())
96        {
97            self.telemetry_enabled = enabled;
98        }
99    }
100}
101
102/// Workspace configuration for module resolution
103///
104/// Controls how the LSP server resolves module imports and finds
105/// Perl module files across the workspace.
106#[derive(Debug, Clone)]
107pub struct WorkspaceConfig {
108    /// Custom include paths for module resolution (relative to workspace root)
109    /// Default: `["lib", ".", "local/lib/perl5"]`
110    pub include_paths: Vec<String>,
111
112    /// Whether to include system @INC paths in module resolution
113    /// Default: false (avoids blocking on network filesystems)
114    pub use_system_inc: bool,
115
116    /// Cached system @INC paths (populated lazily when use_system_inc is true)
117    system_inc_cache: Option<Vec<PathBuf>>,
118
119    /// Resolution timeout in milliseconds
120    /// Default: 50ms
121    pub resolution_timeout_ms: u64,
122}
123
124impl Default for WorkspaceConfig {
125    fn default() -> Self {
126        Self {
127            include_paths: vec!["lib".to_string(), ".".to_string(), "local/lib/perl5".to_string()],
128            use_system_inc: false,
129            system_inc_cache: None,
130            resolution_timeout_ms: 50,
131        }
132    }
133}
134
135impl WorkspaceConfig {
136    /// Update workspace configuration from LSP settings.
137    pub fn update_from_value(&mut self, settings: &serde_json::Value) {
138        if let Some(workspace) = settings.get("workspace") {
139            if let Some(paths) = workspace.get("includePaths").and_then(|v| v.as_array()) {
140                self.include_paths =
141                    paths.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect();
142            }
143            if let Some(use_inc) = workspace.get("useSystemInc").and_then(|v| v.as_bool()) {
144                if use_inc != self.use_system_inc {
145                    self.system_inc_cache = None;
146                }
147                self.use_system_inc = use_inc;
148            }
149            if let Some(timeout) = workspace.get("resolutionTimeout").and_then(|v| v.as_u64()) {
150                self.resolution_timeout_ms = timeout;
151            }
152        }
153    }
154
155    /// Get system @INC paths (lazily populated).
156    pub fn get_system_inc(&mut self) -> &[PathBuf] {
157        if !self.use_system_inc {
158            return &[];
159        }
160
161        if self.system_inc_cache.is_none() {
162            self.system_inc_cache = Some(Self::fetch_perl_inc());
163        }
164
165        self.system_inc_cache.as_deref().unwrap_or(&[])
166    }
167
168    #[cfg(not(target_arch = "wasm32"))]
169    fn fetch_perl_inc() -> Vec<PathBuf> {
170        let output = Command::new("perl").args(["-e", "print join(\"\\n\", @INC)"]).output();
171
172        match output {
173            Ok(out) if out.status.success() => String::from_utf8_lossy(&out.stdout)
174                .lines()
175                .filter(|line| !line.is_empty() && *line != ".")
176                .map(PathBuf::from)
177                .collect(),
178            _ => Vec::new(),
179        }
180    }
181
182    #[cfg(target_arch = "wasm32")]
183    fn fetch_perl_inc() -> Vec<PathBuf> {
184        Vec::new()
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::{ServerConfig, WorkspaceConfig};
191    use serde_json::json;
192
193    #[test]
194    fn server_config_updates_selected_fields() {
195        let mut config = ServerConfig::default();
196        config.update_from_value(&json!({
197            "inlayHints": { "enabled": false, "maxLength": 42 },
198            "testRunner": { "enabled": false, "command": "prove", "args": ["-l"] },
199            "telemetry": { "enabled": true }
200        }));
201
202        assert!(!config.inlay_hints_enabled);
203        assert_eq!(config.inlay_hints_max_length, 42);
204        assert!(!config.test_runner_enabled);
205        assert_eq!(config.test_runner_command, "prove");
206        assert_eq!(config.test_runner_args, vec!["-l"]);
207        assert!(config.telemetry_enabled);
208    }
209
210    #[test]
211    fn workspace_config_defaults_include_common_paths() {
212        let config = WorkspaceConfig::default();
213        assert_eq!(config.include_paths, vec!["lib", ".", "local/lib/perl5"]);
214        assert!(!config.use_system_inc);
215        assert_eq!(config.resolution_timeout_ms, 50);
216    }
217}