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