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