css_variable_lsp/
runtime_config.rs1use std::collections::HashMap;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum PathDisplayMode {
5 Relative,
6 Absolute,
7 Abbreviated,
8}
9
10#[derive(Debug, Clone)]
11pub struct RuntimeConfig {
12 pub enable_color_provider: bool,
13 pub color_only_on_variables: bool,
14 pub lookup_files: Option<Vec<String>>,
15 pub ignore_globs: Option<Vec<String>>,
16 pub path_display_mode: PathDisplayMode,
17 pub path_display_abbrev_length: usize,
18}
19
20fn get_arg_value(args: &[String], name: &str) -> Option<String> {
21 let flag = format!("--{name}");
22 if let Some(idx) = args.iter().position(|arg| arg == &flag) {
23 if let Some(candidate) = args.get(idx + 1) {
24 if !candidate.starts_with('-') {
25 return Some(candidate.to_string());
26 }
27 }
28 return None;
29 }
30
31 let prefix = format!("{}=", flag);
32 for arg in args {
33 if arg.starts_with(&prefix) {
34 return Some(arg[prefix.len()..].to_string());
35 }
36 }
37
38 None
39}
40
41fn parse_optional_int(value: Option<&str>) -> Option<i64> {
42 let raw = value?.trim();
43 if raw.is_empty() {
44 return None;
45 }
46 raw.parse::<i64>().ok()
47}
48
49fn normalize_path_display_mode(value: Option<&str>) -> Option<PathDisplayMode> {
50 let raw = value?.trim();
51 if raw.is_empty() {
52 return None;
53 }
54 match raw.to_lowercase().as_str() {
55 "relative" => Some(PathDisplayMode::Relative),
56 "absolute" => Some(PathDisplayMode::Absolute),
57 "abbreviated" | "abbr" | "fish" => Some(PathDisplayMode::Abbreviated),
58 _ => None,
59 }
60}
61
62fn parse_path_display(value: Option<&str>) -> (Option<PathDisplayMode>, Option<i64>) {
63 let raw = match value {
64 Some(v) if !v.trim().is_empty() => v,
65 _ => return (None, None),
66 };
67
68 let mut parts = raw.splitn(2, ':');
69 let mode_part = parts.next();
70 let length_part = parts.next();
71
72 (
73 normalize_path_display_mode(mode_part),
74 parse_optional_int(length_part),
75 )
76}
77
78fn split_lookup_list(value: &str) -> Vec<String> {
79 value
80 .split(',')
81 .map(|entry| entry.trim())
82 .filter(|entry| !entry.is_empty())
83 .map(|entry| entry.to_string())
84 .collect()
85}
86
87fn resolve_lookup_files(args: &[String], env: &HashMap<String, String>) -> Option<Vec<String>> {
88 let mut cli_files = Vec::new();
89 let mut i = 0;
90 while i < args.len() {
91 let arg = &args[i];
92 if arg == "--lookup-files" {
93 if let Some(next) = args.get(i + 1) {
94 if !next.starts_with('-') {
95 cli_files.extend(split_lookup_list(next));
96 i += 1;
97 }
98 }
99 } else if let Some(rest) = arg.strip_prefix("--lookup-files=") {
100 cli_files.extend(split_lookup_list(rest));
101 } else if arg == "--lookup-file" {
102 if let Some(next) = args.get(i + 1) {
103 if !next.starts_with('-') {
104 cli_files.push(next.to_string());
105 i += 1;
106 }
107 }
108 } else if let Some(rest) = arg.strip_prefix("--lookup-file=") {
109 cli_files.push(rest.to_string());
110 }
111 i += 1;
112 }
113
114 if !cli_files.is_empty() {
115 return Some(cli_files);
116 }
117
118 if let Some(env_value) = env.get("CSS_LSP_LOOKUP_FILES") {
119 let env_files = split_lookup_list(env_value);
120 if !env_files.is_empty() {
121 return Some(env_files);
122 }
123 }
124
125 None
126}
127
128fn resolve_ignore_globs(args: &[String], env: &HashMap<String, String>) -> Option<Vec<String>> {
129 let mut cli_globs = Vec::new();
130 let mut i = 0;
131 while i < args.len() {
132 let arg = &args[i];
133 if arg == "--ignore-globs" {
134 if let Some(next) = args.get(i + 1) {
135 if !next.starts_with('-') {
136 cli_globs.extend(split_lookup_list(next));
137 i += 1;
138 }
139 }
140 } else if let Some(rest) = arg.strip_prefix("--ignore-globs=") {
141 cli_globs.extend(split_lookup_list(rest));
142 } else if arg == "--ignore-glob" {
143 if let Some(next) = args.get(i + 1) {
144 if !next.starts_with('-') {
145 cli_globs.push(next.to_string());
146 i += 1;
147 }
148 }
149 } else if let Some(rest) = arg.strip_prefix("--ignore-glob=") {
150 cli_globs.push(rest.to_string());
151 }
152 i += 1;
153 }
154
155 if !cli_globs.is_empty() {
156 return Some(cli_globs);
157 }
158
159 if let Some(env_value) = env.get("CSS_LSP_IGNORE_GLOBS") {
160 let env_globs = split_lookup_list(env_value);
161 if !env_globs.is_empty() {
162 return Some(env_globs);
163 }
164 }
165
166 None
167}
168
169pub fn build_runtime_config_with_env(
170 args: &[String],
171 env: &HashMap<String, String>,
172) -> RuntimeConfig {
173 let enable_color_provider = !args.iter().any(|arg| arg == "--no-color-preview");
174 let color_only_on_variables = args.iter().any(|arg| arg == "--color-only-variables")
175 || env
176 .get("CSS_LSP_COLOR_ONLY_VARIABLES")
177 .map(|v| v == "1")
178 .unwrap_or(false);
179
180 let lookup_files = resolve_lookup_files(args, env);
181 let ignore_globs = resolve_ignore_globs(args, env);
182
183 let path_display_arg = get_arg_value(args, "path-display");
184 let path_display_env = env.get("CSS_LSP_PATH_DISPLAY").cloned();
185 let (mode_override, length_override) =
186 parse_path_display(path_display_arg.as_deref().or(path_display_env.as_deref()));
187
188 let path_display_mode = mode_override.unwrap_or(PathDisplayMode::Relative);
189
190 let length_arg = get_arg_value(args, "path-display-length");
191 let length_env = env.get("CSS_LSP_PATH_DISPLAY_LENGTH").cloned();
192 let length_raw = parse_optional_int(length_arg.as_deref().or(length_env.as_deref()))
193 .or(length_override)
194 .unwrap_or(1);
195 let path_display_abbrev_length = length_raw.max(0) as usize;
196
197 RuntimeConfig {
198 enable_color_provider,
199 color_only_on_variables,
200 lookup_files,
201 ignore_globs,
202 path_display_mode,
203 path_display_abbrev_length,
204 }
205}
206
207pub fn build_runtime_config(args: &[String]) -> RuntimeConfig {
208 let env: HashMap<String, String> = std::env::vars().collect();
209 build_runtime_config_with_env(args, &env)
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn runtime_config_prefers_cli_over_env() {
218 let args = vec![
219 "--no-color-preview".to_string(),
220 "--color-only-variables".to_string(),
221 "--lookup-files".to_string(),
222 "a.css,b.html".to_string(),
223 "--ignore-glob=dist/**".to_string(),
224 "--path-display=abbreviated:2".to_string(),
225 ];
226 let mut env = HashMap::new();
227 env.insert(
228 "CSS_LSP_LOOKUP_FILES".to_string(),
229 "ignored.css".to_string(),
230 );
231 env.insert("CSS_LSP_IGNORE_GLOBS".to_string(), "ignored/**".to_string());
232 env.insert("CSS_LSP_PATH_DISPLAY".to_string(), "absolute".to_string());
233
234 let config = build_runtime_config_with_env(&args, &env);
235
236 assert!(!config.enable_color_provider);
237 assert!(config.color_only_on_variables);
238 assert_eq!(
239 config.lookup_files.as_ref().unwrap(),
240 &vec!["a.css".to_string(), "b.html".to_string()]
241 );
242 assert_eq!(
243 config.ignore_globs.as_ref().unwrap(),
244 &vec!["dist/**".to_string()]
245 );
246 assert_eq!(config.path_display_mode, PathDisplayMode::Abbreviated);
247 assert_eq!(config.path_display_abbrev_length, 2);
248 }
249
250 #[test]
251 fn runtime_config_uses_env_when_cli_missing() {
252 let args: Vec<String> = Vec::new();
253 let mut env = HashMap::new();
254 env.insert(
255 "CSS_LSP_LOOKUP_FILES".to_string(),
256 "one.css,two.html".to_string(),
257 );
258 env.insert(
259 "CSS_LSP_IGNORE_GLOBS".to_string(),
260 "dist/**,out/**".to_string(),
261 );
262 env.insert("CSS_LSP_PATH_DISPLAY".to_string(), "relative".to_string());
263 env.insert("CSS_LSP_PATH_DISPLAY_LENGTH".to_string(), "3".to_string());
264
265 let config = build_runtime_config_with_env(&args, &env);
266
267 assert!(config.enable_color_provider);
268 assert!(!config.color_only_on_variables);
269 assert_eq!(
270 config.lookup_files.as_ref().unwrap(),
271 &vec!["one.css".to_string(), "two.html".to_string()]
272 );
273 assert_eq!(
274 config.ignore_globs.as_ref().unwrap(),
275 &vec!["dist/**".to_string(), "out/**".to_string()]
276 );
277 assert_eq!(config.path_display_mode, PathDisplayMode::Relative);
278 assert_eq!(config.path_display_abbrev_length, 3);
279 }
280}