1use anyhow::Result;
2use futures::{stream, SinkExt, StreamExt as _};
3use gpui::{
4 executor,
5 font_cache::{FamilyId, FontCache},
6};
7use language::Language;
8use parking_lot::Mutex;
9use postage::{prelude::Stream, watch};
10use project::Fs;
11use serde::Deserialize;
12use std::{collections::HashMap, path::Path, sync::Arc, time::Duration};
13use theme::{Theme, ThemeRegistry};
14use util::ResultExt;
15
16#[derive(Clone)]
17pub struct Settings {
18 pub buffer_font_family: FamilyId,
19 pub buffer_font_size: f32,
20 pub tab_size: usize,
21 pub soft_wrap: SoftWrap,
22 pub preferred_line_length: u32,
23 pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
24 pub theme: Arc<Theme>,
25}
26
27#[derive(Clone, Debug, Default, Deserialize)]
28pub struct LanguageOverride {
29 pub tab_size: Option<usize>,
30 pub soft_wrap: Option<SoftWrap>,
31 pub preferred_line_length: Option<u32>,
32}
33
34#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq)]
35#[serde(rename_all = "snake_case")]
36pub enum SoftWrap {
37 None,
38 EditorWidth,
39 PreferredLineLength,
40}
41
42#[derive(Clone)]
43pub struct SettingsFile(watch::Receiver<SettingsFileContent>);
44
45#[derive(Clone, Debug, Default, Deserialize)]
46struct SettingsFileContent {
47 #[serde(default)]
48 buffer_font_family: Option<String>,
49 #[serde(default)]
50 buffer_font_size: Option<f32>,
51 #[serde(flatten)]
52 editor: LanguageOverride,
53 #[serde(default)]
54 language_overrides: HashMap<Arc<str>, LanguageOverride>,
55 #[serde(default)]
56 theme: Option<String>,
57}
58
59impl SettingsFile {
60 pub async fn new(
61 fs: Arc<dyn Fs>,
62 executor: &executor::Background,
63 path: impl Into<Arc<Path>>,
64 ) -> Self {
65 let path = path.into();
66 let settings = Self::load(fs.clone(), &path).await.unwrap_or_default();
67 let mut events = fs.watch(&path, Duration::from_millis(500)).await;
68 let (mut tx, mut rx) = watch::channel_with(settings);
69 rx.recv().await;
70 executor
71 .spawn(async move {
72 while events.next().await.is_some() {
73 if let Some(settings) = Self::load(fs.clone(), &path).await {
74 if tx.send(settings).await.is_err() {
75 break;
76 }
77 }
78 }
79 })
80 .detach();
81 Self(rx)
82 }
83
84 async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<SettingsFileContent> {
85 if fs.is_file(&path).await {
86 fs.load(&path)
87 .await
88 .log_err()
89 .and_then(|data| serde_json::from_str(&data).log_err())
90 } else {
91 Some(SettingsFileContent::default())
92 }
93 }
94}
95
96impl Settings {
97 pub fn from_files(
98 defaults: Self,
99 sources: Vec<SettingsFile>,
100 executor: Arc<executor::Background>,
101 theme_registry: Arc<ThemeRegistry>,
102 font_cache: Arc<FontCache>,
103 ) -> (Arc<Mutex<watch::Sender<Self>>>, watch::Receiver<Self>) {
104 let (tx, mut rx) = watch::channel_with(defaults.clone());
105 let tx = Arc::new(Mutex::new(tx));
106 executor
107 .spawn({
108 let tx = tx.clone();
109 async move {
110 let mut stream =
111 stream::select_all(sources.iter().map(|source| source.0.clone()));
112 while stream.next().await.is_some() {
113 let mut settings = defaults.clone();
114 for source in &sources {
115 settings.merge(&*source.0.borrow(), &theme_registry, &font_cache);
116 }
117 *tx.lock().borrow_mut() = settings;
118 }
119 }
120 })
121 .detach();
122 rx.try_recv().ok();
123 (tx, rx)
124 }
125
126 pub fn new(
127 buffer_font_family: &str,
128 font_cache: &FontCache,
129 theme: Arc<Theme>,
130 ) -> Result<Self> {
131 Ok(Self {
132 buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
133 buffer_font_size: 15.,
134 tab_size: 4,
135 soft_wrap: SoftWrap::None,
136 preferred_line_length: 80,
137 language_overrides: Default::default(),
138 theme,
139 })
140 }
141
142 pub fn with_overrides(
143 mut self,
144 language_name: impl Into<Arc<str>>,
145 overrides: LanguageOverride,
146 ) -> Self {
147 self.language_overrides
148 .insert(language_name.into(), overrides);
149 self
150 }
151
152 pub fn tab_size(&self, language: Option<&Arc<Language>>) -> usize {
153 language
154 .and_then(|language| self.language_overrides.get(language.name().as_ref()))
155 .and_then(|settings| settings.tab_size)
156 .unwrap_or(self.tab_size)
157 }
158
159 pub fn soft_wrap(&self, language: Option<&Arc<Language>>) -> SoftWrap {
160 language
161 .and_then(|language| self.language_overrides.get(language.name().as_ref()))
162 .and_then(|settings| settings.soft_wrap)
163 .unwrap_or(self.soft_wrap)
164 }
165
166 pub fn preferred_line_length(&self, language: Option<&Arc<Language>>) -> u32 {
167 language
168 .and_then(|language| self.language_overrides.get(language.name().as_ref()))
169 .and_then(|settings| settings.preferred_line_length)
170 .unwrap_or(self.preferred_line_length)
171 }
172
173 #[cfg(any(test, feature = "test-support"))]
174 pub fn test(cx: &gpui::AppContext) -> Settings {
175 Settings {
176 buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
177 buffer_font_size: 14.,
178 tab_size: 4,
179 soft_wrap: SoftWrap::None,
180 preferred_line_length: 80,
181 language_overrides: Default::default(),
182 theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
183 }
184 }
185
186 fn merge(
187 &mut self,
188 data: &SettingsFileContent,
189 theme_registry: &ThemeRegistry,
190 font_cache: &FontCache,
191 ) {
192 if let Some(value) = &data.buffer_font_family {
193 if let Some(id) = font_cache.load_family(&[value]).log_err() {
194 self.buffer_font_family = id;
195 }
196 }
197 if let Some(value) = &data.theme {
198 if let Some(theme) = theme_registry.get(value).log_err() {
199 self.theme = theme;
200 }
201 }
202
203 merge(&mut self.buffer_font_size, data.buffer_font_size);
204 merge(&mut self.soft_wrap, data.editor.soft_wrap);
205 merge(&mut self.tab_size, data.editor.tab_size);
206 merge(
207 &mut self.preferred_line_length,
208 data.editor.preferred_line_length,
209 );
210
211 for (language_name, settings) in &data.language_overrides {
212 let target = self
213 .language_overrides
214 .entry(language_name.clone())
215 .or_default();
216
217 merge_option(&mut target.tab_size, settings.tab_size);
218 merge_option(&mut target.soft_wrap, settings.soft_wrap);
219 merge_option(
220 &mut target.preferred_line_length,
221 settings.preferred_line_length,
222 );
223 }
224 }
225}
226
227fn merge<T: Copy>(target: &mut T, value: Option<T>) {
228 if let Some(value) = value {
229 *target = value;
230 }
231}
232
233fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) {
234 if value.is_some() {
235 *target = value;
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use postage::prelude::Stream;
243 use project::FakeFs;
244
245 #[gpui::test]
246 async fn test_settings_from_files(cx: &mut gpui::TestAppContext) {
247 let executor = cx.background();
248 let fs = FakeFs::new(executor.clone());
249
250 fs.save(
251 "/settings1.json".as_ref(),
252 &r#"
253 {
254 "buffer_font_size": 24,
255 "soft_wrap": "editor_width",
256 "language_overrides": {
257 "Markdown": {
258 "preferred_line_length": 100,
259 "soft_wrap": "preferred_line_length"
260 }
261 }
262 }
263 "#
264 .into(),
265 )
266 .await
267 .unwrap();
268
269 let source1 = SettingsFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await;
270 let source2 = SettingsFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await;
271 let source3 = SettingsFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await;
272
273 let (_, mut settings_rx) = Settings::from_files(
274 cx.read(Settings::test),
275 vec![source1, source2, source3],
276 cx.background(),
277 ThemeRegistry::new((), cx.font_cache()),
278 cx.font_cache(),
279 );
280
281 let settings = settings_rx.recv().await.unwrap();
282 let md_settings = settings.language_overrides.get("Markdown").unwrap();
283 assert_eq!(settings.soft_wrap, SoftWrap::EditorWidth);
284 assert_eq!(settings.buffer_font_size, 24.0);
285 assert_eq!(settings.tab_size, 4);
286 assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
287 assert_eq!(md_settings.preferred_line_length, Some(100));
288
289 fs.save(
290 "/settings2.json".as_ref(),
291 &r#"
292 {
293 "tab_size": 2,
294 "soft_wrap": "none",
295 "language_overrides": {
296 "Markdown": {
297 "preferred_line_length": 120
298 }
299 }
300 }
301 "#
302 .into(),
303 )
304 .await
305 .unwrap();
306
307 let settings = settings_rx.recv().await.unwrap();
308 let md_settings = settings.language_overrides.get("Markdown").unwrap();
309 assert_eq!(settings.soft_wrap, SoftWrap::None);
310 assert_eq!(settings.buffer_font_size, 24.0);
311 assert_eq!(settings.tab_size, 2);
312 assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
313 assert_eq!(md_settings.preferred_line_length, Some(120));
314
315 fs.remove_file("/settings2.json".as_ref(), Default::default())
316 .await
317 .unwrap();
318
319 let settings = settings_rx.recv().await.unwrap();
320 assert_eq!(settings.tab_size, 4);
321 }
322}