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