vtcode_config/defaults/
provider.rs1use std::path::{Path, PathBuf};
2use std::sync::{Arc, RwLock};
3
4use directories::ProjectDirs;
5use once_cell::sync::Lazy;
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(|| {
13 vec![
14 "rust",
15 "python",
16 "javascript",
17 "typescript",
18 "go",
19 "java",
20 "cpp",
21 "c",
22 "php",
23 "html",
24 "css",
25 "sql",
26 "csharp",
27 "bash",
28 "sh",
29 "shell",
30 "zsh",
31 "swift",
32 "diff",
33 "patch",
34 "udiff",
35 "git",
36 "json",
37 "yaml",
38 "toml",
39 "markdown",
40 "md",
41 ]
42 .into_iter()
43 .map(String::from)
44 .collect()
45});
46
47static CONFIG_DEFAULTS: Lazy<RwLock<Arc<dyn ConfigDefaultsProvider>>> =
48 Lazy::new(|| RwLock::new(Arc::new(DefaultConfigDefaults)));
49
50pub trait ConfigDefaultsProvider: Send + Sync {
53 fn config_file_name(&self) -> &str {
55 DEFAULT_CONFIG_FILE_NAME
56 }
57
58 fn workspace_paths_for(&self, workspace_root: &Path) -> Box<dyn WorkspacePaths>;
61
62 fn home_config_paths(&self, config_file_name: &str) -> Vec<PathBuf>;
65
66 fn syntax_theme(&self) -> String;
68
69 fn syntax_languages(&self) -> Vec<String>;
71}
72
73#[derive(Debug, Default)]
74struct DefaultConfigDefaults;
75
76impl ConfigDefaultsProvider for DefaultConfigDefaults {
77 fn workspace_paths_for(&self, workspace_root: &Path) -> Box<dyn WorkspacePaths> {
78 Box::new(DefaultWorkspacePaths::new(workspace_root.to_path_buf()))
79 }
80
81 fn home_config_paths(&self, config_file_name: &str) -> Vec<PathBuf> {
82 default_home_paths(config_file_name)
83 }
84
85 fn syntax_theme(&self) -> String {
86 DEFAULT_SYNTAX_THEME.to_string()
87 }
88
89 fn syntax_languages(&self) -> Vec<String> {
90 default_syntax_languages()
91 }
92}
93
94pub fn install_config_defaults_provider(
96 provider: Arc<dyn ConfigDefaultsProvider>,
97) -> Arc<dyn ConfigDefaultsProvider> {
98 let mut guard = CONFIG_DEFAULTS.write().unwrap_or_else(|poisoned| {
99 tracing::warn!(
100 "config defaults provider lock poisoned while installing provider; recovering"
101 );
102 poisoned.into_inner()
103 });
104 std::mem::replace(&mut *guard, provider)
105}
106
107pub fn reset_to_default_config_defaults() {
109 let _ = install_config_defaults_provider(Arc::new(DefaultConfigDefaults));
110}
111
112pub fn with_config_defaults<F, R>(operation: F) -> R
114where
115 F: FnOnce(&dyn ConfigDefaultsProvider) -> R,
116{
117 let guard = CONFIG_DEFAULTS.read().unwrap_or_else(|poisoned| {
118 tracing::warn!("config defaults provider lock poisoned while reading provider; recovering");
119 poisoned.into_inner()
120 });
121 operation(guard.as_ref())
122}
123
124pub fn current_config_defaults() -> Arc<dyn ConfigDefaultsProvider> {
126 let guard = CONFIG_DEFAULTS.read().unwrap_or_else(|poisoned| {
127 tracing::warn!("config defaults provider lock poisoned while cloning provider; recovering");
128 poisoned.into_inner()
129 });
130 Arc::clone(&*guard)
131}
132
133pub fn with_config_defaults_provider_for_test<F, R>(
134 provider: Arc<dyn ConfigDefaultsProvider>,
135 action: F,
136) -> R
137where
138 F: FnOnce() -> R,
139{
140 use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
141
142 let previous = install_config_defaults_provider(provider);
143 let result = catch_unwind(AssertUnwindSafe(action));
144 let _ = install_config_defaults_provider(previous);
145
146 match result {
147 Ok(value) => value,
148 Err(payload) => resume_unwind(payload),
149 }
150}
151
152pub fn get_config_dir() -> Option<PathBuf> {
161 if let Ok(custom_dir) = std::env::var("VTCODE_CONFIG") {
163 return Some(PathBuf::from(custom_dir));
164 }
165
166 if let Some(proj_dirs) = ProjectDirs::from("com", "vinhnx", "vtcode") {
168 return Some(proj_dirs.config_local_dir().to_path_buf());
169 }
170
171 dirs::home_dir().map(|home| home.join(DEFAULT_CONFIG_DIR_NAME))
173}
174
175pub fn get_data_dir() -> Option<PathBuf> {
184 if let Ok(custom_dir) = std::env::var("VTCODE_DATA") {
186 return Some(PathBuf::from(custom_dir));
187 }
188
189 if let Some(proj_dirs) = ProjectDirs::from("com", "vinhnx", "vtcode") {
191 return Some(proj_dirs.data_local_dir().to_path_buf());
192 }
193
194 dirs::home_dir().map(|home| home.join(DEFAULT_CONFIG_DIR_NAME).join("cache"))
196}
197
198fn default_home_paths(config_file_name: &str) -> Vec<PathBuf> {
199 get_config_dir()
200 .map(|config_dir| config_dir.join(config_file_name))
201 .into_iter()
202 .collect()
203}
204
205fn default_syntax_languages() -> Vec<String> {
206 DEFAULT_SYNTAX_LANGUAGES.clone()
207}
208
209#[derive(Debug, Clone)]
210struct DefaultWorkspacePaths {
211 root: PathBuf,
212}
213
214impl DefaultWorkspacePaths {
215 fn new(root: PathBuf) -> Self {
216 Self { root }
217 }
218
219 fn config_dir_path(&self) -> PathBuf {
220 self.root.join(DEFAULT_CONFIG_DIR_NAME)
221 }
222}
223
224impl WorkspacePaths for DefaultWorkspacePaths {
225 fn workspace_root(&self) -> &Path {
226 &self.root
227 }
228
229 fn config_dir(&self) -> PathBuf {
230 self.config_dir_path()
231 }
232
233 fn cache_dir(&self) -> Option<PathBuf> {
234 Some(self.config_dir_path().join("cache"))
235 }
236
237 fn telemetry_dir(&self) -> Option<PathBuf> {
238 Some(self.config_dir_path().join("telemetry"))
239 }
240}
241
242#[derive(Debug, Clone)]
245pub struct WorkspacePathsDefaults<P>
246where
247 P: WorkspacePaths + ?Sized,
248{
249 paths: Arc<P>,
250 config_file_name: String,
251 home_paths: Option<Vec<PathBuf>>,
252 syntax_theme: String,
253 syntax_languages: Vec<String>,
254}
255
256impl<P> WorkspacePathsDefaults<P>
257where
258 P: WorkspacePaths + 'static,
259{
260 pub fn new(paths: Arc<P>) -> Self {
263 Self {
264 paths,
265 config_file_name: DEFAULT_CONFIG_FILE_NAME.to_string(),
266 home_paths: None,
267 syntax_theme: DEFAULT_SYNTAX_THEME.to_string(),
268 syntax_languages: default_syntax_languages(),
269 }
270 }
271
272 pub fn with_config_file_name(mut self, file_name: impl Into<String>) -> Self {
274 self.config_file_name = file_name.into();
275 self
276 }
277
278 pub fn with_home_paths(mut self, home_paths: Vec<PathBuf>) -> Self {
280 self.home_paths = Some(home_paths);
281 self
282 }
283
284 pub fn with_syntax_theme(mut self, theme: impl Into<String>) -> Self {
286 self.syntax_theme = theme.into();
287 self
288 }
289
290 pub fn with_syntax_languages(mut self, languages: Vec<String>) -> Self {
292 self.syntax_languages = languages;
293 self
294 }
295
296 pub fn build(self) -> Box<dyn ConfigDefaultsProvider> {
298 Box::new(self)
299 }
300}
301
302impl<P> ConfigDefaultsProvider for WorkspacePathsDefaults<P>
303where
304 P: WorkspacePaths + 'static,
305{
306 fn config_file_name(&self) -> &str {
307 &self.config_file_name
308 }
309
310 fn workspace_paths_for(&self, _workspace_root: &Path) -> Box<dyn WorkspacePaths> {
311 Box::new(WorkspacePathsWrapper {
312 inner: Arc::clone(&self.paths),
313 })
314 }
315
316 fn home_config_paths(&self, config_file_name: &str) -> Vec<PathBuf> {
317 self.home_paths
318 .clone()
319 .unwrap_or_else(|| default_home_paths(config_file_name))
320 }
321
322 fn syntax_theme(&self) -> String {
323 self.syntax_theme.clone()
324 }
325
326 fn syntax_languages(&self) -> Vec<String> {
327 self.syntax_languages.clone()
328 }
329}
330
331#[derive(Debug, Clone)]
332struct WorkspacePathsWrapper<P>
333where
334 P: WorkspacePaths + ?Sized,
335{
336 inner: Arc<P>,
337}
338
339impl<P> WorkspacePaths for WorkspacePathsWrapper<P>
340where
341 P: WorkspacePaths + ?Sized,
342{
343 fn workspace_root(&self) -> &Path {
344 self.inner.workspace_root()
345 }
346
347 fn config_dir(&self) -> PathBuf {
348 self.inner.config_dir()
349 }
350
351 fn cache_dir(&self) -> Option<PathBuf> {
352 self.inner.cache_dir()
353 }
354
355 fn telemetry_dir(&self) -> Option<PathBuf> {
356 self.inner.telemetry_dir()
357 }
358}