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