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