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