project_settings.rs

  1use collections::HashMap;
  2use fs::Fs;
  3use gpui::{AppContext, AsyncAppContext, BorrowAppContext, EventEmitter, Model, ModelContext};
  4use paths::local_settings_file_relative_path;
  5use rpc::{proto, AnyProtoClient, TypedEnvelope};
  6use schemars::JsonSchema;
  7use serde::{Deserialize, Serialize};
  8use settings::{InvalidSettingsError, Settings, SettingsSources, SettingsStore};
  9use std::{
 10    path::{Path, PathBuf},
 11    sync::Arc,
 12    time::Duration,
 13};
 14use util::ResultExt;
 15use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
 16
 17use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
 18
 19#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
 20pub struct ProjectSettings {
 21    /// Configuration for language servers.
 22    ///
 23    /// The following settings can be overridden for specific language servers:
 24    /// - initialization_options
 25    ///
 26    /// To override settings for a language, add an entry for that language server's
 27    /// name to the lsp value.
 28    /// Default: null
 29    #[serde(default)]
 30    pub lsp: HashMap<Arc<str>, LspSettings>,
 31
 32    /// Configuration for Git-related features
 33    #[serde(default)]
 34    pub git: GitSettings,
 35
 36    /// Configuration for how direnv configuration should be loaded
 37    #[serde(default)]
 38    pub load_direnv: DirenvSettings,
 39
 40    /// Configuration for session-related features
 41    #[serde(default)]
 42    pub session: SessionSettings,
 43}
 44
 45#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 46#[serde(rename_all = "snake_case")]
 47pub enum DirenvSettings {
 48    /// Load direnv configuration through a shell hook
 49    #[default]
 50    ShellHook,
 51    /// Load direnv configuration directly using `direnv export json`
 52    ///
 53    /// Warning: This option is experimental and might cause some inconsistent behavior compared to using the shell hook.
 54    /// If it does, please report it to GitHub
 55    Direct,
 56}
 57
 58#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 59pub struct GitSettings {
 60    /// Whether or not to show the git gutter.
 61    ///
 62    /// Default: tracked_files
 63    pub git_gutter: Option<GitGutterSetting>,
 64    pub gutter_debounce: Option<u64>,
 65    /// Whether or not to show git blame data inline in
 66    /// the currently focused line.
 67    ///
 68    /// Default: on
 69    pub inline_blame: Option<InlineBlameSettings>,
 70}
 71
 72impl GitSettings {
 73    pub fn inline_blame_enabled(&self) -> bool {
 74        #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
 75        match self.inline_blame {
 76            Some(InlineBlameSettings { enabled, .. }) => enabled,
 77            _ => false,
 78        }
 79    }
 80
 81    pub fn inline_blame_delay(&self) -> Option<Duration> {
 82        match self.inline_blame {
 83            Some(InlineBlameSettings {
 84                delay_ms: Some(delay_ms),
 85                ..
 86            }) if delay_ms > 0 => Some(Duration::from_millis(delay_ms)),
 87            _ => None,
 88        }
 89    }
 90}
 91
 92#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
 93#[serde(rename_all = "snake_case")]
 94pub enum GitGutterSetting {
 95    /// Show git gutter in tracked files.
 96    #[default]
 97    TrackedFiles,
 98    /// Hide git gutter
 99    Hide,
100}
101
102#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
103#[serde(rename_all = "snake_case")]
104pub struct InlineBlameSettings {
105    /// Whether or not to show git blame data inline in
106    /// the currently focused line.
107    ///
108    /// Default: true
109    #[serde(default = "true_value")]
110    pub enabled: bool,
111    /// Whether to only show the inline blame information
112    /// after a delay once the cursor stops moving.
113    ///
114    /// Default: 0
115    pub delay_ms: Option<u64>,
116    /// The minimum column number to show the inline blame information at
117    ///
118    /// Default: 0
119    pub min_column: Option<u32>,
120}
121
122const fn true_value() -> bool {
123    true
124}
125
126#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
127pub struct BinarySettings {
128    pub path: Option<String>,
129    pub arguments: Option<Vec<String>>,
130    pub path_lookup: Option<bool>,
131}
132
133#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
134#[serde(rename_all = "snake_case")]
135pub struct LspSettings {
136    pub binary: Option<BinarySettings>,
137    pub initialization_options: Option<serde_json::Value>,
138    pub settings: Option<serde_json::Value>,
139}
140
141#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
142pub struct SessionSettings {
143    /// Whether or not to restore unsaved buffers on restart.
144    ///
145    /// If this is true, user won't be prompted whether to save/discard
146    /// dirty files when closing the application.
147    ///
148    /// Default: true
149    pub restore_unsaved_buffers: bool,
150}
151
152impl Default for SessionSettings {
153    fn default() -> Self {
154        Self {
155            restore_unsaved_buffers: true,
156        }
157    }
158}
159
160impl Settings for ProjectSettings {
161    const KEY: Option<&'static str> = None;
162
163    type FileContent = Self;
164
165    fn load(
166        sources: SettingsSources<Self::FileContent>,
167        _: &mut AppContext,
168    ) -> anyhow::Result<Self> {
169        sources.json_merge()
170    }
171}
172
173pub enum SettingsObserverMode {
174    Local(Arc<dyn Fs>),
175    Ssh(AnyProtoClient),
176    Remote,
177}
178
179#[derive(Clone, Debug, PartialEq)]
180pub enum SettingsObserverEvent {
181    LocalSettingsUpdated(Result<(), InvalidSettingsError>),
182}
183
184impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
185
186pub struct SettingsObserver {
187    mode: SettingsObserverMode,
188    downstream_client: Option<AnyProtoClient>,
189    worktree_store: Model<WorktreeStore>,
190    project_id: u64,
191}
192
193/// SettingsObserver observers changes to .zed/settings.json files in local worktrees
194/// (or the equivalent protobuf messages from upstream) and updates local settings
195/// and sends notifications downstream.
196/// In ssh mode it also monitors ~/.config/zed/settings.json and sends the content
197/// upstream.
198impl SettingsObserver {
199    pub fn init(client: &AnyProtoClient) {
200        client.add_model_message_handler(Self::handle_update_worktree_settings);
201        client.add_model_message_handler(Self::handle_update_user_settings)
202    }
203
204    pub fn new_local(
205        fs: Arc<dyn Fs>,
206        worktree_store: Model<WorktreeStore>,
207        cx: &mut ModelContext<Self>,
208    ) -> Self {
209        cx.subscribe(&worktree_store, Self::on_worktree_store_event)
210            .detach();
211
212        Self {
213            worktree_store,
214            mode: SettingsObserverMode::Local(fs),
215            downstream_client: None,
216            project_id: 0,
217        }
218    }
219
220    pub fn new_ssh(
221        client: AnyProtoClient,
222        worktree_store: Model<WorktreeStore>,
223        cx: &mut ModelContext<Self>,
224    ) -> Self {
225        let this = Self {
226            worktree_store,
227            mode: SettingsObserverMode::Ssh(client.clone()),
228            downstream_client: None,
229            project_id: 0,
230        };
231        this.maintain_ssh_settings(client, cx);
232        this
233    }
234
235    pub fn new_remote(worktree_store: Model<WorktreeStore>, _: &mut ModelContext<Self>) -> Self {
236        Self {
237            worktree_store,
238            mode: SettingsObserverMode::Remote,
239            downstream_client: None,
240            project_id: 0,
241        }
242    }
243
244    pub fn shared(
245        &mut self,
246        project_id: u64,
247        downstream_client: AnyProtoClient,
248        cx: &mut ModelContext<Self>,
249    ) {
250        self.project_id = project_id;
251        self.downstream_client = Some(downstream_client.clone());
252
253        let store = cx.global::<SettingsStore>();
254        for worktree in self.worktree_store.read(cx).worktrees() {
255            let worktree_id = worktree.read(cx).id().to_proto();
256            for (path, content) in store.local_settings(worktree.read(cx).id()) {
257                downstream_client
258                    .send(proto::UpdateWorktreeSettings {
259                        project_id,
260                        worktree_id,
261                        path: path.to_string_lossy().into(),
262                        content: Some(content),
263                    })
264                    .log_err();
265            }
266        }
267    }
268
269    pub fn unshared(&mut self, _: &mut ModelContext<Self>) {
270        self.downstream_client = None;
271    }
272
273    async fn handle_update_worktree_settings(
274        this: Model<Self>,
275        envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
276        mut cx: AsyncAppContext,
277    ) -> anyhow::Result<()> {
278        this.update(&mut cx, |this, cx| {
279            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
280            let Some(worktree) = this
281                .worktree_store
282                .read(cx)
283                .worktree_for_id(worktree_id, cx)
284            else {
285                return;
286            };
287            this.update_settings(
288                worktree,
289                [(
290                    PathBuf::from(&envelope.payload.path).into(),
291                    envelope.payload.content,
292                )],
293                cx,
294            );
295        })?;
296        Ok(())
297    }
298
299    pub async fn handle_update_user_settings(
300        _: Model<Self>,
301        envelope: TypedEnvelope<proto::UpdateUserSettings>,
302        mut cx: AsyncAppContext,
303    ) -> anyhow::Result<()> {
304        cx.update_global(move |settings_store: &mut SettingsStore, cx| {
305            settings_store.set_user_settings(&envelope.payload.content, cx)
306        })??;
307
308        Ok(())
309    }
310
311    pub fn maintain_ssh_settings(&self, ssh: AnyProtoClient, cx: &mut ModelContext<Self>) {
312        let mut settings = cx.global::<SettingsStore>().raw_user_settings().clone();
313        if let Some(content) = serde_json::to_string(&settings).log_err() {
314            ssh.send(proto::UpdateUserSettings {
315                project_id: 0,
316                content,
317            })
318            .log_err();
319        }
320
321        cx.observe_global::<SettingsStore>(move |_, cx| {
322            let new_settings = cx.global::<SettingsStore>().raw_user_settings();
323            if &settings != new_settings {
324                settings = new_settings.clone()
325            }
326            if let Some(content) = serde_json::to_string(&settings).log_err() {
327                ssh.send(proto::UpdateUserSettings {
328                    project_id: 0,
329                    content,
330                })
331                .log_err();
332            }
333        })
334        .detach();
335    }
336
337    fn on_worktree_store_event(
338        &mut self,
339        _: Model<WorktreeStore>,
340        event: &WorktreeStoreEvent,
341        cx: &mut ModelContext<Self>,
342    ) {
343        if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
344            cx.subscribe(worktree, |this, worktree, event, cx| {
345                if let worktree::Event::UpdatedEntries(changes) = event {
346                    this.update_local_worktree_settings(&worktree, changes, cx)
347                }
348            })
349            .detach()
350        }
351    }
352
353    fn update_local_worktree_settings(
354        &mut self,
355        worktree: &Model<Worktree>,
356        changes: &UpdatedEntriesSet,
357        cx: &mut ModelContext<Self>,
358    ) {
359        let SettingsObserverMode::Local(fs) = &self.mode else {
360            return;
361        };
362
363        let mut settings_contents = Vec::new();
364        for (path, _, change) in changes.iter() {
365            let removed = change == &PathChange::Removed;
366            let abs_path = match worktree.read(cx).absolutize(path) {
367                Ok(abs_path) => abs_path,
368                Err(e) => {
369                    log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
370                    continue;
371                }
372            };
373
374            if path.ends_with(local_settings_file_relative_path()) {
375                let settings_dir = Arc::from(
376                    path.ancestors()
377                        .nth(local_settings_file_relative_path().components().count())
378                        .unwrap(),
379                );
380                let fs = fs.clone();
381                settings_contents.push(async move {
382                    (
383                        settings_dir,
384                        if removed {
385                            None
386                        } else {
387                            Some(async move { fs.load(&abs_path).await }.await)
388                        },
389                    )
390                });
391            }
392        }
393
394        if settings_contents.is_empty() {
395            return;
396        }
397
398        let worktree = worktree.clone();
399        cx.spawn(move |this, cx| async move {
400            let settings_contents: Vec<(Arc<Path>, _)> =
401                futures::future::join_all(settings_contents).await;
402            cx.update(|cx| {
403                this.update(cx, |this, cx| {
404                    this.update_settings(
405                        worktree,
406                        settings_contents
407                            .into_iter()
408                            .map(|(path, content)| (path, content.and_then(|c| c.log_err()))),
409                        cx,
410                    )
411                })
412            })
413        })
414        .detach();
415    }
416
417    fn update_settings(
418        &mut self,
419        worktree: Model<Worktree>,
420        settings_contents: impl IntoIterator<Item = (Arc<Path>, Option<String>)>,
421        cx: &mut ModelContext<Self>,
422    ) {
423        let worktree_id = worktree.read(cx).id();
424        let remote_worktree_id = worktree.read(cx).id();
425
426        let result = cx.update_global::<SettingsStore, anyhow::Result<()>>(|store, cx| {
427            for (directory, file_content) in settings_contents {
428                store.set_local_settings(
429                    worktree_id,
430                    directory.clone(),
431                    file_content.as_deref(),
432                    cx,
433                )?;
434
435                if let Some(downstream_client) = &self.downstream_client {
436                    downstream_client
437                        .send(proto::UpdateWorktreeSettings {
438                            project_id: self.project_id,
439                            worktree_id: remote_worktree_id.to_proto(),
440                            path: directory.to_string_lossy().into_owned(),
441                            content: file_content,
442                        })
443                        .log_err();
444                }
445            }
446            anyhow::Ok(())
447        });
448
449        match result {
450            Err(error) => {
451                if let Ok(error) = error.downcast::<InvalidSettingsError>() {
452                    if let InvalidSettingsError::LocalSettings {
453                        ref path,
454                        ref message,
455                    } = error
456                    {
457                        log::error!("Failed to set local settings in {:?}: {:?}", path, message);
458                        cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(error)));
459                    }
460                }
461            }
462            Ok(()) => {
463                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(())));
464            }
465        }
466    }
467}