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