project_settings.rs

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