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 "swift",
29 "diff",
30 "patch",
31 "udiff",
32 "git",
33 "json",
34 "yaml",
35 "toml",
36 "markdown",
37 ]
38 .into_iter()
39 .map(String::from)
40 .collect()
41});
42
43static CONFIG_DEFAULTS: Lazy<RwLock<Arc<dyn ConfigDefaultsProvider>>> =
44 Lazy::new(|| RwLock::new(Arc::new(DefaultConfigDefaults)));
45
46pub trait ConfigDefaultsProvider: Send + Sync {
49 fn config_file_name(&self) -> &str {
51 DEFAULT_CONFIG_FILE_NAME
52 }
53
54 fn workspace_paths_for(&self, workspace_root: &Path) -> Box<dyn WorkspacePaths>;
57
58 fn home_config_paths(&self, config_file_name: &str) -> Vec<PathBuf>;
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 syntax_theme(&self) -> String {
82 DEFAULT_SYNTAX_THEME.to_string()
83 }
84
85 fn syntax_languages(&self) -> Vec<String> {
86 default_syntax_languages()
87 }
88}
89
90pub fn install_config_defaults_provider(
92 provider: Arc<dyn ConfigDefaultsProvider>,
93) -> Arc<dyn ConfigDefaultsProvider> {
94 let mut guard = CONFIG_DEFAULTS.write().unwrap_or_else(|poisoned| {
95 tracing::warn!(
96 "config defaults provider lock poisoned while installing provider; recovering"
97 );
98 poisoned.into_inner()
99 });
100 std::mem::replace(&mut *guard, provider)
101}
102
103pub fn reset_to_default_config_defaults() {
105 let _ = install_config_defaults_provider(Arc::new(DefaultConfigDefaults));
106}
107
108pub fn with_config_defaults<F, R>(operation: F) -> R
110where
111 F: FnOnce(&dyn ConfigDefaultsProvider) -> R,
112{
113 let guard = CONFIG_DEFAULTS.read().unwrap_or_else(|poisoned| {
114 tracing::warn!("config defaults provider lock poisoned while reading provider; recovering");
115 poisoned.into_inner()
116 });
117 operation(guard.as_ref())
118}
119
120pub fn current_config_defaults() -> Arc<dyn ConfigDefaultsProvider> {
122 let guard = CONFIG_DEFAULTS.read().unwrap_or_else(|poisoned| {
123 tracing::warn!("config defaults provider lock poisoned while cloning provider; recovering");
124 poisoned.into_inner()
125 });
126 Arc::clone(&*guard)
127}
128
129pub fn with_config_defaults_provider_for_test<F, R>(
130 provider: Arc<dyn ConfigDefaultsProvider>,
131 action: F,
132) -> R
133where
134 F: FnOnce() -> R,
135{
136 use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
137
138 let previous = install_config_defaults_provider(provider);
139 let result = catch_unwind(AssertUnwindSafe(action));
140 let _ = install_config_defaults_provider(previous);
141
142 match result {
143 Ok(value) => value,
144 Err(payload) => resume_unwind(payload),
145 }
146}
147
148pub fn get_config_dir() -> Option<PathBuf> {
157 if let Ok(custom_dir) = std::env::var("VTCODE_CONFIG") {
159 return Some(PathBuf::from(custom_dir));
160 }
161
162 if let Some(proj_dirs) = ProjectDirs::from("com", "vinhnx", "vtcode") {
164 return Some(proj_dirs.config_local_dir().to_path_buf());
165 }
166
167 dirs::home_dir().map(|home| home.join(DEFAULT_CONFIG_DIR_NAME))
169}
170
171pub fn get_data_dir() -> Option<PathBuf> {
180 if let Ok(custom_dir) = std::env::var("VTCODE_DATA") {
182 return Some(PathBuf::from(custom_dir));
183 }
184
185 if let Some(proj_dirs) = ProjectDirs::from("com", "vinhnx", "vtcode") {
187 return Some(proj_dirs.data_local_dir().to_path_buf());
188 }
189
190 dirs::home_dir().map(|home| home.join(DEFAULT_CONFIG_DIR_NAME).join("cache"))
192}
193
194fn default_home_paths(config_file_name: &str) -> Vec<PathBuf> {
195 get_config_dir()
196 .map(|config_dir| config_dir.join(config_file_name))
197 .into_iter()
198 .collect()
199}
200
201fn default_syntax_languages() -> Vec<String> {
202 DEFAULT_SYNTAX_LANGUAGES.clone()
203}
204
205#[derive(Debug, Clone)]
206struct DefaultWorkspacePaths {
207 root: PathBuf,
208}
209
210impl DefaultWorkspacePaths {
211 fn new(root: PathBuf) -> Self {
212 Self { root }
213 }
214
215 fn config_dir_path(&self) -> PathBuf {
216 self.root.join(DEFAULT_CONFIG_DIR_NAME)
217 }
218}
219
220impl WorkspacePaths for DefaultWorkspacePaths {
221 fn workspace_root(&self) -> &Path {
222 &self.root
223 }
224
225 fn config_dir(&self) -> PathBuf {
226 self.config_dir_path()
227 }
228
229 fn cache_dir(&self) -> Option<PathBuf> {
230 Some(self.config_dir_path().join("cache"))
231 }
232
233 fn telemetry_dir(&self) -> Option<PathBuf> {
234 Some(self.config_dir_path().join("telemetry"))
235 }
236}
237
238#[derive(Debug, Clone)]
241pub struct WorkspacePathsDefaults<P>
242where
243 P: WorkspacePaths + ?Sized,
244{
245 paths: Arc<P>,
246 config_file_name: String,
247 home_paths: Option<Vec<PathBuf>>,
248 syntax_theme: String,
249 syntax_languages: Vec<String>,
250}
251
252impl<P> WorkspacePathsDefaults<P>
253where
254 P: WorkspacePaths + 'static,
255{
256 pub fn new(paths: Arc<P>) -> Self {
259 Self {
260 paths,
261 config_file_name: DEFAULT_CONFIG_FILE_NAME.to_string(),
262 home_paths: None,
263 syntax_theme: DEFAULT_SYNTAX_THEME.to_string(),
264 syntax_languages: default_syntax_languages(),
265 }
266 }
267
268 pub fn with_config_file_name(mut self, file_name: impl Into<String>) -> Self {
270 self.config_file_name = file_name.into();
271 self
272 }
273
274 pub fn with_home_paths(mut self, home_paths: Vec<PathBuf>) -> Self {
276 self.home_paths = Some(home_paths);
277 self
278 }
279
280 pub fn with_syntax_theme(mut self, theme: impl Into<String>) -> Self {
282 self.syntax_theme = theme.into();
283 self
284 }
285
286 pub fn with_syntax_languages(mut self, languages: Vec<String>) -> Self {
288 self.syntax_languages = languages;
289 self
290 }
291
292 pub fn build(self) -> Box<dyn ConfigDefaultsProvider> {
294 Box::new(self)
295 }
296}
297
298impl<P> ConfigDefaultsProvider for WorkspacePathsDefaults<P>
299where
300 P: WorkspacePaths + 'static,
301{
302 fn config_file_name(&self) -> &str {
303 &self.config_file_name
304 }
305
306 fn workspace_paths_for(&self, _workspace_root: &Path) -> Box<dyn WorkspacePaths> {
307 Box::new(WorkspacePathsWrapper {
308 inner: Arc::clone(&self.paths),
309 })
310 }
311
312 fn home_config_paths(&self, config_file_name: &str) -> Vec<PathBuf> {
313 self.home_paths
314 .clone()
315 .unwrap_or_else(|| default_home_paths(config_file_name))
316 }
317
318 fn syntax_theme(&self) -> String {
319 self.syntax_theme.clone()
320 }
321
322 fn syntax_languages(&self) -> Vec<String> {
323 self.syntax_languages.clone()
324 }
325}
326
327#[derive(Debug, Clone)]
328struct WorkspacePathsWrapper<P>
329where
330 P: WorkspacePaths + ?Sized,
331{
332 inner: Arc<P>,
333}
334
335impl<P> WorkspacePaths for WorkspacePathsWrapper<P>
336where
337 P: WorkspacePaths + ?Sized,
338{
339 fn workspace_root(&self) -> &Path {
340 self.inner.workspace_root()
341 }
342
343 fn config_dir(&self) -> PathBuf {
344 self.inner.config_dir()
345 }
346
347 fn cache_dir(&self) -> Option<PathBuf> {
348 self.inner.cache_dir()
349 }
350
351 fn telemetry_dir(&self) -> Option<PathBuf> {
352 self.inner.telemetry_dir()
353 }
354}