project_settings.rs

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