project_settings.rs

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