project_settings.rs

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