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