project_settings.rs

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