project_settings.rs

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