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