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 seconds.
  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(60),
 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 { via_collab: bool },
 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        via_collab: bool,
 743        cx: &mut Context<Self>,
 744    ) -> Self {
 745        let mut user_settings_watcher = None;
 746        if cx.try_global::<SettingsStore>().is_some() {
 747            if let Some(upstream_client) = upstream_client {
 748                let mut user_settings = None;
 749                user_settings_watcher = Some(cx.observe_global::<SettingsStore>(move |_, cx| {
 750                    if let Some(new_settings) = cx.global::<SettingsStore>().raw_user_settings() {
 751                        if Some(new_settings) != user_settings.as_ref() {
 752                            if let Some(new_settings_string) =
 753                                serde_json::to_string(new_settings).ok()
 754                            {
 755                                user_settings = Some(new_settings.clone());
 756                                upstream_client
 757                                    .send(proto::UpdateUserSettings {
 758                                        project_id: REMOTE_SERVER_PROJECT_ID,
 759                                        contents: new_settings_string,
 760                                    })
 761                                    .log_err();
 762                            }
 763                        }
 764                    }
 765                }));
 766            }
 767        };
 768
 769        Self {
 770            worktree_store,
 771            task_store,
 772            mode: SettingsObserverMode::Remote { via_collab },
 773            downstream_client: None,
 774            project_id: REMOTE_SERVER_PROJECT_ID,
 775            _trusted_worktrees_watcher: None,
 776            pending_local_settings: HashMap::default(),
 777            _user_settings_watcher: user_settings_watcher,
 778            _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
 779                fs.clone(),
 780                paths::tasks_file().clone(),
 781                cx,
 782            ),
 783            _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
 784                fs.clone(),
 785                paths::debug_scenarios_file().clone(),
 786                cx,
 787            ),
 788        }
 789    }
 790
 791    pub fn shared(
 792        &mut self,
 793        project_id: u64,
 794        downstream_client: AnyProtoClient,
 795        cx: &mut Context<Self>,
 796    ) {
 797        self.project_id = project_id;
 798        self.downstream_client = Some(downstream_client.clone());
 799
 800        let store = cx.global::<SettingsStore>();
 801        for worktree in self.worktree_store.read(cx).worktrees() {
 802            let worktree_id = worktree.read(cx).id().to_proto();
 803            for (path, content) in store.local_settings(worktree.read(cx).id()) {
 804                let content = serde_json::to_string(&content).unwrap();
 805                downstream_client
 806                    .send(proto::UpdateWorktreeSettings {
 807                        project_id,
 808                        worktree_id,
 809                        path: path.to_proto(),
 810                        content: Some(content),
 811                        kind: Some(
 812                            local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
 813                        ),
 814                    })
 815                    .log_err();
 816            }
 817            for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
 818                downstream_client
 819                    .send(proto::UpdateWorktreeSettings {
 820                        project_id,
 821                        worktree_id,
 822                        path: path.to_proto(),
 823                        content: Some(content),
 824                        kind: Some(
 825                            local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
 826                        ),
 827                    })
 828                    .log_err();
 829            }
 830        }
 831    }
 832
 833    pub fn unshared(&mut self, _: &mut Context<Self>) {
 834        self.downstream_client = None;
 835    }
 836
 837    async fn handle_update_worktree_settings(
 838        this: Entity<Self>,
 839        envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
 840        mut cx: AsyncApp,
 841    ) -> anyhow::Result<()> {
 842        let kind = match envelope.payload.kind {
 843            Some(kind) => proto::LocalSettingsKind::from_i32(kind)
 844                .with_context(|| format!("unknown kind {kind}"))?,
 845            None => proto::LocalSettingsKind::Settings,
 846        };
 847        let path = RelPath::from_proto(&envelope.payload.path)?;
 848        this.update(&mut cx, |this, cx| {
 849            let is_via_collab = match &this.mode {
 850                SettingsObserverMode::Local(..) => false,
 851                SettingsObserverMode::Remote { via_collab } => *via_collab,
 852            };
 853            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
 854            let Some(worktree) = this
 855                .worktree_store
 856                .read(cx)
 857                .worktree_for_id(worktree_id, cx)
 858            else {
 859                return;
 860            };
 861
 862            this.update_settings(
 863                worktree,
 864                [(
 865                    path,
 866                    local_settings_kind_from_proto(kind),
 867                    envelope.payload.content,
 868                )],
 869                is_via_collab,
 870                cx,
 871            );
 872        })?;
 873        Ok(())
 874    }
 875
 876    async fn handle_update_user_settings(
 877        _: Entity<Self>,
 878        envelope: TypedEnvelope<proto::UpdateUserSettings>,
 879        cx: AsyncApp,
 880    ) -> anyhow::Result<()> {
 881        cx.update_global(|settings_store: &mut SettingsStore, cx| {
 882            settings_store
 883                .set_user_settings(&envelope.payload.contents, cx)
 884                .result()
 885                .context("setting new user settings")?;
 886            anyhow::Ok(())
 887        })??;
 888        Ok(())
 889    }
 890
 891    fn on_worktree_store_event(
 892        &mut self,
 893        _: Entity<WorktreeStore>,
 894        event: &WorktreeStoreEvent,
 895        cx: &mut Context<Self>,
 896    ) {
 897        match event {
 898            WorktreeStoreEvent::WorktreeAdded(worktree) => cx
 899                .subscribe(worktree, |this, worktree, event, cx| {
 900                    if let worktree::Event::UpdatedEntries(changes) = event {
 901                        this.update_local_worktree_settings(&worktree, changes, cx)
 902                    }
 903                })
 904                .detach(),
 905            WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => {
 906                cx.update_global::<SettingsStore, _>(|store, cx| {
 907                    store.clear_local_settings(*worktree_id, cx).log_err();
 908                });
 909            }
 910            _ => {}
 911        }
 912    }
 913
 914    fn update_local_worktree_settings(
 915        &mut self,
 916        worktree: &Entity<Worktree>,
 917        changes: &UpdatedEntriesSet,
 918        cx: &mut Context<Self>,
 919    ) {
 920        let SettingsObserverMode::Local(fs) = &self.mode else {
 921            return;
 922        };
 923
 924        let mut settings_contents = Vec::new();
 925        for (path, _, change) in changes.iter() {
 926            let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
 927                let settings_dir = path
 928                    .ancestors()
 929                    .nth(local_settings_file_relative_path().components().count())
 930                    .unwrap()
 931                    .into();
 932                (settings_dir, LocalSettingsKind::Settings)
 933            } else if path.ends_with(local_tasks_file_relative_path()) {
 934                let settings_dir = path
 935                    .ancestors()
 936                    .nth(
 937                        local_tasks_file_relative_path()
 938                            .components()
 939                            .count()
 940                            .saturating_sub(1),
 941                    )
 942                    .unwrap()
 943                    .into();
 944                (settings_dir, LocalSettingsKind::Tasks)
 945            } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
 946                let settings_dir = path
 947                    .ancestors()
 948                    .nth(
 949                        local_vscode_tasks_file_relative_path()
 950                            .components()
 951                            .count()
 952                            .saturating_sub(1),
 953                    )
 954                    .unwrap()
 955                    .into();
 956                (settings_dir, LocalSettingsKind::Tasks)
 957            } else if path.ends_with(local_debug_file_relative_path()) {
 958                let settings_dir = path
 959                    .ancestors()
 960                    .nth(
 961                        local_debug_file_relative_path()
 962                            .components()
 963                            .count()
 964                            .saturating_sub(1),
 965                    )
 966                    .unwrap()
 967                    .into();
 968                (settings_dir, LocalSettingsKind::Debug)
 969            } else if path.ends_with(local_vscode_launch_file_relative_path()) {
 970                let settings_dir = path
 971                    .ancestors()
 972                    .nth(
 973                        local_vscode_tasks_file_relative_path()
 974                            .components()
 975                            .count()
 976                            .saturating_sub(1),
 977                    )
 978                    .unwrap()
 979                    .into();
 980                (settings_dir, LocalSettingsKind::Debug)
 981            } else if path.ends_with(RelPath::unix(EDITORCONFIG_NAME).unwrap()) {
 982                let Some(settings_dir) = path.parent().map(Arc::from) else {
 983                    continue;
 984                };
 985                (settings_dir, LocalSettingsKind::Editorconfig)
 986            } else {
 987                continue;
 988            };
 989
 990            let removed = change == &PathChange::Removed;
 991            let fs = fs.clone();
 992            let abs_path = worktree.read(cx).absolutize(path);
 993            settings_contents.push(async move {
 994                (
 995                    settings_dir,
 996                    kind,
 997                    if removed {
 998                        None
 999                    } else {
1000                        Some(
1001                            async move {
1002                                let content = fs.load(&abs_path).await?;
1003                                if abs_path.ends_with(local_vscode_tasks_file_relative_path().as_std_path()) {
1004                                    let vscode_tasks =
1005                                        parse_json_with_comments::<VsCodeTaskFile>(&content)
1006                                            .with_context(|| {
1007                                                format!("parsing VSCode tasks, file {abs_path:?}")
1008                                            })?;
1009                                    let zed_tasks = TaskTemplates::try_from(vscode_tasks)
1010                                        .with_context(|| {
1011                                            format!(
1012                                        "converting VSCode tasks into Zed ones, file {abs_path:?}"
1013                                    )
1014                                        })?;
1015                                    serde_json::to_string(&zed_tasks).with_context(|| {
1016                                        format!(
1017                                            "serializing Zed tasks into JSON, file {abs_path:?}"
1018                                        )
1019                                    })
1020                                } else if abs_path.ends_with(local_vscode_launch_file_relative_path().as_std_path()) {
1021                                    let vscode_tasks =
1022                                        parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
1023                                            .with_context(|| {
1024                                                format!("parsing VSCode debug tasks, file {abs_path:?}")
1025                                            })?;
1026                                    let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
1027                                        .with_context(|| {
1028                                            format!(
1029                                        "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
1030                                    )
1031                                        })?;
1032                                    serde_json::to_string(&zed_tasks).with_context(|| {
1033                                        format!(
1034                                            "serializing Zed tasks into JSON, file {abs_path:?}"
1035                                        )
1036                                    })
1037                                } else {
1038                                    Ok(content)
1039                                }
1040                            }
1041                            .await,
1042                        )
1043                    },
1044                )
1045            });
1046        }
1047
1048        if settings_contents.is_empty() {
1049            return;
1050        }
1051
1052        let worktree = worktree.clone();
1053        cx.spawn(async move |this, cx| {
1054            let settings_contents: Vec<(Arc<RelPath>, _, _)> =
1055                futures::future::join_all(settings_contents).await;
1056            cx.update(|cx| {
1057                this.update(cx, |this, cx| {
1058                    this.update_settings(
1059                        worktree,
1060                        settings_contents.into_iter().map(|(path, kind, content)| {
1061                            (path, kind, content.and_then(|c| c.log_err()))
1062                        }),
1063                        false,
1064                        cx,
1065                    )
1066                })
1067            })
1068        })
1069        .detach();
1070    }
1071
1072    fn update_settings(
1073        &mut self,
1074        worktree: Entity<Worktree>,
1075        settings_contents: impl IntoIterator<Item = (Arc<RelPath>, LocalSettingsKind, Option<String>)>,
1076        is_via_collab: bool,
1077        cx: &mut Context<Self>,
1078    ) {
1079        let worktree_id = worktree.read(cx).id();
1080        let remote_worktree_id = worktree.read(cx).id();
1081        let task_store = self.task_store.clone();
1082        let can_trust_worktree = if is_via_collab {
1083            OnceCell::from(true)
1084        } else {
1085            OnceCell::new()
1086        };
1087        for (directory, kind, file_content) in settings_contents {
1088            let mut applied = true;
1089            match kind {
1090                LocalSettingsKind::Settings => {
1091                    if *can_trust_worktree.get_or_init(|| {
1092                        if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1093                            trusted_worktrees.update(cx, |trusted_worktrees, cx| {
1094                                trusted_worktrees.can_trust(&self.worktree_store, worktree_id, cx)
1095                            })
1096                        } else {
1097                            true
1098                        }
1099                    }) {
1100                        apply_local_settings(worktree_id, &directory, kind, &file_content, cx)
1101                    } else {
1102                        applied = false;
1103                        self.pending_local_settings
1104                            .entry(PathTrust::Worktree(worktree_id))
1105                            .or_default()
1106                            .insert((worktree_id, directory.clone()), file_content.clone());
1107                    }
1108                }
1109                LocalSettingsKind::Editorconfig => {
1110                    apply_local_settings(worktree_id, &directory, kind, &file_content, cx)
1111                }
1112                LocalSettingsKind::Tasks => {
1113                    let result = task_store.update(cx, |task_store, cx| {
1114                        task_store.update_user_tasks(
1115                            TaskSettingsLocation::Worktree(SettingsLocation {
1116                                worktree_id,
1117                                path: directory.as_ref(),
1118                            }),
1119                            file_content.as_deref(),
1120                            cx,
1121                        )
1122                    });
1123
1124                    match result {
1125                        Err(InvalidSettingsError::Tasks { path, message }) => {
1126                            log::error!("Failed to set local tasks in {path:?}: {message:?}");
1127                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1128                                InvalidSettingsError::Tasks { path, message },
1129                            )));
1130                        }
1131                        Err(e) => {
1132                            log::error!("Failed to set local tasks: {e}");
1133                        }
1134                        Ok(()) => {
1135                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(directory
1136                                .as_std_path()
1137                                .join(task_file_name()))));
1138                        }
1139                    }
1140                }
1141                LocalSettingsKind::Debug => {
1142                    let result = task_store.update(cx, |task_store, cx| {
1143                        task_store.update_user_debug_scenarios(
1144                            TaskSettingsLocation::Worktree(SettingsLocation {
1145                                worktree_id,
1146                                path: directory.as_ref(),
1147                            }),
1148                            file_content.as_deref(),
1149                            cx,
1150                        )
1151                    });
1152
1153                    match result {
1154                        Err(InvalidSettingsError::Debug { path, message }) => {
1155                            log::error!(
1156                                "Failed to set local debug scenarios in {path:?}: {message:?}"
1157                            );
1158                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1159                                InvalidSettingsError::Debug { path, message },
1160                            )));
1161                        }
1162                        Err(e) => {
1163                            log::error!("Failed to set local tasks: {e}");
1164                        }
1165                        Ok(()) => {
1166                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(directory
1167                                .as_std_path()
1168                                .join(task_file_name()))));
1169                        }
1170                    }
1171                }
1172            };
1173
1174            if applied {
1175                if let Some(downstream_client) = &self.downstream_client {
1176                    downstream_client
1177                        .send(proto::UpdateWorktreeSettings {
1178                            project_id: self.project_id,
1179                            worktree_id: remote_worktree_id.to_proto(),
1180                            path: directory.to_proto(),
1181                            content: file_content.clone(),
1182                            kind: Some(local_settings_kind_to_proto(kind).into()),
1183                        })
1184                        .log_err();
1185                }
1186            }
1187        }
1188    }
1189
1190    fn subscribe_to_global_task_file_changes(
1191        fs: Arc<dyn Fs>,
1192        file_path: PathBuf,
1193        cx: &mut Context<Self>,
1194    ) -> Task<()> {
1195        let mut user_tasks_file_rx =
1196            watch_config_file(cx.background_executor(), fs, file_path.clone());
1197        let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1198        let weak_entry = cx.weak_entity();
1199        cx.spawn(async move |settings_observer, cx| {
1200            let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1201                settings_observer.task_store.clone()
1202            }) else {
1203                return;
1204            };
1205            if let Some(user_tasks_content) = user_tasks_content {
1206                let Ok(()) = task_store.update(cx, |task_store, cx| {
1207                    task_store
1208                        .update_user_tasks(
1209                            TaskSettingsLocation::Global(&file_path),
1210                            Some(&user_tasks_content),
1211                            cx,
1212                        )
1213                        .log_err();
1214                }) else {
1215                    return;
1216                };
1217            }
1218            while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1219                let Ok(result) = task_store.update(cx, |task_store, cx| {
1220                    task_store.update_user_tasks(
1221                        TaskSettingsLocation::Global(&file_path),
1222                        Some(&user_tasks_content),
1223                        cx,
1224                    )
1225                }) else {
1226                    break;
1227                };
1228
1229                weak_entry
1230                    .update(cx, |_, cx| match result {
1231                        Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1232                            file_path.clone()
1233                        ))),
1234                        Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1235                            InvalidSettingsError::Tasks {
1236                                path: file_path.clone(),
1237                                message: err.to_string(),
1238                            },
1239                        ))),
1240                    })
1241                    .ok();
1242            }
1243        })
1244    }
1245    fn subscribe_to_global_debug_scenarios_changes(
1246        fs: Arc<dyn Fs>,
1247        file_path: PathBuf,
1248        cx: &mut Context<Self>,
1249    ) -> Task<()> {
1250        let mut user_tasks_file_rx =
1251            watch_config_file(cx.background_executor(), fs, file_path.clone());
1252        let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1253        let weak_entry = cx.weak_entity();
1254        cx.spawn(async move |settings_observer, cx| {
1255            let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1256                settings_observer.task_store.clone()
1257            }) else {
1258                return;
1259            };
1260            if let Some(user_tasks_content) = user_tasks_content {
1261                let Ok(()) = task_store.update(cx, |task_store, cx| {
1262                    task_store
1263                        .update_user_debug_scenarios(
1264                            TaskSettingsLocation::Global(&file_path),
1265                            Some(&user_tasks_content),
1266                            cx,
1267                        )
1268                        .log_err();
1269                }) else {
1270                    return;
1271                };
1272            }
1273            while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1274                let Ok(result) = task_store.update(cx, |task_store, cx| {
1275                    task_store.update_user_debug_scenarios(
1276                        TaskSettingsLocation::Global(&file_path),
1277                        Some(&user_tasks_content),
1278                        cx,
1279                    )
1280                }) else {
1281                    break;
1282                };
1283
1284                weak_entry
1285                    .update(cx, |_, cx| match result {
1286                        Ok(()) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(Ok(
1287                            file_path.clone(),
1288                        ))),
1289                        Err(err) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(
1290                            Err(InvalidSettingsError::Tasks {
1291                                path: file_path.clone(),
1292                                message: err.to_string(),
1293                            }),
1294                        )),
1295                    })
1296                    .ok();
1297            }
1298        })
1299    }
1300}
1301
1302fn apply_local_settings(
1303    worktree_id: WorktreeId,
1304    directory: &Arc<RelPath>,
1305    kind: LocalSettingsKind,
1306    file_content: &Option<String>,
1307    cx: &mut Context<'_, SettingsObserver>,
1308) {
1309    cx.update_global::<SettingsStore, _>(|store, cx| {
1310        let result = store.set_local_settings(
1311            worktree_id,
1312            directory.clone(),
1313            kind,
1314            file_content.as_deref(),
1315            cx,
1316        );
1317
1318        match result {
1319            Err(InvalidSettingsError::LocalSettings { path, message }) => {
1320                log::error!("Failed to set local settings in {path:?}: {message}");
1321                cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
1322                    InvalidSettingsError::LocalSettings { path, message },
1323                )));
1324            }
1325            Err(e) => log::error!("Failed to set local settings: {e}"),
1326            Ok(()) => cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(directory
1327                .as_std_path()
1328                .join(local_settings_file_relative_path().as_std_path())))),
1329        }
1330    })
1331}
1332
1333pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
1334    match kind {
1335        proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
1336        proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
1337        proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
1338        proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
1339    }
1340}
1341
1342pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
1343    match kind {
1344        LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
1345        LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
1346        LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
1347        LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
1348    }
1349}
1350
1351#[derive(Debug, Clone)]
1352pub struct DapSettings {
1353    pub binary: DapBinary,
1354    pub args: Vec<String>,
1355    pub env: HashMap<String, String>,
1356}
1357
1358impl From<DapSettingsContent> for DapSettings {
1359    fn from(content: DapSettingsContent) -> Self {
1360        DapSettings {
1361            binary: content
1362                .binary
1363                .map_or_else(|| DapBinary::Default, |binary| DapBinary::Custom(binary)),
1364            args: content.args.unwrap_or_default(),
1365            env: content.env.unwrap_or_default(),
1366        }
1367    }
1368}
1369
1370#[derive(Debug, Clone)]
1371pub enum DapBinary {
1372    Default,
1373    Custom(String),
1374}