project_settings.rs

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