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