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() -> &'static str {
128    static CACHED: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
129        let mut value = crate::parse_json_with_comments::<serde_json::Value>(
130            crate::default_settings().as_ref(),
131        )
132        .unwrap();
133        #[cfg(not(target_os = "windows"))]
134        util::merge_non_null_json_value_into(
135            serde_json::json!({
136                "ui_font_family": "Courier",
137                "ui_font_features": {},
138                "ui_font_size": 14,
139                "ui_font_fallback": [],
140                "buffer_font_family": "Courier",
141                "buffer_font_features": {},
142                "buffer_font_size": 14,
143                "buffer_font_fallbacks": [],
144                "theme": EMPTY_THEME_NAME,
145            }),
146            &mut value,
147        );
148        #[cfg(target_os = "windows")]
149        util::merge_non_null_json_value_into(
150            serde_json::json!({
151                "ui_font_family": "Courier New",
152                "ui_font_features": {},
153                "ui_font_size": 14,
154                "ui_font_fallback": [],
155                "buffer_font_family": "Courier New",
156                "buffer_font_features": {},
157                "buffer_font_size": 14,
158                "buffer_font_fallbacks": [],
159                "theme": EMPTY_THEME_NAME,
160            }),
161            &mut value,
162        );
163        value.as_object_mut().unwrap().remove("languages");
164        serde_json::to_string(&value).unwrap()
165    });
166    &CACHED
167}
168
169pub fn watch_config_file(
170    executor: &BackgroundExecutor,
171    fs: Arc<dyn Fs>,
172    path: PathBuf,
173) -> (mpsc::UnboundedReceiver<String>, gpui::Task<()>) {
174    let (tx, rx) = mpsc::unbounded();
175    let task = executor.spawn(async move {
176        let path = fs.canonicalize(&path).await.unwrap_or_else(|_| path);
177        let (events, _) = fs.watch(&path, Duration::from_millis(100)).await;
178        futures::pin_mut!(events);
179
180        let contents = fs.load(&path).await.unwrap_or_default();
181        if tx.unbounded_send(contents).is_err() {
182            return;
183        }
184
185        loop {
186            if events.next().await.is_none() {
187                break;
188            }
189
190            if let Ok(contents) = fs.load(&path).await
191                && tx.unbounded_send(contents).is_err()
192            {
193                break;
194            }
195        }
196    });
197    (rx, task)
198}
199
200pub fn watch_config_dir(
201    executor: &BackgroundExecutor,
202    fs: Arc<dyn Fs>,
203    dir_path: PathBuf,
204    config_paths: HashSet<PathBuf>,
205) -> mpsc::UnboundedReceiver<String> {
206    let (tx, rx) = mpsc::unbounded();
207    executor
208        .spawn(async move {
209            for file_path in &config_paths {
210                if fs.metadata(file_path).await.is_ok_and(|v| v.is_some())
211                    && let Ok(contents) = fs.load(file_path).await
212                    && tx.unbounded_send(contents).is_err()
213                {
214                    return;
215                }
216            }
217
218            let (events, _) = fs.watch(&dir_path, Duration::from_millis(100)).await;
219            futures::pin_mut!(events);
220
221            while let Some(event_batch) = events.next().await {
222                for event in event_batch {
223                    if config_paths.contains(&event.path) {
224                        match event.kind {
225                            Some(PathEventKind::Removed) => {
226                                if tx.unbounded_send(String::new()).is_err() {
227                                    return;
228                                }
229                            }
230                            Some(PathEventKind::Created) | Some(PathEventKind::Changed) => {
231                                if let Ok(contents) = fs.load(&event.path).await
232                                    && tx.unbounded_send(contents).is_err()
233                                {
234                                    return;
235                                }
236                            }
237                            Some(PathEventKind::Rescan) => {
238                                for file_path in &config_paths {
239                                    if let Ok(contents) = fs.load(file_path).await
240                                        && tx.unbounded_send(contents).is_err()
241                                    {
242                                        return;
243                                    }
244                                }
245                            }
246                            _ => {}
247                        }
248                    } else if matches!(event.kind, Some(PathEventKind::Rescan))
249                        && event.path == dir_path
250                    {
251                        for file_path in &config_paths {
252                            if let Ok(contents) = fs.load(file_path).await
253                                && tx.unbounded_send(contents).is_err()
254                            {
255                                return;
256                            }
257                        }
258                    }
259                }
260            }
261        })
262        .detach();
263
264    rx
265}
266
267pub fn update_settings_file(
268    fs: Arc<dyn Fs>,
269    cx: &App,
270    update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
271) {
272    SettingsStore::global(cx).update_settings_file(fs, update)
273}
274
275pub fn update_settings_file_with_completion(
276    fs: Arc<dyn Fs>,
277    cx: &App,
278    update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
279) -> futures::channel::oneshot::Receiver<anyhow::Result<()>> {
280    SettingsStore::global(cx).update_settings_file_with_completion(fs, update)
281}