Skip to main content

css_variable_lsp/
runtime_config.rs

1use std::collections::HashMap;
2
3use crate::flags::{
4    flag_bool, flag_bool_simple, flag_enum, flag_opt, get_arg_value, parse_optional_int,
5};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub enum PathDisplayMode {
9    #[default]
10    Relative,
11    Absolute,
12    Abbreviated,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum UndefinedVarFallbackMode {
17    #[default]
18    Warning,
19    Info,
20    Off,
21}
22
23#[derive(Debug, Clone)]
24pub struct RuntimeConfig {
25    pub enable_color_provider: bool,
26    pub color_only_on_variables: bool,
27    pub lookup_files: Option<Vec<String>>,
28    pub ignore_globs: Option<Vec<String>>,
29    pub path_display_mode: PathDisplayMode,
30    pub path_display_abbrev_length: usize,
31    pub undefined_var_fallback: UndefinedVarFallbackMode,
32    pub suggest_add_fallback: bool,
33    pub suggest_exact_color_variables: bool,
34}
35
36fn normalize_path_display_mode(value: Option<&str>) -> Option<PathDisplayMode> {
37    let raw = value?.trim().to_lowercase();
38    match raw.as_str() {
39        "relative" => Some(PathDisplayMode::Relative),
40        "absolute" => Some(PathDisplayMode::Absolute),
41        "abbreviated" | "abbr" | "fish" => Some(PathDisplayMode::Abbreviated),
42        _ => None,
43    }
44}
45
46fn normalize_undefined_var_fallback_mode(value: Option<&str>) -> Option<UndefinedVarFallbackMode> {
47    let raw = value?.trim().to_lowercase();
48    match raw.as_str() {
49        "warning" | "warn" => Some(UndefinedVarFallbackMode::Warning),
50        "info" | "information" => Some(UndefinedVarFallbackMode::Info),
51        "off" | "omit" | "none" | "disable" | "disabled" => Some(UndefinedVarFallbackMode::Off),
52        _ => None,
53    }
54}
55
56fn parse_path_display(value: Option<&str>) -> (Option<PathDisplayMode>, Option<i64>) {
57    let raw = match value {
58        Some(v) if !v.trim().is_empty() => v,
59        _ => return (None, None),
60    };
61
62    let mut parts = raw.splitn(2, ':');
63    let mode_part = parts.next();
64    let length_part = parts.next();
65
66    (
67        normalize_path_display_mode(mode_part),
68        parse_optional_int(length_part),
69    )
70}
71
72pub fn build_runtime_config_with_env(
73    args: &[String],
74    env: &HashMap<String, String>,
75) -> RuntimeConfig {
76    let enable_color_provider = flag_bool(
77        args,
78        env,
79        "color-preview",
80        "CSS_LSP_COLOR_PREVIEW",
81        "--no-color-preview",
82        true,
83    );
84
85    let color_only_on_variables = flag_bool_simple(
86        args,
87        env,
88        "CSS_LSP_COLOR_ONLY_VARIABLES",
89        "--color-only-variables",
90        false,
91    );
92
93    let lookup_files = flag_opt(args, env, "lookup-files", "CSS_LSP_LOOKUP_FILES", None);
94
95    let ignore_globs = flag_opt(args, env, "ignore-globs", "CSS_LSP_IGNORE_GLOBS", None);
96
97    let path_display_arg = get_arg_value(args, "path-display");
98    let path_display_env = env.get("CSS_LSP_PATH_DISPLAY").cloned();
99    let (mode_override, length_override) =
100        parse_path_display(path_display_arg.as_deref().or(path_display_env.as_deref()));
101    let path_display_mode = mode_override.unwrap_or(PathDisplayMode::Relative);
102
103    let length_arg = get_arg_value(args, "path-display-length");
104    let length_env = env.get("CSS_LSP_PATH_DISPLAY_LENGTH").cloned();
105    let length_raw = parse_optional_int(length_arg.as_deref().or(length_env.as_deref()))
106        .or(length_override)
107        .unwrap_or(1);
108    let path_display_abbrev_length = length_raw.max(0) as usize;
109
110    let undefined_var_fallback = flag_enum(
111        args,
112        env,
113        "undefined-var-fallback",
114        "CSS_LSP_UNDEFINED_VAR_FALLBACK",
115        None,
116        normalize_undefined_var_fallback_mode,
117        UndefinedVarFallbackMode::Warning,
118    );
119
120    let suggest_add_fallback = flag_bool(
121        args,
122        env,
123        "suggest-add-fallback",
124        "CSS_LSP_SUGGEST_ADD_FALLBACK",
125        "--no-suggest-add-fallback",
126        true,
127    );
128
129    let suggest_exact_color_variables = flag_bool(
130        args,
131        env,
132        "suggest-exact-color-variables",
133        "CSS_LSP_SUGGEST_EXACT_COLOR_VARIABLES",
134        "--no-suggest-exact-color-variables",
135        true,
136    );
137
138    RuntimeConfig {
139        enable_color_provider,
140        color_only_on_variables,
141        lookup_files,
142        ignore_globs,
143        path_display_mode,
144        path_display_abbrev_length,
145        undefined_var_fallback,
146        suggest_add_fallback,
147        suggest_exact_color_variables,
148    }
149}
150
151pub fn build_runtime_config(args: &[String]) -> RuntimeConfig {
152    let env: HashMap<String, String> = std::env::vars().collect();
153    build_runtime_config_with_env(args, &env)
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn runtime_config_prefers_cli_over_env() {
162        let args = vec![
163            "--no-color-preview".to_string(),
164            "--color-only-variables".to_string(),
165            "--lookup-files".to_string(),
166            "a.css,b.html".to_string(),
167            "--ignore-globs=dist/**".to_string(),
168            "--path-display=abbreviated:2".to_string(),
169            "--undefined-var-fallback=info".to_string(),
170        ];
171        let mut env = HashMap::new();
172        env.insert(
173            "CSS_LSP_LOOKUP_FILES".to_string(),
174            "ignored.css".to_string(),
175        );
176        env.insert("CSS_LSP_IGNORE_GLOBS".to_string(), "ignored/**".to_string());
177        env.insert("CSS_LSP_PATH_DISPLAY".to_string(), "absolute".to_string());
178        env.insert(
179            "CSS_LSP_UNDEFINED_VAR_FALLBACK".to_string(),
180            "off".to_string(),
181        );
182
183        let config = build_runtime_config_with_env(&args, &env);
184
185        assert!(!config.enable_color_provider);
186        assert!(config.color_only_on_variables);
187        assert_eq!(
188            config.lookup_files.as_ref().unwrap(),
189            &vec!["a.css".to_string(), "b.html".to_string()]
190        );
191        assert_eq!(
192            config.ignore_globs.as_ref().unwrap(),
193            &vec!["dist/**".to_string()]
194        );
195        assert_eq!(config.path_display_mode, PathDisplayMode::Abbreviated);
196        assert_eq!(config.path_display_abbrev_length, 2);
197        assert_eq!(
198            config.undefined_var_fallback,
199            UndefinedVarFallbackMode::Info
200        );
201    }
202
203    #[test]
204    fn runtime_config_uses_env_when_cli_missing() {
205        let args: Vec<String> = Vec::new();
206        let mut env = HashMap::new();
207        env.insert(
208            "CSS_LSP_LOOKUP_FILES".to_string(),
209            "one.css,two.html".to_string(),
210        );
211        env.insert(
212            "CSS_LSP_IGNORE_GLOBS".to_string(),
213            "dist/**,out/**".to_string(),
214        );
215        env.insert("CSS_LSP_PATH_DISPLAY".to_string(), "relative".to_string());
216        env.insert("CSS_LSP_PATH_DISPLAY_LENGTH".to_string(), "3".to_string());
217        env.insert(
218            "CSS_LSP_UNDEFINED_VAR_FALLBACK".to_string(),
219            "omit".to_string(),
220        );
221
222        let config = build_runtime_config_with_env(&args, &env);
223
224        assert!(config.enable_color_provider);
225        assert!(!config.color_only_on_variables);
226        assert_eq!(
227            config.lookup_files.as_ref().unwrap(),
228            &vec!["one.css".to_string(), "two.html".to_string()]
229        );
230        assert_eq!(
231            config.ignore_globs.as_ref().unwrap(),
232            &vec!["dist/**".to_string(), "out/**".to_string()]
233        );
234        assert_eq!(config.path_display_mode, PathDisplayMode::Relative);
235        assert_eq!(config.path_display_abbrev_length, 3);
236        assert_eq!(config.undefined_var_fallback, UndefinedVarFallbackMode::Off);
237    }
238
239    #[test]
240    fn runtime_config_undefined_var_fallback_defaults_on_invalid() {
241        let args = vec!["--undefined-var-fallback=maybe".to_string()];
242        let mut env = HashMap::new();
243        env.insert(
244            "CSS_LSP_UNDEFINED_VAR_FALLBACK".to_string(),
245            "surely".to_string(),
246        );
247
248        let config = build_runtime_config_with_env(&args, &env);
249
250        assert_eq!(
251            config.undefined_var_fallback,
252            UndefinedVarFallbackMode::Warning
253        );
254    }
255
256    #[test]
257    fn runtime_config_suggest_add_fallback_enabled_by_default() {
258        let args: Vec<String> = Vec::new();
259        let env = HashMap::new();
260        let config = build_runtime_config_with_env(&args, &env);
261        assert!(config.suggest_add_fallback);
262    }
263
264    #[test]
265    fn runtime_config_suggest_add_fallback_disabled_by_flag() {
266        let args = vec!["--no-suggest-add-fallback".to_string()];
267        let env = HashMap::new();
268        let config = build_runtime_config_with_env(&args, &env);
269        assert!(!config.suggest_add_fallback);
270    }
271
272    #[test]
273    fn runtime_config_suggest_add_fallback_disabled_by_env() {
274        let args: Vec<String> = Vec::new();
275        let mut env = HashMap::new();
276        env.insert("CSS_LSP_SUGGEST_ADD_FALLBACK".to_string(), "0".to_string());
277        let config = build_runtime_config_with_env(&args, &env);
278        assert!(!config.suggest_add_fallback);
279    }
280
281    #[test]
282    fn runtime_config_suggest_add_fallback_enabled_by_env() {
283        let args: Vec<String> = Vec::new();
284        let mut env = HashMap::new();
285        env.insert("CSS_LSP_SUGGEST_ADD_FALLBACK".to_string(), "1".to_string());
286        let config = build_runtime_config_with_env(&args, &env);
287        assert!(config.suggest_add_fallback);
288    }
289
290    #[test]
291    fn runtime_config_suggest_exact_color_variables_enabled_by_default() {
292        let args: Vec<String> = Vec::new();
293        let env = HashMap::new();
294        let config = build_runtime_config_with_env(&args, &env);
295        assert!(config.suggest_exact_color_variables);
296    }
297
298    #[test]
299    fn runtime_config_suggest_exact_color_variables_disabled_by_flag() {
300        let args = vec!["--no-suggest-exact-color-variables".to_string()];
301        let env = HashMap::new();
302        let config = build_runtime_config_with_env(&args, &env);
303        assert!(!config.suggest_exact_color_variables);
304    }
305
306    #[test]
307    fn runtime_config_suggest_exact_color_variables_disabled_by_env() {
308        let args: Vec<String> = Vec::new();
309        let mut env = HashMap::new();
310        env.insert(
311            "CSS_LSP_SUGGEST_EXACT_COLOR_VARIABLES".to_string(),
312            "0".to_string(),
313        );
314        let config = build_runtime_config_with_env(&args, &env);
315        assert!(!config.suggest_exact_color_variables);
316    }
317
318    #[test]
319    fn runtime_config_accepts_singular_lookup_file_flag() {
320        let args = vec![
321            "--lookup-file".to_string(),
322            "a.css".to_string(),
323            "--lookup-file".to_string(),
324            "b.scss".to_string(),
325        ];
326        let env = HashMap::new();
327        let config = build_runtime_config_with_env(&args, &env);
328
329        assert_eq!(
330            config.lookup_files.as_ref().unwrap(),
331            &vec!["a.css".to_string(), "b.scss".to_string()]
332        );
333    }
334
335    #[test]
336    fn runtime_config_accepts_singular_ignore_glob_flag() {
337        let args = vec![
338            "--ignore-glob".to_string(),
339            "dist/**".to_string(),
340            "--ignore-glob".to_string(),
341            "node_modules/**".to_string(),
342        ];
343        let env = HashMap::new();
344        let config = build_runtime_config_with_env(&args, &env);
345
346        assert_eq!(
347            config.ignore_globs.as_ref().unwrap(),
348            &vec!["dist/**".to_string(), "node_modules/**".to_string()]
349        );
350    }
351
352    #[test]
353    fn runtime_config_primary_and_singular_flags_work_together() {
354        let args = vec![
355            "--lookup-files".to_string(),
356            "a.css".to_string(),
357            "--lookup-file".to_string(),
358            "b.scss".to_string(),
359            "--ignore-globs".to_string(),
360            "dist/**".to_string(),
361            "--ignore-glob".to_string(),
362            "out/**".to_string(),
363        ];
364        let env = HashMap::new();
365        let config = build_runtime_config_with_env(&args, &env);
366
367        assert_eq!(
368            config.lookup_files.as_ref().unwrap(),
369            &vec!["a.css".to_string(), "b.scss".to_string()]
370        );
371        assert_eq!(
372            config.ignore_globs.as_ref().unwrap(),
373            &vec!["dist/**".to_string(), "out/**".to_string()]
374        );
375    }
376}