project_settings.rs

  1use anyhow::Context as _;
  2use collections::HashMap;
  3use dap::adapters::DebugAdapterName;
  4use fs::Fs;
  5use futures::StreamExt as _;
  6use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Task};
  7use lsp::LanguageServerName;
  8use paths::{
  9    EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_file_relative_path,
 10    local_tasks_file_relative_path, local_vscode_launch_file_relative_path,
 11    local_vscode_tasks_file_relative_path, task_file_name,
 12};
 13use rpc::{
 14    AnyProtoClient, TypedEnvelope,
 15    proto::{self, FromProto, ToProto},
 16};
 17use schemars::JsonSchema;
 18use serde::{Deserialize, Serialize};
 19use settings::{
 20    InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources,
 21    SettingsStore, parse_json_with_comments, watch_config_file,
 22};
 23use std::{
 24    path::{Path, PathBuf},
 25    sync::Arc,
 26    time::Duration,
 27};
 28use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile};
 29use util::{ResultExt, serde::default_true};
 30use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
 31
 32use crate::{
 33    task_store::{TaskSettingsLocation, TaskStore},
 34    worktree_store::{WorktreeStore, WorktreeStoreEvent},
 35};
 36
 37#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
 38pub struct ProjectSettings {
 39    /// Configuration for language servers.
 40    ///
 41    /// The following settings can be overridden for specific language servers:
 42    /// - initialization_options
 43    ///
 44    /// To override settings for a language, add an entry for that language server's
 45    /// name to the lsp value.
 46    /// Default: null
 47    #[serde(default)]
 48    pub lsp: HashMap<LanguageServerName, LspSettings>,
 49
 50    /// Configuration for Debugger-related features
 51    #[serde(default)]
 52    pub dap: HashMap<DebugAdapterName, DapSettings>,
 53
 54    /// Configuration for Diagnostics-related features.
 55    #[serde(default)]
 56    pub diagnostics: DiagnosticsSettings,
 57
 58    /// Configuration for Git-related features
 59    #[serde(default)]
 60    pub git: GitSettings,
 61
 62    /// Configuration for Node-related features
 63    #[serde(default)]
 64    pub node: NodeBinarySettings,
 65
 66    /// Configuration for how direnv configuration should be loaded
 67    #[serde(default)]
 68    pub load_direnv: DirenvSettings,
 69
 70    /// Configuration for session-related features
 71    #[serde(default)]
 72    pub session: SessionSettings,
 73}
 74
 75#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
 76#[serde(rename_all = "snake_case")]
 77pub struct DapSettings {
 78    pub binary: Option<String>,
 79}
 80
 81#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
 82pub struct NodeBinarySettings {
 83    /// The path to the Node binary.
 84    pub path: Option<String>,
 85    /// The path to the npm binary Zed should use (defaults to `.path/../npm`).
 86    pub npm_path: Option<String>,
 87    /// If enabled, Zed will download its own copy of Node.
 88    #[serde(default)]
 89    pub ignore_system_version: Option<bool>,
 90}
 91
 92#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 93#[serde(rename_all = "snake_case")]
 94pub enum DirenvSettings {
 95    /// Load direnv configuration through a shell hook
 96    ShellHook,
 97    /// Load direnv configuration directly using `direnv export json`
 98    #[default]
 99    Direct,
100}
101
102#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
103pub struct DiagnosticsSettings {
104    /// Whether or not to include warning diagnostics
105    #[serde(default = "true_value")]
106    pub include_warnings: bool,
107
108    /// Settings for showing inline diagnostics
109    #[serde(default)]
110    pub inline: InlineDiagnosticsSettings,
111
112    /// Configuration, related to Rust language diagnostics.
113    #[serde(default)]
114    pub cargo: Option<CargoDiagnosticsSettings>,
115}
116
117impl DiagnosticsSettings {
118    pub fn fetch_cargo_diagnostics(&self) -> bool {
119        self.cargo.as_ref().map_or(false, |cargo_diagnostics| {
120            cargo_diagnostics.fetch_cargo_diagnostics
121        })
122    }
123}
124
125#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
126pub struct InlineDiagnosticsSettings {
127    /// Whether or not to show inline diagnostics
128    ///
129    /// Default: false
130    #[serde(default)]
131    pub enabled: bool,
132    /// Whether to only show the inline diaganostics after a delay after the
133    /// last editor event.
134    ///
135    /// Default: 150
136    #[serde(default = "default_inline_diagnostics_debounce_ms")]
137    pub update_debounce_ms: u64,
138    /// The amount of padding between the end of the source line and the start
139    /// of the inline diagnostic in units of columns.
140    ///
141    /// Default: 4
142    #[serde(default = "default_inline_diagnostics_padding")]
143    pub padding: u32,
144    /// The minimum column to display inline diagnostics. This setting can be
145    /// used to horizontally align inline diagnostics at some position. Lines
146    /// longer than this value will still push diagnostics further to the right.
147    ///
148    /// Default: 0
149    #[serde(default)]
150    pub min_column: u32,
151
152    #[serde(default)]
153    pub max_severity: Option<DiagnosticSeverity>,
154}
155
156#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
157pub struct CargoDiagnosticsSettings {
158    /// When enabled, Zed runs `cargo check --message-format=json`-based commands and
159    /// collect cargo diagnostics instead of rust-analyzer.
160    ///
161    /// Default: false
162    #[serde(default)]
163    pub fetch_cargo_diagnostics: bool,
164
165    /// A command override for fetching the cargo diagnostics.
166    /// First argument is the command, followed by the arguments.
167    ///
168    /// Default: ["cargo", "check", "--quiet", "--workspace", "--message-format=json", "--all-targets", "--keep-going"]
169    #[serde(default = "default_diagnostics_fetch_command")]
170    pub diagnostics_fetch_command: Vec<String>,
171
172    /// Extra environment variables to pass to the diagnostics fetch command.
173    ///
174    /// Default: {}
175    #[serde(default)]
176    pub env: HashMap<String, String>,
177}
178
179fn default_diagnostics_fetch_command() -> Vec<String> {
180    vec![
181        "cargo".to_string(),
182        "check".to_string(),
183        "--quiet".to_string(),
184        "--workspace".to_string(),
185        "--message-format=json".to_string(),
186        "--all-targets".to_string(),
187        "--keep-going".to_string(),
188    ]
189}
190
191#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
192#[serde(rename_all = "snake_case")]
193pub enum DiagnosticSeverity {
194    Error,
195    Warning,
196    Info,
197    Hint,
198}
199
200impl Default for InlineDiagnosticsSettings {
201    fn default() -> Self {
202        Self {
203            enabled: false,
204            update_debounce_ms: default_inline_diagnostics_debounce_ms(),
205            padding: default_inline_diagnostics_padding(),
206            min_column: 0,
207            max_severity: None,
208        }
209    }
210}
211
212fn default_inline_diagnostics_debounce_ms() -> u64 {
213    150
214}
215
216fn default_inline_diagnostics_padding() -> u32 {
217    4
218}
219
220#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
221pub struct GitSettings {
222    /// Whether or not to show the git gutter.
223    ///
224    /// Default: tracked_files
225    pub git_gutter: Option<GitGutterSetting>,
226    /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
227    ///
228    /// Default: null
229    pub gutter_debounce: Option<u64>,
230    /// Whether or not to show git blame data inline in
231    /// the currently focused line.
232    ///
233    /// Default: on
234    pub inline_blame: Option<InlineBlameSettings>,
235    /// How hunks are displayed visually in the editor.
236    ///
237    /// Default: staged_hollow
238    pub hunk_style: Option<GitHunkStyleSetting>,
239}
240
241impl GitSettings {
242    pub fn inline_blame_enabled(&self) -> bool {
243        #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
244        match self.inline_blame {
245            Some(InlineBlameSettings { enabled, .. }) => enabled,
246            _ => false,
247        }
248    }
249
250    pub fn inline_blame_delay(&self) -> Option<Duration> {
251        match self.inline_blame {
252            Some(InlineBlameSettings {
253                delay_ms: Some(delay_ms),
254                ..
255            }) if delay_ms > 0 => Some(Duration::from_millis(delay_ms)),
256            _ => None,
257        }
258    }
259
260    pub fn show_inline_commit_summary(&self) -> bool {
261        match self.inline_blame {
262            Some(InlineBlameSettings {
263                show_commit_summary,
264                ..
265            }) => show_commit_summary,
266            _ => false,
267        }
268    }
269}
270
271#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
272#[serde(rename_all = "snake_case")]
273pub enum GitHunkStyleSetting {
274    /// Show unstaged hunks with a filled background and staged hunks hollow.
275    #[default]
276    StagedHollow,
277    /// Show unstaged hunks hollow and staged hunks with a filled background.
278    UnstagedHollow,
279}
280
281#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
282#[serde(rename_all = "snake_case")]
283pub enum GitGutterSetting {
284    /// Show git gutter in tracked files.
285    #[default]
286    TrackedFiles,
287    /// Hide git gutter
288    Hide,
289}
290
291#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
292#[serde(rename_all = "snake_case")]
293pub struct InlineBlameSettings {
294    /// Whether or not to show git blame data inline in
295    /// the currently focused line.
296    ///
297    /// Default: true
298    #[serde(default = "true_value")]
299    pub enabled: bool,
300    /// Whether to only show the inline blame information
301    /// after a delay once the cursor stops moving.
302    ///
303    /// Default: 0
304    pub delay_ms: Option<u64>,
305    /// The minimum column number to show the inline blame information at
306    ///
307    /// Default: 0
308    pub min_column: Option<u32>,
309    /// Whether to show commit summary as part of the inline blame.
310    ///
311    /// Default: false
312    #[serde(default)]
313    pub show_commit_summary: bool,
314}
315
316const fn true_value() -> bool {
317    true
318}
319
320#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
321pub struct BinarySettings {
322    pub path: Option<String>,
323    pub arguments: Option<Vec<String>>,
324    // this can't be an FxHashMap because the extension APIs require the default SipHash
325    pub env: Option<std::collections::HashMap<String, String, std::hash::RandomState>>,
326    pub ignore_system_version: Option<bool>,
327}
328
329#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
330#[serde(rename_all = "snake_case")]
331pub struct LspSettings {
332    pub binary: Option<BinarySettings>,
333    pub initialization_options: Option<serde_json::Value>,
334    pub settings: Option<serde_json::Value>,
335    /// If the server supports sending tasks over LSP extensions,
336    /// this setting can be used to enable or disable them in Zed.
337    /// Default: true
338    #[serde(default = "default_true")]
339    pub enable_lsp_tasks: bool,
340}
341
342impl Default for LspSettings {
343    fn default() -> Self {
344        Self {
345            binary: None,
346            initialization_options: None,
347            settings: None,
348            enable_lsp_tasks: true,
349        }
350    }
351}
352
353#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
354pub struct SessionSettings {
355    /// Whether or not to restore unsaved buffers on restart.
356    ///
357    /// If this is true, user won't be prompted whether to save/discard
358    /// dirty files when closing the application.
359    ///
360    /// Default: true
361    pub restore_unsaved_buffers: bool,
362}
363
364impl Default for SessionSettings {
365    fn default() -> Self {
366        Self {
367            restore_unsaved_buffers: true,
368        }
369    }
370}
371
372impl Settings for ProjectSettings {
373    const KEY: Option<&'static str> = None;
374
375    type FileContent = Self;
376
377    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
378        sources.json_merge()
379    }
380
381    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
382        // this just sets the binary name instead of a full path so it relies on path lookup
383        // resolving to the one you want
384        vscode.enum_setting(
385            "npm.packageManager",
386            &mut current.node.npm_path,
387            |s| match s {
388                v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
389                _ => None,
390            },
391        );
392
393        if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") {
394            if let Some(blame) = current.git.inline_blame.as_mut() {
395                blame.enabled = b
396            } else {
397                current.git.inline_blame = Some(InlineBlameSettings {
398                    enabled: b,
399                    ..Default::default()
400                })
401            }
402        }
403
404        // TODO: translate lsp settings for rust-analyzer and other popular ones to old.lsp
405    }
406}
407
408pub enum SettingsObserverMode {
409    Local(Arc<dyn Fs>),
410    Remote,
411}
412
413#[derive(Clone, Debug, PartialEq)]
414pub enum SettingsObserverEvent {
415    LocalSettingsUpdated(Result<PathBuf, InvalidSettingsError>),
416    LocalTasksUpdated(Result<PathBuf, InvalidSettingsError>),
417}
418
419impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
420
421pub struct SettingsObserver {
422    mode: SettingsObserverMode,
423    downstream_client: Option<AnyProtoClient>,
424    worktree_store: Entity<WorktreeStore>,
425    project_id: u64,
426    task_store: Entity<TaskStore>,
427    _global_task_config_watcher: Task<()>,
428}
429
430/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
431/// (or the equivalent protobuf messages from upstream) and updates local settings
432/// and sends notifications downstream.
433/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
434/// upstream.
435impl SettingsObserver {
436    pub fn init(client: &AnyProtoClient) {
437        client.add_entity_message_handler(Self::handle_update_worktree_settings);
438    }
439
440    pub fn new_local(
441        fs: Arc<dyn Fs>,
442        worktree_store: Entity<WorktreeStore>,
443        task_store: Entity<TaskStore>,
444        cx: &mut Context<Self>,
445    ) -> Self {
446        cx.subscribe(&worktree_store, Self::on_worktree_store_event)
447            .detach();
448
449        Self {
450            worktree_store,
451            task_store,
452            mode: SettingsObserverMode::Local(fs.clone()),
453            downstream_client: None,
454            project_id: 0,
455            _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
456                fs.clone(),
457                paths::tasks_file().clone(),
458                cx,
459            ),
460        }
461    }
462
463    pub fn new_remote(
464        fs: Arc<dyn Fs>,
465        worktree_store: Entity<WorktreeStore>,
466        task_store: Entity<TaskStore>,
467        cx: &mut Context<Self>,
468    ) -> Self {
469        Self {
470            worktree_store,
471            task_store,
472            mode: SettingsObserverMode::Remote,
473            downstream_client: None,
474            project_id: 0,
475            _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
476                fs.clone(),
477                paths::tasks_file().clone(),
478                cx,
479            ),
480        }
481    }
482
483    pub fn shared(
484        &mut self,
485        project_id: u64,
486        downstream_client: AnyProtoClient,
487        cx: &mut Context<Self>,
488    ) {
489        self.project_id = project_id;
490        self.downstream_client = Some(downstream_client.clone());
491
492        let store = cx.global::<SettingsStore>();
493        for worktree in self.worktree_store.read(cx).worktrees() {
494            let worktree_id = worktree.read(cx).id().to_proto();
495            for (path, content) in store.local_settings(worktree.read(cx).id()) {
496                downstream_client
497                    .send(proto::UpdateWorktreeSettings {
498                        project_id,
499                        worktree_id,
500                        path: path.to_proto(),
501                        content: Some(content),
502                        kind: Some(
503                            local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
504                        ),
505                    })
506                    .log_err();
507            }
508            for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
509                downstream_client
510                    .send(proto::UpdateWorktreeSettings {
511                        project_id,
512                        worktree_id,
513                        path: path.to_proto(),
514                        content: Some(content),
515                        kind: Some(
516                            local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
517                        ),
518                    })
519                    .log_err();
520            }
521        }
522    }
523
524    pub fn unshared(&mut self, _: &mut Context<Self>) {
525        self.downstream_client = None;
526    }
527
528    async fn handle_update_worktree_settings(
529        this: Entity<Self>,
530        envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
531        mut cx: AsyncApp,
532    ) -> anyhow::Result<()> {
533        let kind = match envelope.payload.kind {
534            Some(kind) => proto::LocalSettingsKind::from_i32(kind)
535                .with_context(|| format!("unknown kind {kind}"))?,
536            None => proto::LocalSettingsKind::Settings,
537        };
538        this.update(&mut cx, |this, cx| {
539            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
540            let Some(worktree) = this
541                .worktree_store
542                .read(cx)
543                .worktree_for_id(worktree_id, cx)
544            else {
545                return;
546            };
547
548            this.update_settings(
549                worktree,
550                [(
551                    Arc::<Path>::from_proto(envelope.payload.path.clone()),
552                    local_settings_kind_from_proto(kind),
553                    envelope.payload.content,
554                )],
555                cx,
556            );
557        })?;
558        Ok(())
559    }
560
561    fn on_worktree_store_event(
562        &mut self,
563        _: Entity<WorktreeStore>,
564        event: &WorktreeStoreEvent,
565        cx: &mut Context<Self>,
566    ) {
567        if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
568            cx.subscribe(worktree, |this, worktree, event, cx| {
569                if let worktree::Event::UpdatedEntries(changes) = event {
570                    this.update_local_worktree_settings(&worktree, changes, cx)
571                }
572            })
573            .detach()
574        }
575    }
576
577    fn update_local_worktree_settings(
578        &mut self,
579        worktree: &Entity<Worktree>,
580        changes: &UpdatedEntriesSet,
581        cx: &mut Context<Self>,
582    ) {
583        let SettingsObserverMode::Local(fs) = &self.mode else {
584            return;
585        };
586
587        let mut settings_contents = Vec::new();
588        for (path, _, change) in changes.iter() {
589            let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
590                let settings_dir = Arc::<Path>::from(
591                    path.ancestors()
592                        .nth(local_settings_file_relative_path().components().count())
593                        .unwrap(),
594                );
595                (settings_dir, LocalSettingsKind::Settings)
596            } else if path.ends_with(local_tasks_file_relative_path()) {
597                let settings_dir = Arc::<Path>::from(
598                    path.ancestors()
599                        .nth(
600                            local_tasks_file_relative_path()
601                                .components()
602                                .count()
603                                .saturating_sub(1),
604                        )
605                        .unwrap(),
606                );
607                (settings_dir, LocalSettingsKind::Tasks)
608            } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
609                let settings_dir = Arc::<Path>::from(
610                    path.ancestors()
611                        .nth(
612                            local_vscode_tasks_file_relative_path()
613                                .components()
614                                .count()
615                                .saturating_sub(1),
616                        )
617                        .unwrap(),
618                );
619                (settings_dir, LocalSettingsKind::Tasks)
620            } else if path.ends_with(local_debug_file_relative_path()) {
621                let settings_dir = Arc::<Path>::from(
622                    path.ancestors()
623                        .nth(
624                            local_debug_file_relative_path()
625                                .components()
626                                .count()
627                                .saturating_sub(1),
628                        )
629                        .unwrap(),
630                );
631                (settings_dir, LocalSettingsKind::Debug)
632            } else if path.ends_with(local_vscode_launch_file_relative_path()) {
633                let settings_dir = Arc::<Path>::from(
634                    path.ancestors()
635                        .nth(
636                            local_vscode_tasks_file_relative_path()
637                                .components()
638                                .count()
639                                .saturating_sub(1),
640                        )
641                        .unwrap(),
642                );
643                (settings_dir, LocalSettingsKind::Debug)
644            } else if path.ends_with(EDITORCONFIG_NAME) {
645                let Some(settings_dir) = path.parent().map(Arc::from) else {
646                    continue;
647                };
648                (settings_dir, LocalSettingsKind::Editorconfig)
649            } else {
650                continue;
651            };
652
653            let removed = change == &PathChange::Removed;
654            let fs = fs.clone();
655            let abs_path = match worktree.read(cx).absolutize(path) {
656                Ok(abs_path) => abs_path,
657                Err(e) => {
658                    log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
659                    continue;
660                }
661            };
662            settings_contents.push(async move {
663                (
664                    settings_dir,
665                    kind,
666                    if removed {
667                        None
668                    } else {
669                        Some(
670                            async move {
671                                let content = fs.load(&abs_path).await?;
672                                if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
673                                    let vscode_tasks =
674                                        parse_json_with_comments::<VsCodeTaskFile>(&content)
675                                            .with_context(|| {
676                                                format!("parsing VSCode tasks, file {abs_path:?}")
677                                            })?;
678                                    let zed_tasks = TaskTemplates::try_from(vscode_tasks)
679                                        .with_context(|| {
680                                            format!(
681                                        "converting VSCode tasks into Zed ones, file {abs_path:?}"
682                                    )
683                                        })?;
684                                    serde_json::to_string(&zed_tasks).with_context(|| {
685                                        format!(
686                                            "serializing Zed tasks into JSON, file {abs_path:?}"
687                                        )
688                                    })
689                                } else if abs_path.ends_with(local_vscode_launch_file_relative_path()) {
690                                    let vscode_tasks =
691                                        parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
692                                            .with_context(|| {
693                                                format!("parsing VSCode debug tasks, file {abs_path:?}")
694                                            })?;
695                                    let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
696                                        .with_context(|| {
697                                            format!(
698                                        "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
699                                    )
700                                        })?;
701                                    serde_json::to_string(&zed_tasks).with_context(|| {
702                                        format!(
703                                            "serializing Zed tasks into JSON, file {abs_path:?}"
704                                        )
705                                    })
706                                } else {
707                                    Ok(content)
708                                }
709                            }
710                            .await,
711                        )
712                    },
713                )
714            });
715        }
716
717        if settings_contents.is_empty() {
718            return;
719        }
720
721        let worktree = worktree.clone();
722        cx.spawn(async move |this, cx| {
723            let settings_contents: Vec<(Arc<Path>, _, _)> =
724                futures::future::join_all(settings_contents).await;
725            cx.update(|cx| {
726                this.update(cx, |this, cx| {
727                    this.update_settings(
728                        worktree,
729                        settings_contents.into_iter().map(|(path, kind, content)| {
730                            (path, kind, content.and_then(|c| c.log_err()))
731                        }),
732                        cx,
733                    )
734                })
735            })
736        })
737        .detach();
738    }
739
740    fn update_settings(
741        &mut self,
742        worktree: Entity<Worktree>,
743        settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
744        cx: &mut Context<Self>,
745    ) {
746        let worktree_id = worktree.read(cx).id();
747        let remote_worktree_id = worktree.read(cx).id();
748        let task_store = self.task_store.clone();
749
750        for (directory, kind, file_content) in settings_contents {
751            match kind {
752                LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
753                    .update_global::<SettingsStore, _>(|store, cx| {
754                        let result = store.set_local_settings(
755                            worktree_id,
756                            directory.clone(),
757                            kind,
758                            file_content.as_deref(),
759                            cx,
760                        );
761
762                        match result {
763                            Err(InvalidSettingsError::LocalSettings { path, message }) => {
764                                log::error!("Failed to set local settings in {path:?}: {message}");
765                                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
766                                    InvalidSettingsError::LocalSettings { path, message },
767                                )));
768                            }
769                            Err(e) => {
770                                log::error!("Failed to set local settings: {e}");
771                            }
772                            Ok(()) => {
773                                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(
774                                    directory.join(local_settings_file_relative_path())
775                                )));
776                            }
777                        }
778                    }),
779                LocalSettingsKind::Tasks => {
780                    let result = task_store.update(cx, |task_store, cx| {
781                        task_store.update_user_tasks(
782                            TaskSettingsLocation::Worktree(SettingsLocation {
783                                worktree_id,
784                                path: directory.as_ref(),
785                            }),
786                            file_content.as_deref(),
787                            cx,
788                        )
789                    });
790
791                    match result {
792                        Err(InvalidSettingsError::Tasks { path, message }) => {
793                            log::error!("Failed to set local tasks in {path:?}: {message:?}");
794                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
795                                InvalidSettingsError::Tasks { path, message },
796                            )));
797                        }
798                        Err(e) => {
799                            log::error!("Failed to set local tasks: {e}");
800                        }
801                        Ok(()) => {
802                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
803                                directory.join(task_file_name())
804                            )));
805                        }
806                    }
807                }
808                LocalSettingsKind::Debug => {
809                    let result = task_store.update(cx, |task_store, cx| {
810                        task_store.update_user_debug_scenarios(
811                            TaskSettingsLocation::Worktree(SettingsLocation {
812                                worktree_id,
813                                path: directory.as_ref(),
814                            }),
815                            file_content.as_deref(),
816                            cx,
817                        )
818                    });
819
820                    match result {
821                        Err(InvalidSettingsError::Debug { path, message }) => {
822                            log::error!(
823                                "Failed to set local debug scenarios in {path:?}: {message:?}"
824                            );
825                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
826                                InvalidSettingsError::Debug { path, message },
827                            )));
828                        }
829                        Err(e) => {
830                            log::error!("Failed to set local tasks: {e}");
831                        }
832                        Ok(()) => {
833                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
834                                directory.join(task_file_name())
835                            )));
836                        }
837                    }
838                }
839            };
840
841            if let Some(downstream_client) = &self.downstream_client {
842                downstream_client
843                    .send(proto::UpdateWorktreeSettings {
844                        project_id: self.project_id,
845                        worktree_id: remote_worktree_id.to_proto(),
846                        path: directory.to_proto(),
847                        content: file_content,
848                        kind: Some(local_settings_kind_to_proto(kind).into()),
849                    })
850                    .log_err();
851            }
852        }
853    }
854
855    fn subscribe_to_global_task_file_changes(
856        fs: Arc<dyn Fs>,
857        file_path: PathBuf,
858        cx: &mut Context<Self>,
859    ) -> Task<()> {
860        let mut user_tasks_file_rx =
861            watch_config_file(&cx.background_executor(), fs, file_path.clone());
862        let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
863        let weak_entry = cx.weak_entity();
864        cx.spawn(async move |settings_observer, cx| {
865            let Ok(task_store) = settings_observer.update(cx, |settings_observer, _| {
866                settings_observer.task_store.clone()
867            }) else {
868                return;
869            };
870            if let Some(user_tasks_content) = user_tasks_content {
871                let Ok(()) = task_store.update(cx, |task_store, cx| {
872                    task_store
873                        .update_user_tasks(
874                            TaskSettingsLocation::Global(&file_path),
875                            Some(&user_tasks_content),
876                            cx,
877                        )
878                        .log_err();
879                }) else {
880                    return;
881                };
882            }
883            while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
884                let Ok(result) = task_store.update(cx, |task_store, cx| {
885                    task_store.update_user_tasks(
886                        TaskSettingsLocation::Global(&file_path),
887                        Some(&user_tasks_content),
888                        cx,
889                    )
890                }) else {
891                    break;
892                };
893
894                weak_entry
895                    .update(cx, |_, cx| match result {
896                        Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
897                            file_path.clone()
898                        ))),
899                        Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
900                            InvalidSettingsError::Tasks {
901                                path: file_path.clone(),
902                                message: err.to_string(),
903                            },
904                        ))),
905                    })
906                    .ok();
907            }
908        })
909    }
910}
911
912pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
913    match kind {
914        proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
915        proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
916        proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
917        proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
918    }
919}
920
921pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
922    match kind {
923        LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
924        LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
925        LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
926        LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
927    }
928}