project_settings.rs

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