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().into_owned())
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 self.content.get(setting)
82 }
83
84 pub fn read_string(&self, setting: &str) -> Option<&str> {
85 self.read_value(setting).and_then(|v| v.as_str())
86 }
87
88 pub fn read_bool(&self, setting: &str) -> Option<bool> {
89 self.read_value(setting).and_then(|v| v.as_bool())
90 }
91
92 pub fn string_setting(&self, key: &str, setting: &mut Option<String>) {
93 if let Some(s) = self.content.get(key).and_then(Value::as_str) {
94 *setting = Some(s.to_owned())
95 }
96 }
97
98 pub fn bool_setting(&self, key: &str, setting: &mut Option<bool>) {
99 if let Some(s) = self.content.get(key).and_then(Value::as_bool) {
100 *setting = Some(s)
101 }
102 }
103
104 pub fn u32_setting(&self, key: &str, setting: &mut Option<u32>) {
105 if let Some(s) = self.content.get(key).and_then(Value::as_u64) {
106 *setting = Some(s as u32)
107 }
108 }
109
110 pub fn u64_setting(&self, key: &str, setting: &mut Option<u64>) {
111 if let Some(s) = self.content.get(key).and_then(Value::as_u64) {
112 *setting = Some(s)
113 }
114 }
115
116 pub fn usize_setting(&self, key: &str, setting: &mut Option<usize>) {
117 if let Some(s) = self.content.get(key).and_then(Value::as_u64) {
118 *setting = Some(s.try_into().unwrap())
119 }
120 }
121
122 pub fn f32_setting(&self, key: &str, setting: &mut Option<f32>) {
123 if let Some(s) = self.content.get(key).and_then(Value::as_f64) {
124 *setting = Some(s as f32)
125 }
126 }
127
128 pub fn from_f32_setting<T: From<f32>>(&self, key: &str, setting: &mut Option<T>) {
129 if let Some(s) = self.content.get(key).and_then(Value::as_f64) {
130 *setting = Some(T::from(s as f32))
131 }
132 }
133
134 pub fn enum_setting<T>(
135 &self,
136 key: &str,
137 setting: &mut Option<T>,
138 f: impl FnOnce(&str) -> Option<T>,
139 ) {
140 if let Some(s) = self.content.get(key).and_then(Value::as_str).and_then(f) {
141 *setting = Some(s)
142 }
143 }
144
145 pub fn read_enum<T>(&self, key: &str, f: impl FnOnce(&str) -> Option<T>) -> Option<T> {
146 self.content.get(key).and_then(Value::as_str).and_then(f)
147 }
148}