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}