settings_file.rs

  1use crate::{settings_content::SettingsContent, settings_store::SettingsStore};
  2use collections::HashSet;
  3use fs::{Fs, PathEventKind};
  4use futures::{StreamExt, channel::mpsc};
  5use gpui::{App, BackgroundExecutor, ReadGlobal};
  6use std::{path::PathBuf, sync::Arc, time::Duration};
  7
  8#[cfg(test)]
  9mod tests {
 10    use super::*;
 11    use fs::FakeFs;
 12
 13    use gpui::TestAppContext;
 14    use serde_json::json;
 15    use std::path::Path;
 16
 17    #[gpui::test]
 18    async fn test_watch_config_dir_reloads_tracked_file_on_rescan(cx: &mut TestAppContext) {
 19        cx.executor().allow_parking();
 20
 21        let fs = FakeFs::new(cx.background_executor.clone());
 22        let config_dir = PathBuf::from("/root/config");
 23        let settings_path = PathBuf::from("/root/config/settings.json");
 24
 25        fs.insert_tree(
 26            Path::new("/root"),
 27            json!({
 28                "config": {
 29                    "settings.json": "A"
 30                }
 31            }),
 32        )
 33        .await;
 34
 35        let mut rx = watch_config_dir(
 36            &cx.background_executor,
 37            fs.clone(),
 38            config_dir.clone(),
 39            HashSet::from_iter([settings_path.clone()]),
 40        );
 41
 42        assert_eq!(rx.next().await.as_deref(), Some("A"));
 43        cx.run_until_parked();
 44
 45        fs.pause_events();
 46        fs.insert_file(&settings_path, b"B".to_vec()).await;
 47        fs.clear_buffered_events();
 48
 49        fs.emit_fs_event(&settings_path, Some(PathEventKind::Rescan));
 50        fs.unpause_events_and_flush();
 51        assert_eq!(rx.next().await.as_deref(), Some("B"));
 52
 53        fs.pause_events();
 54        fs.insert_file(&settings_path, b"A".to_vec()).await;
 55        fs.clear_buffered_events();
 56
 57        fs.emit_fs_event(&config_dir, Some(PathEventKind::Rescan));
 58        fs.unpause_events_and_flush();
 59        assert_eq!(rx.next().await.as_deref(), Some("A"));
 60    }
 61
 62    #[gpui::test]
 63    async fn test_watch_config_file_reloads_when_parent_dir_is_symlink(cx: &mut TestAppContext) {
 64        cx.executor().allow_parking();
 65        let fs = FakeFs::new(cx.background_executor.clone());
 66        let config_settings_path = PathBuf::from("/root/.config/zed/settings.json");
 67        let target_settings_path = PathBuf::from("/root/dotfiles/zed/settings.json");
 68
 69        fs.insert_tree(
 70            Path::new("/root"),
 71            json!({
 72                ".config": {},
 73                "dotfiles": {
 74                    "zed": {
 75                        "settings.json": "A"
 76                    }
 77                }
 78            }),
 79        )
 80        .await;
 81
 82        fs.create_symlink(
 83            Path::new("/root/.config/zed"),
 84            PathBuf::from("/root/dotfiles/zed"),
 85        )
 86        .await
 87        .unwrap();
 88
 89        let (mut rx, _task) =
 90            watch_config_file(&cx.background_executor, fs.clone(), config_settings_path);
 91        assert_eq!(rx.next().await.as_deref(), Some("A"));
 92
 93        fs.insert_file(&target_settings_path, b"B".to_vec()).await;
 94        assert_eq!(rx.next().await.as_deref(), Some("B"));
 95    }
 96}
 97
 98pub const EMPTY_THEME_NAME: &str = "empty-theme";
 99
100/// Settings for visual tests that use proper fonts instead of Courier.
101/// Uses Helvetica Neue for UI (sans-serif) and Menlo for code (monospace),
102/// which are available on all macOS systems.
103#[cfg(any(test, feature = "test-support"))]
104pub fn visual_test_settings() -> String {
105    let mut value =
106        crate::parse_json_with_comments::<serde_json::Value>(crate::default_settings().as_ref())
107            .unwrap();
108    util::merge_non_null_json_value_into(
109        serde_json::json!({
110            "ui_font_family": ".SystemUIFont",
111            "ui_font_features": {},
112            "ui_font_size": 14,
113            "ui_font_fallback": [],
114            "buffer_font_family": "Menlo",
115            "buffer_font_features": {},
116            "buffer_font_size": 14,
117            "buffer_font_fallbacks": [],
118            "theme": EMPTY_THEME_NAME,
119        }),
120        &mut value,
121    );
122    value.as_object_mut().unwrap().remove("languages");
123    serde_json::to_string(&value).unwrap()
124}
125
126#[cfg(any(test, feature = "test-support"))]
127pub fn test_settings() -> String {
128    let mut value =
129        crate::parse_json_with_comments::<serde_json::Value>(crate::default_settings().as_ref())
130            .unwrap();
131    #[cfg(not(target_os = "windows"))]
132    util::merge_non_null_json_value_into(
133        serde_json::json!({
134            "ui_font_family": "Courier",
135            "ui_font_features": {},
136            "ui_font_size": 14,
137            "ui_font_fallback": [],
138            "buffer_font_family": "Courier",
139            "buffer_font_features": {},
140            "buffer_font_size": 14,
141            "buffer_font_fallbacks": [],
142            "theme": EMPTY_THEME_NAME,
143        }),
144        &mut value,
145    );
146    #[cfg(target_os = "windows")]
147    util::merge_non_null_json_value_into(
148        serde_json::json!({
149            "ui_font_family": "Courier New",
150            "ui_font_features": {},
151            "ui_font_size": 14,
152            "ui_font_fallback": [],
153            "buffer_font_family": "Courier New",
154            "buffer_font_features": {},
155            "buffer_font_size": 14,
156            "buffer_font_fallbacks": [],
157            "theme": EMPTY_THEME_NAME,
158        }),
159        &mut value,
160    );
161    value.as_object_mut().unwrap().remove("languages");
162    serde_json::to_string(&value).unwrap()
163}
164
165pub fn watch_config_file(
166    executor: &BackgroundExecutor,
167    fs: Arc<dyn Fs>,
168    path: PathBuf,
169) -> (mpsc::UnboundedReceiver<String>, gpui::Task<()>) {
170    let (tx, rx) = mpsc::unbounded();
171    let task = executor.spawn(async move {
172        let path = fs.canonicalize(&path).await.unwrap_or_else(|_| path);
173        let (events, _) = fs.watch(&path, Duration::from_millis(100)).await;
174        futures::pin_mut!(events);
175
176        let contents = fs.load(&path).await.unwrap_or_default();
177        if tx.unbounded_send(contents).is_err() {
178            return;
179        }
180
181        loop {
182            if events.next().await.is_none() {
183                break;
184            }
185
186            if let Ok(contents) = fs.load(&path).await
187                && tx.unbounded_send(contents).is_err()
188            {
189                break;
190            }
191        }
192    });
193    (rx, task)
194}
195
196pub fn watch_config_dir(
197    executor: &BackgroundExecutor,
198    fs: Arc<dyn Fs>,
199    dir_path: PathBuf,
200    config_paths: HashSet<PathBuf>,
201) -> mpsc::UnboundedReceiver<String> {
202    let (tx, rx) = mpsc::unbounded();
203    executor
204        .spawn(async move {
205            for file_path in &config_paths {
206                if fs.metadata(file_path).await.is_ok_and(|v| v.is_some())
207                    && let Ok(contents) = fs.load(file_path).await
208                    && tx.unbounded_send(contents).is_err()
209                {
210                    return;
211                }
212            }
213
214            let (events, _) = fs.watch(&dir_path, Duration::from_millis(100)).await;
215            futures::pin_mut!(events);
216
217            while let Some(event_batch) = events.next().await {
218                for event in event_batch {
219                    if config_paths.contains(&event.path) {
220                        match event.kind {
221                            Some(PathEventKind::Removed) => {
222                                if tx.unbounded_send(String::new()).is_err() {
223                                    return;
224                                }
225                            }
226                            Some(PathEventKind::Created) | Some(PathEventKind::Changed) => {
227                                if let Ok(contents) = fs.load(&event.path).await
228                                    && tx.unbounded_send(contents).is_err()
229                                {
230                                    return;
231                                }
232                            }
233                            Some(PathEventKind::Rescan) => {
234                                for file_path in &config_paths {
235                                    if let Ok(contents) = fs.load(file_path).await
236                                        && tx.unbounded_send(contents).is_err()
237                                    {
238                                        return;
239                                    }
240                                }
241                            }
242                            _ => {}
243                        }
244                    } else if matches!(event.kind, Some(PathEventKind::Rescan))
245                        && event.path == dir_path
246                    {
247                        for file_path in &config_paths {
248                            if let Ok(contents) = fs.load(file_path).await
249                                && tx.unbounded_send(contents).is_err()
250                            {
251                                return;
252                            }
253                        }
254                    }
255                }
256            }
257        })
258        .detach();
259
260    rx
261}
262
263pub fn update_settings_file(
264    fs: Arc<dyn Fs>,
265    cx: &App,
266    update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
267) {
268    SettingsStore::global(cx).update_settings_file(fs, update)
269}
270
271pub fn update_settings_file_with_completion(
272    fs: Arc<dyn Fs>,
273    cx: &App,
274    update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
275) -> futures::channel::oneshot::Receiver<anyhow::Result<()>> {
276    SettingsStore::global(cx).update_settings_file_with_completion(fs, update)
277}