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