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