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