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