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,
 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, TaskKind, 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_watchers: (Task<()>, 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_watchers: (
409                Self::subscribe_to_global_task_file_changes(
410                    fs.clone(),
411                    TaskKind::Script,
412                    paths::tasks_file().clone(),
413                    cx,
414                ),
415                Self::subscribe_to_global_task_file_changes(
416                    fs,
417                    TaskKind::Debug,
418                    paths::debug_tasks_file().clone(),
419                    cx,
420                ),
421            ),
422        }
423    }
424
425    pub fn new_remote(
426        fs: Arc<dyn Fs>,
427        worktree_store: Entity<WorktreeStore>,
428        task_store: Entity<TaskStore>,
429        cx: &mut Context<Self>,
430    ) -> Self {
431        Self {
432            worktree_store,
433            task_store,
434            mode: SettingsObserverMode::Remote,
435            downstream_client: None,
436            project_id: 0,
437            _global_task_config_watchers: (
438                Self::subscribe_to_global_task_file_changes(
439                    fs.clone(),
440                    TaskKind::Script,
441                    paths::tasks_file().clone(),
442                    cx,
443                ),
444                Self::subscribe_to_global_task_file_changes(
445                    fs.clone(),
446                    TaskKind::Debug,
447                    paths::debug_tasks_file().clone(),
448                    cx,
449                ),
450            ),
451        }
452    }
453
454    pub fn shared(
455        &mut self,
456        project_id: u64,
457        downstream_client: AnyProtoClient,
458        cx: &mut Context<Self>,
459    ) {
460        self.project_id = project_id;
461        self.downstream_client = Some(downstream_client.clone());
462
463        let store = cx.global::<SettingsStore>();
464        for worktree in self.worktree_store.read(cx).worktrees() {
465            let worktree_id = worktree.read(cx).id().to_proto();
466            for (path, content) in store.local_settings(worktree.read(cx).id()) {
467                downstream_client
468                    .send(proto::UpdateWorktreeSettings {
469                        project_id,
470                        worktree_id,
471                        path: path.to_proto(),
472                        content: Some(content),
473                        kind: Some(
474                            local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
475                        ),
476                    })
477                    .log_err();
478            }
479            for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
480                downstream_client
481                    .send(proto::UpdateWorktreeSettings {
482                        project_id,
483                        worktree_id,
484                        path: path.to_proto(),
485                        content: Some(content),
486                        kind: Some(
487                            local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
488                        ),
489                    })
490                    .log_err();
491            }
492        }
493    }
494
495    pub fn unshared(&mut self, _: &mut Context<Self>) {
496        self.downstream_client = None;
497    }
498
499    async fn handle_update_worktree_settings(
500        this: Entity<Self>,
501        envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
502        mut cx: AsyncApp,
503    ) -> anyhow::Result<()> {
504        let kind = match envelope.payload.kind {
505            Some(kind) => proto::LocalSettingsKind::from_i32(kind)
506                .with_context(|| format!("unknown kind {kind}"))?,
507            None => proto::LocalSettingsKind::Settings,
508        };
509        this.update(&mut cx, |this, cx| {
510            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
511            let Some(worktree) = this
512                .worktree_store
513                .read(cx)
514                .worktree_for_id(worktree_id, cx)
515            else {
516                return;
517            };
518
519            this.update_settings(
520                worktree,
521                [(
522                    Arc::<Path>::from_proto(envelope.payload.path.clone()),
523                    local_settings_kind_from_proto(kind),
524                    envelope.payload.content,
525                )],
526                cx,
527            );
528        })?;
529        Ok(())
530    }
531
532    fn on_worktree_store_event(
533        &mut self,
534        _: Entity<WorktreeStore>,
535        event: &WorktreeStoreEvent,
536        cx: &mut Context<Self>,
537    ) {
538        if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
539            cx.subscribe(worktree, |this, worktree, event, cx| {
540                if let worktree::Event::UpdatedEntries(changes) = event {
541                    this.update_local_worktree_settings(&worktree, changes, cx)
542                }
543            })
544            .detach()
545        }
546    }
547
548    fn update_local_worktree_settings(
549        &mut self,
550        worktree: &Entity<Worktree>,
551        changes: &UpdatedEntriesSet,
552        cx: &mut Context<Self>,
553    ) {
554        let SettingsObserverMode::Local(fs) = &self.mode else {
555            return;
556        };
557
558        let mut settings_contents = Vec::new();
559        for (path, _, change) in changes.iter() {
560            let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
561                let settings_dir = Arc::<Path>::from(
562                    path.ancestors()
563                        .nth(local_settings_file_relative_path().components().count())
564                        .unwrap(),
565                );
566                (settings_dir, LocalSettingsKind::Settings)
567            } else if path.ends_with(local_tasks_file_relative_path()) {
568                let settings_dir = Arc::<Path>::from(
569                    path.ancestors()
570                        .nth(
571                            local_tasks_file_relative_path()
572                                .components()
573                                .count()
574                                .saturating_sub(1),
575                        )
576                        .unwrap(),
577                );
578                (settings_dir, LocalSettingsKind::Tasks(TaskKind::Script))
579            } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
580                let settings_dir = Arc::<Path>::from(
581                    path.ancestors()
582                        .nth(
583                            local_vscode_tasks_file_relative_path()
584                                .components()
585                                .count()
586                                .saturating_sub(1),
587                        )
588                        .unwrap(),
589                );
590                (settings_dir, LocalSettingsKind::Tasks(TaskKind::Script))
591            } else if path.ends_with(local_debug_file_relative_path()) {
592                let settings_dir = Arc::<Path>::from(
593                    path.ancestors()
594                        .nth(
595                            local_debug_file_relative_path()
596                                .components()
597                                .count()
598                                .saturating_sub(1),
599                        )
600                        .unwrap(),
601                );
602                (settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug))
603            } else if path.ends_with(local_vscode_launch_file_relative_path()) {
604                let settings_dir = Arc::<Path>::from(
605                    path.ancestors()
606                        .nth(
607                            local_vscode_tasks_file_relative_path()
608                                .components()
609                                .count()
610                                .saturating_sub(1),
611                        )
612                        .unwrap(),
613                );
614                (settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug))
615            } else if path.ends_with(EDITORCONFIG_NAME) {
616                let Some(settings_dir) = path.parent().map(Arc::from) else {
617                    continue;
618                };
619                (settings_dir, LocalSettingsKind::Editorconfig)
620            } else {
621                continue;
622            };
623
624            let removed = change == &PathChange::Removed;
625            let fs = fs.clone();
626            let abs_path = match worktree.read(cx).absolutize(path) {
627                Ok(abs_path) => abs_path,
628                Err(e) => {
629                    log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
630                    continue;
631                }
632            };
633            settings_contents.push(async move {
634                (
635                    settings_dir,
636                    kind,
637                    if removed {
638                        None
639                    } else {
640                        Some(
641                            async move {
642                                let content = fs.load(&abs_path).await?;
643                                if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
644                                    let vscode_tasks =
645                                        parse_json_with_comments::<VsCodeTaskFile>(&content)
646                                            .with_context(|| {
647                                                format!("parsing VSCode tasks, file {abs_path:?}")
648                                            })?;
649                                    let zed_tasks = TaskTemplates::try_from(vscode_tasks)
650                                        .with_context(|| {
651                                            format!(
652                                        "converting VSCode tasks into Zed ones, file {abs_path:?}"
653                                    )
654                                        })?;
655                                    serde_json::to_string(&zed_tasks).with_context(|| {
656                                        format!(
657                                            "serializing Zed tasks into JSON, file {abs_path:?}"
658                                        )
659                                    })
660                                } else if abs_path.ends_with(local_vscode_launch_file_relative_path()) {
661                                    let vscode_tasks =
662                                        parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
663                                            .with_context(|| {
664                                                format!("parsing VSCode debug tasks, file {abs_path:?}")
665                                            })?;
666                                    let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
667                                        .with_context(|| {
668                                            format!(
669                                        "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
670                                    )
671                                        })?;
672                                    serde_json::to_string(&zed_tasks).with_context(|| {
673                                        format!(
674                                            "serializing Zed tasks into JSON, file {abs_path:?}"
675                                        )
676                                    })
677                                } else {
678                                    Ok(content)
679                                }
680                            }
681                            .await,
682                        )
683                    },
684                )
685            });
686        }
687
688        if settings_contents.is_empty() {
689            return;
690        }
691
692        let worktree = worktree.clone();
693        cx.spawn(async move |this, cx| {
694            let settings_contents: Vec<(Arc<Path>, _, _)> =
695                futures::future::join_all(settings_contents).await;
696            cx.update(|cx| {
697                this.update(cx, |this, cx| {
698                    this.update_settings(
699                        worktree,
700                        settings_contents.into_iter().map(|(path, kind, content)| {
701                            (path, kind, content.and_then(|c| c.log_err()))
702                        }),
703                        cx,
704                    )
705                })
706            })
707        })
708        .detach();
709    }
710
711    fn update_settings(
712        &mut self,
713        worktree: Entity<Worktree>,
714        settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
715        cx: &mut Context<Self>,
716    ) {
717        let worktree_id = worktree.read(cx).id();
718        let remote_worktree_id = worktree.read(cx).id();
719        let task_store = self.task_store.clone();
720
721        for (directory, kind, file_content) in settings_contents {
722            match kind {
723                LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
724                    .update_global::<SettingsStore, _>(|store, cx| {
725                        let result = store.set_local_settings(
726                            worktree_id,
727                            directory.clone(),
728                            kind,
729                            file_content.as_deref(),
730                            cx,
731                        );
732
733                        match result {
734                            Err(InvalidSettingsError::LocalSettings { path, message }) => {
735                                log::error!("Failed to set local settings in {path:?}: {message}");
736                                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
737                                    InvalidSettingsError::LocalSettings { path, message },
738                                )));
739                            }
740                            Err(e) => {
741                                log::error!("Failed to set local settings: {e}");
742                            }
743                            Ok(()) => {
744                                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(
745                                    directory.join(local_settings_file_relative_path())
746                                )));
747                            }
748                        }
749                    }),
750                LocalSettingsKind::Tasks(task_kind) => {
751                    let result = task_store.update(cx, |task_store, cx| {
752                        task_store.update_user_tasks(
753                            TaskSettingsLocation::Worktree(SettingsLocation {
754                                worktree_id,
755                                path: directory.as_ref(),
756                            }),
757                            file_content.as_deref(),
758                            task_kind,
759                            cx,
760                        )
761                    });
762
763                    match result {
764                        Err(InvalidSettingsError::Tasks { path, message }) => {
765                            log::error!("Failed to set local tasks in {path:?}: {message:?}");
766                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
767                                InvalidSettingsError::Tasks { path, message },
768                            )));
769                        }
770                        Err(e) => {
771                            log::error!("Failed to set local tasks: {e}");
772                        }
773                        Ok(()) => {
774                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
775                                task_kind.config_in_dir(&directory)
776                            )));
777                        }
778                    }
779                }
780            };
781
782            if let Some(downstream_client) = &self.downstream_client {
783                downstream_client
784                    .send(proto::UpdateWorktreeSettings {
785                        project_id: self.project_id,
786                        worktree_id: remote_worktree_id.to_proto(),
787                        path: directory.to_proto(),
788                        content: file_content,
789                        kind: Some(local_settings_kind_to_proto(kind).into()),
790                    })
791                    .log_err();
792            }
793        }
794    }
795
796    fn subscribe_to_global_task_file_changes(
797        fs: Arc<dyn Fs>,
798        task_kind: TaskKind,
799        file_path: PathBuf,
800        cx: &mut Context<Self>,
801    ) -> Task<()> {
802        let mut user_tasks_file_rx =
803            watch_config_file(&cx.background_executor(), fs, file_path.clone());
804        let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
805        let weak_entry = cx.weak_entity();
806        cx.spawn(async move |settings_observer, cx| {
807            let Ok(task_store) = settings_observer.update(cx, |settings_observer, _| {
808                settings_observer.task_store.clone()
809            }) else {
810                return;
811            };
812            if let Some(user_tasks_content) = user_tasks_content {
813                let Ok(()) = task_store.update(cx, |task_store, cx| {
814                    task_store
815                        .update_user_tasks(
816                            TaskSettingsLocation::Global(&file_path),
817                            Some(&user_tasks_content),
818                            task_kind,
819                            cx,
820                        )
821                        .log_err();
822                }) else {
823                    return;
824                };
825            }
826            while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
827                let Ok(result) = task_store.update(cx, |task_store, cx| {
828                    task_store.update_user_tasks(
829                        TaskSettingsLocation::Global(&file_path),
830                        Some(&user_tasks_content),
831                        task_kind,
832                        cx,
833                    )
834                }) else {
835                    break;
836                };
837
838                weak_entry
839                    .update(cx, |_, cx| match result {
840                        Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
841                            file_path.clone()
842                        ))),
843                        Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
844                            InvalidSettingsError::Tasks {
845                                path: file_path.clone(),
846                                message: err.to_string(),
847                            },
848                        ))),
849                    })
850                    .ok();
851            }
852        })
853    }
854}
855
856pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
857    match kind {
858        proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
859        proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks(TaskKind::Script),
860        proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
861    }
862}
863
864pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
865    match kind {
866        LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
867        LocalSettingsKind::Tasks(_) => proto::LocalSettingsKind::Tasks,
868        LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
869    }
870}