1use anyhow::{Context as _, Result, anyhow};
2use fs::Fs;
3use paths::{cursor_settings_file_paths, vscode_settings_file_paths};
4use serde_json::{Map, Value};
5use std::{path::Path, sync::Arc};
6
7#[derive(Clone, Copy, PartialEq, Eq, Debug)]
8pub enum VsCodeSettingsSource {
9 VsCode,
10 Cursor,
11}
12
13impl std::fmt::Display for VsCodeSettingsSource {
14 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15 match self {
16 VsCodeSettingsSource::VsCode => write!(f, "VS Code"),
17 VsCodeSettingsSource::Cursor => write!(f, "Cursor"),
18 }
19 }
20}
21
22pub struct VsCodeSettings {
23 pub source: VsCodeSettingsSource,
24 pub path: Arc<Path>,
25 content: Map<String, Value>,
26}
27
28impl VsCodeSettings {
29 #[cfg(any(test, feature = "test-support"))]
30 pub fn from_str(content: &str, source: VsCodeSettingsSource) -> Result<Self> {
31 Ok(Self {
32 source,
33 path: Path::new("/example-path/Code/User/settings.json").into(),
34 content: serde_json_lenient::from_str(content)?,
35 })
36 }
37
38 pub async fn load_user_settings(source: VsCodeSettingsSource, fs: Arc<dyn Fs>) -> Result<Self> {
39 let candidate_paths = match source {
40 VsCodeSettingsSource::VsCode => vscode_settings_file_paths(),
41 VsCodeSettingsSource::Cursor => cursor_settings_file_paths(),
42 };
43 let mut path = None;
44 for candidate_path in candidate_paths.iter() {
45 if fs.is_file(candidate_path).await {
46 path = Some(candidate_path.clone());
47 }
48 }
49 let Some(path) = path else {
50 return Err(anyhow!(
51 "No settings file found, expected to find it in one of the following paths:\n{}",
52 candidate_paths
53 .into_iter()
54 .map(|path| path.to_string_lossy().to_string())
55 .collect::<Vec<_>>()
56 .join("\n")
57 ));
58 };
59 let content = fs.load(&path).await.with_context(|| {
60 format!(
61 "Error loading {} settings file from {}",
62 source,
63 path.display()
64 )
65 })?;
66 let content = serde_json_lenient::from_str(&content).with_context(|| {
67 format!(
68 "Error parsing {} settings file from {}",
69 source,
70 path.display()
71 )
72 })?;
73 Ok(Self {
74 source,
75 path: path.into(),
76 content,
77 })
78 }
79
80 pub fn read_value(&self, setting: &str) -> Option<&Value> {
81 if let Some(value) = self.content.get(setting) {
82 return Some(value);
83 }
84 // TODO: maybe check if it's in [platform] settings for current platform as a fallback
85 // TODO: deal with language specific settings
86 None
87 }
88
89 pub fn read_string(&self, setting: &str) -> Option<&str> {
90 self.read_value(setting).and_then(|v| v.as_str())
91 }
92
93 pub fn read_bool(&self, setting: &str) -> Option<bool> {
94 self.read_value(setting).and_then(|v| v.as_bool())
95 }
96
97 pub fn string_setting(&self, key: &str, setting: &mut Option<String>) {
98 if let Some(s) = self.content.get(key).and_then(Value::as_str) {
99 *setting = Some(s.to_owned())
100 }
101 }
102
103 pub fn bool_setting(&self, key: &str, setting: &mut Option<bool>) {
104 if let Some(s) = self.content.get(key).and_then(Value::as_bool) {
105 *setting = Some(s)
106 }
107 }
108
109 pub fn u32_setting(&self, key: &str, setting: &mut Option<u32>) {
110 if let Some(s) = self.content.get(key).and_then(Value::as_u64) {
111 *setting = Some(s as u32)
112 }
113 }
114
115 pub fn u64_setting(&self, key: &str, setting: &mut Option<u64>) {
116 if let Some(s) = self.content.get(key).and_then(Value::as_u64) {
117 *setting = Some(s)
118 }
119 }
120
121 pub fn usize_setting(&self, key: &str, setting: &mut Option<usize>) {
122 if let Some(s) = self.content.get(key).and_then(Value::as_u64) {
123 *setting = Some(s.try_into().unwrap())
124 }
125 }
126
127 pub fn f32_setting(&self, key: &str, setting: &mut Option<f32>) {
128 if let Some(s) = self.content.get(key).and_then(Value::as_f64) {
129 *setting = Some(s as f32)
130 }
131 }
132
133 pub fn enum_setting<T>(
134 &self,
135 key: &str,
136 setting: &mut Option<T>,
137 f: impl FnOnce(&str) -> Option<T>,
138 ) {
139 if let Some(s) = self.content.get(key).and_then(Value::as_str).and_then(f) {
140 *setting = Some(s)
141 }
142 }
143}