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