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