vtcode_config/defaults/
provider.rs1use std::path::{Path, PathBuf};
2use std::sync::{Arc, RwLock};
3
4use once_cell::sync::Lazy;
5use vtcode_commons::VtCodePaths;
6use vtcode_commons::paths::WorkspacePaths;
7
8const DEFAULT_CONFIG_FILE_NAME: &str = "vtcode.toml";
9const DEFAULT_CONFIG_DIR_NAME: &str = ".vtcode";
10const DEFAULT_SYNTAX_THEME: &str = "base16-ocean.dark";
11
12static DEFAULT_SYNTAX_LANGUAGES: Lazy<Vec<String>> = Lazy::new(Vec::new);
15
16static CONFIG_DEFAULTS: Lazy<RwLock<Arc<dyn ConfigDefaultsProvider>>> =
17 Lazy::new(|| RwLock::new(Arc::new(DefaultConfigDefaults)));
18
19fn read_env_var(key: &str) -> Option<String> {
20 crate::env_helpers::read_env_var(key)
21}
22
23pub trait ConfigDefaultsProvider: Send + Sync {
26 fn config_file_name(&self) -> &str {
28 DEFAULT_CONFIG_FILE_NAME
29 }
30
31 fn workspace_paths_for(&self, workspace_root: &Path) -> Box<dyn WorkspacePaths>;
34
35 fn home_config_paths(&self, config_file_name: &str) -> Vec<PathBuf>;
38
39 fn canonical_user_config_dir(&self) -> anyhow::Result<Option<PathBuf>> {
44 Ok(self
45 .canonical_user_config_path(self.config_file_name())?
46 .and_then(|path| path.parent().map(Path::to_path_buf)))
47 }
48
49 fn canonical_user_config_path(&self, config_file_name: &str) -> anyhow::Result<Option<PathBuf>> {
54 Ok(self.home_config_paths(config_file_name).into_iter().last())
55 }
56
57 fn system_config_paths(&self, _config_file_name: &str) -> anyhow::Result<Vec<PathBuf>> {
59 Ok(Vec::new())
60 }
61
62 fn syntax_theme(&self) -> String;
64
65 fn syntax_languages(&self) -> Vec<String>;
67}
68
69#[derive(Debug, Default)]
70struct DefaultConfigDefaults;
71
72impl ConfigDefaultsProvider for DefaultConfigDefaults {
73 fn workspace_paths_for(&self, workspace_root: &Path) -> Box<dyn WorkspacePaths> {
74 Box::new(DefaultWorkspacePaths::new(workspace_root.to_path_buf()))
75 }
76
77 fn home_config_paths(&self, config_file_name: &str) -> Vec<PathBuf> {
78 default_home_paths(config_file_name)
79 }
80
81 fn canonical_user_config_dir(&self) -> anyhow::Result<Option<PathBuf>> {
82 Ok(Some(VtCodePaths::resolve()?.config_dir().to_path_buf()))
83 }
84
85 fn canonical_user_config_path(&self, config_file_name: &str) -> anyhow::Result<Option<PathBuf>> {
86 Ok(Some(VtCodePaths::resolve()?.config_path(config_file_name)?))
87 }
88
89 fn system_config_paths(&self, config_file_name: &str) -> anyhow::Result<Vec<PathBuf>> {
90 VtCodePaths::resolve()?.system_config_paths(config_file_name)
91 }
92
93 fn syntax_theme(&self) -> String {
94 DEFAULT_SYNTAX_THEME.to_string()
95 }
96
97 fn syntax_languages(&self) -> Vec<String> {
98 default_syntax_languages()
99 }
100}
101
102pub fn install_config_defaults_provider(provider: Arc<dyn ConfigDefaultsProvider>) -> Arc<dyn ConfigDefaultsProvider> {
104 let mut guard = CONFIG_DEFAULTS.write().unwrap_or_else(|poisoned| {
105 tracing::warn!("config defaults provider lock poisoned while installing provider; recovering");
106 poisoned.into_inner()
107 });
108 std::mem::replace(&mut *guard, provider)
109}
110
111pub fn reset_to_default_config_defaults() {
113 let _ = install_config_defaults_provider(Arc::new(DefaultConfigDefaults));
114}
115
116pub fn with_config_defaults<F, R>(operation: F) -> R
118where
119 F: FnOnce(&dyn ConfigDefaultsProvider) -> R,
120{
121 let guard = CONFIG_DEFAULTS.read().unwrap_or_else(|poisoned| {
122 tracing::warn!("config defaults provider lock poisoned while reading provider; recovering");
123 poisoned.into_inner()
124 });
125 operation(guard.as_ref())
126}
127
128pub fn current_config_defaults() -> Arc<dyn ConfigDefaultsProvider> {
130 let guard = CONFIG_DEFAULTS.read().unwrap_or_else(|poisoned| {
131 tracing::warn!("config defaults provider lock poisoned while cloning provider; recovering");
132 poisoned.into_inner()
133 });
134 Arc::clone(&*guard)
135}
136
137pub fn with_config_defaults_provider_for_test<F, R>(provider: Arc<dyn ConfigDefaultsProvider>, action: F) -> R
138where
139 F: FnOnce() -> R,
140{
141 use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
142
143 let previous = install_config_defaults_provider(provider);
144 let result = catch_unwind(AssertUnwindSafe(action));
145 let _ = install_config_defaults_provider(previous);
146
147 match result {
148 Ok(value) => value,
149 Err(payload) => resume_unwind(payload),
150 }
151}
152
153pub fn get_config_dir() -> Option<PathBuf> {
159 resolve_vtcode_paths().ok().map(|paths| paths.config_dir().to_path_buf())
160}
161
162pub fn get_data_dir() -> Option<PathBuf> {
167 resolve_vtcode_paths().ok().map(|paths| paths.data_dir().to_path_buf())
168}
169
170fn resolve_vtcode_paths() -> anyhow::Result<VtCodePaths> {
171 #[cfg(test)]
172 {
173 let mut environment: Vec<(String, String)> = std::env::vars().collect();
178 for key in ["VTCODE_CONFIG", "VTCODE_DATA"] {
179 if crate::env_helpers::test_env_overrides::is_overridden(key) {
180 environment.retain(|(name, _)| name != key);
181 if let Some(value) = read_env_var(key) {
182 environment.push((key.to_string(), value));
183 }
184 }
185 }
186 let environment_refs: Vec<(&str, &str)> =
187 environment.iter().map(|(key, value)| (key.as_str(), value.as_str())).collect();
188 VtCodePaths::from_environment(&environment_refs)
189 }
190
191 #[cfg(not(test))]
192 {
193 VtCodePaths::resolve()
194 }
195}
196
197fn default_home_paths(config_file_name: &str) -> Vec<PathBuf> {
198 if let Ok(resolver) = VtCodePaths::resolve() {
199 let mut paths = vec![resolver.legacy_home_dir().join(config_file_name)];
200 if let Ok(canonical_path) = resolver.config_path(config_file_name)
201 && !paths.iter().any(|path| path == &canonical_path)
202 {
203 paths.push(canonical_path);
204 }
205 return paths;
206 }
207
208 let mut paths = Vec::with_capacity(2);
209
210 if let Some(home_dir) = dirs::home_dir() {
212 paths.push(home_dir.join(DEFAULT_CONFIG_DIR_NAME).join(config_file_name));
213 }
214
215 if let Some(config_dir) = get_config_dir() {
217 let xdg_path = config_dir.join(config_file_name);
218 if !paths.iter().any(|p| p == &xdg_path) {
219 paths.push(xdg_path);
220 }
221 }
222
223 paths
224}
225
226fn default_syntax_languages() -> Vec<String> {
227 DEFAULT_SYNTAX_LANGUAGES.clone()
228}
229
230#[derive(Debug, Clone)]
231struct DefaultWorkspacePaths {
232 root: PathBuf,
233}
234
235impl DefaultWorkspacePaths {
236 fn new(root: PathBuf) -> Self {
237 Self { root }
238 }
239
240 fn config_dir_path(&self) -> PathBuf {
241 self.root.join(DEFAULT_CONFIG_DIR_NAME)
242 }
243}
244
245impl WorkspacePaths for DefaultWorkspacePaths {
246 fn workspace_root(&self) -> &Path {
247 &self.root
248 }
249
250 fn config_dir(&self) -> PathBuf {
251 self.config_dir_path()
252 }
253
254 fn cache_dir(&self) -> Option<PathBuf> {
255 Some(self.config_dir_path().join("cache"))
256 }
257
258 fn telemetry_dir(&self) -> Option<PathBuf> {
259 Some(self.config_dir_path().join("telemetry"))
260 }
261}
262
263#[derive(Debug, Clone)]
266pub struct WorkspacePathsDefaults<P>
267where
268 P: WorkspacePaths + ?Sized,
269{
270 paths: Arc<P>,
271 config_file_name: String,
272 home_paths: Option<Vec<PathBuf>>,
273 system_paths: Option<Vec<PathBuf>>,
274 syntax_theme: String,
275 syntax_languages: Vec<String>,
276}
277
278impl<P> WorkspacePathsDefaults<P>
279where
280 P: WorkspacePaths + 'static,
281{
282 pub fn new(paths: Arc<P>) -> Self {
285 Self {
286 paths,
287 config_file_name: DEFAULT_CONFIG_FILE_NAME.to_string(),
288 home_paths: None,
289 system_paths: None,
290 syntax_theme: DEFAULT_SYNTAX_THEME.to_string(),
291 syntax_languages: default_syntax_languages(),
292 }
293 }
294
295 pub fn with_config_file_name(mut self, file_name: impl Into<String>) -> Self {
297 self.config_file_name = file_name.into();
298 self
299 }
300
301 pub fn with_home_paths(mut self, home_paths: Vec<PathBuf>) -> Self {
303 self.home_paths = Some(home_paths);
304 self
305 }
306
307 pub fn with_system_config_paths(mut self, system_paths: Vec<PathBuf>) -> Self {
309 self.system_paths = Some(system_paths);
310 self
311 }
312
313 pub fn with_syntax_theme(mut self, theme: impl Into<String>) -> Self {
315 self.syntax_theme = theme.into();
316 self
317 }
318
319 pub fn with_syntax_languages(mut self, languages: Vec<String>) -> Self {
321 self.syntax_languages = languages;
322 self
323 }
324
325 pub fn build(self) -> Box<dyn ConfigDefaultsProvider> {
327 Box::new(self)
328 }
329}
330
331impl<P> ConfigDefaultsProvider for WorkspacePathsDefaults<P>
332where
333 P: WorkspacePaths + 'static,
334{
335 fn config_file_name(&self) -> &str {
336 &self.config_file_name
337 }
338
339 fn workspace_paths_for(&self, _workspace_root: &Path) -> Box<dyn WorkspacePaths> {
340 Box::new(WorkspacePathsWrapper { inner: Arc::clone(&self.paths) })
341 }
342
343 fn home_config_paths(&self, config_file_name: &str) -> Vec<PathBuf> {
344 self.home_paths.clone().unwrap_or_else(|| default_home_paths(config_file_name))
345 }
346
347 fn system_config_paths(&self, config_file_name: &str) -> anyhow::Result<Vec<PathBuf>> {
348 let _ = config_file_name;
349 Ok(self.system_paths.clone().unwrap_or_default())
350 }
351
352 fn syntax_theme(&self) -> String {
353 self.syntax_theme.clone()
354 }
355
356 fn syntax_languages(&self) -> Vec<String> {
357 self.syntax_languages.clone()
358 }
359}
360
361#[derive(Debug, Clone)]
362struct WorkspacePathsWrapper<P>
363where
364 P: WorkspacePaths + ?Sized,
365{
366 inner: Arc<P>,
367}
368
369impl<P> WorkspacePaths for WorkspacePathsWrapper<P>
370where
371 P: WorkspacePaths + ?Sized,
372{
373 fn workspace_root(&self) -> &Path {
374 self.inner.workspace_root()
375 }
376
377 fn config_dir(&self) -> PathBuf {
378 self.inner.config_dir()
379 }
380
381 fn cache_dir(&self) -> Option<PathBuf> {
382 self.inner.cache_dir()
383 }
384
385 fn telemetry_dir(&self) -> Option<PathBuf> {
386 self.inner.telemetry_dir()
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::{get_config_dir, get_data_dir};
393 use serial_test::serial;
394 use std::path::PathBuf;
395
396 fn with_env_var<F>(key: &str, value: Option<&str>, f: F)
397 where
398 F: FnOnce(),
399 {
400 let previous = crate::env_helpers::test_env_overrides::get(key);
401 crate::env_helpers::test_env_overrides::set(key, value);
402
403 f();
404
405 crate::env_helpers::test_env_overrides::restore(key, previous);
406 }
407
408 #[test]
409 #[serial]
410 fn get_config_dir_uses_env_override() {
411 with_env_var("VTCODE_CONFIG", Some("/tmp/vtcode-config-test"), || {
412 assert_eq!(get_config_dir(), Some(PathBuf::from("/tmp/vtcode-config-test")));
413 });
414 }
415
416 #[test]
417 #[serial]
418 fn get_data_dir_uses_env_override() {
419 with_env_var("VTCODE_DATA", Some("/tmp/vtcode-data-test"), || {
420 assert_eq!(get_data_dir(), Some(PathBuf::from("/tmp/vtcode-data-test")));
421 });
422 }
423
424 #[test]
425 #[serial]
426 fn get_config_dir_ignores_blank_env_override() {
427 with_env_var("VTCODE_CONFIG", Some(" "), || {
428 let resolved = get_config_dir();
429 assert!(resolved.is_some());
430 assert_ne!(resolved, Some(PathBuf::from(" ")));
431 assert_ne!(resolved, Some(PathBuf::new()));
432 });
433 }
434
435 #[test]
436 #[serial]
437 fn get_data_dir_ignores_blank_env_override() {
438 with_env_var("VTCODE_DATA", Some(" "), || {
439 let resolved = get_data_dir();
440 assert!(resolved.is_some());
441 assert_ne!(resolved, Some(PathBuf::from(" ")));
442 assert_ne!(resolved, Some(PathBuf::new()));
443 });
444 }
445
446 #[test]
447 #[serial]
448 fn env_guard_restores_original_value() {
449 let key = "VTCODE_CONFIG";
450 let initial = super::read_env_var(key);
451 with_env_var(key, Some("/tmp/vtcode-config-test"), || {
452 assert_eq!(super::read_env_var(key), Some("/tmp/vtcode-config-test".to_string()));
453 });
454 assert_eq!(super::read_env_var(key), initial);
455 }
456}