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        cx.observe_global::<SettingsStore>(move |_, cx| {
338            let new_settings = cx.global::<SettingsStore>().raw_user_settings();
339            if &settings != new_settings {
340                settings = new_settings.clone()
341            }
342            if let Some(content) = serde_json::to_string(&settings).log_err() {
343                ssh.send(proto::UpdateUserSettings {
344                    project_id: 0,
345                    content,
346                })
347                .log_err();
348            }
349        })
350        .detach();
351    }
352
353    fn on_worktree_store_event(
354        &mut self,
355        _: Model<WorktreeStore>,
356        event: &WorktreeStoreEvent,
357        cx: &mut ModelContext<Self>,
358    ) {
359        if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
360            cx.subscribe(worktree, |this, worktree, event, cx| {
361                if let worktree::Event::UpdatedEntries(changes) = event {
362                    this.update_local_worktree_settings(&worktree, changes, cx)
363                }
364            })
365            .detach()
366        }
367    }
368
369    fn update_local_worktree_settings(
370        &mut self,
371        worktree: &Model<Worktree>,
372        changes: &UpdatedEntriesSet,
373        cx: &mut ModelContext<Self>,
374    ) {
375        let SettingsObserverMode::Local(fs) = &self.mode else {
376            return;
377        };
378
379        let mut settings_contents = Vec::new();
380        for (path, _, change) in changes.iter() {
381            let removed = change == &PathChange::Removed;
382            let abs_path = match worktree.read(cx).absolutize(path) {
383                Ok(abs_path) => abs_path,
384                Err(e) => {
385                    log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
386                    continue;
387                }
388            };
389
390            if path.ends_with(local_settings_file_relative_path()) {
391                let settings_dir = Arc::from(
392                    path.ancestors()
393                        .nth(local_settings_file_relative_path().components().count())
394                        .unwrap(),
395                );
396                let fs = fs.clone();
397                settings_contents.push(async move {
398                    (
399                        settings_dir,
400                        if removed {
401                            None
402                        } else {
403                            Some(async move { fs.load(&abs_path).await }.await)
404                        },
405                    )
406                });
407            }
408        }
409
410        if settings_contents.is_empty() {
411            return;
412        }
413
414        let worktree = worktree.clone();
415        cx.spawn(move |this, cx| async move {
416            let settings_contents: Vec<(Arc<Path>, _)> =
417                futures::future::join_all(settings_contents).await;
418            cx.update(|cx| {
419                this.update(cx, |this, cx| {
420                    this.update_settings(
421                        worktree,
422                        settings_contents
423                            .into_iter()
424                            .map(|(path, content)| (path, content.and_then(|c| c.log_err()))),
425                        cx,
426                    )
427                })
428            })
429        })
430        .detach();
431    }
432
433    fn update_settings(
434        &mut self,
435        worktree: Model<Worktree>,
436        settings_contents: impl IntoIterator<Item = (Arc<Path>, Option<String>)>,
437        cx: &mut ModelContext<Self>,
438    ) {
439        let worktree_id = worktree.read(cx).id();
440        let remote_worktree_id = worktree.read(cx).id();
441
442        let result = cx.update_global::<SettingsStore, anyhow::Result<()>>(|store, cx| {
443            for (directory, file_content) in settings_contents {
444                store.set_local_settings(
445                    worktree_id,
446                    directory.clone(),
447                    file_content.as_deref(),
448                    cx,
449                )?;
450
451                if let Some(downstream_client) = &self.downstream_client {
452                    downstream_client
453                        .send(proto::UpdateWorktreeSettings {
454                            project_id: self.project_id,
455                            worktree_id: remote_worktree_id.to_proto(),
456                            path: directory.to_string_lossy().into_owned(),
457                            content: file_content,
458                        })
459                        .log_err();
460                }
461            }
462            anyhow::Ok(())
463        });
464
465        match result {
466            Err(error) => {
467                if let Ok(error) = error.downcast::<InvalidSettingsError>() {
468                    if let InvalidSettingsError::LocalSettings {
469                        ref path,
470                        ref message,
471                    } = error
472                    {
473                        log::error!("Failed to set local settings in {:?}: {:?}", path, message);
474                        cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(error)));
475                    }
476                }
477            }
478            Ok(()) => {
479                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(())));
480            }
481        }
482    }
483}