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