1use anyhow::Result;
2use futures::{stream, SinkExt, StreamExt as _};
3use gpui::{
4 executor,
5 font_cache::{FamilyId, FontCache},
6};
7use language::Language;
8use postage::{prelude::Stream, watch};
9use project::Fs;
10use schemars::{schema_for, JsonSchema};
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, JsonSchema)]
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, JsonSchema)]
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, JsonSchema)]
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, rx) = watch::channel_with(settings);
69 executor
70 .spawn(async move {
71 while events.next().await.is_some() {
72 if let Some(settings) = Self::load(fs.clone(), &path).await {
73 if tx.send(settings).await.is_err() {
74 break;
75 }
76 }
77 }
78 })
79 .detach();
80 Self(rx)
81 }
82
83 async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<SettingsFileContent> {
84 if fs.is_file(&path).await {
85 fs.load(&path)
86 .await
87 .log_err()
88 .and_then(|data| serde_json::from_str(&data).log_err())
89 } else {
90 Some(SettingsFileContent::default())
91 }
92 }
93}
94
95impl Settings {
96 pub fn file_json_schema() -> serde_json::Value {
97 serde_json::to_value(schema_for!(SettingsFileContent)).unwrap()
98 }
99
100 pub fn from_files(
101 defaults: Self,
102 sources: Vec<SettingsFile>,
103 theme_registry: Arc<ThemeRegistry>,
104 font_cache: Arc<FontCache>,
105 ) -> impl futures::stream::Stream<Item = Self> {
106 stream::select_all(sources.iter().enumerate().map(|(i, source)| {
107 let mut rx = source.0.clone();
108 // Consume the initial item from all of the constituent file watches but one.
109 // This way, the stream will yield exactly one item for the files' initial
110 // state, and won't return any more items until the files change.
111 if i > 0 {
112 rx.try_recv().ok();
113 }
114 rx
115 }))
116 .map(move |_| {
117 let mut settings = defaults.clone();
118 for source in &sources {
119 settings.merge(&*source.0.borrow(), &theme_registry, &font_cache);
120 }
121 settings
122 })
123 }
124
125 pub fn new(
126 buffer_font_family: &str,
127 font_cache: &FontCache,
128 theme: Arc<Theme>,
129 ) -> Result<Self> {
130 Ok(Self {
131 buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
132 buffer_font_size: 15.,
133 tab_size: 4,
134 soft_wrap: SoftWrap::None,
135 preferred_line_length: 80,
136 language_overrides: Default::default(),
137 theme,
138 })
139 }
140
141 pub fn with_overrides(
142 mut self,
143 language_name: impl Into<Arc<str>>,
144 overrides: LanguageOverride,
145 ) -> Self {
146 self.language_overrides
147 .insert(language_name.into(), overrides);
148 self
149 }
150
151 pub fn tab_size(&self, language: Option<&Arc<Language>>) -> usize {
152 language
153 .and_then(|language| self.language_overrides.get(language.name().as_ref()))
154 .and_then(|settings| settings.tab_size)
155 .unwrap_or(self.tab_size)
156 }
157
158 pub fn soft_wrap(&self, language: Option<&Arc<Language>>) -> SoftWrap {
159 language
160 .and_then(|language| self.language_overrides.get(language.name().as_ref()))
161 .and_then(|settings| settings.soft_wrap)
162 .unwrap_or(self.soft_wrap)
163 }
164
165 pub fn preferred_line_length(&self, language: Option<&Arc<Language>>) -> u32 {
166 language
167 .and_then(|language| self.language_overrides.get(language.name().as_ref()))
168 .and_then(|settings| settings.preferred_line_length)
169 .unwrap_or(self.preferred_line_length)
170 }
171
172 #[cfg(any(test, feature = "test-support"))]
173 pub fn test(cx: &gpui::AppContext) -> Settings {
174 Settings {
175 buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
176 buffer_font_size: 14.,
177 tab_size: 4,
178 soft_wrap: SoftWrap::None,
179 preferred_line_length: 80,
180 language_overrides: Default::default(),
181 theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
182 }
183 }
184
185 fn merge(
186 &mut self,
187 data: &SettingsFileContent,
188 theme_registry: &ThemeRegistry,
189 font_cache: &FontCache,
190 ) {
191 if let Some(value) = &data.buffer_font_family {
192 if let Some(id) = font_cache.load_family(&[value]).log_err() {
193 self.buffer_font_family = id;
194 }
195 }
196 if let Some(value) = &data.theme {
197 if let Some(theme) = theme_registry.get(value).log_err() {
198 self.theme = theme;
199 }
200 }
201
202 merge(&mut self.buffer_font_size, data.buffer_font_size);
203 merge(&mut self.soft_wrap, data.editor.soft_wrap);
204 merge(&mut self.tab_size, data.editor.tab_size);
205 merge(
206 &mut self.preferred_line_length,
207 data.editor.preferred_line_length,
208 );
209
210 for (language_name, settings) in &data.language_overrides {
211 let target = self
212 .language_overrides
213 .entry(language_name.clone())
214 .or_default();
215
216 merge_option(&mut target.tab_size, settings.tab_size);
217 merge_option(&mut target.soft_wrap, settings.soft_wrap);
218 merge_option(
219 &mut target.preferred_line_length,
220 settings.preferred_line_length,
221 );
222 }
223 }
224}
225
226fn merge<T: Copy>(target: &mut T, value: Option<T>) {
227 if let Some(value) = value {
228 *target = value;
229 }
230}
231
232fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) {
233 if value.is_some() {
234 *target = value;
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use project::FakeFs;
242
243 #[gpui::test]
244 async fn test_settings_from_files(cx: &mut gpui::TestAppContext) {
245 let executor = cx.background();
246 let fs = FakeFs::new(executor.clone());
247
248 fs.save(
249 "/settings1.json".as_ref(),
250 &r#"
251 {
252 "buffer_font_size": 24,
253 "soft_wrap": "editor_width",
254 "language_overrides": {
255 "Markdown": {
256 "preferred_line_length": 100,
257 "soft_wrap": "preferred_line_length"
258 }
259 }
260 }
261 "#
262 .into(),
263 )
264 .await
265 .unwrap();
266
267 let source1 = SettingsFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await;
268 let source2 = SettingsFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await;
269 let source3 = SettingsFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await;
270
271 let mut settings_rx = Settings::from_files(
272 cx.read(Settings::test),
273 vec![source1, source2, source3],
274 ThemeRegistry::new((), cx.font_cache()),
275 cx.font_cache(),
276 );
277
278 let settings = settings_rx.next().await.unwrap();
279 let md_settings = settings.language_overrides.get("Markdown").unwrap();
280 assert_eq!(settings.soft_wrap, SoftWrap::EditorWidth);
281 assert_eq!(settings.buffer_font_size, 24.0);
282 assert_eq!(settings.tab_size, 4);
283 assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
284 assert_eq!(md_settings.preferred_line_length, Some(100));
285
286 fs.save(
287 "/settings2.json".as_ref(),
288 &r#"
289 {
290 "tab_size": 2,
291 "soft_wrap": "none",
292 "language_overrides": {
293 "Markdown": {
294 "preferred_line_length": 120
295 }
296 }
297 }
298 "#
299 .into(),
300 )
301 .await
302 .unwrap();
303
304 let settings = settings_rx.next().await.unwrap();
305 let md_settings = settings.language_overrides.get("Markdown").unwrap();
306 assert_eq!(settings.soft_wrap, SoftWrap::None);
307 assert_eq!(settings.buffer_font_size, 24.0);
308 assert_eq!(settings.tab_size, 2);
309 assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
310 assert_eq!(md_settings.preferred_line_length, Some(120));
311
312 fs.remove_file("/settings2.json".as_ref(), Default::default())
313 .await
314 .unwrap();
315
316 let settings = settings_rx.next().await.unwrap();
317 assert_eq!(settings.tab_size, 4);
318 }
319}