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