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