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