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