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
 368impl GitSettings {
 369    /// Returns whether git status indicators should be shown.
 370    /// This includes status in the project panel, outline panel, and tabs.
 371    pub fn is_status_enabled(&self) -> bool {
 372        self.enabled.status
 373    }
 374
 375    /// Returns whether git diff features should be shown.
 376    /// This includes gutter diff indicators and scrollbar diff markers.
 377    pub fn is_diff_enabled(&self) -> bool {
 378        self.enabled.diff
 379    }
 380
 381    /// Returns whether the git gutter should be shown.
 382    /// This checks both the global diff setting and the gutter-specific setting.
 383    pub fn is_gutter_enabled(&self) -> bool {
 384        self.is_diff_enabled()
 385            && matches!(self.git_gutter, settings::GitGutterSetting::TrackedFiles)
 386    }
 387}
 388
 389#[derive(Clone, Copy, Debug)]
 390pub struct GitEnabledSettings {
 391    /// Whether git integration is enabled for showing git status.
 392    ///
 393    /// Default: true
 394    pub status: bool,
 395    /// Whether git integration is enabled for showing diffs.
 396    ///
 397    /// Default: true
 398    pub diff: bool,
 399}
 400
 401#[derive(Clone, Copy, Debug, PartialEq, Default)]
 402pub enum GitPathStyle {
 403    #[default]
 404    FileNameFirst,
 405    FilePathFirst,
 406}
 407
 408impl From<settings::GitPathStyle> for GitPathStyle {
 409    fn from(style: settings::GitPathStyle) -> Self {
 410        match style {
 411            settings::GitPathStyle::FileNameFirst => GitPathStyle::FileNameFirst,
 412            settings::GitPathStyle::FilePathFirst => GitPathStyle::FilePathFirst,
 413        }
 414    }
 415}
 416
 417#[derive(Clone, Copy, Debug)]
 418pub struct InlineBlameSettings {
 419    /// Whether or not to show git blame data inline in
 420    /// the currently focused line.
 421    ///
 422    /// Default: true
 423    pub enabled: bool,
 424    /// Whether to only show the inline blame information
 425    /// after a delay once the cursor stops moving.
 426    ///
 427    /// Default: 0
 428    pub delay_ms: settings::DelayMs,
 429    /// The amount of padding between the end of the source line and the start
 430    /// of the inline blame in units of columns.
 431    ///
 432    /// Default: 7
 433    pub padding: u32,
 434    /// The minimum column number to show the inline blame information at
 435    ///
 436    /// Default: 0
 437    pub min_column: u32,
 438    /// Whether to show commit summary as part of the inline blame.
 439    ///
 440    /// Default: false
 441    pub show_commit_summary: bool,
 442}
 443
 444#[derive(Clone, Copy, Debug)]
 445pub struct BlameSettings {
 446    /// Whether to show the avatar of the author of the commit.
 447    ///
 448    /// Default: true
 449    pub show_avatar: bool,
 450}
 451
 452impl GitSettings {
 453    pub fn inline_blame_delay(&self) -> Option<Duration> {
 454        if self.inline_blame.delay_ms.0 > 0 {
 455            Some(Duration::from_millis(self.inline_blame.delay_ms.0))
 456        } else {
 457            None
 458        }
 459    }
 460}
 461
 462#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
 463#[serde(rename_all = "snake_case")]
 464pub struct BranchPickerSettings {
 465    /// Whether to show author name as part of the commit information.
 466    ///
 467    /// Default: false
 468    #[serde(default)]
 469    pub show_author_name: bool,
 470}
 471
 472impl Default for BranchPickerSettings {
 473    fn default() -> Self {
 474        Self {
 475            show_author_name: true,
 476        }
 477    }
 478}
 479
 480#[derive(Clone, Debug)]
 481pub struct DiagnosticsSettings {
 482    /// Whether to show the project diagnostics button in the status bar.
 483    pub button: bool,
 484
 485    /// Whether or not to include warning diagnostics.
 486    pub include_warnings: bool,
 487
 488    /// Settings for using LSP pull diagnostics mechanism in Zed.
 489    pub lsp_pull_diagnostics: LspPullDiagnosticsSettings,
 490
 491    /// Settings for showing inline diagnostics.
 492    pub inline: InlineDiagnosticsSettings,
 493}
 494
 495#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 496pub struct InlineDiagnosticsSettings {
 497    /// Whether or not to show inline diagnostics
 498    ///
 499    /// Default: false
 500    pub enabled: bool,
 501    /// Whether to only show the inline diagnostics after a delay after the
 502    /// last editor event.
 503    ///
 504    /// Default: 150
 505    pub update_debounce_ms: u64,
 506    /// The amount of padding between the end of the source line and the start
 507    /// of the inline diagnostic in units of columns.
 508    ///
 509    /// Default: 4
 510    pub padding: u32,
 511    /// The minimum column to display inline diagnostics. This setting can be
 512    /// used to horizontally align inline diagnostics at some position. Lines
 513    /// longer than this value will still push diagnostics further to the right.
 514    ///
 515    /// Default: 0
 516    pub min_column: u32,
 517
 518    pub max_severity: Option<DiagnosticSeverity>,
 519}
 520
 521#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 522pub struct LspPullDiagnosticsSettings {
 523    /// Whether to pull for diagnostics or not.
 524    ///
 525    /// Default: true
 526    pub enabled: bool,
 527    /// Minimum time to wait before pulling diagnostics from the language server(s).
 528    /// 0 turns the debounce off.
 529    ///
 530    /// Default: 50
 531    pub debounce_ms: u64,
 532}
 533
 534impl Settings for ProjectSettings {
 535    fn from_settings(content: &settings::SettingsContent) -> Self {
 536        let project = &content.project.clone();
 537        let diagnostics = content.diagnostics.as_ref().unwrap();
 538        let lsp_pull_diagnostics = diagnostics.lsp_pull_diagnostics.as_ref().unwrap();
 539        let inline_diagnostics = diagnostics.inline.as_ref().unwrap();
 540
 541        let git = content.git.as_ref().unwrap();
 542        let git_enabled = {
 543            GitEnabledSettings {
 544                status: git.enabled.as_ref().unwrap().is_git_status_enabled(),
 545                diff: git.enabled.as_ref().unwrap().is_git_diff_enabled(),
 546            }
 547        };
 548        let git_settings = GitSettings {
 549            enabled: git_enabled,
 550            git_gutter: git.git_gutter.unwrap(),
 551            gutter_debounce: git.gutter_debounce.unwrap_or_default(),
 552            inline_blame: {
 553                let inline = git.inline_blame.unwrap();
 554                InlineBlameSettings {
 555                    enabled: inline.enabled.unwrap(),
 556                    delay_ms: inline.delay_ms.unwrap(),
 557                    padding: inline.padding.unwrap(),
 558                    min_column: inline.min_column.unwrap(),
 559                    show_commit_summary: inline.show_commit_summary.unwrap(),
 560                }
 561            },
 562            blame: {
 563                let blame = git.blame.unwrap();
 564                BlameSettings {
 565                    show_avatar: blame.show_avatar.unwrap(),
 566                }
 567            },
 568            branch_picker: {
 569                let branch_picker = git.branch_picker.unwrap();
 570                BranchPickerSettings {
 571                    show_author_name: branch_picker.show_author_name.unwrap(),
 572                }
 573            },
 574            hunk_style: git.hunk_style.unwrap(),
 575            path_style: git.path_style.unwrap().into(),
 576        };
 577        Self {
 578            context_servers: project
 579                .context_servers
 580                .clone()
 581                .into_iter()
 582                .map(|(key, value)| (key, value.into()))
 583                .collect(),
 584            lsp: project
 585                .lsp
 586                .clone()
 587                .into_iter()
 588                .map(|(key, value)| (LanguageServerName(key.into()), value))
 589                .collect(),
 590            global_lsp_settings: GlobalLspSettings {
 591                button: content
 592                    .global_lsp_settings
 593                    .as_ref()
 594                    .unwrap()
 595                    .button
 596                    .unwrap(),
 597            },
 598            dap: project
 599                .dap
 600                .clone()
 601                .into_iter()
 602                .map(|(key, value)| (DebugAdapterName(key.into()), DapSettings::from(value)))
 603                .collect(),
 604            diagnostics: DiagnosticsSettings {
 605                button: diagnostics.button.unwrap(),
 606                include_warnings: diagnostics.include_warnings.unwrap(),
 607                lsp_pull_diagnostics: LspPullDiagnosticsSettings {
 608                    enabled: lsp_pull_diagnostics.enabled.unwrap(),
 609                    debounce_ms: lsp_pull_diagnostics.debounce_ms.unwrap().0,
 610                },
 611                inline: InlineDiagnosticsSettings {
 612                    enabled: inline_diagnostics.enabled.unwrap(),
 613                    update_debounce_ms: inline_diagnostics.update_debounce_ms.unwrap().0,
 614                    padding: inline_diagnostics.padding.unwrap(),
 615                    min_column: inline_diagnostics.min_column.unwrap(),
 616                    max_severity: inline_diagnostics.max_severity.map(Into::into),
 617                },
 618            },
 619            git: git_settings,
 620            node: content.node.clone().unwrap().into(),
 621            load_direnv: project.load_direnv.clone().unwrap(),
 622            session: SessionSettings {
 623                restore_unsaved_buffers: content.session.unwrap().restore_unsaved_buffers.unwrap(),
 624                trust_all_worktrees: content.session.unwrap().trust_all_worktrees.unwrap(),
 625            },
 626        }
 627    }
 628}
 629
 630pub enum SettingsObserverMode {
 631    Local(Arc<dyn Fs>),
 632    Remote,
 633}
 634
 635#[derive(Clone, Debug, PartialEq)]
 636pub enum SettingsObserverEvent {
 637    LocalSettingsUpdated(Result<PathBuf, InvalidSettingsError>),
 638    LocalTasksUpdated(Result<PathBuf, InvalidSettingsError>),
 639    LocalDebugScenariosUpdated(Result<PathBuf, InvalidSettingsError>),
 640}
 641
 642impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
 643
 644pub struct SettingsObserver {
 645    mode: SettingsObserverMode,
 646    downstream_client: Option<AnyProtoClient>,
 647    worktree_store: Entity<WorktreeStore>,
 648    project_id: u64,
 649    task_store: Entity<TaskStore>,
 650    pending_local_settings:
 651        HashMap<PathTrust, BTreeMap<(WorktreeId, Arc<RelPath>), Option<String>>>,
 652    _trusted_worktrees_watcher: Option<Subscription>,
 653    _user_settings_watcher: Option<Subscription>,
 654    _global_task_config_watcher: Task<()>,
 655    _global_debug_config_watcher: Task<()>,
 656}
 657
 658/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
 659/// (or the equivalent protobuf messages from upstream) and updates local settings
 660/// and sends notifications downstream.
 661/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
 662/// upstream.
 663impl SettingsObserver {
 664    pub fn init(client: &AnyProtoClient) {
 665        client.add_entity_message_handler(Self::handle_update_worktree_settings);
 666        client.add_entity_message_handler(Self::handle_update_user_settings);
 667    }
 668
 669    pub fn new_local(
 670        fs: Arc<dyn Fs>,
 671        worktree_store: Entity<WorktreeStore>,
 672        task_store: Entity<TaskStore>,
 673        cx: &mut Context<Self>,
 674    ) -> Self {
 675        cx.subscribe(&worktree_store, Self::on_worktree_store_event)
 676            .detach();
 677
 678        let _trusted_worktrees_watcher =
 679            TrustedWorktrees::try_get_global(cx).map(|trusted_worktrees| {
 680                cx.subscribe(
 681                    &trusted_worktrees,
 682                    move |settings_observer, _, e, cx| match e {
 683                        TrustedWorktreesEvent::Trusted(_, trusted_paths) => {
 684                            for trusted_path in trusted_paths {
 685                                if let Some(pending_local_settings) = settings_observer
 686                                    .pending_local_settings
 687                                    .remove(trusted_path)
 688                                {
 689                                    for ((worktree_id, directory_path), settings_contents) in
 690                                        pending_local_settings
 691                                    {
 692                                        apply_local_settings(
 693                                            worktree_id,
 694                                            &directory_path,
 695                                            LocalSettingsKind::Settings,
 696                                            &settings_contents,
 697                                            cx,
 698                                        );
 699                                        if let Some(downstream_client) =
 700                                            &settings_observer.downstream_client
 701                                        {
 702                                            downstream_client
 703                                                .send(proto::UpdateWorktreeSettings {
 704                                                    project_id: settings_observer.project_id,
 705                                                    worktree_id: worktree_id.to_proto(),
 706                                                    path: directory_path.to_proto(),
 707                                                    content: settings_contents,
 708                                                    kind: Some(
 709                                                        local_settings_kind_to_proto(
 710                                                            LocalSettingsKind::Settings,
 711                                                        )
 712                                                        .into(),
 713                                                    ),
 714                                                })
 715                                                .log_err();
 716                                        }
 717                                    }
 718                                }
 719                            }
 720                        }
 721                        TrustedWorktreesEvent::Restricted(..) => {}
 722                    },
 723                )
 724            });
 725
 726        Self {
 727            worktree_store,
 728            task_store,
 729            mode: SettingsObserverMode::Local(fs.clone()),
 730            downstream_client: None,
 731            _trusted_worktrees_watcher,
 732            pending_local_settings: HashMap::default(),
 733            _user_settings_watcher: None,
 734            project_id: REMOTE_SERVER_PROJECT_ID,
 735            _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
 736                fs.clone(),
 737                paths::tasks_file().clone(),
 738                cx,
 739            ),
 740            _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
 741                fs.clone(),
 742                paths::debug_scenarios_file().clone(),
 743                cx,
 744            ),
 745        }
 746    }
 747
 748    pub fn new_remote(
 749        fs: Arc<dyn Fs>,
 750        worktree_store: Entity<WorktreeStore>,
 751        task_store: Entity<TaskStore>,
 752        upstream_client: Option<AnyProtoClient>,
 753        cx: &mut Context<Self>,
 754    ) -> Self {
 755        let mut user_settings_watcher = None;
 756        if cx.try_global::<SettingsStore>().is_some() {
 757            if let Some(upstream_client) = upstream_client {
 758                let mut user_settings = None;
 759                user_settings_watcher = Some(cx.observe_global::<SettingsStore>(move |_, cx| {
 760                    if let Some(new_settings) = cx.global::<SettingsStore>().raw_user_settings() {
 761                        if Some(new_settings) != user_settings.as_ref() {
 762                            if let Some(new_settings_string) =
 763                                serde_json::to_string(new_settings).ok()
 764                            {
 765                                user_settings = Some(new_settings.clone());
 766                                upstream_client
 767                                    .send(proto::UpdateUserSettings {
 768                                        project_id: REMOTE_SERVER_PROJECT_ID,
 769                                        contents: new_settings_string,
 770                                    })
 771                                    .log_err();
 772                            }
 773                        }
 774                    }
 775                }));
 776            }
 777        };
 778
 779        Self {
 780            worktree_store,
 781            task_store,
 782            mode: SettingsObserverMode::Remote,
 783            downstream_client: None,
 784            project_id: REMOTE_SERVER_PROJECT_ID,
 785            _trusted_worktrees_watcher: None,
 786            pending_local_settings: HashMap::default(),
 787            _user_settings_watcher: user_settings_watcher,
 788            _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
 789                fs.clone(),
 790                paths::tasks_file().clone(),
 791                cx,
 792            ),
 793            _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
 794                fs.clone(),
 795                paths::debug_scenarios_file().clone(),
 796                cx,
 797            ),
 798        }
 799    }
 800
 801    pub fn shared(
 802        &mut self,
 803        project_id: u64,
 804        downstream_client: AnyProtoClient,
 805        cx: &mut Context<Self>,
 806    ) {
 807        self.project_id = project_id;
 808        self.downstream_client = Some(downstream_client.clone());
 809
 810        let store = cx.global::<SettingsStore>();
 811        for worktree in self.worktree_store.read(cx).worktrees() {
 812            let worktree_id = worktree.read(cx).id().to_proto();
 813            for (path, content) in store.local_settings(worktree.read(cx).id()) {
 814                let content = serde_json::to_string(&content).unwrap();
 815                downstream_client
 816                    .send(proto::UpdateWorktreeSettings {
 817                        project_id,
 818                        worktree_id,
 819                        path: path.to_proto(),
 820                        content: Some(content),
 821                        kind: Some(
 822                            local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
 823                        ),
 824                    })
 825                    .log_err();
 826            }
 827            for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
 828                downstream_client
 829                    .send(proto::UpdateWorktreeSettings {
 830                        project_id,
 831                        worktree_id,
 832                        path: path.to_proto(),
 833                        content: Some(content),
 834                        kind: Some(
 835                            local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
 836                        ),
 837                    })
 838                    .log_err();
 839            }
 840        }
 841    }
 842
 843    pub fn unshared(&mut self, _: &mut Context<Self>) {
 844        self.downstream_client = None;
 845    }
 846
 847    async fn handle_update_worktree_settings(
 848        this: Entity<Self>,
 849        envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
 850        mut cx: AsyncApp,
 851    ) -> anyhow::Result<()> {
 852        let kind = match envelope.payload.kind {
 853            Some(kind) => proto::LocalSettingsKind::from_i32(kind)
 854                .with_context(|| format!("unknown kind {kind}"))?,
 855            None => proto::LocalSettingsKind::Settings,
 856        };
 857        let path = RelPath::from_proto(&envelope.payload.path)?;
 858        this.update(&mut cx, |this, cx| {
 859            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 860            let Some(worktree) = this
 861                .worktree_store
 862                .read(cx)
 863                .worktree_for_id(worktree_id, cx)
 864            else {
 865                return;
 866            };
 867
 868            this.update_settings(
 869                worktree,
 870                [(
 871                    path,
 872                    local_settings_kind_from_proto(kind),
 873                    envelope.payload.content,
 874                )],
 875                cx,
 876            );
 877        })?;
 878        Ok(())
 879    }
 880
 881    async fn handle_update_user_settings(
 882        _: Entity<Self>,
 883        envelope: TypedEnvelope<proto::UpdateUserSettings>,
 884        cx: AsyncApp,
 885    ) -> anyhow::Result<()> {
 886        cx.update_global(|settings_store: &mut SettingsStore, cx| {
 887            settings_store
 888                .set_user_settings(&envelope.payload.contents, cx)
 889                .result()
 890                .context("setting new user settings")?;
 891            anyhow::Ok(())
 892        })??;
 893        Ok(())
 894    }
 895
 896    fn on_worktree_store_event(
 897        &mut self,
 898        _: Entity<WorktreeStore>,
 899        event: &WorktreeStoreEvent,
 900        cx: &mut Context<Self>,
 901    ) {
 902        match event {
 903            WorktreeStoreEvent::WorktreeAdded(worktree) => cx
 904                .subscribe(worktree, |this, worktree, event, cx| {
 905                    if let worktree::Event::UpdatedEntries(changes) = event {
 906                        this.update_local_worktree_settings(&worktree, changes, cx)
 907                    }
 908                })
 909                .detach(),
 910            WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => {
 911                cx.update_global::<SettingsStore, _>(|store, cx| {
 912                    store.clear_local_settings(*worktree_id, cx).log_err();
 913                });
 914            }
 915            _ => {}
 916        }
 917    }
 918
 919    fn update_local_worktree_settings(
 920        &mut self,
 921        worktree: &Entity<Worktree>,
 922        changes: &UpdatedEntriesSet,
 923        cx: &mut Context<Self>,
 924    ) {
 925        let SettingsObserverMode::Local(fs) = &self.mode else {
 926            return;
 927        };
 928
 929        let mut settings_contents = Vec::new();
 930        for (path, _, change) in changes.iter() {
 931            let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
 932                let settings_dir = path
 933                    .ancestors()
 934                    .nth(local_settings_file_relative_path().components().count())
 935                    .unwrap()
 936                    .into();
 937                (settings_dir, LocalSettingsKind::Settings)
 938            } else if path.ends_with(local_tasks_file_relative_path()) {
 939                let settings_dir = path
 940                    .ancestors()
 941                    .nth(
 942                        local_tasks_file_relative_path()
 943                            .components()
 944                            .count()
 945                            .saturating_sub(1),
 946                    )
 947                    .unwrap()
 948                    .into();
 949                (settings_dir, LocalSettingsKind::Tasks)
 950            } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
 951                let settings_dir = path
 952                    .ancestors()
 953                    .nth(
 954                        local_vscode_tasks_file_relative_path()
 955                            .components()
 956                            .count()
 957                            .saturating_sub(1),
 958                    )
 959                    .unwrap()
 960                    .into();
 961                (settings_dir, LocalSettingsKind::Tasks)
 962            } else if path.ends_with(local_debug_file_relative_path()) {
 963                let settings_dir = path
 964                    .ancestors()
 965                    .nth(
 966                        local_debug_file_relative_path()
 967                            .components()
 968                            .count()
 969                            .saturating_sub(1),
 970                    )
 971                    .unwrap()
 972                    .into();
 973                (settings_dir, LocalSettingsKind::Debug)
 974            } else if path.ends_with(local_vscode_launch_file_relative_path()) {
 975                let settings_dir = path
 976                    .ancestors()
 977                    .nth(
 978                        local_vscode_tasks_file_relative_path()
 979                            .components()
 980                            .count()
 981                            .saturating_sub(1),
 982                    )
 983                    .unwrap()
 984                    .into();
 985                (settings_dir, LocalSettingsKind::Debug)
 986            } else if path.ends_with(RelPath::unix(EDITORCONFIG_NAME).unwrap()) {
 987                let Some(settings_dir) = path.parent().map(Arc::from) else {
 988                    continue;
 989                };
 990                (settings_dir, LocalSettingsKind::Editorconfig)
 991            } else {
 992                continue;
 993            };
 994
 995            let removed = change == &PathChange::Removed;
 996            let fs = fs.clone();
 997            let abs_path = worktree.read(cx).absolutize(path);
 998            settings_contents.push(async move {
 999                (
1000                    settings_dir,
1001                    kind,
1002                    if removed {
1003                        None
1004                    } else {
1005                        Some(
1006                            async move {
1007                                let content = fs.load(&abs_path).await?;
1008                                if abs_path.ends_with(local_vscode_tasks_file_relative_path().as_std_path()) {
1009                                    let vscode_tasks =
1010                                        parse_json_with_comments::<VsCodeTaskFile>(&content)
1011                                            .with_context(|| {
1012                                                format!("parsing VSCode tasks, file {abs_path:?}")
1013                                            })?;
1014                                    let zed_tasks = TaskTemplates::try_from(vscode_tasks)
1015                                        .with_context(|| {
1016                                            format!(
1017                                        "converting VSCode tasks into Zed ones, file {abs_path:?}"
1018                                    )
1019                                        })?;
1020                                    serde_json::to_string(&zed_tasks).with_context(|| {
1021                                        format!(
1022                                            "serializing Zed tasks into JSON, file {abs_path:?}"
1023                                        )
1024                                    })
1025                                } else if abs_path.ends_with(local_vscode_launch_file_relative_path().as_std_path()) {
1026                                    let vscode_tasks =
1027                                        parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
1028                                            .with_context(|| {
1029                                                format!("parsing VSCode debug tasks, file {abs_path:?}")
1030                                            })?;
1031                                    let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
1032                                        .with_context(|| {
1033                                            format!(
1034                                        "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
1035                                    )
1036                                        })?;
1037                                    serde_json::to_string(&zed_tasks).with_context(|| {
1038                                        format!(
1039                                            "serializing Zed tasks into JSON, file {abs_path:?}"
1040                                        )
1041                                    })
1042                                } else {
1043                                    Ok(content)
1044                                }
1045                            }
1046                            .await,
1047                        )
1048                    },
1049                )
1050            });
1051        }
1052
1053        if settings_contents.is_empty() {
1054            return;
1055        }
1056
1057        let worktree = worktree.clone();
1058        cx.spawn(async move |this, cx| {
1059            let settings_contents: Vec<(Arc<RelPath>, _, _)> =
1060                futures::future::join_all(settings_contents).await;
1061            cx.update(|cx| {
1062                this.update(cx, |this, cx| {
1063                    this.update_settings(
1064                        worktree,
1065                        settings_contents.into_iter().map(|(path, kind, content)| {
1066                            (path, kind, content.and_then(|c| c.log_err()))
1067                        }),
1068                        cx,
1069                    )
1070                })
1071            })
1072        })
1073        .detach();
1074    }
1075
1076    fn update_settings(
1077        &mut self,
1078        worktree: Entity<Worktree>,
1079        settings_contents: impl IntoIterator<Item = (Arc<RelPath>, LocalSettingsKind, Option<String>)>,
1080        cx: &mut Context<Self>,
1081    ) {
1082        let worktree_id = worktree.read(cx).id();
1083        let remote_worktree_id = worktree.read(cx).id();
1084        let task_store = self.task_store.clone();
1085        let can_trust_worktree = OnceCell::new();
1086        for (directory, kind, file_content) in settings_contents {
1087            let mut applied = true;
1088            match kind {
1089                LocalSettingsKind::Settings => {
1090                    if *can_trust_worktree.get_or_init(|| {
1091                        if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1092                            trusted_worktrees.update(cx, |trusted_worktrees, cx| {
1093                                trusted_worktrees.can_trust(worktree_id, cx)
1094                            })
1095                        } else {
1096                            true
1097                        }
1098                    }) {
1099                        apply_local_settings(worktree_id, &directory, kind, &file_content, cx)
1100                    } else {
1101                        applied = false;
1102                        self.pending_local_settings
1103                            .entry(PathTrust::Worktree(worktree_id))
1104                            .or_default()
1105                            .insert((worktree_id, directory.clone()), file_content.clone());
1106                    }
1107                }
1108                LocalSettingsKind::Editorconfig => {
1109                    apply_local_settings(worktree_id, &directory, kind, &file_content, cx)
1110                }
1111                LocalSettingsKind::Tasks => {
1112                    let result = task_store.update(cx, |task_store, cx| {
1113                        task_store.update_user_tasks(
1114                            TaskSettingsLocation::Worktree(SettingsLocation {
1115                                worktree_id,
1116                                path: directory.as_ref(),
1117                            }),
1118                            file_content.as_deref(),
1119                            cx,
1120                        )
1121                    });
1122
1123                    match result {
1124                        Err(InvalidSettingsError::Tasks { path, message }) => {
1125                            log::error!("Failed to set local tasks in {path:?}: {message:?}");
1126                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1127                                InvalidSettingsError::Tasks { path, message },
1128                            )));
1129                        }
1130                        Err(e) => {
1131                            log::error!("Failed to set local tasks: {e}");
1132                        }
1133                        Ok(()) => {
1134                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(directory
1135                                .as_std_path()
1136                                .join(task_file_name()))));
1137                        }
1138                    }
1139                }
1140                LocalSettingsKind::Debug => {
1141                    let result = task_store.update(cx, |task_store, cx| {
1142                        task_store.update_user_debug_scenarios(
1143                            TaskSettingsLocation::Worktree(SettingsLocation {
1144                                worktree_id,
1145                                path: directory.as_ref(),
1146                            }),
1147                            file_content.as_deref(),
1148                            cx,
1149                        )
1150                    });
1151
1152                    match result {
1153                        Err(InvalidSettingsError::Debug { path, message }) => {
1154                            log::error!(
1155                                "Failed to set local debug scenarios in {path:?}: {message:?}"
1156                            );
1157                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1158                                InvalidSettingsError::Debug { path, message },
1159                            )));
1160                        }
1161                        Err(e) => {
1162                            log::error!("Failed to set local tasks: {e}");
1163                        }
1164                        Ok(()) => {
1165                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(directory
1166                                .as_std_path()
1167                                .join(task_file_name()))));
1168                        }
1169                    }
1170                }
1171            };
1172
1173            if applied {
1174                if let Some(downstream_client) = &self.downstream_client {
1175                    downstream_client
1176                        .send(proto::UpdateWorktreeSettings {
1177                            project_id: self.project_id,
1178                            worktree_id: remote_worktree_id.to_proto(),
1179                            path: directory.to_proto(),
1180                            content: file_content.clone(),
1181                            kind: Some(local_settings_kind_to_proto(kind).into()),
1182                        })
1183                        .log_err();
1184                }
1185            }
1186        }
1187    }
1188
1189    fn subscribe_to_global_task_file_changes(
1190        fs: Arc<dyn Fs>,
1191        file_path: PathBuf,
1192        cx: &mut Context<Self>,
1193    ) -> Task<()> {
1194        let mut user_tasks_file_rx =
1195            watch_config_file(cx.background_executor(), fs, file_path.clone());
1196        let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1197        let weak_entry = cx.weak_entity();
1198        cx.spawn(async move |settings_observer, cx| {
1199            let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1200                settings_observer.task_store.clone()
1201            }) else {
1202                return;
1203            };
1204            if let Some(user_tasks_content) = user_tasks_content {
1205                let Ok(()) = task_store.update(cx, |task_store, cx| {
1206                    task_store
1207                        .update_user_tasks(
1208                            TaskSettingsLocation::Global(&file_path),
1209                            Some(&user_tasks_content),
1210                            cx,
1211                        )
1212                        .log_err();
1213                }) else {
1214                    return;
1215                };
1216            }
1217            while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1218                let Ok(result) = task_store.update(cx, |task_store, cx| {
1219                    task_store.update_user_tasks(
1220                        TaskSettingsLocation::Global(&file_path),
1221                        Some(&user_tasks_content),
1222                        cx,
1223                    )
1224                }) else {
1225                    break;
1226                };
1227
1228                weak_entry
1229                    .update(cx, |_, cx| match result {
1230                        Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1231                            file_path.clone()
1232                        ))),
1233                        Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1234                            InvalidSettingsError::Tasks {
1235                                path: file_path.clone(),
1236                                message: err.to_string(),
1237                            },
1238                        ))),
1239                    })
1240                    .ok();
1241            }
1242        })
1243    }
1244    fn subscribe_to_global_debug_scenarios_changes(
1245        fs: Arc<dyn Fs>,
1246        file_path: PathBuf,
1247        cx: &mut Context<Self>,
1248    ) -> Task<()> {
1249        let mut user_tasks_file_rx =
1250            watch_config_file(cx.background_executor(), fs, file_path.clone());
1251        let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1252        let weak_entry = cx.weak_entity();
1253        cx.spawn(async move |settings_observer, cx| {
1254            let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1255                settings_observer.task_store.clone()
1256            }) else {
1257                return;
1258            };
1259            if let Some(user_tasks_content) = user_tasks_content {
1260                let Ok(()) = task_store.update(cx, |task_store, cx| {
1261                    task_store
1262                        .update_user_debug_scenarios(
1263                            TaskSettingsLocation::Global(&file_path),
1264                            Some(&user_tasks_content),
1265                            cx,
1266                        )
1267                        .log_err();
1268                }) else {
1269                    return;
1270                };
1271            }
1272            while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1273                let Ok(result) = task_store.update(cx, |task_store, cx| {
1274                    task_store.update_user_debug_scenarios(
1275                        TaskSettingsLocation::Global(&file_path),
1276                        Some(&user_tasks_content),
1277                        cx,
1278                    )
1279                }) else {
1280                    break;
1281                };
1282
1283                weak_entry
1284                    .update(cx, |_, cx| match result {
1285                        Ok(()) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(Ok(
1286                            file_path.clone(),
1287                        ))),
1288                        Err(err) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(
1289                            Err(InvalidSettingsError::Tasks {
1290                                path: file_path.clone(),
1291                                message: err.to_string(),
1292                            }),
1293                        )),
1294                    })
1295                    .ok();
1296            }
1297        })
1298    }
1299}
1300
1301fn apply_local_settings(
1302    worktree_id: WorktreeId,
1303    directory: &Arc<RelPath>,
1304    kind: LocalSettingsKind,
1305    file_content: &Option<String>,
1306    cx: &mut Context<'_, SettingsObserver>,
1307) {
1308    cx.update_global::<SettingsStore, _>(|store, cx| {
1309        let result = store.set_local_settings(
1310            worktree_id,
1311            directory.clone(),
1312            kind,
1313            file_content.as_deref(),
1314            cx,
1315        );
1316
1317        match result {
1318            Err(InvalidSettingsError::LocalSettings { path, message }) => {
1319                log::error!("Failed to set local settings in {path:?}: {message}");
1320                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
1321                    InvalidSettingsError::LocalSettings { path, message },
1322                )));
1323            }
1324            Err(e) => log::error!("Failed to set local settings: {e}"),
1325            Ok(()) => cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(directory
1326                .as_std_path()
1327                .join(local_settings_file_relative_path().as_std_path())))),
1328        }
1329    })
1330}
1331
1332pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
1333    match kind {
1334        proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
1335        proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
1336        proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
1337        proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
1338    }
1339}
1340
1341pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
1342    match kind {
1343        LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
1344        LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
1345        LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
1346        LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
1347    }
1348}
1349
1350#[derive(Debug, Clone)]
1351pub struct DapSettings {
1352    pub binary: DapBinary,
1353    pub args: Vec<String>,
1354    pub env: HashMap<String, String>,
1355}
1356
1357impl From<DapSettingsContent> for DapSettings {
1358    fn from(content: DapSettingsContent) -> Self {
1359        DapSettings {
1360            binary: content
1361                .binary
1362                .map_or_else(|| DapBinary::Default, |binary| DapBinary::Custom(binary)),
1363            args: content.args.unwrap_or_default(),
1364            env: content.env.unwrap_or_default(),
1365        }
1366    }
1367}
1368
1369#[derive(Debug, Clone)]
1370pub enum DapBinary {
1371    Default,
1372    Custom(String),
1373}