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, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
 520#[serde(rename_all = "snake_case")]
 521pub struct LspSettings {
 522    pub binary: Option<BinarySettings>,
 523    pub initialization_options: Option<serde_json::Value>,
 524    pub settings: Option<serde_json::Value>,
 525    /// If the server supports sending tasks over LSP extensions,
 526    /// this setting can be used to enable or disable them in Zed.
 527    /// Default: true
 528    #[serde(default = "default_true")]
 529    pub enable_lsp_tasks: bool,
 530}
 531
 532impl Default for LspSettings {
 533    fn default() -> Self {
 534        Self {
 535            binary: None,
 536            initialization_options: None,
 537            settings: None,
 538            enable_lsp_tasks: true,
 539        }
 540    }
 541}
 542
 543#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
 544pub struct SessionSettings {
 545    /// Whether or not to restore unsaved buffers on restart.
 546    ///
 547    /// If this is true, user won't be prompted whether to save/discard
 548    /// dirty files when closing the application.
 549    ///
 550    /// Default: true
 551    pub restore_unsaved_buffers: bool,
 552}
 553
 554impl Default for SessionSettings {
 555    fn default() -> Self {
 556        Self {
 557            restore_unsaved_buffers: true,
 558        }
 559    }
 560}
 561
 562impl Settings for ProjectSettings {
 563    const KEY: Option<&'static str> = None;
 564
 565    type FileContent = Self;
 566
 567    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
 568        sources.json_merge()
 569    }
 570
 571    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
 572        // this just sets the binary name instead of a full path so it relies on path lookup
 573        // resolving to the one you want
 574        vscode.enum_setting(
 575            "npm.packageManager",
 576            &mut current.node.npm_path,
 577            |s| match s {
 578                v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
 579                _ => None,
 580            },
 581        );
 582
 583        if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") {
 584            if let Some(blame) = current.git.inline_blame.as_mut() {
 585                blame.enabled = b
 586            } else {
 587                current.git.inline_blame = Some(InlineBlameSettings {
 588                    enabled: b,
 589                    ..Default::default()
 590                })
 591            }
 592        }
 593
 594        #[derive(Deserialize)]
 595        struct VsCodeContextServerCommand {
 596            command: PathBuf,
 597            args: Option<Vec<String>>,
 598            env: Option<HashMap<String, String>>,
 599            // note: we don't support envFile and type
 600        }
 601        impl From<VsCodeContextServerCommand> for ContextServerCommand {
 602            fn from(cmd: VsCodeContextServerCommand) -> Self {
 603                Self {
 604                    path: cmd.command,
 605                    args: cmd.args.unwrap_or_default(),
 606                    env: cmd.env,
 607                    timeout: None,
 608                }
 609            }
 610        }
 611        if let Some(mcp) = vscode.read_value("mcp").and_then(|v| v.as_object()) {
 612            current
 613                .context_servers
 614                .extend(mcp.iter().filter_map(|(k, v)| {
 615                    Some((
 616                        k.clone().into(),
 617                        ContextServerSettings::Custom {
 618                            enabled: true,
 619                            command: serde_json::from_value::<VsCodeContextServerCommand>(
 620                                v.clone(),
 621                            )
 622                            .ok()?
 623                            .into(),
 624                        },
 625                    ))
 626                }));
 627        }
 628
 629        // TODO: translate lsp settings for rust-analyzer and other popular ones to old.lsp
 630    }
 631}
 632
 633pub enum SettingsObserverMode {
 634    Local(Arc<dyn Fs>),
 635    Remote,
 636}
 637
 638#[derive(Clone, Debug, PartialEq)]
 639pub enum SettingsObserverEvent {
 640    LocalSettingsUpdated(Result<PathBuf, InvalidSettingsError>),
 641    LocalTasksUpdated(Result<PathBuf, InvalidSettingsError>),
 642    LocalDebugScenariosUpdated(Result<PathBuf, InvalidSettingsError>),
 643}
 644
 645impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
 646
 647pub struct SettingsObserver {
 648    mode: SettingsObserverMode,
 649    downstream_client: Option<AnyProtoClient>,
 650    worktree_store: Entity<WorktreeStore>,
 651    project_id: u64,
 652    task_store: Entity<TaskStore>,
 653    _global_task_config_watcher: Task<()>,
 654    _global_debug_config_watcher: Task<()>,
 655}
 656
 657/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
 658/// (or the equivalent protobuf messages from upstream) and updates local settings
 659/// and sends notifications downstream.
 660/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
 661/// upstream.
 662impl SettingsObserver {
 663    pub fn init(client: &AnyProtoClient) {
 664        client.add_entity_message_handler(Self::handle_update_worktree_settings);
 665    }
 666
 667    pub fn new_local(
 668        fs: Arc<dyn Fs>,
 669        worktree_store: Entity<WorktreeStore>,
 670        task_store: Entity<TaskStore>,
 671        cx: &mut Context<Self>,
 672    ) -> Self {
 673        cx.subscribe(&worktree_store, Self::on_worktree_store_event)
 674            .detach();
 675
 676        Self {
 677            worktree_store,
 678            task_store,
 679            mode: SettingsObserverMode::Local(fs.clone()),
 680            downstream_client: None,
 681            project_id: 0,
 682            _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
 683                fs.clone(),
 684                paths::tasks_file().clone(),
 685                cx,
 686            ),
 687            _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
 688                fs.clone(),
 689                paths::debug_scenarios_file().clone(),
 690                cx,
 691            ),
 692        }
 693    }
 694
 695    pub fn new_remote(
 696        fs: Arc<dyn Fs>,
 697        worktree_store: Entity<WorktreeStore>,
 698        task_store: Entity<TaskStore>,
 699        cx: &mut Context<Self>,
 700    ) -> Self {
 701        Self {
 702            worktree_store,
 703            task_store,
 704            mode: SettingsObserverMode::Remote,
 705            downstream_client: None,
 706            project_id: 0,
 707            _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
 708                fs.clone(),
 709                paths::tasks_file().clone(),
 710                cx,
 711            ),
 712            _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
 713                fs.clone(),
 714                paths::debug_scenarios_file().clone(),
 715                cx,
 716            ),
 717        }
 718    }
 719
 720    pub fn shared(
 721        &mut self,
 722        project_id: u64,
 723        downstream_client: AnyProtoClient,
 724        cx: &mut Context<Self>,
 725    ) {
 726        self.project_id = project_id;
 727        self.downstream_client = Some(downstream_client.clone());
 728
 729        let store = cx.global::<SettingsStore>();
 730        for worktree in self.worktree_store.read(cx).worktrees() {
 731            let worktree_id = worktree.read(cx).id().to_proto();
 732            for (path, content) in store.local_settings(worktree.read(cx).id()) {
 733                downstream_client
 734                    .send(proto::UpdateWorktreeSettings {
 735                        project_id,
 736                        worktree_id,
 737                        path: path.to_proto(),
 738                        content: Some(content),
 739                        kind: Some(
 740                            local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
 741                        ),
 742                    })
 743                    .log_err();
 744            }
 745            for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
 746                downstream_client
 747                    .send(proto::UpdateWorktreeSettings {
 748                        project_id,
 749                        worktree_id,
 750                        path: path.to_proto(),
 751                        content: Some(content),
 752                        kind: Some(
 753                            local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
 754                        ),
 755                    })
 756                    .log_err();
 757            }
 758        }
 759    }
 760
 761    pub fn unshared(&mut self, _: &mut Context<Self>) {
 762        self.downstream_client = None;
 763    }
 764
 765    async fn handle_update_worktree_settings(
 766        this: Entity<Self>,
 767        envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
 768        mut cx: AsyncApp,
 769    ) -> anyhow::Result<()> {
 770        let kind = match envelope.payload.kind {
 771            Some(kind) => proto::LocalSettingsKind::from_i32(kind)
 772                .with_context(|| format!("unknown kind {kind}"))?,
 773            None => proto::LocalSettingsKind::Settings,
 774        };
 775        this.update(&mut cx, |this, cx| {
 776            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 777            let Some(worktree) = this
 778                .worktree_store
 779                .read(cx)
 780                .worktree_for_id(worktree_id, cx)
 781            else {
 782                return;
 783            };
 784
 785            this.update_settings(
 786                worktree,
 787                [(
 788                    Arc::<Path>::from_proto(envelope.payload.path.clone()),
 789                    local_settings_kind_from_proto(kind),
 790                    envelope.payload.content,
 791                )],
 792                cx,
 793            );
 794        })?;
 795        Ok(())
 796    }
 797
 798    fn on_worktree_store_event(
 799        &mut self,
 800        _: Entity<WorktreeStore>,
 801        event: &WorktreeStoreEvent,
 802        cx: &mut Context<Self>,
 803    ) {
 804        if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
 805            cx.subscribe(worktree, |this, worktree, event, cx| {
 806                if let worktree::Event::UpdatedEntries(changes) = event {
 807                    this.update_local_worktree_settings(&worktree, changes, cx)
 808                }
 809            })
 810            .detach()
 811        }
 812    }
 813
 814    fn update_local_worktree_settings(
 815        &mut self,
 816        worktree: &Entity<Worktree>,
 817        changes: &UpdatedEntriesSet,
 818        cx: &mut Context<Self>,
 819    ) {
 820        let SettingsObserverMode::Local(fs) = &self.mode else {
 821            return;
 822        };
 823
 824        let mut settings_contents = Vec::new();
 825        for (path, _, change) in changes.iter() {
 826            let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
 827                let settings_dir = Arc::<Path>::from(
 828                    path.ancestors()
 829                        .nth(local_settings_file_relative_path().components().count())
 830                        .unwrap(),
 831                );
 832                (settings_dir, LocalSettingsKind::Settings)
 833            } else if path.ends_with(local_tasks_file_relative_path()) {
 834                let settings_dir = Arc::<Path>::from(
 835                    path.ancestors()
 836                        .nth(
 837                            local_tasks_file_relative_path()
 838                                .components()
 839                                .count()
 840                                .saturating_sub(1),
 841                        )
 842                        .unwrap(),
 843                );
 844                (settings_dir, LocalSettingsKind::Tasks)
 845            } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
 846                let settings_dir = Arc::<Path>::from(
 847                    path.ancestors()
 848                        .nth(
 849                            local_vscode_tasks_file_relative_path()
 850                                .components()
 851                                .count()
 852                                .saturating_sub(1),
 853                        )
 854                        .unwrap(),
 855                );
 856                (settings_dir, LocalSettingsKind::Tasks)
 857            } else if path.ends_with(local_debug_file_relative_path()) {
 858                let settings_dir = Arc::<Path>::from(
 859                    path.ancestors()
 860                        .nth(
 861                            local_debug_file_relative_path()
 862                                .components()
 863                                .count()
 864                                .saturating_sub(1),
 865                        )
 866                        .unwrap(),
 867                );
 868                (settings_dir, LocalSettingsKind::Debug)
 869            } else if path.ends_with(local_vscode_launch_file_relative_path()) {
 870                let settings_dir = Arc::<Path>::from(
 871                    path.ancestors()
 872                        .nth(
 873                            local_vscode_tasks_file_relative_path()
 874                                .components()
 875                                .count()
 876                                .saturating_sub(1),
 877                        )
 878                        .unwrap(),
 879                );
 880                (settings_dir, LocalSettingsKind::Debug)
 881            } else if path.ends_with(EDITORCONFIG_NAME) {
 882                let Some(settings_dir) = path.parent().map(Arc::from) else {
 883                    continue;
 884                };
 885                (settings_dir, LocalSettingsKind::Editorconfig)
 886            } else {
 887                continue;
 888            };
 889
 890            let removed = change == &PathChange::Removed;
 891            let fs = fs.clone();
 892            let abs_path = match worktree.read(cx).absolutize(path) {
 893                Ok(abs_path) => abs_path,
 894                Err(e) => {
 895                    log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
 896                    continue;
 897                }
 898            };
 899            settings_contents.push(async move {
 900                (
 901                    settings_dir,
 902                    kind,
 903                    if removed {
 904                        None
 905                    } else {
 906                        Some(
 907                            async move {
 908                                let content = fs.load(&abs_path).await?;
 909                                if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
 910                                    let vscode_tasks =
 911                                        parse_json_with_comments::<VsCodeTaskFile>(&content)
 912                                            .with_context(|| {
 913                                                format!("parsing VSCode tasks, file {abs_path:?}")
 914                                            })?;
 915                                    let zed_tasks = TaskTemplates::try_from(vscode_tasks)
 916                                        .with_context(|| {
 917                                            format!(
 918                                        "converting VSCode tasks into Zed ones, file {abs_path:?}"
 919                                    )
 920                                        })?;
 921                                    serde_json::to_string(&zed_tasks).with_context(|| {
 922                                        format!(
 923                                            "serializing Zed tasks into JSON, file {abs_path:?}"
 924                                        )
 925                                    })
 926                                } else if abs_path.ends_with(local_vscode_launch_file_relative_path()) {
 927                                    let vscode_tasks =
 928                                        parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
 929                                            .with_context(|| {
 930                                                format!("parsing VSCode debug tasks, file {abs_path:?}")
 931                                            })?;
 932                                    let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
 933                                        .with_context(|| {
 934                                            format!(
 935                                        "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
 936                                    )
 937                                        })?;
 938                                    serde_json::to_string(&zed_tasks).with_context(|| {
 939                                        format!(
 940                                            "serializing Zed tasks into JSON, file {abs_path:?}"
 941                                        )
 942                                    })
 943                                } else {
 944                                    Ok(content)
 945                                }
 946                            }
 947                            .await,
 948                        )
 949                    },
 950                )
 951            });
 952        }
 953
 954        if settings_contents.is_empty() {
 955            return;
 956        }
 957
 958        let worktree = worktree.clone();
 959        cx.spawn(async move |this, cx| {
 960            let settings_contents: Vec<(Arc<Path>, _, _)> =
 961                futures::future::join_all(settings_contents).await;
 962            cx.update(|cx| {
 963                this.update(cx, |this, cx| {
 964                    this.update_settings(
 965                        worktree,
 966                        settings_contents.into_iter().map(|(path, kind, content)| {
 967                            (path, kind, content.and_then(|c| c.log_err()))
 968                        }),
 969                        cx,
 970                    )
 971                })
 972            })
 973        })
 974        .detach();
 975    }
 976
 977    fn update_settings(
 978        &mut self,
 979        worktree: Entity<Worktree>,
 980        settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
 981        cx: &mut Context<Self>,
 982    ) {
 983        let worktree_id = worktree.read(cx).id();
 984        let remote_worktree_id = worktree.read(cx).id();
 985        let task_store = self.task_store.clone();
 986
 987        for (directory, kind, file_content) in settings_contents {
 988            match kind {
 989                LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
 990                    .update_global::<SettingsStore, _>(|store, cx| {
 991                        let result = store.set_local_settings(
 992                            worktree_id,
 993                            directory.clone(),
 994                            kind,
 995                            file_content.as_deref(),
 996                            cx,
 997                        );
 998
 999                        match result {
1000                            Err(InvalidSettingsError::LocalSettings { path, message }) => {
1001                                log::error!("Failed to set local settings in {path:?}: {message}");
1002                                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
1003                                    InvalidSettingsError::LocalSettings { path, message },
1004                                )));
1005                            }
1006                            Err(e) => {
1007                                log::error!("Failed to set local settings: {e}");
1008                            }
1009                            Ok(()) => {
1010                                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(
1011                                    directory.join(local_settings_file_relative_path())
1012                                )));
1013                            }
1014                        }
1015                    }),
1016                LocalSettingsKind::Tasks => {
1017                    let result = task_store.update(cx, |task_store, cx| {
1018                        task_store.update_user_tasks(
1019                            TaskSettingsLocation::Worktree(SettingsLocation {
1020                                worktree_id,
1021                                path: directory.as_ref(),
1022                            }),
1023                            file_content.as_deref(),
1024                            cx,
1025                        )
1026                    });
1027
1028                    match result {
1029                        Err(InvalidSettingsError::Tasks { path, message }) => {
1030                            log::error!("Failed to set local tasks in {path:?}: {message:?}");
1031                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1032                                InvalidSettingsError::Tasks { path, message },
1033                            )));
1034                        }
1035                        Err(e) => {
1036                            log::error!("Failed to set local tasks: {e}");
1037                        }
1038                        Ok(()) => {
1039                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1040                                directory.join(task_file_name())
1041                            )));
1042                        }
1043                    }
1044                }
1045                LocalSettingsKind::Debug => {
1046                    let result = task_store.update(cx, |task_store, cx| {
1047                        task_store.update_user_debug_scenarios(
1048                            TaskSettingsLocation::Worktree(SettingsLocation {
1049                                worktree_id,
1050                                path: directory.as_ref(),
1051                            }),
1052                            file_content.as_deref(),
1053                            cx,
1054                        )
1055                    });
1056
1057                    match result {
1058                        Err(InvalidSettingsError::Debug { path, message }) => {
1059                            log::error!(
1060                                "Failed to set local debug scenarios in {path:?}: {message:?}"
1061                            );
1062                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1063                                InvalidSettingsError::Debug { path, message },
1064                            )));
1065                        }
1066                        Err(e) => {
1067                            log::error!("Failed to set local tasks: {e}");
1068                        }
1069                        Ok(()) => {
1070                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1071                                directory.join(task_file_name())
1072                            )));
1073                        }
1074                    }
1075                }
1076            };
1077
1078            if let Some(downstream_client) = &self.downstream_client {
1079                downstream_client
1080                    .send(proto::UpdateWorktreeSettings {
1081                        project_id: self.project_id,
1082                        worktree_id: remote_worktree_id.to_proto(),
1083                        path: directory.to_proto(),
1084                        content: file_content,
1085                        kind: Some(local_settings_kind_to_proto(kind).into()),
1086                    })
1087                    .log_err();
1088            }
1089        }
1090    }
1091
1092    fn subscribe_to_global_task_file_changes(
1093        fs: Arc<dyn Fs>,
1094        file_path: PathBuf,
1095        cx: &mut Context<Self>,
1096    ) -> Task<()> {
1097        let mut user_tasks_file_rx =
1098            watch_config_file(cx.background_executor(), fs, file_path.clone());
1099        let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1100        let weak_entry = cx.weak_entity();
1101        cx.spawn(async move |settings_observer, cx| {
1102            let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1103                settings_observer.task_store.clone()
1104            }) else {
1105                return;
1106            };
1107            if let Some(user_tasks_content) = user_tasks_content {
1108                let Ok(()) = task_store.update(cx, |task_store, cx| {
1109                    task_store
1110                        .update_user_tasks(
1111                            TaskSettingsLocation::Global(&file_path),
1112                            Some(&user_tasks_content),
1113                            cx,
1114                        )
1115                        .log_err();
1116                }) else {
1117                    return;
1118                };
1119            }
1120            while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1121                let Ok(result) = task_store.update(cx, |task_store, cx| {
1122                    task_store.update_user_tasks(
1123                        TaskSettingsLocation::Global(&file_path),
1124                        Some(&user_tasks_content),
1125                        cx,
1126                    )
1127                }) else {
1128                    break;
1129                };
1130
1131                weak_entry
1132                    .update(cx, |_, cx| match result {
1133                        Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1134                            file_path.clone()
1135                        ))),
1136                        Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1137                            InvalidSettingsError::Tasks {
1138                                path: file_path.clone(),
1139                                message: err.to_string(),
1140                            },
1141                        ))),
1142                    })
1143                    .ok();
1144            }
1145        })
1146    }
1147    fn subscribe_to_global_debug_scenarios_changes(
1148        fs: Arc<dyn Fs>,
1149        file_path: PathBuf,
1150        cx: &mut Context<Self>,
1151    ) -> Task<()> {
1152        let mut user_tasks_file_rx =
1153            watch_config_file(cx.background_executor(), fs, file_path.clone());
1154        let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1155        let weak_entry = cx.weak_entity();
1156        cx.spawn(async move |settings_observer, cx| {
1157            let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1158                settings_observer.task_store.clone()
1159            }) else {
1160                return;
1161            };
1162            if let Some(user_tasks_content) = user_tasks_content {
1163                let Ok(()) = task_store.update(cx, |task_store, cx| {
1164                    task_store
1165                        .update_user_debug_scenarios(
1166                            TaskSettingsLocation::Global(&file_path),
1167                            Some(&user_tasks_content),
1168                            cx,
1169                        )
1170                        .log_err();
1171                }) else {
1172                    return;
1173                };
1174            }
1175            while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1176                let Ok(result) = task_store.update(cx, |task_store, cx| {
1177                    task_store.update_user_debug_scenarios(
1178                        TaskSettingsLocation::Global(&file_path),
1179                        Some(&user_tasks_content),
1180                        cx,
1181                    )
1182                }) else {
1183                    break;
1184                };
1185
1186                weak_entry
1187                    .update(cx, |_, cx| match result {
1188                        Ok(()) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(Ok(
1189                            file_path.clone(),
1190                        ))),
1191                        Err(err) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(
1192                            Err(InvalidSettingsError::Tasks {
1193                                path: file_path.clone(),
1194                                message: err.to_string(),
1195                            }),
1196                        )),
1197                    })
1198                    .ok();
1199            }
1200        })
1201    }
1202}
1203
1204pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
1205    match kind {
1206        proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
1207        proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
1208        proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
1209        proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
1210    }
1211}
1212
1213pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
1214    match kind {
1215        LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
1216        LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
1217        LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
1218        LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
1219    }
1220}