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