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