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