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