project_settings.rs

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