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
7use crate::FontFamilyName;
8
9#[derive(Clone, Copy, PartialEq, Eq, Debug)]
10pub enum VsCodeSettingsSource {
11 VsCode,
12 Cursor,
13}
14
15impl std::fmt::Display for VsCodeSettingsSource {
16 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17 match self {
18 VsCodeSettingsSource::VsCode => write!(f, "VS Code"),
19 VsCodeSettingsSource::Cursor => write!(f, "Cursor"),
20 }
21 }
22}
23
24pub struct VsCodeSettings {
25 pub source: VsCodeSettingsSource,
26 pub path: Arc<Path>,
27 content: Map<String, Value>,
28}
29
30impl VsCodeSettings {
31 #[cfg(any(test, feature = "test-support"))]
32 pub fn from_str(content: &str, source: VsCodeSettingsSource) -> Result<Self> {
33 Ok(Self {
34 source,
35 path: Path::new("/example-path/Code/User/settings.json").into(),
36 content: serde_json_lenient::from_str(content)?,
37 })
38 }
39
40 pub async fn load_user_settings(source: VsCodeSettingsSource, fs: Arc<dyn Fs>) -> Result<Self> {
41 let candidate_paths = match source {
42 VsCodeSettingsSource::VsCode => vscode_settings_file_paths(),
43 VsCodeSettingsSource::Cursor => cursor_settings_file_paths(),
44 };
45 let mut path = None;
46 for candidate_path in candidate_paths.iter() {
47 if fs.is_file(candidate_path).await {
48 path = Some(candidate_path.clone());
49 }
50 }
51 let Some(path) = path else {
52 return Err(anyhow!(
53 "No settings file found, expected to find it in one of the following paths:\n{}",
54 candidate_paths
55 .into_iter()
56 .map(|path| path.to_string_lossy().into_owned())
57 .collect::<Vec<_>>()
58 .join("\n")
59 ));
60 };
61 let content = fs.load(&path).await.with_context(|| {
62 format!(
63 "Error loading {} settings file from {}",
64 source,
65 path.display()
66 )
67 })?;
68 let content = serde_json_lenient::from_str(&content).with_context(|| {
69 format!(
70 "Error parsing {} settings file from {}",
71 source,
72 path.display()
73 )
74 })?;
75 Ok(Self {
76 source,
77 path: path.into(),
78 content,
79 })
80 }
81
82 pub fn read_value(&self, setting: &str) -> Option<&Value> {
83 self.content.get(setting)
84 }
85
86 pub fn read_string(&self, setting: &str) -> Option<&str> {
87 self.read_value(setting).and_then(|v| v.as_str())
88 }
89
90 pub fn read_bool(&self, setting: &str) -> Option<bool> {
91 self.read_value(setting).and_then(|v| v.as_bool())
92 }
93
94 pub fn string_setting(&self, key: &str, setting: &mut Option<String>) {
95 if let Some(s) = self.content.get(key).and_then(Value::as_str) {
96 *setting = Some(s.to_owned())
97 }
98 }
99
100 pub fn bool_setting(&self, key: &str, setting: &mut Option<bool>) {
101 if let Some(s) = self.content.get(key).and_then(Value::as_bool) {
102 *setting = Some(s)
103 }
104 }
105
106 pub fn u32_setting(&self, key: &str, setting: &mut Option<u32>) {
107 if let Some(s) = self.content.get(key).and_then(Value::as_u64) {
108 *setting = Some(s as u32)
109 }
110 }
111
112 pub fn u64_setting(&self, key: &str, setting: &mut Option<u64>) {
113 if let Some(s) = self.content.get(key).and_then(Value::as_u64) {
114 *setting = Some(s)
115 }
116 }
117
118 pub fn usize_setting(&self, key: &str, setting: &mut Option<usize>) {
119 if let Some(s) = self.content.get(key).and_then(Value::as_u64) {
120 *setting = Some(s.try_into().unwrap())
121 }
122 }
123
124 pub fn f32_setting(&self, key: &str, setting: &mut Option<f32>) {
125 if let Some(s) = self.content.get(key).and_then(Value::as_f64) {
126 *setting = Some(s as f32)
127 }
128 }
129
130 pub fn from_f32_setting<T: From<f32>>(&self, key: &str, setting: &mut Option<T>) {
131 if let Some(s) = self.content.get(key).and_then(Value::as_f64) {
132 *setting = Some(T::from(s as f32))
133 }
134 }
135
136 pub fn enum_setting<T>(
137 &self,
138 key: &str,
139 setting: &mut Option<T>,
140 f: impl FnOnce(&str) -> Option<T>,
141 ) {
142 if let Some(s) = self.content.get(key).and_then(Value::as_str).and_then(f) {
143 *setting = Some(s)
144 }
145 }
146
147 pub fn read_enum<T>(&self, key: &str, f: impl FnOnce(&str) -> Option<T>) -> Option<T> {
148 self.content.get(key).and_then(Value::as_str).and_then(f)
149 }
150
151 pub fn font_family_setting(
152 &self,
153 key: &str,
154 font_family: &mut Option<FontFamilyName>,
155 font_fallbacks: &mut Option<Vec<FontFamilyName>>,
156 ) {
157 let Some(css_name) = self.content.get(key).and_then(Value::as_str) else {
158 return;
159 };
160
161 let mut name_buffer = String::new();
162 let mut quote_char: Option<char> = None;
163 let mut fonts = Vec::new();
164 let mut add_font = |buffer: &mut String| {
165 let trimmed = buffer.trim();
166 if !trimmed.is_empty() {
167 fonts.push(trimmed.to_string().into());
168 }
169
170 buffer.clear();
171 };
172
173 for ch in css_name.chars() {
174 match (ch, quote_char) {
175 ('"' | '\'', None) => {
176 quote_char = Some(ch);
177 }
178 (_, Some(q)) if ch == q => {
179 quote_char = None;
180 }
181 (',', None) => {
182 add_font(&mut name_buffer);
183 }
184 _ => {
185 name_buffer.push(ch);
186 }
187 }
188 }
189
190 add_font(&mut name_buffer);
191
192 let mut iter = fonts.into_iter();
193 *font_family = iter.next();
194 let fallbacks: Vec<_> = iter.collect();
195 if !fallbacks.is_empty() {
196 *font_fallbacks = Some(fallbacks);
197 }
198 }
199}