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