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