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