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::{AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Subscription, 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, REMOTE_SERVER_PROJECT_ID},
  17};
  18use schemars::JsonSchema;
  19use serde::{Deserialize, Serialize};
  20pub use settings::BinarySettings;
  21pub use settings::DirenvSettings;
  22pub use settings::LspSettings;
  23use settings::{
  24    DapSettingsContent, EditorconfigEvent, InvalidSettingsError, LocalSettingsKind,
  25    LocalSettingsPath, RegisterSetting, Settings, SettingsLocation, SettingsStore,
  26    parse_json_with_comments, watch_config_file,
  27};
  28use std::{cell::OnceCell, collections::BTreeMap, path::PathBuf, sync::Arc, time::Duration};
  29use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile};
  30use util::{ResultExt, rel_path::RelPath, serde::default_true};
  31use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
  32
  33use crate::{
  34    task_store::{TaskSettingsLocation, TaskStore},
  35    trusted_worktrees::{PathTrust, TrustedWorktrees, TrustedWorktreesEvent},
  36    worktree_store::{WorktreeStore, WorktreeStoreEvent},
  37};
  38
  39#[derive(Debug, Clone, RegisterSetting)]
  40pub struct ProjectSettings {
  41    /// Configuration for language servers.
  42    ///
  43    /// The following settings can be overridden for specific language servers:
  44    /// - initialization_options
  45    ///
  46    /// To override settings for a language, add an entry for that language server's
  47    /// name to the lsp value.
  48    /// Default: null
  49    // todo(settings-follow-up)
  50    // We should change to use a non content type (settings::LspSettings is a content type)
  51    // Note: Will either require merging with defaults, which also requires deciding where the defaults come from,
  52    //       or case by case deciding which fields are optional and which are actually required.
  53    pub lsp: HashMap<LanguageServerName, settings::LspSettings>,
  54
  55    /// Common language server settings.
  56    pub global_lsp_settings: GlobalLspSettings,
  57
  58    /// Configuration for Debugger-related features
  59    pub dap: HashMap<DebugAdapterName, DapSettings>,
  60
  61    /// Settings for context servers used for AI-related features.
  62    pub context_servers: HashMap<Arc<str>, ContextServerSettings>,
  63
  64    /// Default timeout for context server requests in seconds.
  65    pub context_server_timeout: u64,
  66
  67    /// Configuration for Diagnostics-related features.
  68    pub diagnostics: DiagnosticsSettings,
  69
  70    /// Configuration for Git-related features
  71    pub git: GitSettings,
  72
  73    /// Configuration for Node-related features
  74    pub node: NodeBinarySettings,
  75
  76    /// Configuration for how direnv configuration should be loaded
  77    pub load_direnv: DirenvSettings,
  78
  79    /// Configuration for session-related features
  80    pub session: SessionSettings,
  81}
  82
  83#[derive(Copy, Clone, Debug)]
  84pub struct SessionSettings {
  85    /// Whether or not to restore unsaved buffers on restart.
  86    ///
  87    /// If this is true, user won't be prompted whether to save/discard
  88    /// dirty files when closing the application.
  89    ///
  90    /// Default: true
  91    pub restore_unsaved_buffers: bool,
  92    /// Whether or not to skip worktree trust checks.
  93    /// When trusted, project settings are synchronized automatically,
  94    /// language and MCP servers are downloaded and started automatically.
  95    ///
  96    /// Default: false
  97    pub trust_all_worktrees: bool,
  98}
  99
 100#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
 101pub struct NodeBinarySettings {
 102    /// The path to the Node binary.
 103    pub path: Option<String>,
 104    /// The path to the npm binary Zed should use (defaults to `.path/../npm`).
 105    pub npm_path: Option<String>,
 106    /// If enabled, Zed will download its own copy of Node.
 107    pub ignore_system_version: bool,
 108}
 109
 110impl From<settings::NodeBinarySettings> for NodeBinarySettings {
 111    fn from(settings: settings::NodeBinarySettings) -> Self {
 112        Self {
 113            path: settings.path,
 114            npm_path: settings.npm_path,
 115            ignore_system_version: settings.ignore_system_version.unwrap_or(false),
 116        }
 117    }
 118}
 119
 120/// Common language server settings.
 121#[derive(Debug, Clone, PartialEq)]
 122pub struct GlobalLspSettings {
 123    /// Whether to show the LSP servers button in the status bar.
 124    ///
 125    /// Default: `true`
 126    pub button: bool,
 127    pub notifications: LspNotificationSettings,
 128}
 129
 130#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
 131#[serde(tag = "source", rename_all = "snake_case")]
 132pub struct LspNotificationSettings {
 133    /// Timeout in milliseconds for automatically dismissing language server notifications.
 134    /// Set to 0 to disable auto-dismiss.
 135    ///
 136    /// Default: 5000
 137    pub dismiss_timeout_ms: Option<u64>,
 138}
 139
 140#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
 141#[serde(tag = "source", rename_all = "snake_case")]
 142pub enum ContextServerSettings {
 143    Stdio {
 144        /// Whether the context server is enabled.
 145        #[serde(default = "default_true")]
 146        enabled: bool,
 147        /// If true, run this server on the remote server when using remote development.
 148        #[serde(default)]
 149        remote: bool,
 150        #[serde(flatten)]
 151        command: ContextServerCommand,
 152    },
 153    Http {
 154        /// Whether the context server is enabled.
 155        #[serde(default = "default_true")]
 156        enabled: bool,
 157        /// The URL of the remote context server.
 158        url: String,
 159        /// Optional authentication configuration for the remote server.
 160        #[serde(skip_serializing_if = "HashMap::is_empty", default)]
 161        headers: HashMap<String, String>,
 162        /// Timeout for tool calls in milliseconds.
 163        timeout: Option<u64>,
 164    },
 165    Extension {
 166        /// Whether the context server is enabled.
 167        #[serde(default = "default_true")]
 168        enabled: bool,
 169        /// If true, run this server on the remote server when using remote development.
 170        #[serde(default)]
 171        remote: bool,
 172        /// The settings for this context server specified by the extension.
 173        ///
 174        /// Consult the documentation for the context server to see what settings
 175        /// are supported.
 176        settings: serde_json::Value,
 177    },
 178}
 179
 180impl From<settings::ContextServerSettingsContent> for ContextServerSettings {
 181    fn from(value: settings::ContextServerSettingsContent) -> Self {
 182        match value {
 183            settings::ContextServerSettingsContent::Stdio {
 184                enabled,
 185                remote,
 186                command,
 187            } => ContextServerSettings::Stdio {
 188                enabled,
 189                remote,
 190                command,
 191            },
 192            settings::ContextServerSettingsContent::Extension {
 193                enabled,
 194                remote,
 195                settings,
 196            } => ContextServerSettings::Extension {
 197                enabled,
 198                remote,
 199                settings,
 200            },
 201            settings::ContextServerSettingsContent::Http {
 202                enabled,
 203                url,
 204                headers,
 205                timeout,
 206            } => ContextServerSettings::Http {
 207                enabled,
 208                url,
 209                headers,
 210                timeout,
 211            },
 212        }
 213    }
 214}
 215impl Into<settings::ContextServerSettingsContent> for ContextServerSettings {
 216    fn into(self) -> settings::ContextServerSettingsContent {
 217        match self {
 218            ContextServerSettings::Stdio {
 219                enabled,
 220                remote,
 221                command,
 222            } => settings::ContextServerSettingsContent::Stdio {
 223                enabled,
 224                remote,
 225                command,
 226            },
 227            ContextServerSettings::Extension {
 228                enabled,
 229                remote,
 230                settings,
 231            } => settings::ContextServerSettingsContent::Extension {
 232                enabled,
 233                remote,
 234                settings,
 235            },
 236            ContextServerSettings::Http {
 237                enabled,
 238                url,
 239                headers,
 240                timeout,
 241            } => settings::ContextServerSettingsContent::Http {
 242                enabled,
 243                url,
 244                headers,
 245                timeout,
 246            },
 247        }
 248    }
 249}
 250
 251impl ContextServerSettings {
 252    pub fn default_extension() -> Self {
 253        Self::Extension {
 254            enabled: true,
 255            remote: false,
 256            settings: serde_json::json!({}),
 257        }
 258    }
 259
 260    pub fn enabled(&self) -> bool {
 261        match self {
 262            ContextServerSettings::Stdio { enabled, .. } => *enabled,
 263            ContextServerSettings::Http { enabled, .. } => *enabled,
 264            ContextServerSettings::Extension { enabled, .. } => *enabled,
 265        }
 266    }
 267
 268    pub fn set_enabled(&mut self, enabled: bool) {
 269        match self {
 270            ContextServerSettings::Stdio { enabled: e, .. } => *e = enabled,
 271            ContextServerSettings::Http { enabled: e, .. } => *e = enabled,
 272            ContextServerSettings::Extension { enabled: e, .. } => *e = enabled,
 273        }
 274    }
 275}
 276
 277#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
 278pub enum DiagnosticSeverity {
 279    // No diagnostics are shown.
 280    Off,
 281    Error,
 282    Warning,
 283    Info,
 284    Hint,
 285}
 286
 287impl DiagnosticSeverity {
 288    pub fn into_lsp(self) -> Option<lsp::DiagnosticSeverity> {
 289        match self {
 290            DiagnosticSeverity::Off => None,
 291            DiagnosticSeverity::Error => Some(lsp::DiagnosticSeverity::ERROR),
 292            DiagnosticSeverity::Warning => Some(lsp::DiagnosticSeverity::WARNING),
 293            DiagnosticSeverity::Info => Some(lsp::DiagnosticSeverity::INFORMATION),
 294            DiagnosticSeverity::Hint => Some(lsp::DiagnosticSeverity::HINT),
 295        }
 296    }
 297}
 298
 299impl From<settings::DiagnosticSeverityContent> for DiagnosticSeverity {
 300    fn from(severity: settings::DiagnosticSeverityContent) -> Self {
 301        match severity {
 302            settings::DiagnosticSeverityContent::Off => DiagnosticSeverity::Off,
 303            settings::DiagnosticSeverityContent::Error => DiagnosticSeverity::Error,
 304            settings::DiagnosticSeverityContent::Warning => DiagnosticSeverity::Warning,
 305            settings::DiagnosticSeverityContent::Info => DiagnosticSeverity::Info,
 306            settings::DiagnosticSeverityContent::Hint => DiagnosticSeverity::Hint,
 307            settings::DiagnosticSeverityContent::All => DiagnosticSeverity::Hint,
 308        }
 309    }
 310}
 311
 312/// Determines the severity of the diagnostic that should be moved to.
 313#[derive(PartialEq, PartialOrd, Clone, Copy, Debug, Eq, Deserialize, JsonSchema)]
 314#[serde(rename_all = "snake_case")]
 315pub enum GoToDiagnosticSeverity {
 316    /// Errors
 317    Error = 3,
 318    /// Warnings
 319    Warning = 2,
 320    /// Information
 321    Information = 1,
 322    /// Hints
 323    Hint = 0,
 324}
 325
 326impl From<lsp::DiagnosticSeverity> for GoToDiagnosticSeverity {
 327    fn from(severity: lsp::DiagnosticSeverity) -> Self {
 328        match severity {
 329            lsp::DiagnosticSeverity::ERROR => Self::Error,
 330            lsp::DiagnosticSeverity::WARNING => Self::Warning,
 331            lsp::DiagnosticSeverity::INFORMATION => Self::Information,
 332            lsp::DiagnosticSeverity::HINT => Self::Hint,
 333            _ => Self::Error,
 334        }
 335    }
 336}
 337
 338impl GoToDiagnosticSeverity {
 339    pub fn min() -> Self {
 340        Self::Hint
 341    }
 342
 343    pub fn max() -> Self {
 344        Self::Error
 345    }
 346}
 347
 348/// Allows filtering diagnostics that should be moved to.
 349#[derive(PartialEq, Clone, Copy, Debug, Deserialize, JsonSchema)]
 350#[serde(untagged)]
 351pub enum GoToDiagnosticSeverityFilter {
 352    /// Move to diagnostics of a specific severity.
 353    Only(GoToDiagnosticSeverity),
 354
 355    /// Specify a range of severities to include.
 356    Range {
 357        /// Minimum severity to move to. Defaults no "error".
 358        #[serde(default = "GoToDiagnosticSeverity::min")]
 359        min: GoToDiagnosticSeverity,
 360        /// Maximum severity to move to. Defaults to "hint".
 361        #[serde(default = "GoToDiagnosticSeverity::max")]
 362        max: GoToDiagnosticSeverity,
 363    },
 364}
 365
 366impl Default for GoToDiagnosticSeverityFilter {
 367    fn default() -> Self {
 368        Self::Range {
 369            min: GoToDiagnosticSeverity::min(),
 370            max: GoToDiagnosticSeverity::max(),
 371        }
 372    }
 373}
 374
 375impl GoToDiagnosticSeverityFilter {
 376    pub fn matches(&self, severity: lsp::DiagnosticSeverity) -> bool {
 377        let severity: GoToDiagnosticSeverity = severity.into();
 378        match self {
 379            Self::Only(target) => *target == severity,
 380            Self::Range { min, max } => severity >= *min && severity <= *max,
 381        }
 382    }
 383}
 384
 385#[derive(Copy, Clone, Debug)]
 386pub struct GitSettings {
 387    /// Whether or not git integration is enabled.
 388    ///
 389    /// Default: true
 390    pub enabled: GitEnabledSettings,
 391    /// Whether or not to show the git gutter.
 392    ///
 393    /// Default: tracked_files
 394    pub git_gutter: settings::GitGutterSetting,
 395    /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
 396    ///
 397    /// Default: 0
 398    pub gutter_debounce: u64,
 399    /// Whether or not to show git blame data inline in
 400    /// the currently focused line.
 401    ///
 402    /// Default: on
 403    pub inline_blame: InlineBlameSettings,
 404    /// Git blame settings.
 405    pub blame: BlameSettings,
 406    /// Which information to show in the branch picker.
 407    ///
 408    /// Default: on
 409    pub branch_picker: BranchPickerSettings,
 410    /// How hunks are displayed visually in the editor.
 411    ///
 412    /// Default: staged_hollow
 413    pub hunk_style: settings::GitHunkStyleSetting,
 414    /// How file paths are displayed in the git gutter.
 415    ///
 416    /// Default: file_name_first
 417    pub path_style: GitPathStyle,
 418}
 419
 420#[derive(Clone, Copy, Debug)]
 421pub struct GitEnabledSettings {
 422    /// Whether git integration is enabled for showing git status.
 423    ///
 424    /// Default: true
 425    pub status: bool,
 426    /// Whether git integration is enabled for showing diffs.
 427    ///
 428    /// Default: true
 429    pub diff: bool,
 430}
 431
 432#[derive(Clone, Copy, Debug, PartialEq, Default)]
 433pub enum GitPathStyle {
 434    #[default]
 435    FileNameFirst,
 436    FilePathFirst,
 437}
 438
 439impl From<settings::GitPathStyle> for GitPathStyle {
 440    fn from(style: settings::GitPathStyle) -> Self {
 441        match style {
 442            settings::GitPathStyle::FileNameFirst => GitPathStyle::FileNameFirst,
 443            settings::GitPathStyle::FilePathFirst => GitPathStyle::FilePathFirst,
 444        }
 445    }
 446}
 447
 448#[derive(Clone, Copy, Debug)]
 449pub struct InlineBlameSettings {
 450    /// Whether or not to show git blame data inline in
 451    /// the currently focused line.
 452    ///
 453    /// Default: true
 454    pub enabled: bool,
 455    /// Whether to only show the inline blame information
 456    /// after a delay once the cursor stops moving.
 457    ///
 458    /// Default: 0
 459    pub delay_ms: settings::DelayMs,
 460    /// The amount of padding between the end of the source line and the start
 461    /// of the inline blame in units of columns.
 462    ///
 463    /// Default: 7
 464    pub padding: u32,
 465    /// The minimum column number to show the inline blame information at
 466    ///
 467    /// Default: 0
 468    pub min_column: u32,
 469    /// Whether to show commit summary as part of the inline blame.
 470    ///
 471    /// Default: false
 472    pub show_commit_summary: bool,
 473}
 474
 475#[derive(Clone, Copy, Debug)]
 476pub struct BlameSettings {
 477    /// Whether to show the avatar of the author of the commit.
 478    ///
 479    /// Default: true
 480    pub show_avatar: bool,
 481}
 482
 483impl GitSettings {
 484    pub fn inline_blame_delay(&self) -> Option<Duration> {
 485        if self.inline_blame.delay_ms.0 > 0 {
 486            Some(Duration::from_millis(self.inline_blame.delay_ms.0))
 487        } else {
 488            None
 489        }
 490    }
 491}
 492
 493#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
 494#[serde(rename_all = "snake_case")]
 495pub struct BranchPickerSettings {
 496    /// Whether to show author name as part of the commit information.
 497    ///
 498    /// Default: false
 499    #[serde(default)]
 500    pub show_author_name: bool,
 501}
 502
 503impl Default for BranchPickerSettings {
 504    fn default() -> Self {
 505        Self {
 506            show_author_name: true,
 507        }
 508    }
 509}
 510
 511#[derive(Clone, Debug)]
 512pub struct DiagnosticsSettings {
 513    /// Whether to show the project diagnostics button in the status bar.
 514    pub button: bool,
 515
 516    /// Whether or not to include warning diagnostics.
 517    pub include_warnings: bool,
 518
 519    /// Settings for using LSP pull diagnostics mechanism in Zed.
 520    pub lsp_pull_diagnostics: LspPullDiagnosticsSettings,
 521
 522    /// Settings for showing inline diagnostics.
 523    pub inline: InlineDiagnosticsSettings,
 524}
 525
 526#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 527pub struct InlineDiagnosticsSettings {
 528    /// Whether or not to show inline diagnostics
 529    ///
 530    /// Default: false
 531    pub enabled: bool,
 532    /// Whether to only show the inline diagnostics after a delay after the
 533    /// last editor event.
 534    ///
 535    /// Default: 150
 536    pub update_debounce_ms: u64,
 537    /// The amount of padding between the end of the source line and the start
 538    /// of the inline diagnostic in units of columns.
 539    ///
 540    /// Default: 4
 541    pub padding: u32,
 542    /// The minimum column to display inline diagnostics. This setting can be
 543    /// used to horizontally align inline diagnostics at some position. Lines
 544    /// longer than this value will still push diagnostics further to the right.
 545    ///
 546    /// Default: 0
 547    pub min_column: u32,
 548
 549    pub max_severity: Option<DiagnosticSeverity>,
 550}
 551
 552#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 553pub struct LspPullDiagnosticsSettings {
 554    /// Whether to pull for diagnostics or not.
 555    ///
 556    /// Default: true
 557    pub enabled: bool,
 558    /// Minimum time to wait before pulling diagnostics from the language server(s).
 559    /// 0 turns the debounce off.
 560    ///
 561    /// Default: 50
 562    pub debounce_ms: u64,
 563}
 564
 565impl Settings for ProjectSettings {
 566    fn from_settings(content: &settings::SettingsContent) -> Self {
 567        let project = &content.project.clone();
 568        let diagnostics = content.diagnostics.as_ref().unwrap();
 569        let lsp_pull_diagnostics = diagnostics.lsp_pull_diagnostics.as_ref().unwrap();
 570        let inline_diagnostics = diagnostics.inline.as_ref().unwrap();
 571
 572        let git = content.git.as_ref().unwrap();
 573        let git_enabled = {
 574            GitEnabledSettings {
 575                status: git.enabled.as_ref().unwrap().is_git_status_enabled(),
 576                diff: git.enabled.as_ref().unwrap().is_git_diff_enabled(),
 577            }
 578        };
 579        let git_settings = GitSettings {
 580            enabled: git_enabled,
 581            git_gutter: git.git_gutter.unwrap(),
 582            gutter_debounce: git.gutter_debounce.unwrap_or_default(),
 583            inline_blame: {
 584                let inline = git.inline_blame.unwrap();
 585                InlineBlameSettings {
 586                    enabled: inline.enabled.unwrap(),
 587                    delay_ms: inline.delay_ms.unwrap(),
 588                    padding: inline.padding.unwrap(),
 589                    min_column: inline.min_column.unwrap(),
 590                    show_commit_summary: inline.show_commit_summary.unwrap(),
 591                }
 592            },
 593            blame: {
 594                let blame = git.blame.unwrap();
 595                BlameSettings {
 596                    show_avatar: blame.show_avatar.unwrap(),
 597                }
 598            },
 599            branch_picker: {
 600                let branch_picker = git.branch_picker.unwrap();
 601                BranchPickerSettings {
 602                    show_author_name: branch_picker.show_author_name.unwrap(),
 603                }
 604            },
 605            hunk_style: git.hunk_style.unwrap(),
 606            path_style: git.path_style.unwrap().into(),
 607        };
 608        Self {
 609            context_servers: project
 610                .context_servers
 611                .clone()
 612                .into_iter()
 613                .map(|(key, value)| (key, value.into()))
 614                .collect(),
 615            context_server_timeout: project.context_server_timeout.unwrap_or(60),
 616            lsp: project
 617                .lsp
 618                .clone()
 619                .into_iter()
 620                .map(|(key, value)| (LanguageServerName(key.into()), value))
 621                .collect(),
 622            global_lsp_settings: GlobalLspSettings {
 623                button: content
 624                    .global_lsp_settings
 625                    .as_ref()
 626                    .unwrap()
 627                    .button
 628                    .unwrap(),
 629                notifications: LspNotificationSettings {
 630                    dismiss_timeout_ms: content
 631                        .global_lsp_settings
 632                        .as_ref()
 633                        .unwrap()
 634                        .notifications
 635                        .as_ref()
 636                        .unwrap()
 637                        .dismiss_timeout_ms,
 638                },
 639            },
 640            dap: project
 641                .dap
 642                .clone()
 643                .into_iter()
 644                .map(|(key, value)| (DebugAdapterName(key.into()), DapSettings::from(value)))
 645                .collect(),
 646            diagnostics: DiagnosticsSettings {
 647                button: diagnostics.button.unwrap(),
 648                include_warnings: diagnostics.include_warnings.unwrap(),
 649                lsp_pull_diagnostics: LspPullDiagnosticsSettings {
 650                    enabled: lsp_pull_diagnostics.enabled.unwrap(),
 651                    debounce_ms: lsp_pull_diagnostics.debounce_ms.unwrap().0,
 652                },
 653                inline: InlineDiagnosticsSettings {
 654                    enabled: inline_diagnostics.enabled.unwrap(),
 655                    update_debounce_ms: inline_diagnostics.update_debounce_ms.unwrap().0,
 656                    padding: inline_diagnostics.padding.unwrap(),
 657                    min_column: inline_diagnostics.min_column.unwrap(),
 658                    max_severity: inline_diagnostics.max_severity.map(Into::into),
 659                },
 660            },
 661            git: git_settings,
 662            node: content.node.clone().unwrap().into(),
 663            load_direnv: project.load_direnv.clone().unwrap(),
 664            session: SessionSettings {
 665                restore_unsaved_buffers: content.session.unwrap().restore_unsaved_buffers.unwrap(),
 666                trust_all_worktrees: content.session.unwrap().trust_all_worktrees.unwrap(),
 667            },
 668        }
 669    }
 670}
 671
 672pub enum SettingsObserverMode {
 673    Local(Arc<dyn Fs>),
 674    Remote { via_collab: bool },
 675}
 676
 677#[derive(Clone, Debug, PartialEq)]
 678pub enum SettingsObserverEvent {
 679    LocalSettingsUpdated(Result<PathBuf, InvalidSettingsError>),
 680    LocalTasksUpdated(Result<PathBuf, InvalidSettingsError>),
 681    LocalDebugScenariosUpdated(Result<PathBuf, InvalidSettingsError>),
 682}
 683
 684impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
 685
 686pub struct SettingsObserver {
 687    mode: SettingsObserverMode,
 688    downstream_client: Option<AnyProtoClient>,
 689    worktree_store: Entity<WorktreeStore>,
 690    project_id: u64,
 691    task_store: Entity<TaskStore>,
 692    pending_local_settings:
 693        HashMap<PathTrust, BTreeMap<(WorktreeId, Arc<RelPath>), Option<String>>>,
 694    _trusted_worktrees_watcher: Option<Subscription>,
 695    _user_settings_watcher: Option<Subscription>,
 696    _editorconfig_watcher: Option<Subscription>,
 697    _global_task_config_watcher: Task<()>,
 698    _global_debug_config_watcher: Task<()>,
 699}
 700
 701/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
 702/// (or the equivalent protobuf messages from upstream) and updates local settings
 703/// and sends notifications downstream.
 704/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
 705/// upstream.
 706impl SettingsObserver {
 707    pub fn init(client: &AnyProtoClient) {
 708        client.add_entity_message_handler(Self::handle_update_worktree_settings);
 709        client.add_entity_message_handler(Self::handle_update_user_settings);
 710    }
 711
 712    pub fn new_local(
 713        fs: Arc<dyn Fs>,
 714        worktree_store: Entity<WorktreeStore>,
 715        task_store: Entity<TaskStore>,
 716        watch_global_configs: bool,
 717        cx: &mut Context<Self>,
 718    ) -> Self {
 719        cx.subscribe(&worktree_store, Self::on_worktree_store_event)
 720            .detach();
 721
 722        let _trusted_worktrees_watcher =
 723            TrustedWorktrees::try_get_global(cx).map(|trusted_worktrees| {
 724                cx.subscribe(
 725                    &trusted_worktrees,
 726                    move |settings_observer, _, e, cx| match e {
 727                        TrustedWorktreesEvent::Trusted(_, trusted_paths) => {
 728                            for trusted_path in trusted_paths {
 729                                if let Some(pending_local_settings) = settings_observer
 730                                    .pending_local_settings
 731                                    .remove(trusted_path)
 732                                {
 733                                    for ((worktree_id, directory_path), settings_contents) in
 734                                        pending_local_settings
 735                                    {
 736                                        let path =
 737                                            LocalSettingsPath::InWorktree(directory_path.clone());
 738                                        apply_local_settings(
 739                                            worktree_id,
 740                                            path.clone(),
 741                                            LocalSettingsKind::Settings,
 742                                            &settings_contents,
 743                                            cx,
 744                                        );
 745                                        if let Some(downstream_client) =
 746                                            &settings_observer.downstream_client
 747                                        {
 748                                            downstream_client
 749                                                .send(proto::UpdateWorktreeSettings {
 750                                                    project_id: settings_observer.project_id,
 751                                                    worktree_id: worktree_id.to_proto(),
 752                                                    path: path.to_proto(),
 753                                                    content: settings_contents,
 754                                                    kind: Some(
 755                                                        local_settings_kind_to_proto(
 756                                                            LocalSettingsKind::Settings,
 757                                                        )
 758                                                        .into(),
 759                                                    ),
 760                                                    outside_worktree: Some(false),
 761                                                })
 762                                                .log_err();
 763                                        }
 764                                    }
 765                                }
 766                            }
 767                        }
 768                        TrustedWorktreesEvent::Restricted(..) => {}
 769                    },
 770                )
 771            });
 772
 773        let editorconfig_store = cx.global::<SettingsStore>().editorconfig_store.clone();
 774        let _editorconfig_watcher = cx.subscribe(
 775            &editorconfig_store,
 776            |this, _, event: &EditorconfigEvent, cx| {
 777                let EditorconfigEvent::ExternalConfigChanged {
 778                    path,
 779                    content,
 780                    affected_worktree_ids,
 781                } = event;
 782                for worktree_id in affected_worktree_ids {
 783                    if let Some(worktree) = this
 784                        .worktree_store
 785                        .read(cx)
 786                        .worktree_for_id(*worktree_id, cx)
 787                    {
 788                        this.update_settings(
 789                            worktree,
 790                            [(
 791                                path.clone(),
 792                                LocalSettingsKind::Editorconfig,
 793                                content.clone(),
 794                            )],
 795                            false,
 796                            cx,
 797                        );
 798                    }
 799                }
 800            },
 801        );
 802
 803        Self {
 804            worktree_store,
 805            task_store,
 806            mode: SettingsObserverMode::Local(fs.clone()),
 807            downstream_client: None,
 808            _trusted_worktrees_watcher,
 809            pending_local_settings: HashMap::default(),
 810            _user_settings_watcher: None,
 811            _editorconfig_watcher: Some(_editorconfig_watcher),
 812            project_id: REMOTE_SERVER_PROJECT_ID,
 813            _global_task_config_watcher: if watch_global_configs {
 814                Self::subscribe_to_global_task_file_changes(
 815                    fs.clone(),
 816                    paths::tasks_file().clone(),
 817                    cx,
 818                )
 819            } else {
 820                Task::ready(())
 821            },
 822            _global_debug_config_watcher: if watch_global_configs {
 823                Self::subscribe_to_global_debug_scenarios_changes(
 824                    fs.clone(),
 825                    paths::debug_scenarios_file().clone(),
 826                    cx,
 827                )
 828            } else {
 829                Task::ready(())
 830            },
 831        }
 832    }
 833
 834    pub fn new_remote(
 835        fs: Arc<dyn Fs>,
 836        worktree_store: Entity<WorktreeStore>,
 837        task_store: Entity<TaskStore>,
 838        upstream_client: Option<AnyProtoClient>,
 839        via_collab: bool,
 840        cx: &mut Context<Self>,
 841    ) -> Self {
 842        let mut user_settings_watcher = None;
 843        if cx.try_global::<SettingsStore>().is_some() {
 844            if let Some(upstream_client) = upstream_client {
 845                let mut user_settings = None;
 846                user_settings_watcher = Some(cx.observe_global::<SettingsStore>(move |_, cx| {
 847                    if let Some(new_settings) = cx.global::<SettingsStore>().raw_user_settings() {
 848                        if Some(new_settings) != user_settings.as_ref() {
 849                            if let Some(new_settings_string) =
 850                                serde_json::to_string(new_settings).ok()
 851                            {
 852                                user_settings = Some(new_settings.clone());
 853                                upstream_client
 854                                    .send(proto::UpdateUserSettings {
 855                                        project_id: REMOTE_SERVER_PROJECT_ID,
 856                                        contents: new_settings_string,
 857                                    })
 858                                    .log_err();
 859                            }
 860                        }
 861                    }
 862                }));
 863            }
 864        };
 865
 866        Self {
 867            worktree_store,
 868            task_store,
 869            mode: SettingsObserverMode::Remote { via_collab },
 870            downstream_client: None,
 871            project_id: REMOTE_SERVER_PROJECT_ID,
 872            _trusted_worktrees_watcher: None,
 873            pending_local_settings: HashMap::default(),
 874            _user_settings_watcher: user_settings_watcher,
 875            _editorconfig_watcher: None,
 876            _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
 877                fs.clone(),
 878                paths::tasks_file().clone(),
 879                cx,
 880            ),
 881            _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
 882                fs.clone(),
 883                paths::debug_scenarios_file().clone(),
 884                cx,
 885            ),
 886        }
 887    }
 888
 889    pub fn shared(
 890        &mut self,
 891        project_id: u64,
 892        downstream_client: AnyProtoClient,
 893        cx: &mut Context<Self>,
 894    ) {
 895        self.project_id = project_id;
 896        self.downstream_client = Some(downstream_client.clone());
 897
 898        let store = cx.global::<SettingsStore>();
 899        for worktree in self.worktree_store.read(cx).worktrees() {
 900            let worktree_id = worktree.read(cx).id().to_proto();
 901            for (path, content) in store.local_settings(worktree.read(cx).id()) {
 902                let content = serde_json::to_string(&content).unwrap();
 903                downstream_client
 904                    .send(proto::UpdateWorktreeSettings {
 905                        project_id,
 906                        worktree_id,
 907                        path: path.to_proto(),
 908                        content: Some(content),
 909                        kind: Some(
 910                            local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
 911                        ),
 912                        outside_worktree: Some(false),
 913                    })
 914                    .log_err();
 915            }
 916            for (path, content, _) in store
 917                .editorconfig_store
 918                .read(cx)
 919                .local_editorconfig_settings(worktree.read(cx).id())
 920            {
 921                downstream_client
 922                    .send(proto::UpdateWorktreeSettings {
 923                        project_id,
 924                        worktree_id,
 925                        path: path.to_proto(),
 926                        content: Some(content.to_owned()),
 927                        kind: Some(
 928                            local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
 929                        ),
 930                        outside_worktree: Some(path.is_outside_worktree()),
 931                    })
 932                    .log_err();
 933            }
 934        }
 935    }
 936
 937    pub fn unshared(&mut self, _: &mut Context<Self>) {
 938        self.downstream_client = None;
 939    }
 940
 941    async fn handle_update_worktree_settings(
 942        this: Entity<Self>,
 943        envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
 944        mut cx: AsyncApp,
 945    ) -> anyhow::Result<()> {
 946        let kind = match envelope.payload.kind {
 947            Some(kind) => proto::LocalSettingsKind::from_i32(kind)
 948                .with_context(|| format!("unknown kind {kind}"))?,
 949            None => proto::LocalSettingsKind::Settings,
 950        };
 951
 952        let path = LocalSettingsPath::from_proto(
 953            &envelope.payload.path,
 954            envelope.payload.outside_worktree.unwrap_or(false),
 955        )?;
 956
 957        this.update(&mut cx, |this, cx| {
 958            let is_via_collab = match &this.mode {
 959                SettingsObserverMode::Local(..) => false,
 960                SettingsObserverMode::Remote { via_collab } => *via_collab,
 961            };
 962            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 963            let Some(worktree) = this
 964                .worktree_store
 965                .read(cx)
 966                .worktree_for_id(worktree_id, cx)
 967            else {
 968                return;
 969            };
 970
 971            this.update_settings(
 972                worktree,
 973                [(
 974                    path,
 975                    local_settings_kind_from_proto(kind),
 976                    envelope.payload.content,
 977                )],
 978                is_via_collab,
 979                cx,
 980            );
 981        });
 982        Ok(())
 983    }
 984
 985    async fn handle_update_user_settings(
 986        _: Entity<Self>,
 987        envelope: TypedEnvelope<proto::UpdateUserSettings>,
 988        cx: AsyncApp,
 989    ) -> anyhow::Result<()> {
 990        cx.update_global(|settings_store: &mut SettingsStore, cx| {
 991            settings_store
 992                .set_user_settings(&envelope.payload.contents, cx)
 993                .result()
 994                .context("setting new user settings")?;
 995            anyhow::Ok(())
 996        })?;
 997        Ok(())
 998    }
 999
1000    fn on_worktree_store_event(
1001        &mut self,
1002        _: Entity<WorktreeStore>,
1003        event: &WorktreeStoreEvent,
1004        cx: &mut Context<Self>,
1005    ) {
1006        match event {
1007            WorktreeStoreEvent::WorktreeAdded(worktree) => cx
1008                .subscribe(worktree, |this, worktree, event, cx| {
1009                    if let worktree::Event::UpdatedEntries(changes) = event {
1010                        this.update_local_worktree_settings(&worktree, changes, cx)
1011                    }
1012                })
1013                .detach(),
1014            WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => {
1015                cx.update_global::<SettingsStore, _>(|store, cx| {
1016                    store.clear_local_settings(*worktree_id, cx).log_err();
1017                });
1018            }
1019            _ => {}
1020        }
1021    }
1022
1023    fn update_local_worktree_settings(
1024        &mut self,
1025        worktree: &Entity<Worktree>,
1026        changes: &UpdatedEntriesSet,
1027        cx: &mut Context<Self>,
1028    ) {
1029        let SettingsObserverMode::Local(fs) = &self.mode else {
1030            return;
1031        };
1032
1033        let mut settings_contents = Vec::new();
1034        for (path, _, change) in changes.iter() {
1035            let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
1036                let settings_dir = path
1037                    .ancestors()
1038                    .nth(local_settings_file_relative_path().components().count())
1039                    .unwrap()
1040                    .into();
1041                (settings_dir, LocalSettingsKind::Settings)
1042            } else if path.ends_with(local_tasks_file_relative_path()) {
1043                let settings_dir = path
1044                    .ancestors()
1045                    .nth(
1046                        local_tasks_file_relative_path()
1047                            .components()
1048                            .count()
1049                            .saturating_sub(1),
1050                    )
1051                    .unwrap()
1052                    .into();
1053                (settings_dir, LocalSettingsKind::Tasks)
1054            } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
1055                let settings_dir = path
1056                    .ancestors()
1057                    .nth(
1058                        local_vscode_tasks_file_relative_path()
1059                            .components()
1060                            .count()
1061                            .saturating_sub(1),
1062                    )
1063                    .unwrap()
1064                    .into();
1065                (settings_dir, LocalSettingsKind::Tasks)
1066            } else if path.ends_with(local_debug_file_relative_path()) {
1067                let settings_dir = path
1068                    .ancestors()
1069                    .nth(
1070                        local_debug_file_relative_path()
1071                            .components()
1072                            .count()
1073                            .saturating_sub(1),
1074                    )
1075                    .unwrap()
1076                    .into();
1077                (settings_dir, LocalSettingsKind::Debug)
1078            } else if path.ends_with(local_vscode_launch_file_relative_path()) {
1079                let settings_dir = path
1080                    .ancestors()
1081                    .nth(
1082                        local_vscode_tasks_file_relative_path()
1083                            .components()
1084                            .count()
1085                            .saturating_sub(1),
1086                    )
1087                    .unwrap()
1088                    .into();
1089                (settings_dir, LocalSettingsKind::Debug)
1090            } else if path.ends_with(RelPath::unix(EDITORCONFIG_NAME).unwrap()) {
1091                let Some(settings_dir) = path.parent().map(Arc::from) else {
1092                    continue;
1093                };
1094                if matches!(change, PathChange::Loaded) || matches!(change, PathChange::Added) {
1095                    let worktree_id = worktree.read(cx).id();
1096                    let worktree_path = worktree.read(cx).abs_path();
1097                    let fs = fs.clone();
1098                    cx.update_global::<SettingsStore, _>(|store, cx| {
1099                        store
1100                            .editorconfig_store
1101                            .update(cx, |editorconfig_store, cx| {
1102                                editorconfig_store.discover_local_external_configs_chain(
1103                                    worktree_id,
1104                                    worktree_path,
1105                                    fs,
1106                                    cx,
1107                                );
1108                            });
1109                    });
1110                }
1111                (settings_dir, LocalSettingsKind::Editorconfig)
1112            } else {
1113                continue;
1114            };
1115
1116            let removed = change == &PathChange::Removed;
1117            let fs = fs.clone();
1118            let abs_path = worktree.read(cx).absolutize(path);
1119            settings_contents.push(async move {
1120                (
1121                    settings_dir,
1122                    kind,
1123                    if removed {
1124                        None
1125                    } else {
1126                        Some(
1127                            async move {
1128                                let content = fs.load(&abs_path).await?;
1129                                if abs_path.ends_with(local_vscode_tasks_file_relative_path().as_std_path()) {
1130                                    let vscode_tasks =
1131                                        parse_json_with_comments::<VsCodeTaskFile>(&content)
1132                                            .with_context(|| {
1133                                                format!("parsing VSCode tasks, file {abs_path:?}")
1134                                            })?;
1135                                    let zed_tasks = TaskTemplates::try_from(vscode_tasks)
1136                                        .with_context(|| {
1137                                            format!(
1138                                        "converting VSCode tasks into Zed ones, file {abs_path:?}"
1139                                    )
1140                                        })?;
1141                                    serde_json::to_string(&zed_tasks).with_context(|| {
1142                                        format!(
1143                                            "serializing Zed tasks into JSON, file {abs_path:?}"
1144                                        )
1145                                    })
1146                                } else if abs_path.ends_with(local_vscode_launch_file_relative_path().as_std_path()) {
1147                                    let vscode_tasks =
1148                                        parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
1149                                            .with_context(|| {
1150                                                format!("parsing VSCode debug tasks, file {abs_path:?}")
1151                                            })?;
1152                                    let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
1153                                        .with_context(|| {
1154                                            format!(
1155                                        "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
1156                                    )
1157                                        })?;
1158                                    serde_json::to_string(&zed_tasks).with_context(|| {
1159                                        format!(
1160                                            "serializing Zed tasks into JSON, file {abs_path:?}"
1161                                        )
1162                                    })
1163                                } else {
1164                                    Ok(content)
1165                                }
1166                            }
1167                            .await,
1168                        )
1169                    },
1170                )
1171            });
1172        }
1173
1174        if settings_contents.is_empty() {
1175            return;
1176        }
1177
1178        let worktree = worktree.clone();
1179        cx.spawn(async move |this, cx| {
1180            let settings_contents: Vec<(Arc<RelPath>, _, _)> =
1181                futures::future::join_all(settings_contents).await;
1182            cx.update(|cx| {
1183                this.update(cx, |this, cx| {
1184                    this.update_settings(
1185                        worktree,
1186                        settings_contents.into_iter().map(|(path, kind, content)| {
1187                            (
1188                                LocalSettingsPath::InWorktree(path),
1189                                kind,
1190                                content.and_then(|c| c.log_err()),
1191                            )
1192                        }),
1193                        false,
1194                        cx,
1195                    )
1196                })
1197            })
1198        })
1199        .detach();
1200    }
1201
1202    fn update_settings(
1203        &mut self,
1204        worktree: Entity<Worktree>,
1205        settings_contents: impl IntoIterator<
1206            Item = (LocalSettingsPath, LocalSettingsKind, Option<String>),
1207        >,
1208        is_via_collab: bool,
1209        cx: &mut Context<Self>,
1210    ) {
1211        let worktree_id = worktree.read(cx).id();
1212        let remote_worktree_id = worktree.read(cx).id();
1213        let task_store = self.task_store.clone();
1214        let can_trust_worktree = if is_via_collab {
1215            OnceCell::from(true)
1216        } else {
1217            OnceCell::new()
1218        };
1219        for (directory_path, kind, file_content) in settings_contents {
1220            let mut applied = true;
1221            match (&directory_path, kind) {
1222                (LocalSettingsPath::InWorktree(directory), LocalSettingsKind::Settings) => {
1223                    if *can_trust_worktree.get_or_init(|| {
1224                        if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1225                            trusted_worktrees.update(cx, |trusted_worktrees, cx| {
1226                                trusted_worktrees.can_trust(&self.worktree_store, worktree_id, cx)
1227                            })
1228                        } else {
1229                            true
1230                        }
1231                    }) {
1232                        apply_local_settings(
1233                            worktree_id,
1234                            LocalSettingsPath::InWorktree(directory.clone()),
1235                            kind,
1236                            &file_content,
1237                            cx,
1238                        )
1239                    } else {
1240                        applied = false;
1241                        self.pending_local_settings
1242                            .entry(PathTrust::Worktree(worktree_id))
1243                            .or_default()
1244                            .insert((worktree_id, directory.clone()), file_content.clone());
1245                    }
1246                }
1247                (LocalSettingsPath::InWorktree(directory), LocalSettingsKind::Tasks) => {
1248                    let result = task_store.update(cx, |task_store, cx| {
1249                        task_store.update_user_tasks(
1250                            TaskSettingsLocation::Worktree(SettingsLocation {
1251                                worktree_id,
1252                                path: directory.as_ref(),
1253                            }),
1254                            file_content.as_deref(),
1255                            cx,
1256                        )
1257                    });
1258
1259                    match result {
1260                        Err(InvalidSettingsError::Tasks { path, message }) => {
1261                            log::error!("Failed to set local tasks in {path:?}: {message:?}");
1262                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1263                                InvalidSettingsError::Tasks { path, message },
1264                            )));
1265                        }
1266                        Err(e) => {
1267                            log::error!("Failed to set local tasks: {e}");
1268                        }
1269                        Ok(()) => {
1270                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(directory
1271                                .as_std_path()
1272                                .join(task_file_name()))));
1273                        }
1274                    }
1275                }
1276                (LocalSettingsPath::InWorktree(directory), LocalSettingsKind::Debug) => {
1277                    let result = task_store.update(cx, |task_store, cx| {
1278                        task_store.update_user_debug_scenarios(
1279                            TaskSettingsLocation::Worktree(SettingsLocation {
1280                                worktree_id,
1281                                path: directory.as_ref(),
1282                            }),
1283                            file_content.as_deref(),
1284                            cx,
1285                        )
1286                    });
1287
1288                    match result {
1289                        Err(InvalidSettingsError::Debug { path, message }) => {
1290                            log::error!(
1291                                "Failed to set local debug scenarios in {path:?}: {message:?}"
1292                            );
1293                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1294                                InvalidSettingsError::Debug { path, message },
1295                            )));
1296                        }
1297                        Err(e) => {
1298                            log::error!("Failed to set local tasks: {e}");
1299                        }
1300                        Ok(()) => {
1301                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(directory
1302                                .as_std_path()
1303                                .join(task_file_name()))));
1304                        }
1305                    }
1306                }
1307                (directory, LocalSettingsKind::Editorconfig) => {
1308                    apply_local_settings(worktree_id, directory.clone(), kind, &file_content, cx);
1309                }
1310                (LocalSettingsPath::OutsideWorktree(path), kind) => {
1311                    log::error!(
1312                        "OutsideWorktree path {:?} with kind {:?} is only supported by editorconfig",
1313                        path,
1314                        kind
1315                    );
1316                    continue;
1317                }
1318            };
1319
1320            if applied {
1321                if let Some(downstream_client) = &self.downstream_client {
1322                    downstream_client
1323                        .send(proto::UpdateWorktreeSettings {
1324                            project_id: self.project_id,
1325                            worktree_id: remote_worktree_id.to_proto(),
1326                            path: directory_path.to_proto(),
1327                            content: file_content.clone(),
1328                            kind: Some(local_settings_kind_to_proto(kind).into()),
1329                            outside_worktree: Some(directory_path.is_outside_worktree()),
1330                        })
1331                        .log_err();
1332                }
1333            }
1334        }
1335    }
1336
1337    fn subscribe_to_global_task_file_changes(
1338        fs: Arc<dyn Fs>,
1339        file_path: PathBuf,
1340        cx: &mut Context<Self>,
1341    ) -> Task<()> {
1342        let (mut user_tasks_file_rx, watcher_task) =
1343            watch_config_file(cx.background_executor(), fs, file_path.clone());
1344        let user_tasks_content = cx.foreground_executor().block_on(user_tasks_file_rx.next());
1345        let weak_entry = cx.weak_entity();
1346        cx.spawn(async move |settings_observer, cx| {
1347            let _watcher_task = watcher_task;
1348            let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1349                settings_observer.task_store.clone()
1350            }) else {
1351                return;
1352            };
1353            if let Some(user_tasks_content) = user_tasks_content {
1354                task_store.update(cx, |task_store, cx| {
1355                    task_store
1356                        .update_user_tasks(
1357                            TaskSettingsLocation::Global(&file_path),
1358                            Some(&user_tasks_content),
1359                            cx,
1360                        )
1361                        .log_err();
1362                });
1363            }
1364            while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1365                let result = task_store.update(cx, |task_store, cx| {
1366                    task_store.update_user_tasks(
1367                        TaskSettingsLocation::Global(&file_path),
1368                        Some(&user_tasks_content),
1369                        cx,
1370                    )
1371                });
1372
1373                weak_entry
1374                    .update(cx, |_, cx| match result {
1375                        Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1376                            file_path.clone()
1377                        ))),
1378                        Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1379                            InvalidSettingsError::Tasks {
1380                                path: file_path.clone(),
1381                                message: err.to_string(),
1382                            },
1383                        ))),
1384                    })
1385                    .ok();
1386            }
1387        })
1388    }
1389    fn subscribe_to_global_debug_scenarios_changes(
1390        fs: Arc<dyn Fs>,
1391        file_path: PathBuf,
1392        cx: &mut Context<Self>,
1393    ) -> Task<()> {
1394        let (mut user_tasks_file_rx, watcher_task) =
1395            watch_config_file(cx.background_executor(), fs, file_path.clone());
1396        let user_tasks_content = cx.foreground_executor().block_on(user_tasks_file_rx.next());
1397        let weak_entry = cx.weak_entity();
1398        cx.spawn(async move |settings_observer, cx| {
1399            let _watcher_task = watcher_task;
1400            let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1401                settings_observer.task_store.clone()
1402            }) else {
1403                return;
1404            };
1405            if let Some(user_tasks_content) = user_tasks_content {
1406                task_store.update(cx, |task_store, cx| {
1407                    task_store
1408                        .update_user_debug_scenarios(
1409                            TaskSettingsLocation::Global(&file_path),
1410                            Some(&user_tasks_content),
1411                            cx,
1412                        )
1413                        .log_err();
1414                });
1415            }
1416            while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1417                let result = task_store.update(cx, |task_store, cx| {
1418                    task_store.update_user_debug_scenarios(
1419                        TaskSettingsLocation::Global(&file_path),
1420                        Some(&user_tasks_content),
1421                        cx,
1422                    )
1423                });
1424
1425                weak_entry
1426                    .update(cx, |_, cx| match result {
1427                        Ok(()) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(Ok(
1428                            file_path.clone(),
1429                        ))),
1430                        Err(err) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(
1431                            Err(InvalidSettingsError::Tasks {
1432                                path: file_path.clone(),
1433                                message: err.to_string(),
1434                            }),
1435                        )),
1436                    })
1437                    .ok();
1438            }
1439        })
1440    }
1441}
1442
1443fn apply_local_settings(
1444    worktree_id: WorktreeId,
1445    path: LocalSettingsPath,
1446    kind: LocalSettingsKind,
1447    file_content: &Option<String>,
1448    cx: &mut Context<'_, SettingsObserver>,
1449) {
1450    cx.update_global::<SettingsStore, _>(|store, cx| {
1451        let result =
1452            store.set_local_settings(worktree_id, path.clone(), kind, file_content.as_deref(), cx);
1453
1454        match result {
1455            Err(InvalidSettingsError::LocalSettings { path, message }) => {
1456                log::error!("Failed to set local settings in {path:?}: {message}");
1457                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
1458                    InvalidSettingsError::LocalSettings { path, message },
1459                )));
1460            }
1461            Err(e) => log::error!("Failed to set local settings: {e}"),
1462            Ok(()) => {
1463                let settings_path = match &path {
1464                    LocalSettingsPath::InWorktree(rel_path) => rel_path
1465                        .as_std_path()
1466                        .join(local_settings_file_relative_path().as_std_path()),
1467                    LocalSettingsPath::OutsideWorktree(abs_path) => abs_path.to_path_buf(),
1468                };
1469                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(
1470                    settings_path,
1471                )))
1472            }
1473        }
1474    })
1475}
1476
1477pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
1478    match kind {
1479        proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
1480        proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
1481        proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
1482        proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
1483    }
1484}
1485
1486pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
1487    match kind {
1488        LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
1489        LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
1490        LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
1491        LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
1492    }
1493}
1494
1495#[derive(Debug, Clone)]
1496pub struct DapSettings {
1497    pub binary: DapBinary,
1498    pub args: Vec<String>,
1499    pub env: HashMap<String, String>,
1500}
1501
1502impl From<DapSettingsContent> for DapSettings {
1503    fn from(content: DapSettingsContent) -> Self {
1504        DapSettings {
1505            binary: content
1506                .binary
1507                .map_or_else(|| DapBinary::Default, |binary| DapBinary::Custom(binary)),
1508            args: content.args.unwrap_or_default(),
1509            env: content.env.unwrap_or_default(),
1510        }
1511    }
1512}
1513
1514#[derive(Debug, Clone)]
1515pub enum DapBinary {
1516    Default,
1517    Custom(String),
1518}