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