project_settings.rs

   1use anyhow::Context as _;
   2use collections::HashMap;
   3use context_server::ContextServerCommand;
   4use dap::adapters::DebugAdapterName;
   5use fs::Fs;
   6use futures::StreamExt as _;
   7use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Task};
   8use lsp::LanguageServerName;
   9use paths::{
  10    EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_file_relative_path,
  11    local_tasks_file_relative_path, local_vscode_launch_file_relative_path,
  12    local_vscode_tasks_file_relative_path, task_file_name,
  13};
  14use rpc::{
  15    AnyProtoClient, TypedEnvelope,
  16    proto::{self, FromProto, ToProto},
  17};
  18use schemars::JsonSchema;
  19use serde::{Deserialize, Serialize};
  20use settings::{
  21    InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources,
  22    SettingsStore, parse_json_with_comments, watch_config_file,
  23};
  24use std::{
  25    path::{Path, PathBuf},
  26    sync::Arc,
  27    time::Duration,
  28};
  29use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile};
  30use util::{ResultExt, serde::default_true};
  31use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
  32
  33use crate::{
  34    task_store::{TaskSettingsLocation, TaskStore},
  35    worktree_store::{WorktreeStore, WorktreeStoreEvent},
  36};
  37
  38#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
  39#[schemars(deny_unknown_fields)]
  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    /// Configuration, related to Rust language diagnostics.
 186    pub cargo: Option<CargoDiagnosticsSettings>,
 187}
 188
 189impl DiagnosticsSettings {
 190    pub fn fetch_cargo_diagnostics(&self) -> bool {
 191        self.cargo.as_ref().map_or(false, |cargo_diagnostics| {
 192            cargo_diagnostics.fetch_cargo_diagnostics
 193        })
 194    }
 195}
 196
 197#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
 198#[serde(default)]
 199pub struct LspPullDiagnosticsSettings {
 200    /// Whether to pull for diagnostics or not.
 201    ///
 202    /// Default: true
 203    #[serde(default = "default_true")]
 204    pub enabled: bool,
 205    /// Minimum time to wait before pulling diagnostics from the language server(s).
 206    /// 0 turns the debounce off.
 207    ///
 208    /// Default: 50
 209    #[serde(default = "default_lsp_diagnostics_pull_debounce_ms")]
 210    pub debounce_ms: u64,
 211}
 212
 213fn default_lsp_diagnostics_pull_debounce_ms() -> u64 {
 214    50
 215}
 216
 217#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
 218#[serde(default)]
 219pub struct InlineDiagnosticsSettings {
 220    /// Whether or not to show inline diagnostics
 221    ///
 222    /// Default: false
 223    pub enabled: bool,
 224    /// Whether to only show the inline diagnostics after a delay after the
 225    /// last editor event.
 226    ///
 227    /// Default: 150
 228    #[serde(default = "default_inline_diagnostics_update_debounce_ms")]
 229    pub update_debounce_ms: u64,
 230    /// The amount of padding between the end of the source line and the start
 231    /// of the inline diagnostic in units of columns.
 232    ///
 233    /// Default: 4
 234    #[serde(default = "default_inline_diagnostics_padding")]
 235    pub padding: u32,
 236    /// The minimum column to display inline diagnostics. This setting can be
 237    /// used to horizontally align inline diagnostics at some position. Lines
 238    /// longer than this value will still push diagnostics further to the right.
 239    ///
 240    /// Default: 0
 241    pub min_column: u32,
 242
 243    pub max_severity: Option<DiagnosticSeverity>,
 244}
 245
 246fn default_inline_diagnostics_update_debounce_ms() -> u64 {
 247    150
 248}
 249
 250fn default_inline_diagnostics_padding() -> u32 {
 251    4
 252}
 253
 254impl Default for DiagnosticsSettings {
 255    fn default() -> Self {
 256        Self {
 257            button: true,
 258            include_warnings: true,
 259            lsp_pull_diagnostics: LspPullDiagnosticsSettings::default(),
 260            inline: InlineDiagnosticsSettings::default(),
 261            cargo: None,
 262        }
 263    }
 264}
 265
 266impl Default for LspPullDiagnosticsSettings {
 267    fn default() -> Self {
 268        Self {
 269            enabled: true,
 270            debounce_ms: default_lsp_diagnostics_pull_debounce_ms(),
 271        }
 272    }
 273}
 274
 275impl Default for InlineDiagnosticsSettings {
 276    fn default() -> Self {
 277        Self {
 278            enabled: false,
 279            update_debounce_ms: default_inline_diagnostics_update_debounce_ms(),
 280            padding: default_inline_diagnostics_padding(),
 281            min_column: 0,
 282            max_severity: None,
 283        }
 284    }
 285}
 286
 287impl Default for GlobalLspSettings {
 288    fn default() -> Self {
 289        Self {
 290            button: default_true(),
 291        }
 292    }
 293}
 294
 295#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 296pub struct CargoDiagnosticsSettings {
 297    /// When enabled, Zed disables rust-analyzer's check on save and starts to query
 298    /// Cargo diagnostics separately.
 299    ///
 300    /// Default: false
 301    #[serde(default)]
 302    pub fetch_cargo_diagnostics: bool,
 303}
 304
 305#[derive(
 306    Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema,
 307)]
 308#[serde(rename_all = "snake_case")]
 309pub enum DiagnosticSeverity {
 310    // No diagnostics are shown.
 311    Off,
 312    Error,
 313    Warning,
 314    Info,
 315    Hint,
 316}
 317
 318impl DiagnosticSeverity {
 319    pub fn into_lsp(self) -> Option<lsp::DiagnosticSeverity> {
 320        match self {
 321            DiagnosticSeverity::Off => None,
 322            DiagnosticSeverity::Error => Some(lsp::DiagnosticSeverity::ERROR),
 323            DiagnosticSeverity::Warning => Some(lsp::DiagnosticSeverity::WARNING),
 324            DiagnosticSeverity::Info => Some(lsp::DiagnosticSeverity::INFORMATION),
 325            DiagnosticSeverity::Hint => Some(lsp::DiagnosticSeverity::HINT),
 326        }
 327    }
 328}
 329
 330#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 331pub struct GitSettings {
 332    /// Whether or not to show the git gutter.
 333    ///
 334    /// Default: tracked_files
 335    pub git_gutter: Option<GitGutterSetting>,
 336    /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
 337    ///
 338    /// Default: null
 339    pub gutter_debounce: Option<u64>,
 340    /// Whether or not to show git blame data inline in
 341    /// the currently focused line.
 342    ///
 343    /// Default: on
 344    pub inline_blame: Option<InlineBlameSettings>,
 345    /// How hunks are displayed visually in the editor.
 346    ///
 347    /// Default: staged_hollow
 348    pub hunk_style: Option<GitHunkStyleSetting>,
 349}
 350
 351impl GitSettings {
 352    pub fn inline_blame_enabled(&self) -> bool {
 353        #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
 354        match self.inline_blame {
 355            Some(InlineBlameSettings { enabled, .. }) => enabled,
 356            _ => false,
 357        }
 358    }
 359
 360    pub fn inline_blame_delay(&self) -> Option<Duration> {
 361        match self.inline_blame {
 362            Some(InlineBlameSettings {
 363                delay_ms: Some(delay_ms),
 364                ..
 365            }) if delay_ms > 0 => Some(Duration::from_millis(delay_ms)),
 366            _ => None,
 367        }
 368    }
 369
 370    pub fn show_inline_commit_summary(&self) -> bool {
 371        match self.inline_blame {
 372            Some(InlineBlameSettings {
 373                show_commit_summary,
 374                ..
 375            }) => show_commit_summary,
 376            _ => false,
 377        }
 378    }
 379}
 380
 381#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
 382#[serde(rename_all = "snake_case")]
 383pub enum GitHunkStyleSetting {
 384    /// Show unstaged hunks with a filled background and staged hunks hollow.
 385    #[default]
 386    StagedHollow,
 387    /// Show unstaged hunks hollow and staged hunks with a filled background.
 388    UnstagedHollow,
 389}
 390
 391#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
 392#[serde(rename_all = "snake_case")]
 393pub enum GitGutterSetting {
 394    /// Show git gutter in tracked files.
 395    #[default]
 396    TrackedFiles,
 397    /// Hide git gutter
 398    Hide,
 399}
 400
 401#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
 402#[serde(rename_all = "snake_case")]
 403pub struct InlineBlameSettings {
 404    /// Whether or not to show git blame data inline in
 405    /// the currently focused line.
 406    ///
 407    /// Default: true
 408    #[serde(default = "default_true")]
 409    pub enabled: bool,
 410    /// Whether to only show the inline blame information
 411    /// after a delay once the cursor stops moving.
 412    ///
 413    /// Default: 0
 414    pub delay_ms: Option<u64>,
 415    /// The minimum column number to show the inline blame information at
 416    ///
 417    /// Default: 0
 418    pub min_column: Option<u32>,
 419    /// Whether to show commit summary as part of the inline blame.
 420    ///
 421    /// Default: false
 422    #[serde(default)]
 423    pub show_commit_summary: bool,
 424}
 425
 426#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 427pub struct BinarySettings {
 428    pub path: Option<String>,
 429    pub arguments: Option<Vec<String>>,
 430    // this can't be an FxHashMap because the extension APIs require the default SipHash
 431    pub env: Option<std::collections::HashMap<String, String>>,
 432    pub ignore_system_version: Option<bool>,
 433}
 434
 435#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 436#[serde(rename_all = "snake_case")]
 437pub struct LspSettings {
 438    pub binary: Option<BinarySettings>,
 439    pub initialization_options: Option<serde_json::Value>,
 440    pub settings: Option<serde_json::Value>,
 441    /// If the server supports sending tasks over LSP extensions,
 442    /// this setting can be used to enable or disable them in Zed.
 443    /// Default: true
 444    #[serde(default = "default_true")]
 445    pub enable_lsp_tasks: bool,
 446}
 447
 448impl Default for LspSettings {
 449    fn default() -> Self {
 450        Self {
 451            binary: None,
 452            initialization_options: None,
 453            settings: None,
 454            enable_lsp_tasks: true,
 455        }
 456    }
 457}
 458
 459#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
 460pub struct SessionSettings {
 461    /// Whether or not to restore unsaved buffers on restart.
 462    ///
 463    /// If this is true, user won't be prompted whether to save/discard
 464    /// dirty files when closing the application.
 465    ///
 466    /// Default: true
 467    pub restore_unsaved_buffers: bool,
 468}
 469
 470impl Default for SessionSettings {
 471    fn default() -> Self {
 472        Self {
 473            restore_unsaved_buffers: true,
 474        }
 475    }
 476}
 477
 478impl Settings for ProjectSettings {
 479    const KEY: Option<&'static str> = None;
 480
 481    type FileContent = Self;
 482
 483    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
 484        sources.json_merge()
 485    }
 486
 487    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
 488        // this just sets the binary name instead of a full path so it relies on path lookup
 489        // resolving to the one you want
 490        vscode.enum_setting(
 491            "npm.packageManager",
 492            &mut current.node.npm_path,
 493            |s| match s {
 494                v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
 495                _ => None,
 496            },
 497        );
 498
 499        if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") {
 500            if let Some(blame) = current.git.inline_blame.as_mut() {
 501                blame.enabled = b
 502            } else {
 503                current.git.inline_blame = Some(InlineBlameSettings {
 504                    enabled: b,
 505                    ..Default::default()
 506                })
 507            }
 508        }
 509
 510        #[derive(Deserialize)]
 511        struct VsCodeContextServerCommand {
 512            command: String,
 513            args: Option<Vec<String>>,
 514            env: Option<HashMap<String, String>>,
 515            // note: we don't support envFile and type
 516        }
 517        impl From<VsCodeContextServerCommand> for ContextServerCommand {
 518            fn from(cmd: VsCodeContextServerCommand) -> Self {
 519                Self {
 520                    path: cmd.command,
 521                    args: cmd.args.unwrap_or_default(),
 522                    env: cmd.env,
 523                }
 524            }
 525        }
 526        if let Some(mcp) = vscode.read_value("mcp").and_then(|v| v.as_object()) {
 527            current
 528                .context_servers
 529                .extend(mcp.iter().filter_map(|(k, v)| {
 530                    Some((
 531                        k.clone().into(),
 532                        ContextServerSettings::Custom {
 533                            enabled: true,
 534                            command: serde_json::from_value::<VsCodeContextServerCommand>(
 535                                v.clone(),
 536                            )
 537                            .ok()?
 538                            .into(),
 539                        },
 540                    ))
 541                }));
 542        }
 543
 544        // TODO: translate lsp settings for rust-analyzer and other popular ones to old.lsp
 545    }
 546}
 547
 548pub enum SettingsObserverMode {
 549    Local(Arc<dyn Fs>),
 550    Remote,
 551}
 552
 553#[derive(Clone, Debug, PartialEq)]
 554pub enum SettingsObserverEvent {
 555    LocalSettingsUpdated(Result<PathBuf, InvalidSettingsError>),
 556    LocalTasksUpdated(Result<PathBuf, InvalidSettingsError>),
 557}
 558
 559impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
 560
 561pub struct SettingsObserver {
 562    mode: SettingsObserverMode,
 563    downstream_client: Option<AnyProtoClient>,
 564    worktree_store: Entity<WorktreeStore>,
 565    project_id: u64,
 566    task_store: Entity<TaskStore>,
 567    _global_task_config_watcher: Task<()>,
 568}
 569
 570/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
 571/// (or the equivalent protobuf messages from upstream) and updates local settings
 572/// and sends notifications downstream.
 573/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
 574/// upstream.
 575impl SettingsObserver {
 576    pub fn init(client: &AnyProtoClient) {
 577        client.add_entity_message_handler(Self::handle_update_worktree_settings);
 578    }
 579
 580    pub fn new_local(
 581        fs: Arc<dyn Fs>,
 582        worktree_store: Entity<WorktreeStore>,
 583        task_store: Entity<TaskStore>,
 584        cx: &mut Context<Self>,
 585    ) -> Self {
 586        cx.subscribe(&worktree_store, Self::on_worktree_store_event)
 587            .detach();
 588
 589        Self {
 590            worktree_store,
 591            task_store,
 592            mode: SettingsObserverMode::Local(fs.clone()),
 593            downstream_client: None,
 594            project_id: 0,
 595            _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
 596                fs.clone(),
 597                paths::tasks_file().clone(),
 598                cx,
 599            ),
 600        }
 601    }
 602
 603    pub fn new_remote(
 604        fs: Arc<dyn Fs>,
 605        worktree_store: Entity<WorktreeStore>,
 606        task_store: Entity<TaskStore>,
 607        cx: &mut Context<Self>,
 608    ) -> Self {
 609        Self {
 610            worktree_store,
 611            task_store,
 612            mode: SettingsObserverMode::Remote,
 613            downstream_client: None,
 614            project_id: 0,
 615            _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
 616                fs.clone(),
 617                paths::tasks_file().clone(),
 618                cx,
 619            ),
 620        }
 621    }
 622
 623    pub fn shared(
 624        &mut self,
 625        project_id: u64,
 626        downstream_client: AnyProtoClient,
 627        cx: &mut Context<Self>,
 628    ) {
 629        self.project_id = project_id;
 630        self.downstream_client = Some(downstream_client.clone());
 631
 632        let store = cx.global::<SettingsStore>();
 633        for worktree in self.worktree_store.read(cx).worktrees() {
 634            let worktree_id = worktree.read(cx).id().to_proto();
 635            for (path, content) in store.local_settings(worktree.read(cx).id()) {
 636                downstream_client
 637                    .send(proto::UpdateWorktreeSettings {
 638                        project_id,
 639                        worktree_id,
 640                        path: path.to_proto(),
 641                        content: Some(content),
 642                        kind: Some(
 643                            local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
 644                        ),
 645                    })
 646                    .log_err();
 647            }
 648            for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
 649                downstream_client
 650                    .send(proto::UpdateWorktreeSettings {
 651                        project_id,
 652                        worktree_id,
 653                        path: path.to_proto(),
 654                        content: Some(content),
 655                        kind: Some(
 656                            local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
 657                        ),
 658                    })
 659                    .log_err();
 660            }
 661        }
 662    }
 663
 664    pub fn unshared(&mut self, _: &mut Context<Self>) {
 665        self.downstream_client = None;
 666    }
 667
 668    async fn handle_update_worktree_settings(
 669        this: Entity<Self>,
 670        envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
 671        mut cx: AsyncApp,
 672    ) -> anyhow::Result<()> {
 673        let kind = match envelope.payload.kind {
 674            Some(kind) => proto::LocalSettingsKind::from_i32(kind)
 675                .with_context(|| format!("unknown kind {kind}"))?,
 676            None => proto::LocalSettingsKind::Settings,
 677        };
 678        this.update(&mut cx, |this, cx| {
 679            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 680            let Some(worktree) = this
 681                .worktree_store
 682                .read(cx)
 683                .worktree_for_id(worktree_id, cx)
 684            else {
 685                return;
 686            };
 687
 688            this.update_settings(
 689                worktree,
 690                [(
 691                    Arc::<Path>::from_proto(envelope.payload.path.clone()),
 692                    local_settings_kind_from_proto(kind),
 693                    envelope.payload.content,
 694                )],
 695                cx,
 696            );
 697        })?;
 698        Ok(())
 699    }
 700
 701    fn on_worktree_store_event(
 702        &mut self,
 703        _: Entity<WorktreeStore>,
 704        event: &WorktreeStoreEvent,
 705        cx: &mut Context<Self>,
 706    ) {
 707        if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
 708            cx.subscribe(worktree, |this, worktree, event, cx| {
 709                if let worktree::Event::UpdatedEntries(changes) = event {
 710                    this.update_local_worktree_settings(&worktree, changes, cx)
 711                }
 712            })
 713            .detach()
 714        }
 715    }
 716
 717    fn update_local_worktree_settings(
 718        &mut self,
 719        worktree: &Entity<Worktree>,
 720        changes: &UpdatedEntriesSet,
 721        cx: &mut Context<Self>,
 722    ) {
 723        let SettingsObserverMode::Local(fs) = &self.mode else {
 724            return;
 725        };
 726
 727        let mut settings_contents = Vec::new();
 728        for (path, _, change) in changes.iter() {
 729            let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
 730                let settings_dir = Arc::<Path>::from(
 731                    path.ancestors()
 732                        .nth(local_settings_file_relative_path().components().count())
 733                        .unwrap(),
 734                );
 735                (settings_dir, LocalSettingsKind::Settings)
 736            } else if path.ends_with(local_tasks_file_relative_path()) {
 737                let settings_dir = Arc::<Path>::from(
 738                    path.ancestors()
 739                        .nth(
 740                            local_tasks_file_relative_path()
 741                                .components()
 742                                .count()
 743                                .saturating_sub(1),
 744                        )
 745                        .unwrap(),
 746                );
 747                (settings_dir, LocalSettingsKind::Tasks)
 748            } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
 749                let settings_dir = Arc::<Path>::from(
 750                    path.ancestors()
 751                        .nth(
 752                            local_vscode_tasks_file_relative_path()
 753                                .components()
 754                                .count()
 755                                .saturating_sub(1),
 756                        )
 757                        .unwrap(),
 758                );
 759                (settings_dir, LocalSettingsKind::Tasks)
 760            } else if path.ends_with(local_debug_file_relative_path()) {
 761                let settings_dir = Arc::<Path>::from(
 762                    path.ancestors()
 763                        .nth(
 764                            local_debug_file_relative_path()
 765                                .components()
 766                                .count()
 767                                .saturating_sub(1),
 768                        )
 769                        .unwrap(),
 770                );
 771                (settings_dir, LocalSettingsKind::Debug)
 772            } else if path.ends_with(local_vscode_launch_file_relative_path()) {
 773                let settings_dir = Arc::<Path>::from(
 774                    path.ancestors()
 775                        .nth(
 776                            local_vscode_tasks_file_relative_path()
 777                                .components()
 778                                .count()
 779                                .saturating_sub(1),
 780                        )
 781                        .unwrap(),
 782                );
 783                (settings_dir, LocalSettingsKind::Debug)
 784            } else if path.ends_with(EDITORCONFIG_NAME) {
 785                let Some(settings_dir) = path.parent().map(Arc::from) else {
 786                    continue;
 787                };
 788                (settings_dir, LocalSettingsKind::Editorconfig)
 789            } else {
 790                continue;
 791            };
 792
 793            let removed = change == &PathChange::Removed;
 794            let fs = fs.clone();
 795            let abs_path = match worktree.read(cx).absolutize(path) {
 796                Ok(abs_path) => abs_path,
 797                Err(e) => {
 798                    log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
 799                    continue;
 800                }
 801            };
 802            settings_contents.push(async move {
 803                (
 804                    settings_dir,
 805                    kind,
 806                    if removed {
 807                        None
 808                    } else {
 809                        Some(
 810                            async move {
 811                                let content = fs.load(&abs_path).await?;
 812                                if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
 813                                    let vscode_tasks =
 814                                        parse_json_with_comments::<VsCodeTaskFile>(&content)
 815                                            .with_context(|| {
 816                                                format!("parsing VSCode tasks, file {abs_path:?}")
 817                                            })?;
 818                                    let zed_tasks = TaskTemplates::try_from(vscode_tasks)
 819                                        .with_context(|| {
 820                                            format!(
 821                                        "converting VSCode tasks into Zed ones, file {abs_path:?}"
 822                                    )
 823                                        })?;
 824                                    serde_json::to_string(&zed_tasks).with_context(|| {
 825                                        format!(
 826                                            "serializing Zed tasks into JSON, file {abs_path:?}"
 827                                        )
 828                                    })
 829                                } else if abs_path.ends_with(local_vscode_launch_file_relative_path()) {
 830                                    let vscode_tasks =
 831                                        parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
 832                                            .with_context(|| {
 833                                                format!("parsing VSCode debug tasks, file {abs_path:?}")
 834                                            })?;
 835                                    let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
 836                                        .with_context(|| {
 837                                            format!(
 838                                        "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
 839                                    )
 840                                        })?;
 841                                    serde_json::to_string(&zed_tasks).with_context(|| {
 842                                        format!(
 843                                            "serializing Zed tasks into JSON, file {abs_path:?}"
 844                                        )
 845                                    })
 846                                } else {
 847                                    Ok(content)
 848                                }
 849                            }
 850                            .await,
 851                        )
 852                    },
 853                )
 854            });
 855        }
 856
 857        if settings_contents.is_empty() {
 858            return;
 859        }
 860
 861        let worktree = worktree.clone();
 862        cx.spawn(async move |this, cx| {
 863            let settings_contents: Vec<(Arc<Path>, _, _)> =
 864                futures::future::join_all(settings_contents).await;
 865            cx.update(|cx| {
 866                this.update(cx, |this, cx| {
 867                    this.update_settings(
 868                        worktree,
 869                        settings_contents.into_iter().map(|(path, kind, content)| {
 870                            (path, kind, content.and_then(|c| c.log_err()))
 871                        }),
 872                        cx,
 873                    )
 874                })
 875            })
 876        })
 877        .detach();
 878    }
 879
 880    fn update_settings(
 881        &mut self,
 882        worktree: Entity<Worktree>,
 883        settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
 884        cx: &mut Context<Self>,
 885    ) {
 886        let worktree_id = worktree.read(cx).id();
 887        let remote_worktree_id = worktree.read(cx).id();
 888        let task_store = self.task_store.clone();
 889
 890        for (directory, kind, file_content) in settings_contents {
 891            match kind {
 892                LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
 893                    .update_global::<SettingsStore, _>(|store, cx| {
 894                        let result = store.set_local_settings(
 895                            worktree_id,
 896                            directory.clone(),
 897                            kind,
 898                            file_content.as_deref(),
 899                            cx,
 900                        );
 901
 902                        match result {
 903                            Err(InvalidSettingsError::LocalSettings { path, message }) => {
 904                                log::error!("Failed to set local settings in {path:?}: {message}");
 905                                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
 906                                    InvalidSettingsError::LocalSettings { path, message },
 907                                )));
 908                            }
 909                            Err(e) => {
 910                                log::error!("Failed to set local settings: {e}");
 911                            }
 912                            Ok(()) => {
 913                                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(
 914                                    directory.join(local_settings_file_relative_path())
 915                                )));
 916                            }
 917                        }
 918                    }),
 919                LocalSettingsKind::Tasks => {
 920                    let result = task_store.update(cx, |task_store, cx| {
 921                        task_store.update_user_tasks(
 922                            TaskSettingsLocation::Worktree(SettingsLocation {
 923                                worktree_id,
 924                                path: directory.as_ref(),
 925                            }),
 926                            file_content.as_deref(),
 927                            cx,
 928                        )
 929                    });
 930
 931                    match result {
 932                        Err(InvalidSettingsError::Tasks { path, message }) => {
 933                            log::error!("Failed to set local tasks in {path:?}: {message:?}");
 934                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
 935                                InvalidSettingsError::Tasks { path, message },
 936                            )));
 937                        }
 938                        Err(e) => {
 939                            log::error!("Failed to set local tasks: {e}");
 940                        }
 941                        Ok(()) => {
 942                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
 943                                directory.join(task_file_name())
 944                            )));
 945                        }
 946                    }
 947                }
 948                LocalSettingsKind::Debug => {
 949                    let result = task_store.update(cx, |task_store, cx| {
 950                        task_store.update_user_debug_scenarios(
 951                            TaskSettingsLocation::Worktree(SettingsLocation {
 952                                worktree_id,
 953                                path: directory.as_ref(),
 954                            }),
 955                            file_content.as_deref(),
 956                            cx,
 957                        )
 958                    });
 959
 960                    match result {
 961                        Err(InvalidSettingsError::Debug { path, message }) => {
 962                            log::error!(
 963                                "Failed to set local debug scenarios in {path:?}: {message:?}"
 964                            );
 965                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
 966                                InvalidSettingsError::Debug { path, message },
 967                            )));
 968                        }
 969                        Err(e) => {
 970                            log::error!("Failed to set local tasks: {e}");
 971                        }
 972                        Ok(()) => {
 973                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
 974                                directory.join(task_file_name())
 975                            )));
 976                        }
 977                    }
 978                }
 979            };
 980
 981            if let Some(downstream_client) = &self.downstream_client {
 982                downstream_client
 983                    .send(proto::UpdateWorktreeSettings {
 984                        project_id: self.project_id,
 985                        worktree_id: remote_worktree_id.to_proto(),
 986                        path: directory.to_proto(),
 987                        content: file_content,
 988                        kind: Some(local_settings_kind_to_proto(kind).into()),
 989                    })
 990                    .log_err();
 991            }
 992        }
 993    }
 994
 995    fn subscribe_to_global_task_file_changes(
 996        fs: Arc<dyn Fs>,
 997        file_path: PathBuf,
 998        cx: &mut Context<Self>,
 999    ) -> Task<()> {
1000        let mut user_tasks_file_rx =
1001            watch_config_file(&cx.background_executor(), fs, file_path.clone());
1002        let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1003        let weak_entry = cx.weak_entity();
1004        cx.spawn(async move |settings_observer, cx| {
1005            let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1006                settings_observer.task_store.clone()
1007            }) else {
1008                return;
1009            };
1010            if let Some(user_tasks_content) = user_tasks_content {
1011                let Ok(()) = task_store.update(cx, |task_store, cx| {
1012                    task_store
1013                        .update_user_tasks(
1014                            TaskSettingsLocation::Global(&file_path),
1015                            Some(&user_tasks_content),
1016                            cx,
1017                        )
1018                        .log_err();
1019                }) else {
1020                    return;
1021                };
1022            }
1023            while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1024                let Ok(result) = task_store.update(cx, |task_store, cx| {
1025                    task_store.update_user_tasks(
1026                        TaskSettingsLocation::Global(&file_path),
1027                        Some(&user_tasks_content),
1028                        cx,
1029                    )
1030                }) else {
1031                    break;
1032                };
1033
1034                weak_entry
1035                    .update(cx, |_, cx| match result {
1036                        Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1037                            file_path.clone()
1038                        ))),
1039                        Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1040                            InvalidSettingsError::Tasks {
1041                                path: file_path.clone(),
1042                                message: err.to_string(),
1043                            },
1044                        ))),
1045                    })
1046                    .ok();
1047            }
1048        })
1049    }
1050}
1051
1052pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
1053    match kind {
1054        proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
1055        proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
1056        proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
1057        proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
1058    }
1059}
1060
1061pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
1062    match kind {
1063        LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
1064        LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
1065        LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
1066        LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
1067    }
1068}