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}
172
173impl GitSettings {
174    pub fn inline_blame_enabled(&self) -> bool {
175        #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
176        match self.inline_blame {
177            Some(InlineBlameSettings { enabled, .. }) => enabled,
178            _ => false,
179        }
180    }
181
182    pub fn inline_blame_delay(&self) -> Option<Duration> {
183        match self.inline_blame {
184            Some(InlineBlameSettings {
185                delay_ms: Some(delay_ms),
186                ..
187            }) if delay_ms > 0 => Some(Duration::from_millis(delay_ms)),
188            _ => None,
189        }
190    }
191
192    pub fn show_inline_commit_summary(&self) -> bool {
193        match self.inline_blame {
194            Some(InlineBlameSettings {
195                show_commit_summary,
196                ..
197            }) => show_commit_summary,
198            _ => false,
199        }
200    }
201}
202
203#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
204#[serde(rename_all = "snake_case")]
205pub enum GitHunkStyleSetting {
206    /// Show unstaged hunks with a transparent background
207    #[default]
208    Transparent,
209    /// Show unstaged hunks with a pattern background
210    Pattern,
211    /// Show unstaged hunks with a border background
212    Border,
213
214    /// Show staged hunks with a pattern background
215    StagedPattern,
216    /// Show staged hunks with a pattern background
217    StagedTransparent,
218    /// Show staged hunks with a pattern background
219    StagedBorder,
220}
221
222#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
223#[serde(rename_all = "snake_case")]
224pub enum GitGutterSetting {
225    /// Show git gutter in tracked files.
226    #[default]
227    TrackedFiles,
228    /// Hide git gutter
229    Hide,
230}
231
232#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
233#[serde(rename_all = "snake_case")]
234pub struct InlineBlameSettings {
235    /// Whether or not to show git blame data inline in
236    /// the currently focused line.
237    ///
238    /// Default: true
239    #[serde(default = "true_value")]
240    pub enabled: bool,
241    /// Whether to only show the inline blame information
242    /// after a delay once the cursor stops moving.
243    ///
244    /// Default: 0
245    pub delay_ms: Option<u64>,
246    /// The minimum column number to show the inline blame information at
247    ///
248    /// Default: 0
249    pub min_column: Option<u32>,
250    /// Whether to show commit summary as part of the inline blame.
251    ///
252    /// Default: false
253    #[serde(default)]
254    pub show_commit_summary: bool,
255}
256
257const fn true_value() -> bool {
258    true
259}
260
261#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
262pub struct BinarySettings {
263    pub path: Option<String>,
264    pub arguments: Option<Vec<String>>,
265    pub ignore_system_version: Option<bool>,
266}
267
268#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
269#[serde(rename_all = "snake_case")]
270pub struct LspSettings {
271    pub binary: Option<BinarySettings>,
272    pub initialization_options: Option<serde_json::Value>,
273    pub settings: Option<serde_json::Value>,
274}
275
276#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
277pub struct SessionSettings {
278    /// Whether or not to restore unsaved buffers on restart.
279    ///
280    /// If this is true, user won't be prompted whether to save/discard
281    /// dirty files when closing the application.
282    ///
283    /// Default: true
284    pub restore_unsaved_buffers: bool,
285}
286
287impl Default for SessionSettings {
288    fn default() -> Self {
289        Self {
290            restore_unsaved_buffers: true,
291        }
292    }
293}
294
295impl Settings for ProjectSettings {
296    const KEY: Option<&'static str> = None;
297
298    type FileContent = Self;
299
300    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
301        sources.json_merge()
302    }
303}
304
305pub enum SettingsObserverMode {
306    Local(Arc<dyn Fs>),
307    Remote,
308}
309
310#[derive(Clone, Debug, PartialEq)]
311pub enum SettingsObserverEvent {
312    LocalSettingsUpdated(Result<(), InvalidSettingsError>),
313}
314
315impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
316
317pub struct SettingsObserver {
318    mode: SettingsObserverMode,
319    downstream_client: Option<AnyProtoClient>,
320    worktree_store: Entity<WorktreeStore>,
321    project_id: u64,
322    task_store: Entity<TaskStore>,
323}
324
325/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
326/// (or the equivalent protobuf messages from upstream) and updates local settings
327/// and sends notifications downstream.
328/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
329/// upstream.
330impl SettingsObserver {
331    pub fn init(client: &AnyProtoClient) {
332        client.add_entity_message_handler(Self::handle_update_worktree_settings);
333    }
334
335    pub fn new_local(
336        fs: Arc<dyn Fs>,
337        worktree_store: Entity<WorktreeStore>,
338        task_store: Entity<TaskStore>,
339        cx: &mut Context<Self>,
340    ) -> Self {
341        cx.subscribe(&worktree_store, Self::on_worktree_store_event)
342            .detach();
343
344        Self {
345            worktree_store,
346            task_store,
347            mode: SettingsObserverMode::Local(fs),
348            downstream_client: None,
349            project_id: 0,
350        }
351    }
352
353    pub fn new_remote(
354        worktree_store: Entity<WorktreeStore>,
355        task_store: Entity<TaskStore>,
356        _: &mut Context<Self>,
357    ) -> Self {
358        Self {
359            worktree_store,
360            task_store,
361            mode: SettingsObserverMode::Remote,
362            downstream_client: None,
363            project_id: 0,
364        }
365    }
366
367    pub fn shared(
368        &mut self,
369        project_id: u64,
370        downstream_client: AnyProtoClient,
371        cx: &mut Context<Self>,
372    ) {
373        self.project_id = project_id;
374        self.downstream_client = Some(downstream_client.clone());
375
376        let store = cx.global::<SettingsStore>();
377        for worktree in self.worktree_store.read(cx).worktrees() {
378            let worktree_id = worktree.read(cx).id().to_proto();
379            for (path, content) in store.local_settings(worktree.read(cx).id()) {
380                downstream_client
381                    .send(proto::UpdateWorktreeSettings {
382                        project_id,
383                        worktree_id,
384                        path: path.to_proto(),
385                        content: Some(content),
386                        kind: Some(
387                            local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
388                        ),
389                    })
390                    .log_err();
391            }
392            for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
393                downstream_client
394                    .send(proto::UpdateWorktreeSettings {
395                        project_id,
396                        worktree_id,
397                        path: path.to_proto(),
398                        content: Some(content),
399                        kind: Some(
400                            local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
401                        ),
402                    })
403                    .log_err();
404            }
405        }
406    }
407
408    pub fn unshared(&mut self, _: &mut Context<Self>) {
409        self.downstream_client = None;
410    }
411
412    async fn handle_update_worktree_settings(
413        this: Entity<Self>,
414        envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
415        mut cx: AsyncApp,
416    ) -> anyhow::Result<()> {
417        let kind = match envelope.payload.kind {
418            Some(kind) => proto::LocalSettingsKind::from_i32(kind)
419                .with_context(|| format!("unknown kind {kind}"))?,
420            None => proto::LocalSettingsKind::Settings,
421        };
422        this.update(&mut cx, |this, cx| {
423            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
424            let Some(worktree) = this
425                .worktree_store
426                .read(cx)
427                .worktree_for_id(worktree_id, cx)
428            else {
429                return;
430            };
431
432            this.update_settings(
433                worktree,
434                [(
435                    Arc::<Path>::from_proto(envelope.payload.path.clone()),
436                    local_settings_kind_from_proto(kind),
437                    envelope.payload.content,
438                )],
439                cx,
440            );
441        })?;
442        Ok(())
443    }
444
445    fn on_worktree_store_event(
446        &mut self,
447        _: Entity<WorktreeStore>,
448        event: &WorktreeStoreEvent,
449        cx: &mut Context<Self>,
450    ) {
451        if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
452            cx.subscribe(worktree, |this, worktree, event, cx| {
453                if let worktree::Event::UpdatedEntries(changes) = event {
454                    this.update_local_worktree_settings(&worktree, changes, cx)
455                }
456            })
457            .detach()
458        }
459    }
460
461    fn update_local_worktree_settings(
462        &mut self,
463        worktree: &Entity<Worktree>,
464        changes: &UpdatedEntriesSet,
465        cx: &mut Context<Self>,
466    ) {
467        let SettingsObserverMode::Local(fs) = &self.mode else {
468            return;
469        };
470
471        let mut settings_contents = Vec::new();
472        for (path, _, change) in changes.iter() {
473            let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
474                let settings_dir = Arc::<Path>::from(
475                    path.ancestors()
476                        .nth(local_settings_file_relative_path().components().count())
477                        .unwrap(),
478                );
479                (settings_dir, LocalSettingsKind::Settings)
480            } else if path.ends_with(local_tasks_file_relative_path()) {
481                let settings_dir = Arc::<Path>::from(
482                    path.ancestors()
483                        .nth(
484                            local_tasks_file_relative_path()
485                                .components()
486                                .count()
487                                .saturating_sub(1),
488                        )
489                        .unwrap(),
490                );
491                (settings_dir, LocalSettingsKind::Tasks)
492            } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
493                let settings_dir = Arc::<Path>::from(
494                    path.ancestors()
495                        .nth(
496                            local_vscode_tasks_file_relative_path()
497                                .components()
498                                .count()
499                                .saturating_sub(1),
500                        )
501                        .unwrap(),
502                );
503                (settings_dir, LocalSettingsKind::Tasks)
504            } else if path.ends_with(EDITORCONFIG_NAME) {
505                let Some(settings_dir) = path.parent().map(Arc::from) else {
506                    continue;
507                };
508                (settings_dir, LocalSettingsKind::Editorconfig)
509            } else {
510                continue;
511            };
512
513            let removed = change == &PathChange::Removed;
514            let fs = fs.clone();
515            let abs_path = match worktree.read(cx).absolutize(path) {
516                Ok(abs_path) => abs_path,
517                Err(e) => {
518                    log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
519                    continue;
520                }
521            };
522            settings_contents.push(async move {
523                (
524                    settings_dir,
525                    kind,
526                    if removed {
527                        None
528                    } else {
529                        Some(
530                            async move {
531                                let content = fs.load(&abs_path).await?;
532                                if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
533                                    let vscode_tasks =
534                                        parse_json_with_comments::<VsCodeTaskFile>(&content)
535                                            .with_context(|| {
536                                                format!("parsing VSCode tasks, file {abs_path:?}")
537                                            })?;
538                                    let zed_tasks = TaskTemplates::try_from(vscode_tasks)
539                                        .with_context(|| {
540                                            format!(
541                                        "converting VSCode tasks into Zed ones, file {abs_path:?}"
542                                    )
543                                        })?;
544                                    serde_json::to_string(&zed_tasks).with_context(|| {
545                                        format!(
546                                            "serializing Zed tasks into JSON, file {abs_path:?}"
547                                        )
548                                    })
549                                } else {
550                                    Ok(content)
551                                }
552                            }
553                            .await,
554                        )
555                    },
556                )
557            });
558        }
559
560        if settings_contents.is_empty() {
561            return;
562        }
563
564        let worktree = worktree.clone();
565        cx.spawn(move |this, cx| async move {
566            let settings_contents: Vec<(Arc<Path>, _, _)> =
567                futures::future::join_all(settings_contents).await;
568            cx.update(|cx| {
569                this.update(cx, |this, cx| {
570                    this.update_settings(
571                        worktree,
572                        settings_contents.into_iter().map(|(path, kind, content)| {
573                            (path, kind, content.and_then(|c| c.log_err()))
574                        }),
575                        cx,
576                    )
577                })
578            })
579        })
580        .detach();
581    }
582
583    fn update_settings(
584        &mut self,
585        worktree: Entity<Worktree>,
586        settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
587        cx: &mut Context<Self>,
588    ) {
589        let worktree_id = worktree.read(cx).id();
590        let remote_worktree_id = worktree.read(cx).id();
591        let task_store = self.task_store.clone();
592
593        for (directory, kind, file_content) in settings_contents {
594            match kind {
595                LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
596                    .update_global::<SettingsStore, _>(|store, cx| {
597                        let result = store.set_local_settings(
598                            worktree_id,
599                            directory.clone(),
600                            kind,
601                            file_content.as_deref(),
602                            cx,
603                        );
604
605                        match result {
606                            Err(InvalidSettingsError::LocalSettings { path, message }) => {
607                                log::error!(
608                                    "Failed to set local settings in {:?}: {:?}",
609                                    path,
610                                    message
611                                );
612                                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
613                                    InvalidSettingsError::LocalSettings { path, message },
614                                )));
615                            }
616                            Err(e) => {
617                                log::error!("Failed to set local settings: {e}");
618                            }
619                            Ok(_) => {
620                                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(())));
621                            }
622                        }
623                    }),
624                LocalSettingsKind::Tasks => task_store.update(cx, |task_store, cx| {
625                    task_store
626                        .update_user_tasks(
627                            Some(SettingsLocation {
628                                worktree_id,
629                                path: directory.as_ref(),
630                            }),
631                            file_content.as_deref(),
632                            cx,
633                        )
634                        .log_err();
635                }),
636            };
637
638            if let Some(downstream_client) = &self.downstream_client {
639                downstream_client
640                    .send(proto::UpdateWorktreeSettings {
641                        project_id: self.project_id,
642                        worktree_id: remote_worktree_id.to_proto(),
643                        path: directory.to_proto(),
644                        content: file_content,
645                        kind: Some(local_settings_kind_to_proto(kind).into()),
646                    })
647                    .log_err();
648            }
649        }
650    }
651}
652
653pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
654    match kind {
655        proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
656        proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
657        proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
658    }
659}
660
661pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
662    match kind {
663        LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
664        LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
665        LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
666    }
667}