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