project_settings.rs

  1use anyhow::Context as _;
  2use collections::HashMap;
  3use fs::Fs;
  4use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter};
  5use lsp::LanguageServerName;
  6use paths::{
  7    local_settings_file_relative_path, local_tasks_file_relative_path,
  8    local_vscode_tasks_file_relative_path, EDITORCONFIG_NAME,
  9};
 10use rpc::{
 11    proto::{self, FromProto, ToProto},
 12    AnyProtoClient, TypedEnvelope,
 13};
 14use schemars::JsonSchema;
 15use serde::{Deserialize, Serialize};
 16use settings::{
 17    parse_json_with_comments, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation,
 18    SettingsSources, SettingsStore,
 19};
 20use std::{path::Path, sync::Arc, time::Duration};
 21use task::{TaskTemplates, VsCodeTaskFile};
 22use util::ResultExt;
 23use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
 24
 25use crate::{
 26    task_store::TaskStore,
 27    worktree_store::{WorktreeStore, WorktreeStoreEvent},
 28};
 29
 30#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
 31pub struct ProjectSettings {
 32    /// Configuration for language servers.
 33    ///
 34    /// The following settings can be overridden for specific language servers:
 35    /// - initialization_options
 36    ///
 37    /// To override settings for a language, add an entry for that language server's
 38    /// name to the lsp value.
 39    /// Default: null
 40    #[serde(default)]
 41    pub lsp: HashMap<LanguageServerName, LspSettings>,
 42
 43    /// Configuration for Diagnostics-related features.
 44    #[serde(default)]
 45    pub diagnostics: DiagnosticsSettings,
 46
 47    /// Configuration for Git-related features
 48    #[serde(default)]
 49    pub git: GitSettings,
 50
 51    /// Configuration for Node-related features
 52    #[serde(default)]
 53    pub node: NodeBinarySettings,
 54
 55    /// Configuration for how direnv configuration should be loaded
 56    #[serde(default)]
 57    pub load_direnv: DirenvSettings,
 58
 59    /// Configuration for session-related features
 60    #[serde(default)]
 61    pub session: SessionSettings,
 62}
 63
 64#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
 65pub struct NodeBinarySettings {
 66    /// The path to the Node binary.
 67    pub path: Option<String>,
 68    /// The path to the npm binary Zed should use (defaults to `.path/../npm`).
 69    pub npm_path: Option<String>,
 70    /// If disabled, Zed will download its own copy of Node.
 71    #[serde(default)]
 72    pub ignore_system_version: Option<bool>,
 73}
 74
 75#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 76#[serde(rename_all = "snake_case")]
 77pub enum DirenvSettings {
 78    /// Load direnv configuration through a shell hook
 79    ShellHook,
 80    /// Load direnv configuration directly using `direnv export json`
 81    #[default]
 82    Direct,
 83}
 84
 85#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 86pub struct DiagnosticsSettings {
 87    /// Whether or not to include warning diagnostics
 88    #[serde(default = "true_value")]
 89    pub include_warnings: bool,
 90
 91    /// Settings for showing inline diagnostics
 92    #[serde(default)]
 93    pub inline: InlineDiagnosticsSettings,
 94}
 95
 96#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
 97pub struct InlineDiagnosticsSettings {
 98    /// Whether or not to show inline diagnostics
 99    ///
100    /// Default: false
101    #[serde(default)]
102    pub enabled: bool,
103    /// Whether to only show the inline diaganostics after a delay after the
104    /// last editor event.
105    ///
106    /// Default: 150
107    #[serde(default = "default_inline_diagnostics_debounce_ms")]
108    pub update_debounce_ms: u64,
109    /// The amount of padding between the end of the source line and the start
110    /// of the inline diagnostic in units of columns.
111    ///
112    /// Default: 4
113    #[serde(default = "default_inline_diagnostics_padding")]
114    pub padding: u32,
115    /// The minimum column to display inline diagnostics. This setting can be
116    /// used to horizontally align inline diagnostics at some position. Lines
117    /// longer than this value will still push diagnostics further to the right.
118    ///
119    /// Default: 0
120    #[serde(default)]
121    pub min_column: u32,
122
123    #[serde(default)]
124    pub max_severity: Option<DiagnosticSeverity>,
125}
126
127#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
128#[serde(rename_all = "snake_case")]
129pub enum DiagnosticSeverity {
130    Error,
131    Warning,
132    Info,
133    Hint,
134}
135
136impl Default for InlineDiagnosticsSettings {
137    fn default() -> Self {
138        Self {
139            enabled: false,
140            update_debounce_ms: default_inline_diagnostics_debounce_ms(),
141            padding: default_inline_diagnostics_padding(),
142            min_column: 0,
143            max_severity: None,
144        }
145    }
146}
147
148fn default_inline_diagnostics_debounce_ms() -> u64 {
149    150
150}
151
152fn default_inline_diagnostics_padding() -> u32 {
153    4
154}
155
156#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
157pub struct GitSettings {
158    /// Whether or not to show the git gutter.
159    ///
160    /// Default: tracked_files
161    pub git_gutter: Option<GitGutterSetting>,
162    /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
163    ///
164    /// Default: null
165    pub gutter_debounce: Option<u64>,
166    /// Whether or not to show git blame data inline in
167    /// the currently focused line.
168    ///
169    /// Default: on
170    pub inline_blame: Option<InlineBlameSettings>,
171    /// How hunks are displayed visually in the editor.
172    ///
173    /// Default: staged_hollow
174    pub hunk_style: Option<GitHunkStyleSetting>,
175}
176
177impl GitSettings {
178    pub fn inline_blame_enabled(&self) -> bool {
179        #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
180        match self.inline_blame {
181            Some(InlineBlameSettings { enabled, .. }) => enabled,
182            _ => false,
183        }
184    }
185
186    pub fn inline_blame_delay(&self) -> Option<Duration> {
187        match self.inline_blame {
188            Some(InlineBlameSettings {
189                delay_ms: Some(delay_ms),
190                ..
191            }) if delay_ms > 0 => Some(Duration::from_millis(delay_ms)),
192            _ => None,
193        }
194    }
195
196    pub fn show_inline_commit_summary(&self) -> bool {
197        match self.inline_blame {
198            Some(InlineBlameSettings {
199                show_commit_summary,
200                ..
201            }) => show_commit_summary,
202            _ => false,
203        }
204    }
205}
206
207#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
208#[serde(rename_all = "snake_case")]
209pub enum GitHunkStyleSetting {
210    /// Show unstaged hunks with a filled background and staged hunks hollow.
211    #[default]
212    StagedHollow,
213    /// Show unstaged hunks hollow and staged hunks with a filled background.
214    UnstagedHollow,
215}
216
217#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
218#[serde(rename_all = "snake_case")]
219pub enum GitGutterSetting {
220    /// Show git gutter in tracked files.
221    #[default]
222    TrackedFiles,
223    /// Hide git gutter
224    Hide,
225}
226
227#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
228#[serde(rename_all = "snake_case")]
229pub struct InlineBlameSettings {
230    /// Whether or not to show git blame data inline in
231    /// the currently focused line.
232    ///
233    /// Default: true
234    #[serde(default = "true_value")]
235    pub enabled: bool,
236    /// Whether to only show the inline blame information
237    /// after a delay once the cursor stops moving.
238    ///
239    /// Default: 0
240    pub delay_ms: Option<u64>,
241    /// The minimum column number to show the inline blame information at
242    ///
243    /// Default: 0
244    pub min_column: Option<u32>,
245    /// Whether to show commit summary as part of the inline blame.
246    ///
247    /// Default: false
248    #[serde(default)]
249    pub show_commit_summary: bool,
250}
251
252const fn true_value() -> bool {
253    true
254}
255
256#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
257pub struct BinarySettings {
258    pub path: Option<String>,
259    pub arguments: Option<Vec<String>>,
260    pub ignore_system_version: Option<bool>,
261}
262
263#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
264#[serde(rename_all = "snake_case")]
265pub struct LspSettings {
266    pub binary: Option<BinarySettings>,
267    pub initialization_options: Option<serde_json::Value>,
268    pub settings: Option<serde_json::Value>,
269}
270
271#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
272pub struct SessionSettings {
273    /// Whether or not to restore unsaved buffers on restart.
274    ///
275    /// If this is true, user won't be prompted whether to save/discard
276    /// dirty files when closing the application.
277    ///
278    /// Default: true
279    pub restore_unsaved_buffers: bool,
280}
281
282impl Default for SessionSettings {
283    fn default() -> Self {
284        Self {
285            restore_unsaved_buffers: true,
286        }
287    }
288}
289
290impl Settings for ProjectSettings {
291    const KEY: Option<&'static str> = None;
292
293    type FileContent = Self;
294
295    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
296        sources.json_merge()
297    }
298}
299
300pub enum SettingsObserverMode {
301    Local(Arc<dyn Fs>),
302    Remote,
303}
304
305#[derive(Clone, Debug, PartialEq)]
306pub enum SettingsObserverEvent {
307    LocalSettingsUpdated(Result<(), InvalidSettingsError>),
308}
309
310impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
311
312pub struct SettingsObserver {
313    mode: SettingsObserverMode,
314    downstream_client: Option<AnyProtoClient>,
315    worktree_store: Entity<WorktreeStore>,
316    project_id: u64,
317    task_store: Entity<TaskStore>,
318}
319
320/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
321/// (or the equivalent protobuf messages from upstream) and updates local settings
322/// and sends notifications downstream.
323/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
324/// upstream.
325impl SettingsObserver {
326    pub fn init(client: &AnyProtoClient) {
327        client.add_entity_message_handler(Self::handle_update_worktree_settings);
328    }
329
330    pub fn new_local(
331        fs: Arc<dyn Fs>,
332        worktree_store: Entity<WorktreeStore>,
333        task_store: Entity<TaskStore>,
334        cx: &mut Context<Self>,
335    ) -> Self {
336        cx.subscribe(&worktree_store, Self::on_worktree_store_event)
337            .detach();
338
339        Self {
340            worktree_store,
341            task_store,
342            mode: SettingsObserverMode::Local(fs),
343            downstream_client: None,
344            project_id: 0,
345        }
346    }
347
348    pub fn new_remote(
349        worktree_store: Entity<WorktreeStore>,
350        task_store: Entity<TaskStore>,
351        _: &mut Context<Self>,
352    ) -> Self {
353        Self {
354            worktree_store,
355            task_store,
356            mode: SettingsObserverMode::Remote,
357            downstream_client: None,
358            project_id: 0,
359        }
360    }
361
362    pub fn shared(
363        &mut self,
364        project_id: u64,
365        downstream_client: AnyProtoClient,
366        cx: &mut Context<Self>,
367    ) {
368        self.project_id = project_id;
369        self.downstream_client = Some(downstream_client.clone());
370
371        let store = cx.global::<SettingsStore>();
372        for worktree in self.worktree_store.read(cx).worktrees() {
373            let worktree_id = worktree.read(cx).id().to_proto();
374            for (path, content) in store.local_settings(worktree.read(cx).id()) {
375                downstream_client
376                    .send(proto::UpdateWorktreeSettings {
377                        project_id,
378                        worktree_id,
379                        path: path.to_proto(),
380                        content: Some(content),
381                        kind: Some(
382                            local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
383                        ),
384                    })
385                    .log_err();
386            }
387            for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
388                downstream_client
389                    .send(proto::UpdateWorktreeSettings {
390                        project_id,
391                        worktree_id,
392                        path: path.to_proto(),
393                        content: Some(content),
394                        kind: Some(
395                            local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
396                        ),
397                    })
398                    .log_err();
399            }
400        }
401    }
402
403    pub fn unshared(&mut self, _: &mut Context<Self>) {
404        self.downstream_client = None;
405    }
406
407    async fn handle_update_worktree_settings(
408        this: Entity<Self>,
409        envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
410        mut cx: AsyncApp,
411    ) -> anyhow::Result<()> {
412        let kind = match envelope.payload.kind {
413            Some(kind) => proto::LocalSettingsKind::from_i32(kind)
414                .with_context(|| format!("unknown kind {kind}"))?,
415            None => proto::LocalSettingsKind::Settings,
416        };
417        this.update(&mut cx, |this, cx| {
418            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
419            let Some(worktree) = this
420                .worktree_store
421                .read(cx)
422                .worktree_for_id(worktree_id, cx)
423            else {
424                return;
425            };
426
427            this.update_settings(
428                worktree,
429                [(
430                    Arc::<Path>::from_proto(envelope.payload.path.clone()),
431                    local_settings_kind_from_proto(kind),
432                    envelope.payload.content,
433                )],
434                cx,
435            );
436        })?;
437        Ok(())
438    }
439
440    fn on_worktree_store_event(
441        &mut self,
442        _: Entity<WorktreeStore>,
443        event: &WorktreeStoreEvent,
444        cx: &mut Context<Self>,
445    ) {
446        if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
447            cx.subscribe(worktree, |this, worktree, event, cx| {
448                if let worktree::Event::UpdatedEntries(changes) = event {
449                    this.update_local_worktree_settings(&worktree, changes, cx)
450                }
451            })
452            .detach()
453        }
454    }
455
456    fn update_local_worktree_settings(
457        &mut self,
458        worktree: &Entity<Worktree>,
459        changes: &UpdatedEntriesSet,
460        cx: &mut Context<Self>,
461    ) {
462        let SettingsObserverMode::Local(fs) = &self.mode else {
463            return;
464        };
465
466        let mut settings_contents = Vec::new();
467        for (path, _, change) in changes.iter() {
468            let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
469                let settings_dir = Arc::<Path>::from(
470                    path.ancestors()
471                        .nth(local_settings_file_relative_path().components().count())
472                        .unwrap(),
473                );
474                (settings_dir, LocalSettingsKind::Settings)
475            } else if path.ends_with(local_tasks_file_relative_path()) {
476                let settings_dir = Arc::<Path>::from(
477                    path.ancestors()
478                        .nth(
479                            local_tasks_file_relative_path()
480                                .components()
481                                .count()
482                                .saturating_sub(1),
483                        )
484                        .unwrap(),
485                );
486                (settings_dir, LocalSettingsKind::Tasks)
487            } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
488                let settings_dir = Arc::<Path>::from(
489                    path.ancestors()
490                        .nth(
491                            local_vscode_tasks_file_relative_path()
492                                .components()
493                                .count()
494                                .saturating_sub(1),
495                        )
496                        .unwrap(),
497                );
498                (settings_dir, LocalSettingsKind::Tasks)
499            } else if path.ends_with(EDITORCONFIG_NAME) {
500                let Some(settings_dir) = path.parent().map(Arc::from) else {
501                    continue;
502                };
503                (settings_dir, LocalSettingsKind::Editorconfig)
504            } else {
505                continue;
506            };
507
508            let removed = change == &PathChange::Removed;
509            let fs = fs.clone();
510            let abs_path = match worktree.read(cx).absolutize(path) {
511                Ok(abs_path) => abs_path,
512                Err(e) => {
513                    log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
514                    continue;
515                }
516            };
517            settings_contents.push(async move {
518                (
519                    settings_dir,
520                    kind,
521                    if removed {
522                        None
523                    } else {
524                        Some(
525                            async move {
526                                let content = fs.load(&abs_path).await?;
527                                if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
528                                    let vscode_tasks =
529                                        parse_json_with_comments::<VsCodeTaskFile>(&content)
530                                            .with_context(|| {
531                                                format!("parsing VSCode tasks, file {abs_path:?}")
532                                            })?;
533                                    let zed_tasks = TaskTemplates::try_from(vscode_tasks)
534                                        .with_context(|| {
535                                            format!(
536                                        "converting VSCode tasks into Zed ones, file {abs_path:?}"
537                                    )
538                                        })?;
539                                    serde_json::to_string(&zed_tasks).with_context(|| {
540                                        format!(
541                                            "serializing Zed tasks into JSON, file {abs_path:?}"
542                                        )
543                                    })
544                                } else {
545                                    Ok(content)
546                                }
547                            }
548                            .await,
549                        )
550                    },
551                )
552            });
553        }
554
555        if settings_contents.is_empty() {
556            return;
557        }
558
559        let worktree = worktree.clone();
560        cx.spawn(move |this, cx| async move {
561            let settings_contents: Vec<(Arc<Path>, _, _)> =
562                futures::future::join_all(settings_contents).await;
563            cx.update(|cx| {
564                this.update(cx, |this, cx| {
565                    this.update_settings(
566                        worktree,
567                        settings_contents.into_iter().map(|(path, kind, content)| {
568                            (path, kind, content.and_then(|c| c.log_err()))
569                        }),
570                        cx,
571                    )
572                })
573            })
574        })
575        .detach();
576    }
577
578    fn update_settings(
579        &mut self,
580        worktree: Entity<Worktree>,
581        settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
582        cx: &mut Context<Self>,
583    ) {
584        let worktree_id = worktree.read(cx).id();
585        let remote_worktree_id = worktree.read(cx).id();
586        let task_store = self.task_store.clone();
587
588        for (directory, kind, file_content) in settings_contents {
589            match kind {
590                LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
591                    .update_global::<SettingsStore, _>(|store, cx| {
592                        let result = store.set_local_settings(
593                            worktree_id,
594                            directory.clone(),
595                            kind,
596                            file_content.as_deref(),
597                            cx,
598                        );
599
600                        match result {
601                            Err(InvalidSettingsError::LocalSettings { path, message }) => {
602                                log::error!(
603                                    "Failed to set local settings in {:?}: {:?}",
604                                    path,
605                                    message
606                                );
607                                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
608                                    InvalidSettingsError::LocalSettings { path, message },
609                                )));
610                            }
611                            Err(e) => {
612                                log::error!("Failed to set local settings: {e}");
613                            }
614                            Ok(_) => {
615                                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(())));
616                            }
617                        }
618                    }),
619                LocalSettingsKind::Tasks => task_store.update(cx, |task_store, cx| {
620                    task_store
621                        .update_user_tasks(
622                            Some(SettingsLocation {
623                                worktree_id,
624                                path: directory.as_ref(),
625                            }),
626                            file_content.as_deref(),
627                            cx,
628                        )
629                        .log_err();
630                }),
631            };
632
633            if let Some(downstream_client) = &self.downstream_client {
634                downstream_client
635                    .send(proto::UpdateWorktreeSettings {
636                        project_id: self.project_id,
637                        worktree_id: remote_worktree_id.to_proto(),
638                        path: directory.to_proto(),
639                        content: file_content,
640                        kind: Some(local_settings_kind_to_proto(kind).into()),
641                    })
642                    .log_err();
643            }
644        }
645    }
646}
647
648pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
649    match kind {
650        proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
651        proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
652        proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
653    }
654}
655
656pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
657    match kind {
658        LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
659        LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
660        LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
661    }
662}