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