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