Skip to main content

vtcode_config/defaults/
provider.rs

1use 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
12/// Empty by default — all ~250 syntect grammars are enabled.
13/// Users can set `enabled_languages` in vtcode.toml to restrict to a subset.
14static 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
23/// Provides access to filesystem and syntax defaults used by the configuration
24/// loader.
25pub trait ConfigDefaultsProvider: Send + Sync {
26    /// Returns the primary configuration file name expected in a workspace.
27    fn config_file_name(&self) -> &str {
28        DEFAULT_CONFIG_FILE_NAME
29    }
30
31    /// Creates a [`WorkspacePaths`] implementation for the provided workspace
32    /// root.
33    fn workspace_paths_for(&self, workspace_root: &Path) -> Box<dyn WorkspacePaths>;
34
35    /// Returns the fallback configuration locations searched outside the
36    /// workspace.
37    fn home_config_paths(&self, config_file_name: &str) -> Vec<PathBuf>;
38
39    /// Returns the canonical user configuration directory.
40    ///
41    /// The default preserves custom providers by deriving it from their
42    /// highest-precedence home configuration path.
43    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    /// Returns the canonical user configuration file for `config_file_name`.
50    ///
51    /// Existing providers remain compatible: their last home path has always
52    /// represented the highest-precedence user layer.
53    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    /// Returns system configuration files from lowest to highest precedence.
58    fn system_config_paths(&self, _config_file_name: &str) -> anyhow::Result<Vec<PathBuf>> {
59        Ok(Vec::new())
60    }
61
62    /// Returns the default syntax highlighting theme identifier.
63    fn syntax_theme(&self) -> String;
64
65    /// Returns the default list of syntax highlighting languages.
66    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
102/// Installs a new [`ConfigDefaultsProvider`], returning the previous provider.
103pub 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
111/// Restores the built-in defaults provider.
112pub fn reset_to_default_config_defaults() {
113    let _ = install_config_defaults_provider(Arc::new(DefaultConfigDefaults));
114}
115
116/// Executes the provided function with the currently installed provider.
117pub 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
128/// Returns the currently installed provider as an [`Arc`].
129pub 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
153/// Resolves the canonical configuration directory through the shared path
154/// policy. The legacy directory remains a read-only compatibility candidate;
155/// callers writing configuration must use the canonical path.
156///
157/// Returns `None` if no suitable directory can be determined.
158pub fn get_config_dir() -> Option<PathBuf> {
159    resolve_vtcode_paths().ok().map(|paths| paths.config_dir().to_path_buf())
160}
161
162/// Returns the canonical VT Code data directory. The legacy root is only a
163/// migration/read-compatibility source.
164///
165/// Returns `None` if no suitable directory can be determined.
166pub 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        // The config crate's test environment helper intentionally avoids
174        // mutating the process environment. Feed those overrides through the
175        // shared resolver so the public compatibility APIs keep their test
176        // semantics without duplicating path parsing here.
177        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    // 1. Legacy fallback (lower precedence) — the historical VTCODE_HOME file.
211    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    // 2. Canonical platform config path (higher precedence).
216    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/// Adapter that maps an existing [`WorkspacePaths`] implementation into a
264/// [`ConfigDefaultsProvider`].
265#[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    /// Creates a defaults provider that delegates to the supplied
283    /// [`WorkspacePaths`] implementation.
284    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    /// Overrides the configuration file name returned by the provider.
296    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    /// Overrides the fallback configuration search paths returned by the provider.
302    pub fn with_home_paths(mut self, home_paths: Vec<PathBuf>) -> Self {
303        self.home_paths = Some(home_paths);
304        self
305    }
306
307    /// Overrides the system configuration search paths returned by the provider.
308    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    /// Overrides the default syntax theme returned by the provider.
314    pub fn with_syntax_theme(mut self, theme: impl Into<String>) -> Self {
315        self.syntax_theme = theme.into();
316        self
317    }
318
319    /// Overrides the default syntax languages returned by the provider.
320    pub fn with_syntax_languages(mut self, languages: Vec<String>) -> Self {
321        self.syntax_languages = languages;
322        self
323    }
324
325    /// Consumes the builder, returning a boxed provider implementation.
326    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}