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