project_settings.rs

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