project_settings.rs

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