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