project_settings.rs

   1use anyhow::Context as _;
   2use collections::HashMap;
   3use context_server::ContextServerCommand;
   4use dap::adapters::DebugAdapterName;
   5use fs::Fs;
   6use futures::StreamExt as _;
   7use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Task};
   8use lsp::LanguageServerName;
   9use paths::{
  10    EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_file_relative_path,
  11    local_tasks_file_relative_path, local_vscode_launch_file_relative_path,
  12    local_vscode_tasks_file_relative_path, task_file_name,
  13};
  14use rpc::{
  15    AnyProtoClient, TypedEnvelope,
  16    proto::{self, FromProto, ToProto},
  17};
  18use schemars::JsonSchema;
  19use serde::{Deserialize, Serialize};
  20use settings::{
  21    InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources,
  22    SettingsStore, parse_json_with_comments, watch_config_file,
  23};
  24use std::{
  25    collections::BTreeMap,
  26    path::{Path, PathBuf},
  27    sync::Arc,
  28    time::Duration,
  29};
  30use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile};
  31use util::{ResultExt, serde::default_true};
  32use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
  33
  34use crate::{
  35    task_store::{TaskSettingsLocation, TaskStore},
  36    worktree_store::{WorktreeStore, WorktreeStoreEvent},
  37};
  38
  39#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
  40pub struct ProjectSettings {
  41    /// Configuration for language servers.
  42    ///
  43    /// The following settings can be overridden for specific language servers:
  44    /// - initialization_options
  45    ///
  46    /// To override settings for a language, add an entry for that language server's
  47    /// name to the lsp value.
  48    /// Default: null
  49    #[serde(default)]
  50    pub lsp: HashMap<LanguageServerName, LspSettings>,
  51
  52    /// Common language server settings.
  53    #[serde(default)]
  54    pub global_lsp_settings: GlobalLspSettings,
  55
  56    /// Configuration for Debugger-related features
  57    #[serde(default)]
  58    pub dap: HashMap<DebugAdapterName, DapSettings>,
  59
  60    /// Settings for context servers used for AI-related features.
  61    #[serde(default)]
  62    pub context_servers: HashMap<Arc<str>, ContextServerSettings>,
  63
  64    /// Configuration for Diagnostics-related features.
  65    #[serde(default)]
  66    pub diagnostics: DiagnosticsSettings,
  67
  68    /// Configuration for Git-related features
  69    #[serde(default)]
  70    pub git: GitSettings,
  71
  72    /// Configuration for Node-related features
  73    #[serde(default)]
  74    pub node: NodeBinarySettings,
  75
  76    /// Configuration for how direnv configuration should be loaded
  77    #[serde(default)]
  78    pub load_direnv: DirenvSettings,
  79
  80    /// Configuration for session-related features
  81    #[serde(default)]
  82    pub session: SessionSettings,
  83}
  84
  85#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
  86#[serde(rename_all = "snake_case")]
  87pub struct DapSettings {
  88    pub binary: Option<String>,
  89    #[serde(default)]
  90    pub args: Vec<String>,
  91}
  92
  93#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
  94#[serde(tag = "source", rename_all = "snake_case")]
  95pub enum ContextServerSettings {
  96    Custom {
  97        /// Whether the context server is enabled.
  98        #[serde(default = "default_true")]
  99        enabled: bool,
 100
 101        #[serde(flatten)]
 102        command: ContextServerCommand,
 103    },
 104    Extension {
 105        /// Whether the context server is enabled.
 106        #[serde(default = "default_true")]
 107        enabled: bool,
 108        /// The settings for this context server specified by the extension.
 109        ///
 110        /// Consult the documentation for the context server to see what settings
 111        /// are supported.
 112        settings: serde_json::Value,
 113    },
 114}
 115
 116/// Common language server settings.
 117#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
 118pub struct GlobalLspSettings {
 119    /// Whether to show the LSP servers button in the status bar.
 120    ///
 121    /// Default: `true`
 122    #[serde(default = "default_true")]
 123    pub button: bool,
 124}
 125
 126impl ContextServerSettings {
 127    pub fn default_extension() -> Self {
 128        Self::Extension {
 129            enabled: true,
 130            settings: serde_json::json!({}),
 131        }
 132    }
 133
 134    pub fn enabled(&self) -> bool {
 135        match self {
 136            ContextServerSettings::Custom { enabled, .. } => *enabled,
 137            ContextServerSettings::Extension { enabled, .. } => *enabled,
 138        }
 139    }
 140
 141    pub fn set_enabled(&mut self, enabled: bool) {
 142        match self {
 143            ContextServerSettings::Custom { enabled: e, .. } => *e = enabled,
 144            ContextServerSettings::Extension { enabled: e, .. } => *e = enabled,
 145        }
 146    }
 147}
 148
 149#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
 150pub struct NodeBinarySettings {
 151    /// The path to the Node binary.
 152    pub path: Option<String>,
 153    /// The path to the npm binary Zed should use (defaults to `.path/../npm`).
 154    pub npm_path: Option<String>,
 155    /// If enabled, Zed will download its own copy of Node.
 156    #[serde(default)]
 157    pub ignore_system_version: bool,
 158}
 159
 160#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 161#[serde(rename_all = "snake_case")]
 162pub enum DirenvSettings {
 163    /// Load direnv configuration through a shell hook
 164    ShellHook,
 165    /// Load direnv configuration directly using `direnv export json`
 166    #[default]
 167    Direct,
 168}
 169
 170#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
 171#[serde(default)]
 172pub struct DiagnosticsSettings {
 173    /// Whether to show the project diagnostics button in the status bar.
 174    pub button: bool,
 175
 176    /// Whether or not to include warning diagnostics.
 177    pub include_warnings: bool,
 178
 179    /// Settings for using LSP pull diagnostics mechanism in Zed.
 180    pub lsp_pull_diagnostics: LspPullDiagnosticsSettings,
 181
 182    /// Settings for showing inline diagnostics.
 183    pub inline: InlineDiagnosticsSettings,
 184
 185    /// Configuration, related to Rust language diagnostics.
 186    pub cargo: Option<CargoDiagnosticsSettings>,
 187}
 188
 189impl DiagnosticsSettings {
 190    pub fn fetch_cargo_diagnostics(&self) -> bool {
 191        self.cargo
 192            .as_ref()
 193            .is_some_and(|cargo_diagnostics| cargo_diagnostics.fetch_cargo_diagnostics)
 194    }
 195}
 196
 197#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
 198#[serde(default)]
 199pub struct LspPullDiagnosticsSettings {
 200    /// Whether to pull for diagnostics or not.
 201    ///
 202    /// Default: true
 203    #[serde(default = "default_true")]
 204    pub enabled: bool,
 205    /// Minimum time to wait before pulling diagnostics from the language server(s).
 206    /// 0 turns the debounce off.
 207    ///
 208    /// Default: 50
 209    #[serde(default = "default_lsp_diagnostics_pull_debounce_ms")]
 210    pub debounce_ms: u64,
 211}
 212
 213fn default_lsp_diagnostics_pull_debounce_ms() -> u64 {
 214    50
 215}
 216
 217#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
 218#[serde(default)]
 219pub struct InlineDiagnosticsSettings {
 220    /// Whether or not to show inline diagnostics
 221    ///
 222    /// Default: false
 223    pub enabled: bool,
 224    /// Whether to only show the inline diagnostics after a delay after the
 225    /// last editor event.
 226    ///
 227    /// Default: 150
 228    #[serde(default = "default_inline_diagnostics_update_debounce_ms")]
 229    pub update_debounce_ms: u64,
 230    /// The amount of padding between the end of the source line and the start
 231    /// of the inline diagnostic in units of columns.
 232    ///
 233    /// Default: 4
 234    #[serde(default = "default_inline_diagnostics_padding")]
 235    pub padding: u32,
 236    /// The minimum column to display inline diagnostics. This setting can be
 237    /// used to horizontally align inline diagnostics at some position. Lines
 238    /// longer than this value will still push diagnostics further to the right.
 239    ///
 240    /// Default: 0
 241    pub min_column: u32,
 242
 243    pub max_severity: Option<DiagnosticSeverity>,
 244}
 245
 246fn default_inline_diagnostics_update_debounce_ms() -> u64 {
 247    150
 248}
 249
 250fn default_inline_diagnostics_padding() -> u32 {
 251    4
 252}
 253
 254impl Default for DiagnosticsSettings {
 255    fn default() -> Self {
 256        Self {
 257            button: true,
 258            include_warnings: true,
 259            lsp_pull_diagnostics: LspPullDiagnosticsSettings::default(),
 260            inline: InlineDiagnosticsSettings::default(),
 261            cargo: None,
 262        }
 263    }
 264}
 265
 266impl Default for LspPullDiagnosticsSettings {
 267    fn default() -> Self {
 268        Self {
 269            enabled: true,
 270            debounce_ms: default_lsp_diagnostics_pull_debounce_ms(),
 271        }
 272    }
 273}
 274
 275impl Default for InlineDiagnosticsSettings {
 276    fn default() -> Self {
 277        Self {
 278            enabled: false,
 279            update_debounce_ms: default_inline_diagnostics_update_debounce_ms(),
 280            padding: default_inline_diagnostics_padding(),
 281            min_column: 0,
 282            max_severity: None,
 283        }
 284    }
 285}
 286
 287impl Default for GlobalLspSettings {
 288    fn default() -> Self {
 289        Self {
 290            button: default_true(),
 291        }
 292    }
 293}
 294
 295#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 296pub struct CargoDiagnosticsSettings {
 297    /// When enabled, Zed disables rust-analyzer's check on save and starts to query
 298    /// Cargo diagnostics separately.
 299    ///
 300    /// Default: false
 301    #[serde(default)]
 302    pub fetch_cargo_diagnostics: bool,
 303}
 304
 305#[derive(
 306    Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema,
 307)]
 308#[serde(rename_all = "snake_case")]
 309pub enum DiagnosticSeverity {
 310    // No diagnostics are shown.
 311    Off,
 312    Error,
 313    Warning,
 314    Info,
 315    Hint,
 316}
 317
 318impl DiagnosticSeverity {
 319    pub fn into_lsp(self) -> Option<lsp::DiagnosticSeverity> {
 320        match self {
 321            DiagnosticSeverity::Off => None,
 322            DiagnosticSeverity::Error => Some(lsp::DiagnosticSeverity::ERROR),
 323            DiagnosticSeverity::Warning => Some(lsp::DiagnosticSeverity::WARNING),
 324            DiagnosticSeverity::Info => Some(lsp::DiagnosticSeverity::INFORMATION),
 325            DiagnosticSeverity::Hint => Some(lsp::DiagnosticSeverity::HINT),
 326        }
 327    }
 328}
 329
 330/// Determines the severity of the diagnostic that should be moved to.
 331#[derive(PartialEq, PartialOrd, Clone, Copy, Debug, Eq, Deserialize, JsonSchema)]
 332#[serde(rename_all = "snake_case")]
 333pub enum GoToDiagnosticSeverity {
 334    /// Errors
 335    Error = 3,
 336    /// Warnings
 337    Warning = 2,
 338    /// Information
 339    Information = 1,
 340    /// Hints
 341    Hint = 0,
 342}
 343
 344impl From<lsp::DiagnosticSeverity> for GoToDiagnosticSeverity {
 345    fn from(severity: lsp::DiagnosticSeverity) -> Self {
 346        match severity {
 347            lsp::DiagnosticSeverity::ERROR => Self::Error,
 348            lsp::DiagnosticSeverity::WARNING => Self::Warning,
 349            lsp::DiagnosticSeverity::INFORMATION => Self::Information,
 350            lsp::DiagnosticSeverity::HINT => Self::Hint,
 351            _ => Self::Error,
 352        }
 353    }
 354}
 355
 356impl GoToDiagnosticSeverity {
 357    pub fn min() -> Self {
 358        Self::Hint
 359    }
 360
 361    pub fn max() -> Self {
 362        Self::Error
 363    }
 364}
 365
 366/// Allows filtering diagnostics that should be moved to.
 367#[derive(PartialEq, Clone, Copy, Debug, Deserialize, JsonSchema)]
 368#[serde(untagged)]
 369pub enum GoToDiagnosticSeverityFilter {
 370    /// Move to diagnostics of a specific severity.
 371    Only(GoToDiagnosticSeverity),
 372
 373    /// Specify a range of severities to include.
 374    Range {
 375        /// Minimum severity to move to. Defaults no "error".
 376        #[serde(default = "GoToDiagnosticSeverity::min")]
 377        min: GoToDiagnosticSeverity,
 378        /// Maximum severity to move to. Defaults to "hint".
 379        #[serde(default = "GoToDiagnosticSeverity::max")]
 380        max: GoToDiagnosticSeverity,
 381    },
 382}
 383
 384impl Default for GoToDiagnosticSeverityFilter {
 385    fn default() -> Self {
 386        Self::Range {
 387            min: GoToDiagnosticSeverity::min(),
 388            max: GoToDiagnosticSeverity::max(),
 389        }
 390    }
 391}
 392
 393impl GoToDiagnosticSeverityFilter {
 394    pub fn matches(&self, severity: lsp::DiagnosticSeverity) -> bool {
 395        let severity: GoToDiagnosticSeverity = severity.into();
 396        match self {
 397            Self::Only(target) => *target == severity,
 398            Self::Range { min, max } => severity >= *min && severity <= *max,
 399        }
 400    }
 401}
 402
 403#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 404pub struct GitSettings {
 405    /// Whether or not to show the git gutter.
 406    ///
 407    /// Default: tracked_files
 408    pub git_gutter: Option<GitGutterSetting>,
 409    /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
 410    ///
 411    /// Default: null
 412    pub gutter_debounce: Option<u64>,
 413    /// Whether or not to show git blame data inline in
 414    /// the currently focused line.
 415    ///
 416    /// Default: on
 417    pub inline_blame: Option<InlineBlameSettings>,
 418    /// How hunks are displayed visually in the editor.
 419    ///
 420    /// Default: staged_hollow
 421    pub hunk_style: Option<GitHunkStyleSetting>,
 422}
 423
 424impl GitSettings {
 425    pub fn inline_blame_enabled(&self) -> bool {
 426        #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
 427        match self.inline_blame {
 428            Some(InlineBlameSettings { enabled, .. }) => enabled,
 429            _ => false,
 430        }
 431    }
 432
 433    pub fn inline_blame_delay(&self) -> Option<Duration> {
 434        match self.inline_blame {
 435            Some(InlineBlameSettings { delay_ms, .. }) if delay_ms > 0 => {
 436                Some(Duration::from_millis(delay_ms))
 437            }
 438            _ => None,
 439        }
 440    }
 441
 442    pub fn show_inline_commit_summary(&self) -> bool {
 443        match self.inline_blame {
 444            Some(InlineBlameSettings {
 445                show_commit_summary,
 446                ..
 447            }) => show_commit_summary,
 448            _ => false,
 449        }
 450    }
 451}
 452
 453#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
 454#[serde(rename_all = "snake_case")]
 455pub enum GitHunkStyleSetting {
 456    /// Show unstaged hunks with a filled background and staged hunks hollow.
 457    #[default]
 458    StagedHollow,
 459    /// Show unstaged hunks hollow and staged hunks with a filled background.
 460    UnstagedHollow,
 461}
 462
 463#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
 464#[serde(rename_all = "snake_case")]
 465pub enum GitGutterSetting {
 466    /// Show git gutter in tracked files.
 467    #[default]
 468    TrackedFiles,
 469    /// Hide git gutter
 470    Hide,
 471}
 472
 473#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
 474#[serde(rename_all = "snake_case")]
 475pub struct InlineBlameSettings {
 476    /// Whether or not to show git blame data inline in
 477    /// the currently focused line.
 478    ///
 479    /// Default: true
 480    #[serde(default = "default_true")]
 481    pub enabled: bool,
 482    /// Whether to only show the inline blame information
 483    /// after a delay once the cursor stops moving.
 484    ///
 485    /// Default: 0
 486    #[serde(default)]
 487    pub delay_ms: u64,
 488    /// The amount of padding between the end of the source line and the start
 489    /// of the inline blame in units of columns.
 490    ///
 491    /// Default: 7
 492    #[serde(default = "default_inline_blame_padding")]
 493    pub padding: u32,
 494    /// The minimum column number to show the inline blame information at
 495    ///
 496    /// Default: 0
 497    #[serde(default)]
 498    pub min_column: u32,
 499    /// Whether to show commit summary as part of the inline blame.
 500    ///
 501    /// Default: false
 502    #[serde(default)]
 503    pub show_commit_summary: bool,
 504}
 505
 506fn default_inline_blame_padding() -> u32 {
 507    7
 508}
 509
 510impl Default for InlineBlameSettings {
 511    fn default() -> Self {
 512        Self {
 513            enabled: true,
 514            delay_ms: 0,
 515            padding: default_inline_blame_padding(),
 516            min_column: 0,
 517            show_commit_summary: false,
 518        }
 519    }
 520}
 521
 522#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
 523pub struct BinarySettings {
 524    pub path: Option<String>,
 525    pub arguments: Option<Vec<String>>,
 526    pub env: Option<BTreeMap<String, String>>,
 527    pub ignore_system_version: Option<bool>,
 528}
 529
 530#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
 531#[serde(rename_all = "snake_case")]
 532pub struct LspSettings {
 533    pub binary: Option<BinarySettings>,
 534    pub initialization_options: Option<serde_json::Value>,
 535    pub settings: Option<serde_json::Value>,
 536    /// If the server supports sending tasks over LSP extensions,
 537    /// this setting can be used to enable or disable them in Zed.
 538    /// Default: true
 539    #[serde(default = "default_true")]
 540    pub enable_lsp_tasks: bool,
 541}
 542
 543impl Default for LspSettings {
 544    fn default() -> Self {
 545        Self {
 546            binary: None,
 547            initialization_options: None,
 548            settings: None,
 549            enable_lsp_tasks: true,
 550        }
 551    }
 552}
 553
 554#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
 555pub struct SessionSettings {
 556    /// Whether or not to restore unsaved buffers on restart.
 557    ///
 558    /// If this is true, user won't be prompted whether to save/discard
 559    /// dirty files when closing the application.
 560    ///
 561    /// Default: true
 562    pub restore_unsaved_buffers: bool,
 563}
 564
 565impl Default for SessionSettings {
 566    fn default() -> Self {
 567        Self {
 568            restore_unsaved_buffers: true,
 569        }
 570    }
 571}
 572
 573impl Settings for ProjectSettings {
 574    const KEY: Option<&'static str> = None;
 575
 576    type FileContent = Self;
 577
 578    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
 579        sources.json_merge()
 580    }
 581
 582    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
 583        // this just sets the binary name instead of a full path so it relies on path lookup
 584        // resolving to the one you want
 585        vscode.enum_setting(
 586            "npm.packageManager",
 587            &mut current.node.npm_path,
 588            |s| match s {
 589                v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
 590                _ => None,
 591            },
 592        );
 593
 594        if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") {
 595            if let Some(blame) = current.git.inline_blame.as_mut() {
 596                blame.enabled = b
 597            } else {
 598                current.git.inline_blame = Some(InlineBlameSettings {
 599                    enabled: b,
 600                    ..Default::default()
 601                })
 602            }
 603        }
 604
 605        #[derive(Deserialize)]
 606        struct VsCodeContextServerCommand {
 607            command: PathBuf,
 608            args: Option<Vec<String>>,
 609            env: Option<HashMap<String, String>>,
 610            // note: we don't support envFile and type
 611        }
 612        impl From<VsCodeContextServerCommand> for ContextServerCommand {
 613            fn from(cmd: VsCodeContextServerCommand) -> Self {
 614                Self {
 615                    path: cmd.command,
 616                    args: cmd.args.unwrap_or_default(),
 617                    env: cmd.env,
 618                }
 619            }
 620        }
 621        if let Some(mcp) = vscode.read_value("mcp").and_then(|v| v.as_object()) {
 622            current
 623                .context_servers
 624                .extend(mcp.iter().filter_map(|(k, v)| {
 625                    Some((
 626                        k.clone().into(),
 627                        ContextServerSettings::Custom {
 628                            enabled: true,
 629                            command: serde_json::from_value::<VsCodeContextServerCommand>(
 630                                v.clone(),
 631                            )
 632                            .ok()?
 633                            .into(),
 634                        },
 635                    ))
 636                }));
 637        }
 638
 639        // TODO: translate lsp settings for rust-analyzer and other popular ones to old.lsp
 640    }
 641}
 642
 643pub enum SettingsObserverMode {
 644    Local(Arc<dyn Fs>),
 645    Remote,
 646}
 647
 648#[derive(Clone, Debug, PartialEq)]
 649pub enum SettingsObserverEvent {
 650    LocalSettingsUpdated(Result<PathBuf, InvalidSettingsError>),
 651    LocalTasksUpdated(Result<PathBuf, InvalidSettingsError>),
 652    LocalDebugScenariosUpdated(Result<PathBuf, InvalidSettingsError>),
 653}
 654
 655impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
 656
 657pub struct SettingsObserver {
 658    mode: SettingsObserverMode,
 659    downstream_client: Option<AnyProtoClient>,
 660    worktree_store: Entity<WorktreeStore>,
 661    project_id: u64,
 662    task_store: Entity<TaskStore>,
 663    _global_task_config_watcher: Task<()>,
 664    _global_debug_config_watcher: Task<()>,
 665}
 666
 667/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
 668/// (or the equivalent protobuf messages from upstream) and updates local settings
 669/// and sends notifications downstream.
 670/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
 671/// upstream.
 672impl SettingsObserver {
 673    pub fn init(client: &AnyProtoClient) {
 674        client.add_entity_message_handler(Self::handle_update_worktree_settings);
 675    }
 676
 677    pub fn new_local(
 678        fs: Arc<dyn Fs>,
 679        worktree_store: Entity<WorktreeStore>,
 680        task_store: Entity<TaskStore>,
 681        cx: &mut Context<Self>,
 682    ) -> Self {
 683        cx.subscribe(&worktree_store, Self::on_worktree_store_event)
 684            .detach();
 685
 686        Self {
 687            worktree_store,
 688            task_store,
 689            mode: SettingsObserverMode::Local(fs.clone()),
 690            downstream_client: None,
 691            project_id: 0,
 692            _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
 693                fs.clone(),
 694                paths::tasks_file().clone(),
 695                cx,
 696            ),
 697            _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
 698                fs.clone(),
 699                paths::debug_scenarios_file().clone(),
 700                cx,
 701            ),
 702        }
 703    }
 704
 705    pub fn new_remote(
 706        fs: Arc<dyn Fs>,
 707        worktree_store: Entity<WorktreeStore>,
 708        task_store: Entity<TaskStore>,
 709        cx: &mut Context<Self>,
 710    ) -> Self {
 711        Self {
 712            worktree_store,
 713            task_store,
 714            mode: SettingsObserverMode::Remote,
 715            downstream_client: None,
 716            project_id: 0,
 717            _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
 718                fs.clone(),
 719                paths::tasks_file().clone(),
 720                cx,
 721            ),
 722            _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
 723                fs.clone(),
 724                paths::debug_scenarios_file().clone(),
 725                cx,
 726            ),
 727        }
 728    }
 729
 730    pub fn shared(
 731        &mut self,
 732        project_id: u64,
 733        downstream_client: AnyProtoClient,
 734        cx: &mut Context<Self>,
 735    ) {
 736        self.project_id = project_id;
 737        self.downstream_client = Some(downstream_client.clone());
 738
 739        let store = cx.global::<SettingsStore>();
 740        for worktree in self.worktree_store.read(cx).worktrees() {
 741            let worktree_id = worktree.read(cx).id().to_proto();
 742            for (path, content) in store.local_settings(worktree.read(cx).id()) {
 743                downstream_client
 744                    .send(proto::UpdateWorktreeSettings {
 745                        project_id,
 746                        worktree_id,
 747                        path: path.to_proto(),
 748                        content: Some(content),
 749                        kind: Some(
 750                            local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
 751                        ),
 752                    })
 753                    .log_err();
 754            }
 755            for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
 756                downstream_client
 757                    .send(proto::UpdateWorktreeSettings {
 758                        project_id,
 759                        worktree_id,
 760                        path: path.to_proto(),
 761                        content: Some(content),
 762                        kind: Some(
 763                            local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
 764                        ),
 765                    })
 766                    .log_err();
 767            }
 768        }
 769    }
 770
 771    pub fn unshared(&mut self, _: &mut Context<Self>) {
 772        self.downstream_client = None;
 773    }
 774
 775    async fn handle_update_worktree_settings(
 776        this: Entity<Self>,
 777        envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
 778        mut cx: AsyncApp,
 779    ) -> anyhow::Result<()> {
 780        let kind = match envelope.payload.kind {
 781            Some(kind) => proto::LocalSettingsKind::from_i32(kind)
 782                .with_context(|| format!("unknown kind {kind}"))?,
 783            None => proto::LocalSettingsKind::Settings,
 784        };
 785        this.update(&mut cx, |this, cx| {
 786            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 787            let Some(worktree) = this
 788                .worktree_store
 789                .read(cx)
 790                .worktree_for_id(worktree_id, cx)
 791            else {
 792                return;
 793            };
 794
 795            this.update_settings(
 796                worktree,
 797                [(
 798                    Arc::<Path>::from_proto(envelope.payload.path.clone()),
 799                    local_settings_kind_from_proto(kind),
 800                    envelope.payload.content,
 801                )],
 802                cx,
 803            );
 804        })?;
 805        Ok(())
 806    }
 807
 808    fn on_worktree_store_event(
 809        &mut self,
 810        _: Entity<WorktreeStore>,
 811        event: &WorktreeStoreEvent,
 812        cx: &mut Context<Self>,
 813    ) {
 814        if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
 815            cx.subscribe(worktree, |this, worktree, event, cx| {
 816                if let worktree::Event::UpdatedEntries(changes) = event {
 817                    this.update_local_worktree_settings(&worktree, changes, cx)
 818                }
 819            })
 820            .detach()
 821        }
 822    }
 823
 824    fn update_local_worktree_settings(
 825        &mut self,
 826        worktree: &Entity<Worktree>,
 827        changes: &UpdatedEntriesSet,
 828        cx: &mut Context<Self>,
 829    ) {
 830        let SettingsObserverMode::Local(fs) = &self.mode else {
 831            return;
 832        };
 833
 834        let mut settings_contents = Vec::new();
 835        for (path, _, change) in changes.iter() {
 836            let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
 837                let settings_dir = Arc::<Path>::from(
 838                    path.ancestors()
 839                        .nth(local_settings_file_relative_path().components().count())
 840                        .unwrap(),
 841                );
 842                (settings_dir, LocalSettingsKind::Settings)
 843            } else if path.ends_with(local_tasks_file_relative_path()) {
 844                let settings_dir = Arc::<Path>::from(
 845                    path.ancestors()
 846                        .nth(
 847                            local_tasks_file_relative_path()
 848                                .components()
 849                                .count()
 850                                .saturating_sub(1),
 851                        )
 852                        .unwrap(),
 853                );
 854                (settings_dir, LocalSettingsKind::Tasks)
 855            } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
 856                let settings_dir = Arc::<Path>::from(
 857                    path.ancestors()
 858                        .nth(
 859                            local_vscode_tasks_file_relative_path()
 860                                .components()
 861                                .count()
 862                                .saturating_sub(1),
 863                        )
 864                        .unwrap(),
 865                );
 866                (settings_dir, LocalSettingsKind::Tasks)
 867            } else if path.ends_with(local_debug_file_relative_path()) {
 868                let settings_dir = Arc::<Path>::from(
 869                    path.ancestors()
 870                        .nth(
 871                            local_debug_file_relative_path()
 872                                .components()
 873                                .count()
 874                                .saturating_sub(1),
 875                        )
 876                        .unwrap(),
 877                );
 878                (settings_dir, LocalSettingsKind::Debug)
 879            } else if path.ends_with(local_vscode_launch_file_relative_path()) {
 880                let settings_dir = Arc::<Path>::from(
 881                    path.ancestors()
 882                        .nth(
 883                            local_vscode_tasks_file_relative_path()
 884                                .components()
 885                                .count()
 886                                .saturating_sub(1),
 887                        )
 888                        .unwrap(),
 889                );
 890                (settings_dir, LocalSettingsKind::Debug)
 891            } else if path.ends_with(EDITORCONFIG_NAME) {
 892                let Some(settings_dir) = path.parent().map(Arc::from) else {
 893                    continue;
 894                };
 895                (settings_dir, LocalSettingsKind::Editorconfig)
 896            } else {
 897                continue;
 898            };
 899
 900            let removed = change == &PathChange::Removed;
 901            let fs = fs.clone();
 902            let abs_path = match worktree.read(cx).absolutize(path) {
 903                Ok(abs_path) => abs_path,
 904                Err(e) => {
 905                    log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
 906                    continue;
 907                }
 908            };
 909            settings_contents.push(async move {
 910                (
 911                    settings_dir,
 912                    kind,
 913                    if removed {
 914                        None
 915                    } else {
 916                        Some(
 917                            async move {
 918                                let content = fs.load(&abs_path).await?;
 919                                if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
 920                                    let vscode_tasks =
 921                                        parse_json_with_comments::<VsCodeTaskFile>(&content)
 922                                            .with_context(|| {
 923                                                format!("parsing VSCode tasks, file {abs_path:?}")
 924                                            })?;
 925                                    let zed_tasks = TaskTemplates::try_from(vscode_tasks)
 926                                        .with_context(|| {
 927                                            format!(
 928                                        "converting VSCode tasks into Zed ones, file {abs_path:?}"
 929                                    )
 930                                        })?;
 931                                    serde_json::to_string(&zed_tasks).with_context(|| {
 932                                        format!(
 933                                            "serializing Zed tasks into JSON, file {abs_path:?}"
 934                                        )
 935                                    })
 936                                } else if abs_path.ends_with(local_vscode_launch_file_relative_path()) {
 937                                    let vscode_tasks =
 938                                        parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
 939                                            .with_context(|| {
 940                                                format!("parsing VSCode debug tasks, file {abs_path:?}")
 941                                            })?;
 942                                    let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
 943                                        .with_context(|| {
 944                                            format!(
 945                                        "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
 946                                    )
 947                                        })?;
 948                                    serde_json::to_string(&zed_tasks).with_context(|| {
 949                                        format!(
 950                                            "serializing Zed tasks into JSON, file {abs_path:?}"
 951                                        )
 952                                    })
 953                                } else {
 954                                    Ok(content)
 955                                }
 956                            }
 957                            .await,
 958                        )
 959                    },
 960                )
 961            });
 962        }
 963
 964        if settings_contents.is_empty() {
 965            return;
 966        }
 967
 968        let worktree = worktree.clone();
 969        cx.spawn(async move |this, cx| {
 970            let settings_contents: Vec<(Arc<Path>, _, _)> =
 971                futures::future::join_all(settings_contents).await;
 972            cx.update(|cx| {
 973                this.update(cx, |this, cx| {
 974                    this.update_settings(
 975                        worktree,
 976                        settings_contents.into_iter().map(|(path, kind, content)| {
 977                            (path, kind, content.and_then(|c| c.log_err()))
 978                        }),
 979                        cx,
 980                    )
 981                })
 982            })
 983        })
 984        .detach();
 985    }
 986
 987    fn update_settings(
 988        &mut self,
 989        worktree: Entity<Worktree>,
 990        settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
 991        cx: &mut Context<Self>,
 992    ) {
 993        let worktree_id = worktree.read(cx).id();
 994        let remote_worktree_id = worktree.read(cx).id();
 995        let task_store = self.task_store.clone();
 996
 997        for (directory, kind, file_content) in settings_contents {
 998            match kind {
 999                LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
1000                    .update_global::<SettingsStore, _>(|store, cx| {
1001                        let result = store.set_local_settings(
1002                            worktree_id,
1003                            directory.clone(),
1004                            kind,
1005                            file_content.as_deref(),
1006                            cx,
1007                        );
1008
1009                        match result {
1010                            Err(InvalidSettingsError::LocalSettings { path, message }) => {
1011                                log::error!("Failed to set local settings in {path:?}: {message}");
1012                                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
1013                                    InvalidSettingsError::LocalSettings { path, message },
1014                                )));
1015                            }
1016                            Err(e) => {
1017                                log::error!("Failed to set local settings: {e}");
1018                            }
1019                            Ok(()) => {
1020                                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(
1021                                    directory.join(local_settings_file_relative_path())
1022                                )));
1023                            }
1024                        }
1025                    }),
1026                LocalSettingsKind::Tasks => {
1027                    let result = task_store.update(cx, |task_store, cx| {
1028                        task_store.update_user_tasks(
1029                            TaskSettingsLocation::Worktree(SettingsLocation {
1030                                worktree_id,
1031                                path: directory.as_ref(),
1032                            }),
1033                            file_content.as_deref(),
1034                            cx,
1035                        )
1036                    });
1037
1038                    match result {
1039                        Err(InvalidSettingsError::Tasks { path, message }) => {
1040                            log::error!("Failed to set local tasks in {path:?}: {message:?}");
1041                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1042                                InvalidSettingsError::Tasks { path, message },
1043                            )));
1044                        }
1045                        Err(e) => {
1046                            log::error!("Failed to set local tasks: {e}");
1047                        }
1048                        Ok(()) => {
1049                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1050                                directory.join(task_file_name())
1051                            )));
1052                        }
1053                    }
1054                }
1055                LocalSettingsKind::Debug => {
1056                    let result = task_store.update(cx, |task_store, cx| {
1057                        task_store.update_user_debug_scenarios(
1058                            TaskSettingsLocation::Worktree(SettingsLocation {
1059                                worktree_id,
1060                                path: directory.as_ref(),
1061                            }),
1062                            file_content.as_deref(),
1063                            cx,
1064                        )
1065                    });
1066
1067                    match result {
1068                        Err(InvalidSettingsError::Debug { path, message }) => {
1069                            log::error!(
1070                                "Failed to set local debug scenarios in {path:?}: {message:?}"
1071                            );
1072                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1073                                InvalidSettingsError::Debug { path, message },
1074                            )));
1075                        }
1076                        Err(e) => {
1077                            log::error!("Failed to set local tasks: {e}");
1078                        }
1079                        Ok(()) => {
1080                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1081                                directory.join(task_file_name())
1082                            )));
1083                        }
1084                    }
1085                }
1086            };
1087
1088            if let Some(downstream_client) = &self.downstream_client {
1089                downstream_client
1090                    .send(proto::UpdateWorktreeSettings {
1091                        project_id: self.project_id,
1092                        worktree_id: remote_worktree_id.to_proto(),
1093                        path: directory.to_proto(),
1094                        content: file_content,
1095                        kind: Some(local_settings_kind_to_proto(kind).into()),
1096                    })
1097                    .log_err();
1098            }
1099        }
1100    }
1101
1102    fn subscribe_to_global_task_file_changes(
1103        fs: Arc<dyn Fs>,
1104        file_path: PathBuf,
1105        cx: &mut Context<Self>,
1106    ) -> Task<()> {
1107        let mut user_tasks_file_rx =
1108            watch_config_file(cx.background_executor(), fs, file_path.clone());
1109        let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1110        let weak_entry = cx.weak_entity();
1111        cx.spawn(async move |settings_observer, cx| {
1112            let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1113                settings_observer.task_store.clone()
1114            }) else {
1115                return;
1116            };
1117            if let Some(user_tasks_content) = user_tasks_content {
1118                let Ok(()) = task_store.update(cx, |task_store, cx| {
1119                    task_store
1120                        .update_user_tasks(
1121                            TaskSettingsLocation::Global(&file_path),
1122                            Some(&user_tasks_content),
1123                            cx,
1124                        )
1125                        .log_err();
1126                }) else {
1127                    return;
1128                };
1129            }
1130            while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1131                let Ok(result) = task_store.update(cx, |task_store, cx| {
1132                    task_store.update_user_tasks(
1133                        TaskSettingsLocation::Global(&file_path),
1134                        Some(&user_tasks_content),
1135                        cx,
1136                    )
1137                }) else {
1138                    break;
1139                };
1140
1141                weak_entry
1142                    .update(cx, |_, cx| match result {
1143                        Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1144                            file_path.clone()
1145                        ))),
1146                        Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1147                            InvalidSettingsError::Tasks {
1148                                path: file_path.clone(),
1149                                message: err.to_string(),
1150                            },
1151                        ))),
1152                    })
1153                    .ok();
1154            }
1155        })
1156    }
1157    fn subscribe_to_global_debug_scenarios_changes(
1158        fs: Arc<dyn Fs>,
1159        file_path: PathBuf,
1160        cx: &mut Context<Self>,
1161    ) -> Task<()> {
1162        let mut user_tasks_file_rx =
1163            watch_config_file(cx.background_executor(), fs, file_path.clone());
1164        let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1165        let weak_entry = cx.weak_entity();
1166        cx.spawn(async move |settings_observer, cx| {
1167            let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1168                settings_observer.task_store.clone()
1169            }) else {
1170                return;
1171            };
1172            if let Some(user_tasks_content) = user_tasks_content {
1173                let Ok(()) = task_store.update(cx, |task_store, cx| {
1174                    task_store
1175                        .update_user_debug_scenarios(
1176                            TaskSettingsLocation::Global(&file_path),
1177                            Some(&user_tasks_content),
1178                            cx,
1179                        )
1180                        .log_err();
1181                }) else {
1182                    return;
1183                };
1184            }
1185            while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1186                let Ok(result) = task_store.update(cx, |task_store, cx| {
1187                    task_store.update_user_debug_scenarios(
1188                        TaskSettingsLocation::Global(&file_path),
1189                        Some(&user_tasks_content),
1190                        cx,
1191                    )
1192                }) else {
1193                    break;
1194                };
1195
1196                weak_entry
1197                    .update(cx, |_, cx| match result {
1198                        Ok(()) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(Ok(
1199                            file_path.clone(),
1200                        ))),
1201                        Err(err) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(
1202                            Err(InvalidSettingsError::Tasks {
1203                                path: file_path.clone(),
1204                                message: err.to_string(),
1205                            }),
1206                        )),
1207                    })
1208                    .ok();
1209            }
1210        })
1211    }
1212}
1213
1214pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
1215    match kind {
1216        proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
1217        proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
1218        proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
1219        proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
1220    }
1221}
1222
1223pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
1224    match kind {
1225        LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
1226        LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
1227        LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
1228        LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
1229    }
1230}