zed.rs

   1mod app_menus;
   2pub mod component_preview;
   3pub mod inline_completion_registry;
   4#[cfg(target_os = "macos")]
   5pub(crate) mod mac_only_instance;
   6mod migrate;
   7mod open_listener;
   8mod quick_action_bar;
   9#[cfg(target_os = "windows")]
  10pub(crate) mod windows_only_instance;
  11
  12use agent::AgentDiffToolbar;
  13use anyhow::Context as _;
  14pub use app_menus::*;
  15use assets::Assets;
  16use assistant_context_editor::AgentPanelDelegate;
  17use breadcrumbs::Breadcrumbs;
  18use client::zed_urls;
  19use collections::VecDeque;
  20use debugger_ui::debugger_panel::DebugPanel;
  21use editor::ProposedChangesEditorToolbar;
  22use editor::{Editor, MultiBuffer, scroll::Autoscroll};
  23use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt};
  24use futures::future::Either;
  25use futures::{StreamExt, channel::mpsc, select_biased};
  26use git_ui::git_panel::GitPanel;
  27use git_ui::project_diff::ProjectDiffToolbar;
  28use gpui::{
  29    Action, App, AppContext as _, AsyncWindowContext, Context, DismissEvent, Element, Entity,
  30    Focusable, KeyBinding, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString,
  31    Styled, Task, TitlebarOptions, UpdateGlobal, Window, WindowKind, WindowOptions, actions,
  32    image_cache, point, px, retain_all,
  33};
  34use image_viewer::ImageInfo;
  35use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType};
  36use migrator::{migrate_keymap, migrate_settings};
  37pub use open_listener::*;
  38use outline_panel::OutlinePanel;
  39use paths::{
  40    local_debug_file_relative_path, local_settings_file_relative_path,
  41    local_tasks_file_relative_path,
  42};
  43use project::{DirectoryLister, ProjectItem};
  44use project_panel::ProjectPanel;
  45use prompt_store::PromptBuilder;
  46use quick_action_bar::QuickActionBar;
  47use recent_projects::open_ssh_project;
  48use release_channel::{AppCommitSha, ReleaseChannel};
  49use rope::Rope;
  50use search::project_search::ProjectSearchBar;
  51use settings::{
  52    DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeymapFile, KeymapFileLoadResult, Settings,
  53    SettingsStore, VIM_KEYMAP_PATH, initial_debug_tasks_content, initial_project_settings_content,
  54    initial_tasks_content, update_settings_file,
  55};
  56use std::path::PathBuf;
  57use std::sync::atomic::{self, AtomicBool};
  58use std::{borrow::Cow, path::Path, sync::Arc};
  59use terminal_view::terminal_panel::{self, TerminalPanel};
  60use theme::{ActiveTheme, ThemeSettings};
  61use ui::{PopoverMenuHandle, prelude::*};
  62use util::markdown::MarkdownString;
  63use util::{ResultExt, asset_str};
  64use uuid::Uuid;
  65use vim_mode_setting::VimModeSetting;
  66use welcome::{BaseKeymap, DOCS_URL, MultibufferHint};
  67use workspace::notifications::{NotificationId, dismiss_app_notification, show_app_notification};
  68use workspace::{
  69    AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings,
  70    create_and_open_local_file, notifications::simple_message_notification::MessageNotification,
  71    open_new,
  72};
  73use workspace::{CloseIntent, RestoreBanner};
  74use workspace::{Pane, notifications::DetachAndPromptErr};
  75use zed_actions::{
  76    OpenAccountSettings, OpenBrowser, OpenDocs, OpenServerSettings, OpenSettings, OpenZedUrl, Quit,
  77};
  78
  79actions!(
  80    zed,
  81    [
  82        DebugElements,
  83        Hide,
  84        HideOthers,
  85        Minimize,
  86        OpenDefaultSettings,
  87        OpenProjectSettings,
  88        OpenProjectTasks,
  89        OpenProjectDebugTasks,
  90        OpenTasks,
  91        OpenDebugTasks,
  92        ResetDatabase,
  93        ShowAll,
  94        ToggleFullScreen,
  95        Zoom,
  96        TestPanic,
  97    ]
  98);
  99
 100pub fn init(cx: &mut App) {
 101    #[cfg(target_os = "macos")]
 102    cx.on_action(|_: &Hide, cx| cx.hide());
 103    #[cfg(target_os = "macos")]
 104    cx.on_action(|_: &HideOthers, cx| cx.hide_other_apps());
 105    #[cfg(target_os = "macos")]
 106    cx.on_action(|_: &ShowAll, cx| cx.unhide_other_apps());
 107    cx.on_action(quit);
 108
 109    cx.on_action(|_: &RestoreBanner, cx| title_bar::restore_banner(cx));
 110
 111    if ReleaseChannel::global(cx) == ReleaseChannel::Dev {
 112        cx.on_action(test_panic);
 113    }
 114}
 115
 116fn bind_on_window_closed(cx: &mut App) -> Option<gpui::Subscription> {
 117    WorkspaceSettings::get_global(cx)
 118        .on_last_window_closed
 119        .is_quit_app()
 120        .then(|| {
 121            cx.on_window_closed(|cx| {
 122                if cx.windows().is_empty() {
 123                    cx.quit();
 124                }
 125            })
 126        })
 127}
 128
 129pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut App) -> WindowOptions {
 130    let display = display_uuid.and_then(|uuid| {
 131        cx.displays()
 132            .into_iter()
 133            .find(|display| display.uuid().ok() == Some(uuid))
 134    });
 135    let app_id = ReleaseChannel::global(cx).app_id();
 136    let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") {
 137        Ok(val) if val == "server" => gpui::WindowDecorations::Server,
 138        Ok(val) if val == "client" => gpui::WindowDecorations::Client,
 139        _ => gpui::WindowDecorations::Client,
 140    };
 141
 142    WindowOptions {
 143        titlebar: Some(TitlebarOptions {
 144            title: None,
 145            appears_transparent: true,
 146            traffic_light_position: Some(point(px(9.0), px(9.0))),
 147        }),
 148        window_bounds: None,
 149        focus: false,
 150        show: false,
 151        kind: WindowKind::Normal,
 152        is_movable: true,
 153        display_id: display.map(|display| display.id()),
 154        window_background: cx.theme().window_background_appearance(),
 155        app_id: Some(app_id.to_owned()),
 156        window_decorations: Some(window_decorations),
 157        window_min_size: Some(gpui::Size {
 158            width: px(360.0),
 159            height: px(240.0),
 160        }),
 161    }
 162}
 163
 164pub fn initialize_workspace(
 165    app_state: Arc<AppState>,
 166    prompt_builder: Arc<PromptBuilder>,
 167    cx: &mut App,
 168) {
 169    let mut _on_close_subscription = bind_on_window_closed(cx);
 170    cx.observe_global::<SettingsStore>(move |cx| {
 171        _on_close_subscription = bind_on_window_closed(cx);
 172    })
 173    .detach();
 174
 175    cx.observe_new(move |workspace: &mut Workspace, window, cx| {
 176        let Some(window) = window else {
 177            return;
 178        };
 179
 180        let workspace_handle = cx.entity().clone();
 181        let center_pane = workspace.active_pane().clone();
 182        initialize_pane(workspace, &center_pane, window, cx);
 183
 184        cx.subscribe_in(&workspace_handle, window, {
 185            move |workspace, _, event, window, cx| match event {
 186                workspace::Event::PaneAdded(pane) => {
 187                    initialize_pane(workspace, &pane, window, cx);
 188                }
 189                workspace::Event::OpenBundledFile {
 190                    text,
 191                    title,
 192                    language,
 193                } => open_bundled_file(workspace, text.clone(), title, language, window, cx),
 194                _ => {}
 195            }
 196        })
 197        .detach();
 198
 199        #[cfg(not(target_os = "macos"))]
 200        initialize_file_watcher(window, cx);
 201
 202        if let Some(specs) = window.gpu_specs() {
 203            log::info!("Using GPU: {:?}", specs);
 204            show_software_emulation_warning_if_needed(specs, window, cx);
 205        }
 206
 207        let popover_menu_handle = PopoverMenuHandle::default();
 208
 209        let inline_completion_button = cx.new(|cx| {
 210            inline_completion_button::InlineCompletionButton::new(
 211                app_state.fs.clone(),
 212                app_state.user_store.clone(),
 213                popover_menu_handle.clone(),
 214                cx,
 215            )
 216        });
 217
 218        workspace.register_action({
 219            move |_, _: &inline_completion_button::ToggleMenu, window, cx| {
 220                popover_menu_handle.toggle(window, cx);
 221            }
 222        });
 223
 224        let search_button = cx.new(|_| search::search_status_button::SearchButton::new());
 225        let diagnostic_summary =
 226            cx.new(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
 227        let activity_indicator = activity_indicator::ActivityIndicator::new(
 228            workspace,
 229            app_state.languages.clone(),
 230            window,
 231            cx,
 232        );
 233        let active_buffer_language =
 234            cx.new(|_| language_selector::ActiveBufferLanguage::new(workspace));
 235        let active_toolchain_language =
 236            cx.new(|cx| toolchain_selector::ActiveToolchain::new(workspace, window, cx));
 237        let vim_mode_indicator = cx.new(|cx| vim::ModeIndicator::new(window, cx));
 238        let image_info = cx.new(|_cx| ImageInfo::new(workspace));
 239        let cursor_position =
 240            cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
 241        workspace.status_bar().update(cx, |status_bar, cx| {
 242            status_bar.add_left_item(search_button, window, cx);
 243            status_bar.add_left_item(diagnostic_summary, window, cx);
 244            status_bar.add_left_item(activity_indicator, window, cx);
 245            status_bar.add_right_item(inline_completion_button, window, cx);
 246            status_bar.add_right_item(active_buffer_language, window, cx);
 247            status_bar.add_right_item(active_toolchain_language, window, cx);
 248            status_bar.add_right_item(vim_mode_indicator, window, cx);
 249            status_bar.add_right_item(cursor_position, window, cx);
 250            status_bar.add_right_item(image_info, window, cx);
 251        });
 252
 253        let handle = cx.entity().downgrade();
 254        window.on_window_should_close(cx, move |window, cx| {
 255            handle
 256                .update(cx, |workspace, cx| {
 257                    // We'll handle closing asynchronously
 258                    workspace.close_window(&Default::default(), window, cx);
 259                    false
 260                })
 261                .unwrap_or(true)
 262        });
 263
 264        initialize_panels(prompt_builder.clone(), window, cx);
 265        register_actions(app_state.clone(), workspace, window, cx);
 266
 267        workspace.focus_handle(cx).focus(window);
 268    })
 269    .detach();
 270}
 271
 272#[cfg(any(target_os = "linux", target_os = "freebsd"))]
 273fn initialize_file_watcher(window: &mut Window, cx: &mut Context<Workspace>) {
 274    if let Err(e) = fs::fs_watcher::global(|_| {}) {
 275        let message = format!(
 276            db::indoc! {r#"
 277            inotify_init returned {}
 278
 279            This may be due to system-wide limits on inotify instances. For troubleshooting see: https://zed.dev/docs/linux
 280            "#},
 281            e
 282        );
 283        let prompt = window.prompt(
 284            PromptLevel::Critical,
 285            "Could not start inotify",
 286            Some(&message),
 287            &["Troubleshoot and Quit"],
 288            cx,
 289        );
 290        cx.spawn(async move |_, cx| {
 291            if prompt.await == Ok(0) {
 292                cx.update(|cx| {
 293                    cx.open_url("https://zed.dev/docs/linux#could-not-start-inotify");
 294                    cx.quit();
 295                })
 296                .ok();
 297            }
 298        })
 299        .detach()
 300    }
 301}
 302
 303#[cfg(target_os = "windows")]
 304fn initialize_file_watcher(window: &mut Window, cx: &mut Context<Workspace>) {
 305    if let Err(e) = fs::fs_watcher::global(|_| {}) {
 306        let message = format!(
 307            db::indoc! {r#"
 308            ReadDirectoryChangesW initialization failed: {}
 309
 310            This may occur on network filesystems and WSL paths. For troubleshooting see: https://zed.dev/docs/windows
 311            "#},
 312            e
 313        );
 314        let prompt = window.prompt(
 315            PromptLevel::Critical,
 316            "Could not start ReadDirectoryChangesW",
 317            Some(&message),
 318            &["Troubleshoot and Quit"],
 319            cx,
 320        );
 321        cx.spawn(async move |_, cx| {
 322            if prompt.await == Ok(0) {
 323                cx.update(|cx| {
 324                    cx.open_url("https://zed.dev/docs/windows");
 325                    cx.quit()
 326                })
 327                .ok();
 328            }
 329        })
 330        .detach()
 331    }
 332}
 333
 334fn show_software_emulation_warning_if_needed(
 335    specs: gpui::GpuSpecs,
 336    window: &mut Window,
 337    cx: &mut Context<Workspace>,
 338) {
 339    if specs.is_software_emulated && std::env::var("ZED_ALLOW_EMULATED_GPU").is_err() {
 340        let message = format!(
 341            db::indoc! {r#"
 342            Zed uses Vulkan for rendering and requires a compatible GPU.
 343
 344            Currently you are using a software emulated GPU ({}) which
 345            will result in awful performance.
 346
 347            For troubleshooting see: https://zed.dev/docs/linux
 348            Set ZED_ALLOW_EMULATED_GPU=1 env var to permanently override.
 349            "#},
 350            specs.device_name
 351        );
 352        let prompt = window.prompt(
 353            PromptLevel::Critical,
 354            "Unsupported GPU",
 355            Some(&message),
 356            &["Skip", "Troubleshoot and Quit"],
 357            cx,
 358        );
 359        cx.spawn(async move |_, cx| {
 360            if prompt.await == Ok(1) {
 361                cx.update(|cx| {
 362                    cx.open_url("https://zed.dev/docs/linux#zed-fails-to-open-windows");
 363                    cx.quit();
 364                })
 365                .ok();
 366            }
 367        })
 368        .detach()
 369    }
 370}
 371
 372fn initialize_panels(
 373    prompt_builder: Arc<PromptBuilder>,
 374    window: &mut Window,
 375    cx: &mut Context<Workspace>,
 376) {
 377    let prompt_builder = prompt_builder.clone();
 378
 379    cx.spawn_in(window, async move |workspace_handle, cx| {
 380        let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
 381        let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
 382        let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
 383        let channels_panel =
 384            collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
 385        let chat_panel =
 386            collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
 387        let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
 388            workspace_handle.clone(),
 389            cx.clone(),
 390        );
 391
 392        let (
 393            project_panel,
 394            outline_panel,
 395            terminal_panel,
 396            channels_panel,
 397            chat_panel,
 398            notification_panel,
 399        ) = futures::try_join!(
 400            project_panel,
 401            outline_panel,
 402            terminal_panel,
 403            channels_panel,
 404            chat_panel,
 405            notification_panel,
 406        )?;
 407
 408        workspace_handle.update_in(cx, |workspace, window, cx| {
 409            workspace.add_panel(project_panel, window, cx);
 410            workspace.add_panel(outline_panel, window, cx);
 411            workspace.add_panel(terminal_panel, window, cx);
 412            workspace.add_panel(channels_panel, window, cx);
 413            workspace.add_panel(chat_panel, window, cx);
 414            workspace.add_panel(notification_panel, window, cx);
 415            cx.when_flag_enabled::<DebuggerFeatureFlag>(window, |_, window, cx| {
 416                cx.spawn_in(
 417                    window,
 418                    async move |workspace: gpui::WeakEntity<Workspace>,
 419                                cx: &mut AsyncWindowContext| {
 420                        let debug_panel = DebugPanel::load(workspace.clone(), cx).await?;
 421                        workspace.update_in(cx, |workspace, window, cx| {
 422                            workspace.add_panel(debug_panel, window, cx);
 423                        })?;
 424                        anyhow::Ok(())
 425                    },
 426                )
 427                .detach()
 428            });
 429
 430            let entity = cx.entity();
 431            let project = workspace.project().clone();
 432            let app_state = workspace.app_state().clone();
 433            let git_panel = cx.new(|cx| GitPanel::new(entity, project, app_state, window, cx));
 434            workspace.add_panel(git_panel, window, cx);
 435        })?;
 436
 437        let is_assistant2_enabled = !cfg!(test);
 438        let agent_panel = if is_assistant2_enabled {
 439            let agent_panel =
 440                agent::AgentPanel::load(workspace_handle.clone(), prompt_builder, cx.clone())
 441                    .await?;
 442
 443            Some(agent_panel)
 444        } else {
 445            None
 446        };
 447
 448        workspace_handle.update_in(cx, |workspace, window, cx| {
 449            if let Some(agent_panel) = agent_panel {
 450                workspace.add_panel(agent_panel, window, cx);
 451            }
 452
 453            // Register the actions that are shared between `assistant` and `assistant2`.
 454            //
 455            // We need to do this here instead of within the individual `init`
 456            // functions so that we only register the actions once.
 457            //
 458            // Once we ship `assistant2` we can push this back down into `agent::agent_panel::init`.
 459            if is_assistant2_enabled {
 460                <dyn AgentPanelDelegate>::set_global(
 461                    Arc::new(agent::ConcreteAssistantPanelDelegate),
 462                    cx,
 463                );
 464
 465                workspace
 466                    .register_action(agent::AgentPanel::toggle_focus)
 467                    .register_action(agent::InlineAssistant::inline_assist);
 468            }
 469        })?;
 470
 471        anyhow::Ok(())
 472    })
 473    .detach();
 474}
 475
 476fn register_actions(
 477    app_state: Arc<AppState>,
 478    workspace: &mut Workspace,
 479    _: &mut Window,
 480    cx: &mut Context<Workspace>,
 481) {
 482    workspace
 483        .register_action(about)
 484        .register_action(|_, _: &OpenDocs, _, cx| cx.open_url(DOCS_URL))
 485        .register_action(|_, _: &Minimize, window, _| {
 486            window.minimize_window();
 487        })
 488        .register_action(|_, _: &Zoom, window, _| {
 489            window.zoom_window();
 490        })
 491        .register_action(|_, _: &ToggleFullScreen, window, _| {
 492            window.toggle_fullscreen();
 493        })
 494        .register_action(|_, action: &OpenZedUrl, _, cx| {
 495            OpenListener::global(cx).open_urls(vec![action.url.clone()])
 496        })
 497        .register_action(|_, action: &OpenBrowser, _window, cx| cx.open_url(&action.url))
 498        .register_action(|workspace, _: &workspace::Open, window, cx| {
 499            telemetry::event!("Project Opened");
 500            let paths = workspace.prompt_for_open_path(
 501                PathPromptOptions {
 502                    files: true,
 503                    directories: true,
 504                    multiple: true,
 505                },
 506                DirectoryLister::Local(workspace.app_state().fs.clone()),
 507                window,
 508                cx,
 509            );
 510
 511            cx.spawn_in(window, async move |this, cx| {
 512                let Some(paths) = paths.await.log_err().flatten() else {
 513                    return;
 514                };
 515
 516                if let Some(task) = this
 517                    .update_in(cx, |this, window, cx| {
 518                        this.open_workspace_for_paths(false, paths, window, cx)
 519                    })
 520                    .log_err()
 521                {
 522                    task.await.log_err();
 523                }
 524            })
 525            .detach()
 526        })
 527        .register_action(|workspace, action: &zed_actions::OpenRemote, window, cx| {
 528            if !action.from_existing_connection {
 529                cx.propagate();
 530                return;
 531            }
 532            // You need existing remote connection to open it this way
 533            if workspace.project().read(cx).is_local() {
 534                return;
 535            }
 536            telemetry::event!("Project Opened");
 537            let paths = workspace.prompt_for_open_path(
 538                PathPromptOptions {
 539                    files: true,
 540                    directories: true,
 541                    multiple: true,
 542                },
 543                DirectoryLister::Project(workspace.project().clone()),
 544                window,
 545                cx,
 546            );
 547            cx.spawn_in(window, async move |this, cx| {
 548                let Some(paths) = paths.await.log_err().flatten() else {
 549                    return;
 550                };
 551                if let Some(task) = this
 552                    .update_in(cx, |this, window, cx| {
 553                        open_new_ssh_project_from_project(this, paths, window, cx)
 554                    })
 555                    .log_err()
 556                {
 557                    task.await.log_err();
 558                }
 559            })
 560            .detach()
 561        })
 562        .register_action({
 563            let fs = app_state.fs.clone();
 564            move |_, action: &zed_actions::IncreaseUiFontSize, _window, cx| {
 565                if action.persist {
 566                    update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
 567                        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) + px(1.0);
 568                        let _ = settings
 569                            .ui_font_size
 570                            .insert(theme::clamp_font_size(ui_font_size).0);
 571                    });
 572                } else {
 573                    theme::adjust_ui_font_size(cx, |size| {
 574                        *size += px(1.0);
 575                    });
 576                }
 577            }
 578        })
 579        .register_action({
 580            let fs = app_state.fs.clone();
 581            move |_, action: &zed_actions::DecreaseUiFontSize, _window, cx| {
 582                if action.persist {
 583                    update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
 584                        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) - px(1.0);
 585                        let _ = settings
 586                            .ui_font_size
 587                            .insert(theme::clamp_font_size(ui_font_size).0);
 588                    });
 589                } else {
 590                    theme::adjust_ui_font_size(cx, |size| {
 591                        *size -= px(1.0);
 592                    });
 593                }
 594            }
 595        })
 596        .register_action({
 597            let fs = app_state.fs.clone();
 598            move |_, action: &zed_actions::ResetUiFontSize, _window, cx| {
 599                if action.persist {
 600                    update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, _| {
 601                        settings.ui_font_size = None;
 602                    });
 603                } else {
 604                    theme::reset_ui_font_size(cx);
 605                }
 606            }
 607        })
 608        .register_action({
 609            let fs = app_state.fs.clone();
 610            move |_, action: &zed_actions::IncreaseBufferFontSize, _window, cx| {
 611                if action.persist {
 612                    update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
 613                        let buffer_font_size =
 614                            ThemeSettings::get_global(cx).buffer_font_size(cx) + px(1.0);
 615                        let _ = settings
 616                            .buffer_font_size
 617                            .insert(theme::clamp_font_size(buffer_font_size).0);
 618                    });
 619                } else {
 620                    theme::adjust_buffer_font_size(cx, |size| {
 621                        *size += px(1.0);
 622                    });
 623                }
 624            }
 625        })
 626        .register_action({
 627            let fs = app_state.fs.clone();
 628            move |_, action: &zed_actions::DecreaseBufferFontSize, _window, cx| {
 629                if action.persist {
 630                    update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
 631                        let buffer_font_size =
 632                            ThemeSettings::get_global(cx).buffer_font_size(cx) - px(1.0);
 633                        let _ = settings
 634                            .buffer_font_size
 635                            .insert(theme::clamp_font_size(buffer_font_size).0);
 636                    });
 637                } else {
 638                    theme::adjust_buffer_font_size(cx, |size| {
 639                        *size -= px(1.0);
 640                    });
 641                }
 642            }
 643        })
 644        .register_action({
 645            let fs = app_state.fs.clone();
 646            move |_, action: &zed_actions::ResetBufferFontSize, _window, cx| {
 647                if action.persist {
 648                    update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, _| {
 649                        settings.buffer_font_size = None;
 650                    });
 651                } else {
 652                    theme::reset_buffer_font_size(cx);
 653                }
 654            }
 655        })
 656        .register_action(install_cli)
 657        .register_action(|_, _: &install_cli::RegisterZedScheme, window, cx| {
 658            cx.spawn_in(window, async move |workspace, cx| {
 659                install_cli::register_zed_scheme(&cx).await?;
 660                workspace.update_in(cx, |workspace, _, cx| {
 661                    struct RegisterZedScheme;
 662
 663                    workspace.show_toast(
 664                        Toast::new(
 665                            NotificationId::unique::<RegisterZedScheme>(),
 666                            format!(
 667                                "zed:// links will now open in {}.",
 668                                ReleaseChannel::global(cx).display_name()
 669                            ),
 670                        ),
 671                        cx,
 672                    )
 673                })?;
 674                Ok(())
 675            })
 676            .detach_and_prompt_err(
 677                "Error registering zed:// scheme",
 678                window,
 679                cx,
 680                |_, _, _| None,
 681            );
 682        })
 683        .register_action(|workspace, _: &OpenLog, window, cx| {
 684            open_log_file(workspace, window, cx);
 685        })
 686        .register_action(|workspace, _: &zed_actions::OpenLicenses, window, cx| {
 687            open_bundled_file(
 688                workspace,
 689                asset_str::<Assets>("licenses.md"),
 690                "Open Source License Attribution",
 691                "Markdown",
 692                window,
 693                cx,
 694            );
 695        })
 696        .register_action(
 697            move |workspace: &mut Workspace,
 698                  _: &zed_actions::OpenTelemetryLog,
 699                  window: &mut Window,
 700                  cx: &mut Context<Workspace>| {
 701                open_telemetry_log_file(workspace, window, cx);
 702            },
 703        )
 704        .register_action(
 705            move |_: &mut Workspace, _: &zed_actions::OpenKeymap, window, cx| {
 706                open_settings_file(
 707                    paths::keymap_file(),
 708                    || settings::initial_keymap_content().as_ref().into(),
 709                    window,
 710                    cx,
 711                );
 712            },
 713        )
 714        .register_action(move |_: &mut Workspace, _: &OpenSettings, window, cx| {
 715            open_settings_file(
 716                paths::settings_file(),
 717                || settings::initial_user_settings_content().as_ref().into(),
 718                window,
 719                cx,
 720            );
 721        })
 722        .register_action(
 723            |_: &mut Workspace, _: &OpenAccountSettings, _: &mut Window, cx| {
 724                cx.open_url(&zed_urls::account_url(cx));
 725            },
 726        )
 727        .register_action(move |_: &mut Workspace, _: &OpenTasks, window, cx| {
 728            open_settings_file(
 729                paths::tasks_file(),
 730                || settings::initial_tasks_content().as_ref().into(),
 731                window,
 732                cx,
 733            );
 734        })
 735        .register_action(move |_: &mut Workspace, _: &OpenDebugTasks, window, cx| {
 736            open_settings_file(
 737                paths::debug_scenarios_file(),
 738                || settings::initial_debug_tasks_content().as_ref().into(),
 739                window,
 740                cx,
 741            );
 742        })
 743        .register_action(open_project_settings_file)
 744        .register_action(open_project_tasks_file)
 745        .register_action(open_project_debug_tasks_file)
 746        .register_action(
 747            move |workspace, _: &zed_actions::OpenDefaultKeymap, window, cx| {
 748                open_bundled_file(
 749                    workspace,
 750                    settings::default_keymap(),
 751                    "Default Key Bindings",
 752                    "JSON",
 753                    window,
 754                    cx,
 755                );
 756            },
 757        )
 758        .register_action(move |workspace, _: &OpenDefaultSettings, window, cx| {
 759            open_bundled_file(
 760                workspace,
 761                settings::default_settings(),
 762                "Default Settings",
 763                "JSON",
 764                window,
 765                cx,
 766            );
 767        })
 768        .register_action(
 769            |workspace: &mut Workspace,
 770             _: &project_panel::ToggleFocus,
 771             window: &mut Window,
 772             cx: &mut Context<Workspace>| {
 773                workspace.toggle_panel_focus::<ProjectPanel>(window, cx);
 774            },
 775        )
 776        .register_action(
 777            |workspace: &mut Workspace,
 778             _: &outline_panel::ToggleFocus,
 779             window: &mut Window,
 780             cx: &mut Context<Workspace>| {
 781                workspace.toggle_panel_focus::<OutlinePanel>(window, cx);
 782            },
 783        )
 784        .register_action(
 785            |workspace: &mut Workspace,
 786             _: &collab_ui::collab_panel::ToggleFocus,
 787             window: &mut Window,
 788             cx: &mut Context<Workspace>| {
 789                workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(window, cx);
 790            },
 791        )
 792        .register_action(
 793            |workspace: &mut Workspace,
 794             _: &collab_ui::chat_panel::ToggleFocus,
 795             window: &mut Window,
 796             cx: &mut Context<Workspace>| {
 797                workspace.toggle_panel_focus::<collab_ui::chat_panel::ChatPanel>(window, cx);
 798            },
 799        )
 800        .register_action(
 801            |workspace: &mut Workspace,
 802             _: &collab_ui::notification_panel::ToggleFocus,
 803             window: &mut Window,
 804             cx: &mut Context<Workspace>| {
 805                workspace.toggle_panel_focus::<collab_ui::notification_panel::NotificationPanel>(
 806                    window, cx,
 807                );
 808            },
 809        )
 810        .register_action(
 811            |workspace: &mut Workspace,
 812             _: &terminal_panel::ToggleFocus,
 813             window: &mut Window,
 814             cx: &mut Context<Workspace>| {
 815                workspace.toggle_panel_focus::<TerminalPanel>(window, cx);
 816            },
 817        )
 818        .register_action({
 819            let app_state = Arc::downgrade(&app_state);
 820            move |_, _: &NewWindow, _, cx| {
 821                if let Some(app_state) = app_state.upgrade() {
 822                    open_new(
 823                        Default::default(),
 824                        app_state,
 825                        cx,
 826                        |workspace, window, cx| {
 827                            cx.activate(true);
 828                            Editor::new_file(workspace, &Default::default(), window, cx)
 829                        },
 830                    )
 831                    .detach();
 832                }
 833            }
 834        })
 835        .register_action({
 836            let app_state = Arc::downgrade(&app_state);
 837            move |_, _: &NewFile, _, cx| {
 838                if let Some(app_state) = app_state.upgrade() {
 839                    open_new(
 840                        Default::default(),
 841                        app_state,
 842                        cx,
 843                        |workspace, window, cx| {
 844                            Editor::new_file(workspace, &Default::default(), window, cx)
 845                        },
 846                    )
 847                    .detach();
 848                }
 849            }
 850        });
 851    if workspace.project().read(cx).is_via_ssh() {
 852        workspace.register_action({
 853            move |workspace, _: &OpenServerSettings, window, cx| {
 854                let open_server_settings = workspace
 855                    .project()
 856                    .update(cx, |project, cx| project.open_server_settings(cx));
 857
 858                cx.spawn_in(window, async move |workspace, cx| {
 859                    let buffer = open_server_settings.await?;
 860
 861                    workspace
 862                        .update_in(cx, |workspace, window, cx| {
 863                            workspace.open_path(
 864                                buffer
 865                                    .read(cx)
 866                                    .project_path(cx)
 867                                    .expect("Settings file must have a location"),
 868                                None,
 869                                true,
 870                                window,
 871                                cx,
 872                            )
 873                        })?
 874                        .await?;
 875
 876                    anyhow::Ok(())
 877                })
 878                .detach_and_log_err(cx);
 879            }
 880        });
 881    }
 882}
 883
 884fn initialize_pane(
 885    workspace: &Workspace,
 886    pane: &Entity<Pane>,
 887    window: &mut Window,
 888    cx: &mut Context<Workspace>,
 889) {
 890    pane.update(cx, |pane, cx| {
 891        pane.toolbar().update(cx, |toolbar, cx| {
 892            let multibuffer_hint = cx.new(|_| MultibufferHint::new());
 893            toolbar.add_item(multibuffer_hint, window, cx);
 894            let breadcrumbs = cx.new(|_| Breadcrumbs::new());
 895            toolbar.add_item(breadcrumbs, window, cx);
 896            let buffer_search_bar = cx.new(|cx| {
 897                search::BufferSearchBar::new(
 898                    Some(workspace.project().read(cx).languages().clone()),
 899                    window,
 900                    cx,
 901                )
 902            });
 903            toolbar.add_item(buffer_search_bar.clone(), window, cx);
 904            let proposed_change_bar = cx.new(|_| ProposedChangesEditorToolbar::new());
 905            toolbar.add_item(proposed_change_bar, window, cx);
 906            let quick_action_bar =
 907                cx.new(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx));
 908            toolbar.add_item(quick_action_bar, window, cx);
 909            let diagnostic_editor_controls = cx.new(|_| diagnostics::ToolbarControls::new());
 910            toolbar.add_item(diagnostic_editor_controls, window, cx);
 911            let project_search_bar = cx.new(|_| ProjectSearchBar::new());
 912            toolbar.add_item(project_search_bar, window, cx);
 913            let lsp_log_item = cx.new(|_| language_tools::LspLogToolbarItemView::new());
 914            toolbar.add_item(lsp_log_item, window, cx);
 915            let dap_log_item = cx.new(|_| debugger_tools::DapLogToolbarItemView::new());
 916            toolbar.add_item(dap_log_item, window, cx);
 917            let syntax_tree_item = cx.new(|_| language_tools::SyntaxTreeToolbarItemView::new());
 918            toolbar.add_item(syntax_tree_item, window, cx);
 919            let migration_banner = cx.new(|cx| MigrationBanner::new(workspace, cx));
 920            toolbar.add_item(migration_banner, window, cx);
 921            let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx));
 922            toolbar.add_item(project_diff_toolbar, window, cx);
 923            let agent_diff_toolbar = cx.new(AgentDiffToolbar::new);
 924            toolbar.add_item(agent_diff_toolbar, window, cx);
 925        })
 926    });
 927}
 928
 929fn about(
 930    _: &mut Workspace,
 931    _: &zed_actions::About,
 932    window: &mut Window,
 933    cx: &mut Context<Workspace>,
 934) {
 935    let release_channel = ReleaseChannel::global(cx).display_name();
 936    let version = env!("CARGO_PKG_VERSION");
 937    let debug = if cfg!(debug_assertions) {
 938        "(debug)"
 939    } else {
 940        ""
 941    };
 942    let message = format!("{release_channel} {version} {debug}");
 943    let detail = AppCommitSha::try_global(cx).map(|sha| sha.full());
 944
 945    let prompt = window.prompt(PromptLevel::Info, &message, detail.as_deref(), &["OK"], cx);
 946    cx.foreground_executor()
 947        .spawn(async {
 948            prompt.await.ok();
 949        })
 950        .detach();
 951}
 952
 953fn test_panic(_: &TestPanic, _: &mut App) {
 954    panic!("Ran the TestPanic action")
 955}
 956
 957fn install_cli(
 958    _: &mut Workspace,
 959    _: &install_cli::Install,
 960    window: &mut Window,
 961    cx: &mut Context<Workspace>,
 962) {
 963    install_cli::install_cli(window, cx);
 964}
 965
 966static WAITING_QUIT_CONFIRMATION: AtomicBool = AtomicBool::new(false);
 967fn quit(_: &Quit, cx: &mut App) {
 968    if WAITING_QUIT_CONFIRMATION.load(atomic::Ordering::Acquire) {
 969        return;
 970    }
 971
 972    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
 973    cx.spawn(async move |cx| {
 974        let mut workspace_windows = cx.update(|cx| {
 975            cx.windows()
 976                .into_iter()
 977                .filter_map(|window| window.downcast::<Workspace>())
 978                .collect::<Vec<_>>()
 979        })?;
 980
 981        // If multiple windows have unsaved changes, and need a save prompt,
 982        // prompt in the active window before switching to a different window.
 983        cx.update(|cx| {
 984            workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
 985        })
 986        .log_err();
 987
 988        if should_confirm {
 989            if let Some(workspace) = workspace_windows.first() {
 990                let answer = workspace
 991                    .update(cx, |_, window, cx| {
 992                        window.prompt(
 993                            PromptLevel::Info,
 994                            "Are you sure you want to quit?",
 995                            None,
 996                            &["Quit", "Cancel"],
 997                            cx,
 998                        )
 999                    })
1000                    .log_err();
1001
1002                if let Some(answer) = answer {
1003                    WAITING_QUIT_CONFIRMATION.store(true, atomic::Ordering::Release);
1004                    let answer = answer.await.ok();
1005                    WAITING_QUIT_CONFIRMATION.store(false, atomic::Ordering::Release);
1006                    if answer != Some(0) {
1007                        return Ok(());
1008                    }
1009                }
1010            }
1011        }
1012
1013        // If the user cancels any save prompt, then keep the app open.
1014        for window in workspace_windows {
1015            if let Some(should_close) = window
1016                .update(cx, |workspace, window, cx| {
1017                    workspace.prepare_to_close(CloseIntent::Quit, window, cx)
1018                })
1019                .log_err()
1020            {
1021                if !should_close.await? {
1022                    return Ok(());
1023                }
1024            }
1025        }
1026        cx.update(|cx| cx.quit())?;
1027        anyhow::Ok(())
1028    })
1029    .detach_and_log_err(cx);
1030}
1031
1032fn open_log_file(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
1033    const MAX_LINES: usize = 1000;
1034    workspace
1035        .with_local_workspace(window, cx, move |workspace, window, cx| {
1036            let fs = workspace.app_state().fs.clone();
1037            cx.spawn_in(window, async move |workspace, cx| {
1038                let (old_log, new_log) =
1039                    futures::join!(fs.load(paths::old_log_file()), fs.load(paths::log_file()));
1040                let log = match (old_log, new_log) {
1041                    (Err(_), Err(_)) => None,
1042                    (old_log, new_log) => {
1043                        let mut lines = VecDeque::with_capacity(MAX_LINES);
1044                        for line in old_log
1045                            .iter()
1046                            .flat_map(|log| log.lines())
1047                            .chain(new_log.iter().flat_map(|log| log.lines()))
1048                        {
1049                            if lines.len() == MAX_LINES {
1050                                lines.pop_front();
1051                            }
1052                            lines.push_back(line);
1053                        }
1054                        Some(
1055                            lines
1056                                .into_iter()
1057                                .flat_map(|line| [line, "\n"])
1058                                .collect::<String>(),
1059                        )
1060                    }
1061                };
1062
1063                workspace
1064                    .update_in(cx, |workspace, window, cx| {
1065                        let Some(log) = log else {
1066                            struct OpenLogError;
1067
1068                            workspace.show_notification(
1069                                NotificationId::unique::<OpenLogError>(),
1070                                cx,
1071                                |cx| {
1072                                    cx.new(|cx| {
1073                                        MessageNotification::new(
1074                                            format!(
1075                                                "Unable to access/open log file at path {:?}",
1076                                                paths::log_file().as_path()
1077                                            ),
1078                                            cx,
1079                                        )
1080                                    })
1081                                },
1082                            );
1083                            return;
1084                        };
1085                        let project = workspace.project().clone();
1086                        let buffer = project.update(cx, |project, cx| {
1087                            project.create_local_buffer(&log, None, cx)
1088                        });
1089
1090                        let buffer = cx
1091                            .new(|cx| MultiBuffer::singleton(buffer, cx).with_title("Log".into()));
1092                        let editor = cx.new(|cx| {
1093                            let mut editor =
1094                                Editor::for_multibuffer(buffer, Some(project), window, cx);
1095                            editor.set_read_only(true);
1096                            editor.set_breadcrumb_header(format!(
1097                                "Last {} lines in {}",
1098                                MAX_LINES,
1099                                paths::log_file().display()
1100                            ));
1101                            editor
1102                        });
1103
1104                        editor.update(cx, |editor, cx| {
1105                            let last_multi_buffer_offset = editor.buffer().read(cx).len(cx);
1106                            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
1107                                s.select_ranges(Some(
1108                                    last_multi_buffer_offset..last_multi_buffer_offset,
1109                                ));
1110                            })
1111                        });
1112
1113                        workspace.add_item_to_active_pane(Box::new(editor), None, true, window, cx);
1114                    })
1115                    .log_err();
1116            })
1117            .detach();
1118        })
1119        .detach();
1120}
1121
1122pub fn handle_settings_file_changes(
1123    mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
1124    mut global_settings_file_rx: mpsc::UnboundedReceiver<String>,
1125    cx: &mut App,
1126    settings_changed: impl Fn(Option<anyhow::Error>, &mut App) + 'static,
1127) {
1128    MigrationNotification::set_global(cx.new(|_| MigrationNotification), cx);
1129
1130    // Helper function to process settings content
1131    let process_settings =
1132        move |content: String, is_user: bool, store: &mut SettingsStore, cx: &mut App| -> bool {
1133            // Apply migrations to both user and global settings
1134            let (processed_content, content_migrated) =
1135                if let Ok(Some(migrated_content)) = migrate_settings(&content) {
1136                    (migrated_content, true)
1137                } else {
1138                    (content, false)
1139                };
1140
1141            let result = if is_user {
1142                store.set_user_settings(&processed_content, cx)
1143            } else {
1144                store.set_global_settings(&processed_content, cx)
1145            };
1146
1147            if let Err(err) = &result {
1148                let settings_type = if is_user { "user" } else { "global" };
1149                log::error!("Failed to load {} settings: {err}", settings_type);
1150            }
1151
1152            settings_changed(result.err(), cx);
1153
1154            content_migrated
1155        };
1156
1157    // Initial load of both settings files
1158    let global_content = cx
1159        .background_executor()
1160        .block(global_settings_file_rx.next())
1161        .unwrap();
1162    let user_content = cx
1163        .background_executor()
1164        .block(user_settings_file_rx.next())
1165        .unwrap();
1166
1167    SettingsStore::update_global(cx, |store, cx| {
1168        process_settings(global_content, false, store, cx);
1169        process_settings(user_content, true, store, cx);
1170    });
1171
1172    // Watch for changes in both files
1173    cx.spawn(async move |cx| {
1174        let mut settings_streams = futures::stream::select(
1175            global_settings_file_rx.map(Either::Left),
1176            user_settings_file_rx.map(Either::Right),
1177        );
1178
1179        while let Some(content) = settings_streams.next().await {
1180            let (content, is_user) = match content {
1181                Either::Left(content) => (content, false),
1182                Either::Right(content) => (content, true),
1183            };
1184
1185            let result = cx.update_global(|store: &mut SettingsStore, cx| {
1186                let content_migrated = process_settings(content, is_user, store, cx);
1187
1188                if content_migrated {
1189                    if let Some(notifier) = MigrationNotification::try_global(cx) {
1190                        notifier.update(cx, |_, cx| {
1191                            cx.emit(MigrationEvent::ContentChanged {
1192                                migration_type: MigrationType::Settings,
1193                                migrated: true,
1194                            });
1195                        });
1196                    }
1197                }
1198
1199                cx.refresh_windows();
1200            });
1201
1202            if result.is_err() {
1203                break; // App dropped
1204            }
1205        }
1206    })
1207    .detach();
1208}
1209
1210pub fn handle_keymap_file_changes(
1211    mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
1212    cx: &mut App,
1213) {
1214    BaseKeymap::register(cx);
1215    VimModeSetting::register(cx);
1216
1217    let (base_keymap_tx, mut base_keymap_rx) = mpsc::unbounded();
1218    let (keyboard_layout_tx, mut keyboard_layout_rx) = mpsc::unbounded();
1219    let mut old_base_keymap = *BaseKeymap::get_global(cx);
1220    let mut old_vim_enabled = VimModeSetting::get_global(cx).0;
1221    cx.observe_global::<SettingsStore>(move |cx| {
1222        let new_base_keymap = *BaseKeymap::get_global(cx);
1223        let new_vim_enabled = VimModeSetting::get_global(cx).0;
1224
1225        if new_base_keymap != old_base_keymap || new_vim_enabled != old_vim_enabled {
1226            old_base_keymap = new_base_keymap;
1227            old_vim_enabled = new_vim_enabled;
1228            base_keymap_tx.unbounded_send(()).unwrap();
1229        }
1230    })
1231    .detach();
1232
1233    let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout().id());
1234    cx.on_keyboard_layout_change(move |cx| {
1235        let next_mapping = settings::get_key_equivalents(cx.keyboard_layout().id());
1236        if next_mapping != current_mapping {
1237            current_mapping = next_mapping;
1238            keyboard_layout_tx.unbounded_send(()).ok();
1239        }
1240    })
1241    .detach();
1242
1243    load_default_keymap(cx);
1244
1245    struct KeymapParseErrorNotification;
1246    let notification_id = NotificationId::unique::<KeymapParseErrorNotification>();
1247
1248    cx.spawn(async move |cx| {
1249        let mut user_keymap_content = String::new();
1250        let mut content_migrated = false;
1251        loop {
1252            select_biased! {
1253                _ = base_keymap_rx.next() => {},
1254                _ = keyboard_layout_rx.next() => {},
1255                content = user_keymap_file_rx.next() => {
1256                    if let Some(content) = content {
1257                        if let Ok(Some(migrated_content)) = migrate_keymap(&content) {
1258                            user_keymap_content = migrated_content;
1259                            content_migrated = true;
1260                        } else {
1261                            user_keymap_content = content;
1262                            content_migrated = false;
1263                        }
1264                    }
1265                }
1266            };
1267            cx.update(|cx| {
1268                if let Some(notifier) = MigrationNotification::try_global(cx) {
1269                    notifier.update(cx, |_, cx| {
1270                        cx.emit(MigrationEvent::ContentChanged {
1271                            migration_type: MigrationType::Keymap,
1272                            migrated: content_migrated,
1273                        });
1274                    });
1275                }
1276                let load_result = KeymapFile::load(&user_keymap_content, cx);
1277                match load_result {
1278                    KeymapFileLoadResult::Success { key_bindings } => {
1279                        reload_keymaps(cx, key_bindings);
1280                        dismiss_app_notification(&notification_id.clone(), cx);
1281                    }
1282                    KeymapFileLoadResult::SomeFailedToLoad {
1283                        key_bindings,
1284                        error_message,
1285                    } => {
1286                        if !key_bindings.is_empty() {
1287                            reload_keymaps(cx, key_bindings);
1288                        }
1289                        show_keymap_file_load_error(notification_id.clone(), error_message, cx);
1290                    }
1291                    KeymapFileLoadResult::JsonParseFailure { error } => {
1292                        show_keymap_file_json_error(notification_id.clone(), &error, cx)
1293                    }
1294                }
1295            })
1296            .ok();
1297        }
1298    })
1299    .detach();
1300}
1301
1302fn show_keymap_file_json_error(
1303    notification_id: NotificationId,
1304    error: &anyhow::Error,
1305    cx: &mut App,
1306) {
1307    let message: SharedString =
1308        format!("JSON parse error in keymap file. Bindings not reloaded.\n\n{error}").into();
1309    show_app_notification(notification_id, cx, move |cx| {
1310        cx.new(|cx| {
1311            MessageNotification::new(message.clone(), cx)
1312                .primary_message("Open Keymap File")
1313                .primary_on_click(|window, cx| {
1314                    window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx);
1315                    cx.emit(DismissEvent);
1316                })
1317        })
1318    });
1319}
1320
1321fn show_keymap_file_load_error(
1322    notification_id: NotificationId,
1323    error_message: MarkdownString,
1324    cx: &mut App,
1325) {
1326    show_markdown_app_notification(
1327        notification_id.clone(),
1328        error_message,
1329        "Open Keymap File".into(),
1330        |window, cx| {
1331            window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx);
1332            cx.emit(DismissEvent);
1333        },
1334        cx,
1335    )
1336}
1337
1338fn show_markdown_app_notification<F>(
1339    notification_id: NotificationId,
1340    message: MarkdownString,
1341    primary_button_message: SharedString,
1342    primary_button_on_click: F,
1343    cx: &mut App,
1344) where
1345    F: 'static + Send + Sync + Fn(&mut Window, &mut Context<MessageNotification>),
1346{
1347    let parsed_markdown = cx.background_spawn(async move {
1348        let file_location_directory = None;
1349        let language_registry = None;
1350        markdown_preview::markdown_parser::parse_markdown(
1351            &message.0,
1352            file_location_directory,
1353            language_registry,
1354        )
1355        .await
1356    });
1357
1358    cx.spawn(async move |cx| {
1359        let parsed_markdown = Arc::new(parsed_markdown.await);
1360        let primary_button_message = primary_button_message.clone();
1361        let primary_button_on_click = Arc::new(primary_button_on_click);
1362        cx.update(|cx| {
1363            show_app_notification(notification_id, cx, move |cx| {
1364                let workspace_handle = cx.entity().downgrade();
1365                let parsed_markdown = parsed_markdown.clone();
1366                let primary_button_message = primary_button_message.clone();
1367                let primary_button_on_click = primary_button_on_click.clone();
1368                cx.new(move |cx| {
1369                    MessageNotification::new_from_builder(cx, move |window, cx| {
1370                        image_cache(retain_all("notification-cache"))
1371                            .text_xs()
1372                            .child(markdown_preview::markdown_renderer::render_parsed_markdown(
1373                                &parsed_markdown.clone(),
1374                                Some(workspace_handle.clone()),
1375                                window,
1376                                cx,
1377                            ))
1378                            .into_any()
1379                    })
1380                    .primary_message(primary_button_message)
1381                    .primary_on_click_arc(primary_button_on_click)
1382                })
1383            })
1384        })
1385        .ok();
1386    })
1387    .detach();
1388}
1389
1390fn reload_keymaps(cx: &mut App, user_key_bindings: Vec<KeyBinding>) {
1391    cx.clear_key_bindings();
1392    load_default_keymap(cx);
1393    cx.bind_keys(user_key_bindings);
1394    cx.set_menus(app_menus());
1395    // On Windows, this is set in the `update_jump_list` method of the `HistoryManager`.
1396    #[cfg(not(target_os = "windows"))]
1397    cx.set_dock_menu(vec![gpui::MenuItem::action(
1398        "New Window",
1399        workspace::NewWindow,
1400    )]);
1401}
1402
1403pub fn load_default_keymap(cx: &mut App) {
1404    let base_keymap = *BaseKeymap::get_global(cx);
1405    if base_keymap == BaseKeymap::None {
1406        return;
1407    }
1408
1409    cx.bind_keys(KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap());
1410
1411    if let Some(asset_path) = base_keymap.asset_path() {
1412        cx.bind_keys(KeymapFile::load_asset(asset_path, cx).unwrap());
1413    }
1414
1415    if VimModeSetting::get_global(cx).0 {
1416        cx.bind_keys(KeymapFile::load_asset(VIM_KEYMAP_PATH, cx).unwrap());
1417    }
1418}
1419
1420pub fn handle_settings_changed(error: Option<anyhow::Error>, cx: &mut App) {
1421    struct SettingsParseErrorNotification;
1422    let id = NotificationId::unique::<SettingsParseErrorNotification>();
1423
1424    match error {
1425        Some(error) => {
1426            if let Some(InvalidSettingsError::LocalSettings { .. }) =
1427                error.downcast_ref::<InvalidSettingsError>()
1428            {
1429                // Local settings errors are displayed by the projects
1430                return;
1431            }
1432            show_app_notification(id, cx, move |cx| {
1433                cx.new(|cx| {
1434                    MessageNotification::new(format!("Invalid user settings file\n{error}"), cx)
1435                        .primary_message("Open Settings File")
1436                        .primary_icon(IconName::Settings)
1437                        .primary_on_click(|window, cx| {
1438                            window.dispatch_action(zed_actions::OpenSettings.boxed_clone(), cx);
1439                            cx.emit(DismissEvent);
1440                        })
1441                })
1442            });
1443        }
1444        None => {
1445            dismiss_app_notification(&id, cx);
1446        }
1447    }
1448}
1449
1450pub fn open_new_ssh_project_from_project(
1451    workspace: &mut Workspace,
1452    paths: Vec<PathBuf>,
1453    window: &mut Window,
1454    cx: &mut Context<Workspace>,
1455) -> Task<anyhow::Result<()>> {
1456    let app_state = workspace.app_state().clone();
1457    let Some(ssh_client) = workspace.project().read(cx).ssh_client() else {
1458        return Task::ready(Err(anyhow::anyhow!("Not an ssh project")));
1459    };
1460    let connection_options = ssh_client.read(cx).connection_options();
1461    cx.spawn_in(window, async move |_, cx| {
1462        open_ssh_project(
1463            connection_options,
1464            paths,
1465            app_state,
1466            workspace::OpenOptions {
1467                open_new_workspace: Some(true),
1468                ..Default::default()
1469            },
1470            cx,
1471        )
1472        .await
1473    })
1474}
1475
1476fn open_project_settings_file(
1477    workspace: &mut Workspace,
1478    _: &OpenProjectSettings,
1479    window: &mut Window,
1480    cx: &mut Context<Workspace>,
1481) {
1482    open_local_file(
1483        workspace,
1484        local_settings_file_relative_path(),
1485        initial_project_settings_content(),
1486        window,
1487        cx,
1488    )
1489}
1490
1491fn open_project_tasks_file(
1492    workspace: &mut Workspace,
1493    _: &OpenProjectTasks,
1494    window: &mut Window,
1495    cx: &mut Context<Workspace>,
1496) {
1497    open_local_file(
1498        workspace,
1499        local_tasks_file_relative_path(),
1500        initial_tasks_content(),
1501        window,
1502        cx,
1503    )
1504}
1505
1506fn open_project_debug_tasks_file(
1507    workspace: &mut Workspace,
1508    _: &OpenProjectDebugTasks,
1509    window: &mut Window,
1510    cx: &mut Context<Workspace>,
1511) {
1512    open_local_file(
1513        workspace,
1514        local_debug_file_relative_path(),
1515        initial_debug_tasks_content(),
1516        window,
1517        cx,
1518    )
1519}
1520
1521fn open_local_file(
1522    workspace: &mut Workspace,
1523    settings_relative_path: &'static Path,
1524    initial_contents: Cow<'static, str>,
1525    window: &mut Window,
1526    cx: &mut Context<Workspace>,
1527) {
1528    let project = workspace.project().clone();
1529    let worktree = project
1530        .read(cx)
1531        .visible_worktrees(cx)
1532        .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
1533    if let Some(worktree) = worktree {
1534        let tree_id = worktree.read(cx).id();
1535        cx.spawn_in(window, async move |workspace, cx| {
1536            // Check if the file actually exists on disk (even if it's excluded from worktree)
1537            let file_exists = {
1538                let full_path =
1539                    worktree.update(cx, |tree, _| tree.abs_path().join(settings_relative_path))?;
1540
1541                let fs = project.update(cx, |project, _| project.fs().clone())?;
1542                let file_exists = fs
1543                    .metadata(&full_path)
1544                    .await
1545                    .ok()
1546                    .flatten()
1547                    .map_or(false, |metadata| !metadata.is_dir && !metadata.is_fifo);
1548                file_exists
1549            };
1550
1551            if !file_exists {
1552                if let Some(dir_path) = settings_relative_path.parent() {
1553                    if worktree.update(cx, |tree, _| tree.entry_for_path(dir_path).is_none())? {
1554                        project
1555                            .update(cx, |project, cx| {
1556                                project.create_entry((tree_id, dir_path), true, cx)
1557                            })?
1558                            .await
1559                            .context("worktree was removed")?;
1560                    }
1561                }
1562
1563                if worktree.update(cx, |tree, _| {
1564                    tree.entry_for_path(settings_relative_path).is_none()
1565                })? {
1566                    project
1567                        .update(cx, |project, cx| {
1568                            project.create_entry((tree_id, settings_relative_path), false, cx)
1569                        })?
1570                        .await
1571                        .context("worktree was removed")?;
1572                }
1573            }
1574
1575            let editor = workspace
1576                .update_in(cx, |workspace, window, cx| {
1577                    workspace.open_path((tree_id, settings_relative_path), None, true, window, cx)
1578                })?
1579                .await?
1580                .downcast::<Editor>()
1581                .context("unexpected item type: expected editor item")?;
1582
1583            editor
1584                .downgrade()
1585                .update(cx, |editor, cx| {
1586                    if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
1587                        if buffer.read(cx).is_empty() {
1588                            buffer.update(cx, |buffer, cx| {
1589                                buffer.edit([(0..0, initial_contents)], None, cx)
1590                            });
1591                        }
1592                    }
1593                })
1594                .ok();
1595
1596            anyhow::Ok(())
1597        })
1598        .detach();
1599    } else {
1600        struct NoOpenFolders;
1601
1602        workspace.show_notification(NotificationId::unique::<NoOpenFolders>(), cx, |cx| {
1603            cx.new(|cx| MessageNotification::new("This project has no folders open.", cx))
1604        })
1605    }
1606}
1607
1608fn open_telemetry_log_file(
1609    workspace: &mut Workspace,
1610    window: &mut Window,
1611    cx: &mut Context<Workspace>,
1612) {
1613    workspace.with_local_workspace(window, cx, move |workspace, window, cx| {
1614        let app_state = workspace.app_state().clone();
1615        cx.spawn_in(window, async move |workspace, cx| {
1616            async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
1617                let path = client::telemetry::Telemetry::log_file_path();
1618                app_state.fs.load(&path).await.log_err()
1619            }
1620
1621            let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string());
1622
1623            const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
1624            let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
1625            if let Some(newline_offset) = log[start_offset..].find('\n') {
1626                start_offset += newline_offset + 1;
1627            }
1628            let log_suffix = &log[start_offset..];
1629            let header = concat!(
1630                "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
1631                "// Telemetry can be disabled via the `settings.json` file.\n",
1632                "// Here is the data that has been reported for the current session:\n",
1633            );
1634            let content = format!("{}\n{}", header, log_suffix);
1635            let json = app_state.languages.language_for_name("JSON").await.log_err();
1636
1637            workspace.update_in( cx, |workspace, window, cx| {
1638                let project = workspace.project().clone();
1639                let buffer = project.update(cx, |project, cx| project.create_local_buffer(&content, json, cx));
1640                let buffer = cx.new(|cx| {
1641                    MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
1642                });
1643                workspace.add_item_to_active_pane(
1644                    Box::new(cx.new(|cx| {
1645                        let mut editor = Editor::for_multibuffer(buffer, Some(project), window, cx);
1646                        editor.set_read_only(true);
1647                        editor.set_breadcrumb_header("Telemetry Log".into());
1648                        editor
1649                    })),
1650                    None,
1651                    true,
1652                    window, cx,
1653                );
1654            }).log_err()?;
1655
1656            Some(())
1657        })
1658        .detach();
1659    }).detach();
1660}
1661
1662fn open_bundled_file(
1663    workspace: &Workspace,
1664    text: Cow<'static, str>,
1665    title: &'static str,
1666    language: &'static str,
1667    window: &mut Window,
1668    cx: &mut Context<Workspace>,
1669) {
1670    let language = workspace.app_state().languages.language_for_name(language);
1671    cx.spawn_in(window, async move |workspace, cx| {
1672        let language = language.await.log_err();
1673        workspace
1674            .update_in(cx, |workspace, window, cx| {
1675                workspace.with_local_workspace(window, cx, |workspace, window, cx| {
1676                    let project = workspace.project();
1677                    let buffer = project.update(cx, move |project, cx| {
1678                        project.create_local_buffer(text.as_ref(), language, cx)
1679                    });
1680                    let buffer =
1681                        cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into()));
1682                    workspace.add_item_to_active_pane(
1683                        Box::new(cx.new(|cx| {
1684                            let mut editor =
1685                                Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
1686                            editor.set_read_only(true);
1687                            editor.set_breadcrumb_header(title.into());
1688                            editor
1689                        })),
1690                        None,
1691                        true,
1692                        window,
1693                        cx,
1694                    );
1695                })
1696            })?
1697            .await
1698    })
1699    .detach_and_log_err(cx);
1700}
1701
1702fn open_settings_file(
1703    abs_path: &'static Path,
1704    default_content: impl FnOnce() -> Rope + Send + 'static,
1705    window: &mut Window,
1706    cx: &mut Context<Workspace>,
1707) {
1708    cx.spawn_in(window, async move |workspace, cx| {
1709        let (worktree_creation_task, settings_open_task) = workspace
1710            .update_in(cx, |workspace, window, cx| {
1711                workspace.with_local_workspace(window, cx, move |workspace, window, cx| {
1712                    let worktree_creation_task = workspace.project().update(cx, |project, cx| {
1713                        // Set up a dedicated worktree for settings, since
1714                        // otherwise we're dropping and re-starting LSP servers
1715                        // for each file inside on every settings file
1716                        // close/open
1717
1718                        // TODO: Do note that all other external files (e.g.
1719                        // drag and drop from OS) still have their worktrees
1720                        // released on file close, causing LSP servers'
1721                        // restarts.
1722                        project.find_or_create_worktree(paths::config_dir().as_path(), false, cx)
1723                    });
1724                    let settings_open_task =
1725                        create_and_open_local_file(abs_path, window, cx, default_content);
1726                    (worktree_creation_task, settings_open_task)
1727                })
1728            })?
1729            .await?;
1730        let _ = worktree_creation_task.await?;
1731        let _ = settings_open_task.await?;
1732        anyhow::Ok(())
1733    })
1734    .detach_and_log_err(cx);
1735}
1736
1737#[cfg(test)]
1738mod tests {
1739    use super::*;
1740    use assets::Assets;
1741    use collections::HashSet;
1742    use editor::{DisplayPoint, Editor, display_map::DisplayRow, scroll::Autoscroll};
1743    use gpui::{
1744        Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, SemanticVersion,
1745        TestAppContext, UpdateGlobal, VisualTestContext, WindowHandle, actions,
1746    };
1747    use language::{LanguageMatcher, LanguageRegistry};
1748    use project::{Project, ProjectPath, WorktreeSettings, project_settings::ProjectSettings};
1749    use serde_json::json;
1750    use settings::{SettingsStore, watch_config_file};
1751    use std::{
1752        path::{Path, PathBuf},
1753        time::Duration,
1754    };
1755    use theme::{ThemeRegistry, ThemeSettings};
1756    use util::{path, separator};
1757    use workspace::{
1758        NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection,
1759        WorkspaceHandle,
1760        item::{Item, ItemHandle},
1761        open_new, open_paths, pane,
1762    };
1763
1764    #[gpui::test]
1765    async fn test_open_non_existing_file(cx: &mut TestAppContext) {
1766        let app_state = init_test(cx);
1767        app_state
1768            .fs
1769            .as_fake()
1770            .insert_tree(
1771                path!("/root"),
1772                json!({
1773                    "a": {
1774                    },
1775                }),
1776            )
1777            .await;
1778
1779        cx.update(|cx| {
1780            open_paths(
1781                &[PathBuf::from(path!("/root/a/new"))],
1782                app_state.clone(),
1783                workspace::OpenOptions::default(),
1784                cx,
1785            )
1786        })
1787        .await
1788        .unwrap();
1789        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
1790
1791        let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
1792        workspace
1793            .update(cx, |workspace, _, cx| {
1794                assert!(workspace.active_item_as::<Editor>(cx).is_some())
1795            })
1796            .unwrap();
1797    }
1798
1799    #[gpui::test]
1800    async fn test_open_paths_action(cx: &mut TestAppContext) {
1801        let app_state = init_test(cx);
1802        app_state
1803            .fs
1804            .as_fake()
1805            .insert_tree(
1806                "/root",
1807                json!({
1808                    "a": {
1809                        "aa": null,
1810                        "ab": null,
1811                    },
1812                    "b": {
1813                        "ba": null,
1814                        "bb": null,
1815                    },
1816                    "c": {
1817                        "ca": null,
1818                        "cb": null,
1819                    },
1820                    "d": {
1821                        "da": null,
1822                        "db": null,
1823                    },
1824                    "e": {
1825                        "ea": null,
1826                        "eb": null,
1827                    }
1828                }),
1829            )
1830            .await;
1831
1832        cx.update(|cx| {
1833            open_paths(
1834                &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
1835                app_state.clone(),
1836                workspace::OpenOptions::default(),
1837                cx,
1838            )
1839        })
1840        .await
1841        .unwrap();
1842        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
1843
1844        cx.update(|cx| {
1845            open_paths(
1846                &[PathBuf::from("/root/a")],
1847                app_state.clone(),
1848                workspace::OpenOptions::default(),
1849                cx,
1850            )
1851        })
1852        .await
1853        .unwrap();
1854        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
1855        let workspace_1 = cx
1856            .read(|cx| cx.windows()[0].downcast::<Workspace>())
1857            .unwrap();
1858        cx.run_until_parked();
1859        workspace_1
1860            .update(cx, |workspace, window, cx| {
1861                assert_eq!(workspace.worktrees(cx).count(), 2);
1862                assert!(workspace.left_dock().read(cx).is_open());
1863                assert!(
1864                    workspace
1865                        .active_pane()
1866                        .read(cx)
1867                        .focus_handle(cx)
1868                        .is_focused(window)
1869                );
1870            })
1871            .unwrap();
1872
1873        cx.update(|cx| {
1874            open_paths(
1875                &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
1876                app_state.clone(),
1877                workspace::OpenOptions::default(),
1878                cx,
1879            )
1880        })
1881        .await
1882        .unwrap();
1883        assert_eq!(cx.read(|cx| cx.windows().len()), 2);
1884
1885        // Replace existing windows
1886        let window = cx
1887            .update(|cx| cx.windows()[0].downcast::<Workspace>())
1888            .unwrap();
1889        cx.update(|cx| {
1890            open_paths(
1891                &[PathBuf::from("/root/e")],
1892                app_state,
1893                workspace::OpenOptions {
1894                    replace_window: Some(window),
1895                    ..Default::default()
1896                },
1897                cx,
1898            )
1899        })
1900        .await
1901        .unwrap();
1902        cx.background_executor.run_until_parked();
1903        assert_eq!(cx.read(|cx| cx.windows().len()), 2);
1904        let workspace_1 = cx
1905            .update(|cx| cx.windows()[0].downcast::<Workspace>())
1906            .unwrap();
1907        workspace_1
1908            .update(cx, |workspace, window, cx| {
1909                assert_eq!(
1910                    workspace
1911                        .worktrees(cx)
1912                        .map(|w| w.read(cx).abs_path())
1913                        .collect::<Vec<_>>(),
1914                    &[Path::new("/root/e").into()]
1915                );
1916                assert!(workspace.left_dock().read(cx).is_open());
1917                assert!(workspace.active_pane().focus_handle(cx).is_focused(window));
1918            })
1919            .unwrap();
1920    }
1921
1922    #[gpui::test]
1923    async fn test_open_add_new(cx: &mut TestAppContext) {
1924        let app_state = init_test(cx);
1925        app_state
1926            .fs
1927            .as_fake()
1928            .insert_tree(
1929                path!("/root"),
1930                json!({"a": "hey", "b": "", "dir": {"c": "f"}}),
1931            )
1932            .await;
1933
1934        cx.update(|cx| {
1935            open_paths(
1936                &[PathBuf::from(path!("/root/dir"))],
1937                app_state.clone(),
1938                workspace::OpenOptions::default(),
1939                cx,
1940            )
1941        })
1942        .await
1943        .unwrap();
1944        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1945
1946        cx.update(|cx| {
1947            open_paths(
1948                &[PathBuf::from(path!("/root/a"))],
1949                app_state.clone(),
1950                workspace::OpenOptions {
1951                    open_new_workspace: Some(false),
1952                    ..Default::default()
1953                },
1954                cx,
1955            )
1956        })
1957        .await
1958        .unwrap();
1959        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1960
1961        cx.update(|cx| {
1962            open_paths(
1963                &[PathBuf::from(path!("/root/dir/c"))],
1964                app_state.clone(),
1965                workspace::OpenOptions {
1966                    open_new_workspace: Some(true),
1967                    ..Default::default()
1968                },
1969                cx,
1970            )
1971        })
1972        .await
1973        .unwrap();
1974        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
1975    }
1976
1977    #[gpui::test]
1978    async fn test_open_file_in_many_spaces(cx: &mut TestAppContext) {
1979        let app_state = init_test(cx);
1980        app_state
1981            .fs
1982            .as_fake()
1983            .insert_tree(
1984                path!("/root"),
1985                json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}}),
1986            )
1987            .await;
1988
1989        cx.update(|cx| {
1990            open_paths(
1991                &[PathBuf::from(path!("/root/dir1/a"))],
1992                app_state.clone(),
1993                workspace::OpenOptions::default(),
1994                cx,
1995            )
1996        })
1997        .await
1998        .unwrap();
1999        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2000        let window1 = cx.update(|cx| cx.active_window().unwrap());
2001
2002        cx.update(|cx| {
2003            open_paths(
2004                &[PathBuf::from(path!("/root/dir2/c"))],
2005                app_state.clone(),
2006                workspace::OpenOptions::default(),
2007                cx,
2008            )
2009        })
2010        .await
2011        .unwrap();
2012        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2013
2014        cx.update(|cx| {
2015            open_paths(
2016                &[PathBuf::from(path!("/root/dir2"))],
2017                app_state.clone(),
2018                workspace::OpenOptions::default(),
2019                cx,
2020            )
2021        })
2022        .await
2023        .unwrap();
2024        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2025        let window2 = cx.update(|cx| cx.active_window().unwrap());
2026        assert!(window1 != window2);
2027        cx.update_window(window1, |_, window, _| window.activate_window())
2028            .unwrap();
2029
2030        cx.update(|cx| {
2031            open_paths(
2032                &[PathBuf::from(path!("/root/dir2/c"))],
2033                app_state.clone(),
2034                workspace::OpenOptions::default(),
2035                cx,
2036            )
2037        })
2038        .await
2039        .unwrap();
2040        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2041        // should have opened in window2 because that has dir2 visibly open (window1 has it open, but not in the project panel)
2042        assert!(cx.update(|cx| cx.active_window().unwrap()) == window2);
2043    }
2044
2045    #[gpui::test]
2046    async fn test_window_edit_state_restoring_disabled(cx: &mut TestAppContext) {
2047        let executor = cx.executor();
2048        let app_state = init_test(cx);
2049
2050        cx.update(|cx| {
2051            SettingsStore::update_global(cx, |store, cx| {
2052                store.update_user_settings::<ProjectSettings>(cx, |settings| {
2053                    settings.session.restore_unsaved_buffers = false
2054                });
2055            });
2056        });
2057
2058        app_state
2059            .fs
2060            .as_fake()
2061            .insert_tree(path!("/root"), json!({"a": "hey"}))
2062            .await;
2063
2064        cx.update(|cx| {
2065            open_paths(
2066                &[PathBuf::from(path!("/root/a"))],
2067                app_state.clone(),
2068                workspace::OpenOptions::default(),
2069                cx,
2070            )
2071        })
2072        .await
2073        .unwrap();
2074        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2075
2076        // When opening the workspace, the window is not in a edited state.
2077        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2078
2079        let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
2080            cx.update(|cx| window.read(cx).unwrap().is_edited())
2081        };
2082        let pane = window
2083            .read_with(cx, |workspace, _| workspace.active_pane().clone())
2084            .unwrap();
2085        let editor = window
2086            .read_with(cx, |workspace, cx| {
2087                workspace
2088                    .active_item(cx)
2089                    .unwrap()
2090                    .downcast::<Editor>()
2091                    .unwrap()
2092            })
2093            .unwrap();
2094
2095        assert!(!window_is_edited(window, cx));
2096
2097        // Editing a buffer marks the window as edited.
2098        window
2099            .update(cx, |_, window, cx| {
2100                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2101            })
2102            .unwrap();
2103
2104        assert!(window_is_edited(window, cx));
2105
2106        // Undoing the edit restores the window's edited state.
2107        window
2108            .update(cx, |_, window, cx| {
2109                editor.update(cx, |editor, cx| {
2110                    editor.undo(&Default::default(), window, cx)
2111                });
2112            })
2113            .unwrap();
2114        assert!(!window_is_edited(window, cx));
2115
2116        // Redoing the edit marks the window as edited again.
2117        window
2118            .update(cx, |_, window, cx| {
2119                editor.update(cx, |editor, cx| {
2120                    editor.redo(&Default::default(), window, cx)
2121                });
2122            })
2123            .unwrap();
2124        assert!(window_is_edited(window, cx));
2125        let weak = editor.downgrade();
2126
2127        // Closing the item restores the window's edited state.
2128        let close = window
2129            .update(cx, |_, window, cx| {
2130                pane.update(cx, |pane, cx| {
2131                    drop(editor);
2132                    pane.close_active_item(&Default::default(), window, cx)
2133                        .unwrap()
2134                })
2135            })
2136            .unwrap();
2137        executor.run_until_parked();
2138
2139        cx.simulate_prompt_answer("Don't Save");
2140        close.await.unwrap();
2141
2142        // Advance the clock to ensure that the item has been serialized and dropped from the queue
2143        cx.executor().advance_clock(Duration::from_secs(1));
2144
2145        weak.assert_released();
2146        assert!(!window_is_edited(window, cx));
2147        // Opening the buffer again doesn't impact the window's edited state.
2148        cx.update(|cx| {
2149            open_paths(
2150                &[PathBuf::from(path!("/root/a"))],
2151                app_state,
2152                workspace::OpenOptions::default(),
2153                cx,
2154            )
2155        })
2156        .await
2157        .unwrap();
2158        executor.run_until_parked();
2159
2160        window
2161            .update(cx, |workspace, _, cx| {
2162                let editor = workspace
2163                    .active_item(cx)
2164                    .unwrap()
2165                    .downcast::<Editor>()
2166                    .unwrap();
2167
2168                editor.update(cx, |editor, cx| {
2169                    assert_eq!(editor.text(cx), "hey");
2170                });
2171            })
2172            .unwrap();
2173
2174        let editor = window
2175            .read_with(cx, |workspace, cx| {
2176                workspace
2177                    .active_item(cx)
2178                    .unwrap()
2179                    .downcast::<Editor>()
2180                    .unwrap()
2181            })
2182            .unwrap();
2183        assert!(!window_is_edited(window, cx));
2184
2185        // Editing the buffer marks the window as edited.
2186        window
2187            .update(cx, |_, window, cx| {
2188                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2189            })
2190            .unwrap();
2191        executor.run_until_parked();
2192        assert!(window_is_edited(window, cx));
2193
2194        // Ensure closing the window via the mouse gets preempted due to the
2195        // buffer having unsaved changes.
2196        assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
2197        executor.run_until_parked();
2198        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2199
2200        // The window is successfully closed after the user dismisses the prompt.
2201        cx.simulate_prompt_answer("Don't Save");
2202        executor.run_until_parked();
2203        assert_eq!(cx.update(|cx| cx.windows().len()), 0);
2204    }
2205
2206    #[gpui::test]
2207    async fn test_window_edit_state_restoring_enabled(cx: &mut TestAppContext) {
2208        let app_state = init_test(cx);
2209        app_state
2210            .fs
2211            .as_fake()
2212            .insert_tree(path!("/root"), json!({"a": "hey"}))
2213            .await;
2214
2215        cx.update(|cx| {
2216            open_paths(
2217                &[PathBuf::from(path!("/root/a"))],
2218                app_state.clone(),
2219                workspace::OpenOptions::default(),
2220                cx,
2221            )
2222        })
2223        .await
2224        .unwrap();
2225
2226        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2227
2228        // When opening the workspace, the window is not in a edited state.
2229        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2230
2231        let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
2232            cx.update(|cx| window.read(cx).unwrap().is_edited())
2233        };
2234
2235        let editor = window
2236            .read_with(cx, |workspace, cx| {
2237                workspace
2238                    .active_item(cx)
2239                    .unwrap()
2240                    .downcast::<Editor>()
2241                    .unwrap()
2242            })
2243            .unwrap();
2244
2245        assert!(!window_is_edited(window, cx));
2246
2247        // Editing a buffer marks the window as edited.
2248        window
2249            .update(cx, |_, window, cx| {
2250                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2251            })
2252            .unwrap();
2253
2254        assert!(window_is_edited(window, cx));
2255        cx.run_until_parked();
2256
2257        // Advance the clock to make sure the workspace is serialized
2258        cx.executor().advance_clock(Duration::from_secs(1));
2259
2260        // When closing the window, no prompt shows up and the window is closed.
2261        // buffer having unsaved changes.
2262        assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
2263        cx.run_until_parked();
2264        assert_eq!(cx.update(|cx| cx.windows().len()), 0);
2265
2266        // When we now reopen the window, the edited state and the edited buffer are back
2267        cx.update(|cx| {
2268            open_paths(
2269                &[PathBuf::from(path!("/root/a"))],
2270                app_state.clone(),
2271                workspace::OpenOptions::default(),
2272                cx,
2273            )
2274        })
2275        .await
2276        .unwrap();
2277
2278        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2279        assert!(cx.update(|cx| cx.active_window().is_some()));
2280
2281        cx.run_until_parked();
2282
2283        // When opening the workspace, the window is not in a edited state.
2284        let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
2285        assert!(window_is_edited(window, cx));
2286
2287        window
2288            .update(cx, |workspace, _, cx| {
2289                let editor = workspace
2290                    .active_item(cx)
2291                    .unwrap()
2292                    .downcast::<editor::Editor>()
2293                    .unwrap();
2294                editor.update(cx, |editor, cx| {
2295                    assert_eq!(editor.text(cx), "EDIThey");
2296                    assert!(editor.is_dirty(cx));
2297                });
2298
2299                editor
2300            })
2301            .unwrap();
2302    }
2303
2304    #[gpui::test]
2305    async fn test_new_empty_workspace(cx: &mut TestAppContext) {
2306        let app_state = init_test(cx);
2307        cx.update(|cx| {
2308            open_new(
2309                Default::default(),
2310                app_state.clone(),
2311                cx,
2312                |workspace, window, cx| {
2313                    Editor::new_file(workspace, &Default::default(), window, cx)
2314                },
2315            )
2316        })
2317        .await
2318        .unwrap();
2319        cx.run_until_parked();
2320
2321        let workspace = cx
2322            .update(|cx| cx.windows().first().unwrap().downcast::<Workspace>())
2323            .unwrap();
2324
2325        let editor = workspace
2326            .update(cx, |workspace, _, cx| {
2327                let editor = workspace
2328                    .active_item(cx)
2329                    .unwrap()
2330                    .downcast::<editor::Editor>()
2331                    .unwrap();
2332                editor.update(cx, |editor, cx| {
2333                    assert!(editor.text(cx).is_empty());
2334                    assert!(!editor.is_dirty(cx));
2335                });
2336
2337                editor
2338            })
2339            .unwrap();
2340
2341        let save_task = workspace
2342            .update(cx, |workspace, window, cx| {
2343                workspace.save_active_item(SaveIntent::Save, window, cx)
2344            })
2345            .unwrap();
2346        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
2347        cx.background_executor.run_until_parked();
2348        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
2349        save_task.await.unwrap();
2350        workspace
2351            .update(cx, |_, _, cx| {
2352                editor.update(cx, |editor, cx| {
2353                    assert!(!editor.is_dirty(cx));
2354                    assert_eq!(editor.title(cx), "the-new-name");
2355                });
2356            })
2357            .unwrap();
2358    }
2359
2360    #[gpui::test]
2361    async fn test_open_entry(cx: &mut TestAppContext) {
2362        let app_state = init_test(cx);
2363        app_state
2364            .fs
2365            .as_fake()
2366            .insert_tree(
2367                path!("/root"),
2368                json!({
2369                    "a": {
2370                        "file1": "contents 1",
2371                        "file2": "contents 2",
2372                        "file3": "contents 3",
2373                    },
2374                }),
2375            )
2376            .await;
2377
2378        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2379        project.update(cx, |project, _cx| {
2380            project.languages().add(markdown_language())
2381        });
2382        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2383        let workspace = window.root(cx).unwrap();
2384
2385        let entries = cx.read(|cx| workspace.file_project_paths(cx));
2386        let file1 = entries[0].clone();
2387        let file2 = entries[1].clone();
2388        let file3 = entries[2].clone();
2389
2390        // Open the first entry
2391        let entry_1 = window
2392            .update(cx, |w, window, cx| {
2393                w.open_path(file1.clone(), None, true, window, cx)
2394            })
2395            .unwrap()
2396            .await
2397            .unwrap();
2398        cx.read(|cx| {
2399            let pane = workspace.read(cx).active_pane().read(cx);
2400            assert_eq!(
2401                pane.active_item().unwrap().project_path(cx),
2402                Some(file1.clone())
2403            );
2404            assert_eq!(pane.items_len(), 1);
2405        });
2406
2407        // Open the second entry
2408        window
2409            .update(cx, |w, window, cx| {
2410                w.open_path(file2.clone(), None, true, window, cx)
2411            })
2412            .unwrap()
2413            .await
2414            .unwrap();
2415        cx.read(|cx| {
2416            let pane = workspace.read(cx).active_pane().read(cx);
2417            assert_eq!(
2418                pane.active_item().unwrap().project_path(cx),
2419                Some(file2.clone())
2420            );
2421            assert_eq!(pane.items_len(), 2);
2422        });
2423
2424        // Open the first entry again. The existing pane item is activated.
2425        let entry_1b = window
2426            .update(cx, |w, window, cx| {
2427                w.open_path(file1.clone(), None, true, window, cx)
2428            })
2429            .unwrap()
2430            .await
2431            .unwrap();
2432        assert_eq!(entry_1.item_id(), entry_1b.item_id());
2433
2434        cx.read(|cx| {
2435            let pane = workspace.read(cx).active_pane().read(cx);
2436            assert_eq!(
2437                pane.active_item().unwrap().project_path(cx),
2438                Some(file1.clone())
2439            );
2440            assert_eq!(pane.items_len(), 2);
2441        });
2442
2443        // Split the pane with the first entry, then open the second entry again.
2444        window
2445            .update(cx, |w, window, cx| {
2446                w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx);
2447                w.open_path(file2.clone(), None, true, window, cx)
2448            })
2449            .unwrap()
2450            .await
2451            .unwrap();
2452
2453        window
2454            .read_with(cx, |w, cx| {
2455                assert_eq!(
2456                    w.active_pane()
2457                        .read(cx)
2458                        .active_item()
2459                        .unwrap()
2460                        .project_path(cx),
2461                    Some(file2.clone())
2462                );
2463            })
2464            .unwrap();
2465
2466        // Open the third entry twice concurrently. Only one pane item is added.
2467        let (t1, t2) = window
2468            .update(cx, |w, window, cx| {
2469                (
2470                    w.open_path(file3.clone(), None, true, window, cx),
2471                    w.open_path(file3.clone(), None, true, window, cx),
2472                )
2473            })
2474            .unwrap();
2475        t1.await.unwrap();
2476        t2.await.unwrap();
2477        cx.read(|cx| {
2478            let pane = workspace.read(cx).active_pane().read(cx);
2479            assert_eq!(
2480                pane.active_item().unwrap().project_path(cx),
2481                Some(file3.clone())
2482            );
2483            let pane_entries = pane
2484                .items()
2485                .map(|i| i.project_path(cx).unwrap())
2486                .collect::<Vec<_>>();
2487            assert_eq!(pane_entries, &[file1, file2, file3]);
2488        });
2489    }
2490
2491    #[gpui::test]
2492    async fn test_open_paths(cx: &mut TestAppContext) {
2493        let app_state = init_test(cx);
2494
2495        app_state
2496            .fs
2497            .as_fake()
2498            .insert_tree(
2499                path!("/"),
2500                json!({
2501                    "dir1": {
2502                        "a.txt": ""
2503                    },
2504                    "dir2": {
2505                        "b.txt": ""
2506                    },
2507                    "dir3": {
2508                        "c.txt": ""
2509                    },
2510                    "d.txt": ""
2511                }),
2512            )
2513            .await;
2514
2515        cx.update(|cx| {
2516            open_paths(
2517                &[PathBuf::from(path!("/dir1/"))],
2518                app_state,
2519                workspace::OpenOptions::default(),
2520                cx,
2521            )
2522        })
2523        .await
2524        .unwrap();
2525        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2526        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2527        let workspace = window.root(cx).unwrap();
2528
2529        #[track_caller]
2530        fn assert_project_panel_selection(
2531            workspace: &Workspace,
2532            expected_worktree_path: &Path,
2533            expected_entry_path: &Path,
2534            cx: &App,
2535        ) {
2536            let project_panel = [
2537                workspace.left_dock().read(cx).panel::<ProjectPanel>(),
2538                workspace.right_dock().read(cx).panel::<ProjectPanel>(),
2539                workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
2540            ]
2541            .into_iter()
2542            .find_map(std::convert::identity)
2543            .expect("found no project panels")
2544            .read(cx);
2545            let (selected_worktree, selected_entry) = project_panel
2546                .selected_entry(cx)
2547                .expect("project panel should have a selected entry");
2548            assert_eq!(
2549                selected_worktree.abs_path().as_ref(),
2550                expected_worktree_path,
2551                "Unexpected project panel selected worktree path"
2552            );
2553            assert_eq!(
2554                selected_entry.path.as_ref(),
2555                expected_entry_path,
2556                "Unexpected project panel selected entry path"
2557            );
2558        }
2559
2560        // Open a file within an existing worktree.
2561        window
2562            .update(cx, |workspace, window, cx| {
2563                workspace.open_paths(
2564                    vec![path!("/dir1/a.txt").into()],
2565                    OpenOptions {
2566                        visible: Some(OpenVisible::All),
2567                        ..Default::default()
2568                    },
2569                    None,
2570                    window,
2571                    cx,
2572                )
2573            })
2574            .unwrap()
2575            .await;
2576        cx.read(|cx| {
2577            let workspace = workspace.read(cx);
2578            assert_project_panel_selection(
2579                workspace,
2580                Path::new(path!("/dir1")),
2581                Path::new("a.txt"),
2582                cx,
2583            );
2584            assert_eq!(
2585                workspace
2586                    .active_pane()
2587                    .read(cx)
2588                    .active_item()
2589                    .unwrap()
2590                    .act_as::<Editor>(cx)
2591                    .unwrap()
2592                    .read(cx)
2593                    .title(cx),
2594                "a.txt"
2595            );
2596        });
2597
2598        // Open a file outside of any existing worktree.
2599        window
2600            .update(cx, |workspace, window, cx| {
2601                workspace.open_paths(
2602                    vec![path!("/dir2/b.txt").into()],
2603                    OpenOptions {
2604                        visible: Some(OpenVisible::All),
2605                        ..Default::default()
2606                    },
2607                    None,
2608                    window,
2609                    cx,
2610                )
2611            })
2612            .unwrap()
2613            .await;
2614        cx.read(|cx| {
2615            let workspace = workspace.read(cx);
2616            assert_project_panel_selection(
2617                workspace,
2618                Path::new(path!("/dir2/b.txt")),
2619                Path::new(""),
2620                cx,
2621            );
2622            let worktree_roots = workspace
2623                .worktrees(cx)
2624                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2625                .collect::<HashSet<_>>();
2626            assert_eq!(
2627                worktree_roots,
2628                vec![path!("/dir1"), path!("/dir2/b.txt")]
2629                    .into_iter()
2630                    .map(Path::new)
2631                    .collect(),
2632            );
2633            assert_eq!(
2634                workspace
2635                    .active_pane()
2636                    .read(cx)
2637                    .active_item()
2638                    .unwrap()
2639                    .act_as::<Editor>(cx)
2640                    .unwrap()
2641                    .read(cx)
2642                    .title(cx),
2643                "b.txt"
2644            );
2645        });
2646
2647        // Ensure opening a directory and one of its children only adds one worktree.
2648        window
2649            .update(cx, |workspace, window, cx| {
2650                workspace.open_paths(
2651                    vec![path!("/dir3").into(), path!("/dir3/c.txt").into()],
2652                    OpenOptions {
2653                        visible: Some(OpenVisible::All),
2654                        ..Default::default()
2655                    },
2656                    None,
2657                    window,
2658                    cx,
2659                )
2660            })
2661            .unwrap()
2662            .await;
2663        cx.read(|cx| {
2664            let workspace = workspace.read(cx);
2665            assert_project_panel_selection(
2666                workspace,
2667                Path::new(path!("/dir3")),
2668                Path::new("c.txt"),
2669                cx,
2670            );
2671            let worktree_roots = workspace
2672                .worktrees(cx)
2673                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2674                .collect::<HashSet<_>>();
2675            assert_eq!(
2676                worktree_roots,
2677                vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
2678                    .into_iter()
2679                    .map(Path::new)
2680                    .collect(),
2681            );
2682            assert_eq!(
2683                workspace
2684                    .active_pane()
2685                    .read(cx)
2686                    .active_item()
2687                    .unwrap()
2688                    .act_as::<Editor>(cx)
2689                    .unwrap()
2690                    .read(cx)
2691                    .title(cx),
2692                "c.txt"
2693            );
2694        });
2695
2696        // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
2697        window
2698            .update(cx, |workspace, window, cx| {
2699                workspace.open_paths(
2700                    vec![path!("/d.txt").into()],
2701                    OpenOptions {
2702                        visible: Some(OpenVisible::None),
2703                        ..Default::default()
2704                    },
2705                    None,
2706                    window,
2707                    cx,
2708                )
2709            })
2710            .unwrap()
2711            .await;
2712        cx.read(|cx| {
2713            let workspace = workspace.read(cx);
2714            assert_project_panel_selection(
2715                workspace,
2716                Path::new(path!("/d.txt")),
2717                Path::new(""),
2718                cx,
2719            );
2720            let worktree_roots = workspace
2721                .worktrees(cx)
2722                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2723                .collect::<HashSet<_>>();
2724            assert_eq!(
2725                worktree_roots,
2726                vec![
2727                    path!("/dir1"),
2728                    path!("/dir2/b.txt"),
2729                    path!("/dir3"),
2730                    path!("/d.txt")
2731                ]
2732                .into_iter()
2733                .map(Path::new)
2734                .collect(),
2735            );
2736
2737            let visible_worktree_roots = workspace
2738                .visible_worktrees(cx)
2739                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2740                .collect::<HashSet<_>>();
2741            assert_eq!(
2742                visible_worktree_roots,
2743                vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
2744                    .into_iter()
2745                    .map(Path::new)
2746                    .collect(),
2747            );
2748
2749            assert_eq!(
2750                workspace
2751                    .active_pane()
2752                    .read(cx)
2753                    .active_item()
2754                    .unwrap()
2755                    .act_as::<Editor>(cx)
2756                    .unwrap()
2757                    .read(cx)
2758                    .title(cx),
2759                "d.txt"
2760            );
2761        });
2762    }
2763
2764    #[gpui::test]
2765    async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
2766        let app_state = init_test(cx);
2767        cx.update(|cx| {
2768            cx.update_global::<SettingsStore, _>(|store, cx| {
2769                store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
2770                    project_settings.file_scan_exclusions =
2771                        Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
2772                });
2773            });
2774        });
2775        app_state
2776            .fs
2777            .as_fake()
2778            .insert_tree(
2779                path!("/root"),
2780                json!({
2781                    ".gitignore": "ignored_dir\n",
2782                    ".git": {
2783                        "HEAD": "ref: refs/heads/main",
2784                    },
2785                    "regular_dir": {
2786                        "file": "regular file contents",
2787                    },
2788                    "ignored_dir": {
2789                        "ignored_subdir": {
2790                            "file": "ignored subfile contents",
2791                        },
2792                        "file": "ignored file contents",
2793                    },
2794                    "excluded_dir": {
2795                        "file": "excluded file contents",
2796                        "ignored_subdir": {
2797                            "file": "ignored subfile contents",
2798                        },
2799                    },
2800                }),
2801            )
2802            .await;
2803
2804        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2805        project.update(cx, |project, _cx| {
2806            project.languages().add(markdown_language())
2807        });
2808        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2809        let workspace = window.root(cx).unwrap();
2810
2811        let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
2812        let paths_to_open = [
2813            PathBuf::from(path!("/root/excluded_dir/file")),
2814            PathBuf::from(path!("/root/.git/HEAD")),
2815            PathBuf::from(path!("/root/excluded_dir/ignored_subdir")),
2816        ];
2817        let (opened_workspace, new_items) = cx
2818            .update(|cx| {
2819                workspace::open_paths(
2820                    &paths_to_open,
2821                    app_state,
2822                    workspace::OpenOptions::default(),
2823                    cx,
2824                )
2825            })
2826            .await
2827            .unwrap();
2828
2829        assert_eq!(
2830            opened_workspace.root(cx).unwrap().entity_id(),
2831            workspace.entity_id(),
2832            "Excluded files in subfolders of a workspace root should be opened in the workspace"
2833        );
2834        let mut opened_paths = cx.read(|cx| {
2835            assert_eq!(
2836                new_items.len(),
2837                paths_to_open.len(),
2838                "Expect to get the same number of opened items as submitted paths to open"
2839            );
2840            new_items
2841                .iter()
2842                .zip(paths_to_open.iter())
2843                .map(|(i, path)| {
2844                    match i {
2845                        Some(Ok(i)) => {
2846                            Some(i.project_path(cx).map(|p| p.path.display().to_string()))
2847                        }
2848                        Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
2849                        None => None,
2850                    }
2851                    .flatten()
2852                })
2853                .collect::<Vec<_>>()
2854        });
2855        opened_paths.sort();
2856        assert_eq!(
2857            opened_paths,
2858            vec![
2859                None,
2860                Some(separator!(".git/HEAD").to_string()),
2861                Some(separator!("excluded_dir/file").to_string()),
2862            ],
2863            "Excluded files should get opened, excluded dir should not get opened"
2864        );
2865
2866        let entries = cx.read(|cx| workspace.file_project_paths(cx));
2867        assert_eq!(
2868            initial_entries, entries,
2869            "Workspace entries should not change after opening excluded files and directories paths"
2870        );
2871
2872        cx.read(|cx| {
2873                let pane = workspace.read(cx).active_pane().read(cx);
2874                let mut opened_buffer_paths = pane
2875                    .items()
2876                    .map(|i| {
2877                        i.project_path(cx)
2878                            .expect("all excluded files that got open should have a path")
2879                            .path
2880                            .display()
2881                            .to_string()
2882                    })
2883                    .collect::<Vec<_>>();
2884                opened_buffer_paths.sort();
2885                assert_eq!(
2886                    opened_buffer_paths,
2887                    vec![separator!(".git/HEAD").to_string(), separator!("excluded_dir/file").to_string()],
2888                    "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
2889                );
2890            });
2891    }
2892
2893    #[gpui::test]
2894    async fn test_save_conflicting_item(cx: &mut TestAppContext) {
2895        let app_state = init_test(cx);
2896        app_state
2897            .fs
2898            .as_fake()
2899            .insert_tree(path!("/root"), json!({ "a.txt": "" }))
2900            .await;
2901
2902        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2903        project.update(cx, |project, _cx| {
2904            project.languages().add(markdown_language())
2905        });
2906        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2907        let workspace = window.root(cx).unwrap();
2908
2909        // Open a file within an existing worktree.
2910        window
2911            .update(cx, |workspace, window, cx| {
2912                workspace.open_paths(
2913                    vec![PathBuf::from(path!("/root/a.txt"))],
2914                    OpenOptions {
2915                        visible: Some(OpenVisible::All),
2916                        ..Default::default()
2917                    },
2918                    None,
2919                    window,
2920                    cx,
2921                )
2922            })
2923            .unwrap()
2924            .await;
2925        let editor = cx.read(|cx| {
2926            let pane = workspace.read(cx).active_pane().read(cx);
2927            let item = pane.active_item().unwrap();
2928            item.downcast::<Editor>().unwrap()
2929        });
2930
2931        window
2932            .update(cx, |_, window, cx| {
2933                editor.update(cx, |editor, cx| editor.handle_input("x", window, cx));
2934            })
2935            .unwrap();
2936
2937        app_state
2938            .fs
2939            .as_fake()
2940            .insert_file(path!("/root/a.txt"), b"changed".to_vec())
2941            .await;
2942
2943        cx.run_until_parked();
2944        cx.read(|cx| assert!(editor.is_dirty(cx)));
2945        cx.read(|cx| assert!(editor.has_conflict(cx)));
2946
2947        let save_task = window
2948            .update(cx, |workspace, window, cx| {
2949                workspace.save_active_item(SaveIntent::Save, window, cx)
2950            })
2951            .unwrap();
2952        cx.background_executor.run_until_parked();
2953        cx.simulate_prompt_answer("Overwrite");
2954        save_task.await.unwrap();
2955        window
2956            .update(cx, |_, _, cx| {
2957                editor.update(cx, |editor, cx| {
2958                    assert!(!editor.is_dirty(cx));
2959                    assert!(!editor.has_conflict(cx));
2960                });
2961            })
2962            .unwrap();
2963    }
2964
2965    #[gpui::test]
2966    async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
2967        let app_state = init_test(cx);
2968        app_state
2969            .fs
2970            .create_dir(Path::new(path!("/root")))
2971            .await
2972            .unwrap();
2973
2974        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2975        project.update(cx, |project, _| {
2976            project.languages().add(markdown_language());
2977            project.languages().add(rust_lang());
2978        });
2979        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2980        let worktree = cx.update(|cx| window.read(cx).unwrap().worktrees(cx).next().unwrap());
2981
2982        // Create a new untitled buffer
2983        cx.dispatch_action(window.into(), NewFile);
2984        let editor = window
2985            .read_with(cx, |workspace, cx| {
2986                workspace
2987                    .active_item(cx)
2988                    .unwrap()
2989                    .downcast::<Editor>()
2990                    .unwrap()
2991            })
2992            .unwrap();
2993
2994        window
2995            .update(cx, |_, window, cx| {
2996                editor.update(cx, |editor, cx| {
2997                    assert!(!editor.is_dirty(cx));
2998                    assert_eq!(editor.title(cx), "untitled");
2999                    assert!(Arc::ptr_eq(
3000                        &editor.buffer().read(cx).language_at(0, cx).unwrap(),
3001                        &languages::PLAIN_TEXT
3002                    ));
3003                    editor.handle_input("hi", window, cx);
3004                    assert!(editor.is_dirty(cx));
3005                });
3006            })
3007            .unwrap();
3008
3009        // Save the buffer. This prompts for a filename.
3010        let save_task = window
3011            .update(cx, |workspace, window, cx| {
3012                workspace.save_active_item(SaveIntent::Save, window, cx)
3013            })
3014            .unwrap();
3015        cx.background_executor.run_until_parked();
3016        cx.simulate_new_path_selection(|parent_dir| {
3017            assert_eq!(parent_dir, Path::new(path!("/root")));
3018            Some(parent_dir.join("the-new-name.rs"))
3019        });
3020        cx.read(|cx| {
3021            assert!(editor.is_dirty(cx));
3022            assert_eq!(editor.read(cx).title(cx), "untitled");
3023        });
3024
3025        // When the save completes, the buffer's title is updated and the language is assigned based
3026        // on the path.
3027        save_task.await.unwrap();
3028        window
3029            .update(cx, |_, _, cx| {
3030                editor.update(cx, |editor, cx| {
3031                    assert!(!editor.is_dirty(cx));
3032                    assert_eq!(editor.title(cx), "the-new-name.rs");
3033                    assert_eq!(
3034                        editor.buffer().read(cx).language_at(0, cx).unwrap().name(),
3035                        "Rust".into()
3036                    );
3037                });
3038            })
3039            .unwrap();
3040
3041        // Edit the file and save it again. This time, there is no filename prompt.
3042        window
3043            .update(cx, |_, window, cx| {
3044                editor.update(cx, |editor, cx| {
3045                    editor.handle_input(" there", window, cx);
3046                    assert!(editor.is_dirty(cx));
3047                });
3048            })
3049            .unwrap();
3050
3051        let save_task = window
3052            .update(cx, |workspace, window, cx| {
3053                workspace.save_active_item(SaveIntent::Save, window, cx)
3054            })
3055            .unwrap();
3056        save_task.await.unwrap();
3057
3058        assert!(!cx.did_prompt_for_new_path());
3059        window
3060            .update(cx, |_, _, cx| {
3061                editor.update(cx, |editor, cx| {
3062                    assert!(!editor.is_dirty(cx));
3063                    assert_eq!(editor.title(cx), "the-new-name.rs")
3064                });
3065            })
3066            .unwrap();
3067
3068        // Open the same newly-created file in another pane item. The new editor should reuse
3069        // the same buffer.
3070        cx.dispatch_action(window.into(), NewFile);
3071        window
3072            .update(cx, |workspace, window, cx| {
3073                workspace.split_and_clone(
3074                    workspace.active_pane().clone(),
3075                    SplitDirection::Right,
3076                    window,
3077                    cx,
3078                );
3079                workspace.open_path(
3080                    (worktree.read(cx).id(), "the-new-name.rs"),
3081                    None,
3082                    true,
3083                    window,
3084                    cx,
3085                )
3086            })
3087            .unwrap()
3088            .await
3089            .unwrap();
3090        let editor2 = window
3091            .update(cx, |workspace, _, cx| {
3092                workspace
3093                    .active_item(cx)
3094                    .unwrap()
3095                    .downcast::<Editor>()
3096                    .unwrap()
3097            })
3098            .unwrap();
3099        cx.read(|cx| {
3100            assert_eq!(
3101                editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
3102                editor.read(cx).buffer().read(cx).as_singleton().unwrap()
3103            );
3104        })
3105    }
3106
3107    #[gpui::test]
3108    async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
3109        let app_state = init_test(cx);
3110        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
3111
3112        let project = Project::test(app_state.fs.clone(), [], cx).await;
3113        project.update(cx, |project, _| {
3114            project.languages().add(rust_lang());
3115            project.languages().add(markdown_language());
3116        });
3117        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3118
3119        // Create a new untitled buffer
3120        cx.dispatch_action(window.into(), NewFile);
3121        let editor = window
3122            .read_with(cx, |workspace, cx| {
3123                workspace
3124                    .active_item(cx)
3125                    .unwrap()
3126                    .downcast::<Editor>()
3127                    .unwrap()
3128            })
3129            .unwrap();
3130        window
3131            .update(cx, |_, window, cx| {
3132                editor.update(cx, |editor, cx| {
3133                    assert!(Arc::ptr_eq(
3134                        &editor.buffer().read(cx).language_at(0, cx).unwrap(),
3135                        &languages::PLAIN_TEXT
3136                    ));
3137                    editor.handle_input("hi", window, cx);
3138                    assert!(editor.is_dirty(cx));
3139                });
3140            })
3141            .unwrap();
3142
3143        // Save the buffer. This prompts for a filename.
3144        let save_task = window
3145            .update(cx, |workspace, window, cx| {
3146                workspace.save_active_item(SaveIntent::Save, window, cx)
3147            })
3148            .unwrap();
3149        cx.background_executor.run_until_parked();
3150        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
3151        save_task.await.unwrap();
3152        // The buffer is not dirty anymore and the language is assigned based on the path.
3153        window
3154            .update(cx, |_, _, cx| {
3155                editor.update(cx, |editor, cx| {
3156                    assert!(!editor.is_dirty(cx));
3157                    assert_eq!(
3158                        editor.buffer().read(cx).language_at(0, cx).unwrap().name(),
3159                        "Rust".into()
3160                    )
3161                });
3162            })
3163            .unwrap();
3164    }
3165
3166    #[gpui::test]
3167    async fn test_pane_actions(cx: &mut TestAppContext) {
3168        let app_state = init_test(cx);
3169        app_state
3170            .fs
3171            .as_fake()
3172            .insert_tree(
3173                path!("/root"),
3174                json!({
3175                    "a": {
3176                        "file1": "contents 1",
3177                        "file2": "contents 2",
3178                        "file3": "contents 3",
3179                    },
3180                }),
3181            )
3182            .await;
3183
3184        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3185        project.update(cx, |project, _cx| {
3186            project.languages().add(markdown_language())
3187        });
3188        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3189        let workspace = window.root(cx).unwrap();
3190
3191        let entries = cx.read(|cx| workspace.file_project_paths(cx));
3192        let file1 = entries[0].clone();
3193
3194        let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
3195
3196        window
3197            .update(cx, |w, window, cx| {
3198                w.open_path(file1.clone(), None, true, window, cx)
3199            })
3200            .unwrap()
3201            .await
3202            .unwrap();
3203
3204        let (editor_1, buffer) = window
3205            .update(cx, |_, window, cx| {
3206                pane_1.update(cx, |pane_1, cx| {
3207                    let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
3208                    assert_eq!(editor.project_path(cx), Some(file1.clone()));
3209                    let buffer = editor.update(cx, |editor, cx| {
3210                        editor.insert("dirt", window, cx);
3211                        editor.buffer().downgrade()
3212                    });
3213                    (editor.downgrade(), buffer)
3214                })
3215            })
3216            .unwrap();
3217
3218        cx.dispatch_action(window.into(), pane::SplitRight);
3219        let editor_2 = cx.update(|cx| {
3220            let pane_2 = workspace.read(cx).active_pane().clone();
3221            assert_ne!(pane_1, pane_2);
3222
3223            let pane2_item = pane_2.read(cx).active_item().unwrap();
3224            assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
3225
3226            pane2_item.downcast::<Editor>().unwrap().downgrade()
3227        });
3228        cx.dispatch_action(
3229            window.into(),
3230            workspace::CloseActiveItem {
3231                save_intent: None,
3232                close_pinned: false,
3233            },
3234        );
3235
3236        cx.background_executor.run_until_parked();
3237        window
3238            .read_with(cx, |workspace, _| {
3239                assert_eq!(workspace.panes().len(), 1);
3240                assert_eq!(workspace.active_pane(), &pane_1);
3241            })
3242            .unwrap();
3243
3244        cx.dispatch_action(
3245            window.into(),
3246            workspace::CloseActiveItem {
3247                save_intent: None,
3248                close_pinned: false,
3249            },
3250        );
3251        cx.background_executor.run_until_parked();
3252        cx.simulate_prompt_answer("Don't Save");
3253        cx.background_executor.run_until_parked();
3254
3255        window
3256            .update(cx, |workspace, _, cx| {
3257                assert_eq!(workspace.panes().len(), 1);
3258                assert!(workspace.active_item(cx).is_none());
3259            })
3260            .unwrap();
3261
3262        cx.background_executor
3263            .advance_clock(SERIALIZATION_THROTTLE_TIME);
3264        cx.update(|_| {});
3265        editor_1.assert_released();
3266        editor_2.assert_released();
3267        buffer.assert_released();
3268    }
3269
3270    #[gpui::test]
3271    async fn test_navigation(cx: &mut TestAppContext) {
3272        let app_state = init_test(cx);
3273        app_state
3274            .fs
3275            .as_fake()
3276            .insert_tree(
3277                path!("/root"),
3278                json!({
3279                    "a": {
3280                        "file1": "contents 1\n".repeat(20),
3281                        "file2": "contents 2\n".repeat(20),
3282                        "file3": "contents 3\n".repeat(20),
3283                    },
3284                }),
3285            )
3286            .await;
3287
3288        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3289        project.update(cx, |project, _cx| {
3290            project.languages().add(markdown_language())
3291        });
3292        let workspace =
3293            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3294        let pane = workspace
3295            .read_with(cx, |workspace, _| workspace.active_pane().clone())
3296            .unwrap();
3297
3298        let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
3299        let file1 = entries[0].clone();
3300        let file2 = entries[1].clone();
3301        let file3 = entries[2].clone();
3302
3303        let editor1 = workspace
3304            .update(cx, |w, window, cx| {
3305                w.open_path(file1.clone(), None, true, window, cx)
3306            })
3307            .unwrap()
3308            .await
3309            .unwrap()
3310            .downcast::<Editor>()
3311            .unwrap();
3312        workspace
3313            .update(cx, |_, window, cx| {
3314                editor1.update(cx, |editor, cx| {
3315                    editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
3316                        s.select_display_ranges([DisplayPoint::new(DisplayRow(10), 0)
3317                            ..DisplayPoint::new(DisplayRow(10), 0)])
3318                    });
3319                });
3320            })
3321            .unwrap();
3322
3323        let editor2 = workspace
3324            .update(cx, |w, window, cx| {
3325                w.open_path(file2.clone(), None, true, window, cx)
3326            })
3327            .unwrap()
3328            .await
3329            .unwrap()
3330            .downcast::<Editor>()
3331            .unwrap();
3332        let editor3 = workspace
3333            .update(cx, |w, window, cx| {
3334                w.open_path(file3.clone(), None, true, window, cx)
3335            })
3336            .unwrap()
3337            .await
3338            .unwrap()
3339            .downcast::<Editor>()
3340            .unwrap();
3341
3342        workspace
3343            .update(cx, |_, window, cx| {
3344                editor3.update(cx, |editor, cx| {
3345                    editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
3346                        s.select_display_ranges([DisplayPoint::new(DisplayRow(12), 0)
3347                            ..DisplayPoint::new(DisplayRow(12), 0)])
3348                    });
3349                    editor.newline(&Default::default(), window, cx);
3350                    editor.newline(&Default::default(), window, cx);
3351                    editor.move_down(&Default::default(), window, cx);
3352                    editor.move_down(&Default::default(), window, cx);
3353                    editor.save(true, project.clone(), window, cx)
3354                })
3355            })
3356            .unwrap()
3357            .await
3358            .unwrap();
3359        workspace
3360            .update(cx, |_, window, cx| {
3361                editor3.update(cx, |editor, cx| {
3362                    editor.set_scroll_position(point(0., 12.5), window, cx)
3363                });
3364            })
3365            .unwrap();
3366        assert_eq!(
3367            active_location(&workspace, cx),
3368            (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
3369        );
3370
3371        workspace
3372            .update(cx, |w, window, cx| {
3373                w.go_back(w.active_pane().downgrade(), window, cx)
3374            })
3375            .unwrap()
3376            .await
3377            .unwrap();
3378        assert_eq!(
3379            active_location(&workspace, cx),
3380            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3381        );
3382
3383        workspace
3384            .update(cx, |w, window, cx| {
3385                w.go_back(w.active_pane().downgrade(), window, cx)
3386            })
3387            .unwrap()
3388            .await
3389            .unwrap();
3390        assert_eq!(
3391            active_location(&workspace, cx),
3392            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3393        );
3394
3395        workspace
3396            .update(cx, |w, window, cx| {
3397                w.go_back(w.active_pane().downgrade(), window, cx)
3398            })
3399            .unwrap()
3400            .await
3401            .unwrap();
3402        assert_eq!(
3403            active_location(&workspace, cx),
3404            (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
3405        );
3406
3407        workspace
3408            .update(cx, |w, window, cx| {
3409                w.go_back(w.active_pane().downgrade(), window, cx)
3410            })
3411            .unwrap()
3412            .await
3413            .unwrap();
3414        assert_eq!(
3415            active_location(&workspace, cx),
3416            (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3417        );
3418
3419        // Go back one more time and ensure we don't navigate past the first item in the history.
3420        workspace
3421            .update(cx, |w, window, cx| {
3422                w.go_back(w.active_pane().downgrade(), window, cx)
3423            })
3424            .unwrap()
3425            .await
3426            .unwrap();
3427        assert_eq!(
3428            active_location(&workspace, cx),
3429            (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3430        );
3431
3432        workspace
3433            .update(cx, |w, window, cx| {
3434                w.go_forward(w.active_pane().downgrade(), window, cx)
3435            })
3436            .unwrap()
3437            .await
3438            .unwrap();
3439        assert_eq!(
3440            active_location(&workspace, cx),
3441            (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
3442        );
3443
3444        workspace
3445            .update(cx, |w, window, cx| {
3446                w.go_forward(w.active_pane().downgrade(), window, cx)
3447            })
3448            .unwrap()
3449            .await
3450            .unwrap();
3451        assert_eq!(
3452            active_location(&workspace, cx),
3453            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3454        );
3455
3456        // Go forward to an item that has been closed, ensuring it gets re-opened at the same
3457        // location.
3458        workspace
3459            .update(cx, |_, window, cx| {
3460                pane.update(cx, |pane, cx| {
3461                    let editor3_id = editor3.entity_id();
3462                    drop(editor3);
3463                    pane.close_item_by_id(editor3_id, SaveIntent::Close, window, cx)
3464                })
3465            })
3466            .unwrap()
3467            .await
3468            .unwrap();
3469        workspace
3470            .update(cx, |w, window, cx| {
3471                w.go_forward(w.active_pane().downgrade(), window, cx)
3472            })
3473            .unwrap()
3474            .await
3475            .unwrap();
3476        assert_eq!(
3477            active_location(&workspace, cx),
3478            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3479        );
3480
3481        workspace
3482            .update(cx, |w, window, cx| {
3483                w.go_forward(w.active_pane().downgrade(), window, cx)
3484            })
3485            .unwrap()
3486            .await
3487            .unwrap();
3488        assert_eq!(
3489            active_location(&workspace, cx),
3490            (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
3491        );
3492
3493        workspace
3494            .update(cx, |w, window, cx| {
3495                w.go_back(w.active_pane().downgrade(), window, cx)
3496            })
3497            .unwrap()
3498            .await
3499            .unwrap();
3500        assert_eq!(
3501            active_location(&workspace, cx),
3502            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3503        );
3504
3505        // Go back to an item that has been closed and removed from disk
3506        workspace
3507            .update(cx, |_, window, cx| {
3508                pane.update(cx, |pane, cx| {
3509                    let editor2_id = editor2.entity_id();
3510                    drop(editor2);
3511                    pane.close_item_by_id(editor2_id, SaveIntent::Close, window, cx)
3512                })
3513            })
3514            .unwrap()
3515            .await
3516            .unwrap();
3517        app_state
3518            .fs
3519            .remove_file(Path::new(path!("/root/a/file2")), Default::default())
3520            .await
3521            .unwrap();
3522        cx.background_executor.run_until_parked();
3523
3524        workspace
3525            .update(cx, |w, window, cx| {
3526                w.go_back(w.active_pane().downgrade(), window, cx)
3527            })
3528            .unwrap()
3529            .await
3530            .unwrap();
3531        assert_eq!(
3532            active_location(&workspace, cx),
3533            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3534        );
3535        workspace
3536            .update(cx, |w, window, cx| {
3537                w.go_forward(w.active_pane().downgrade(), window, cx)
3538            })
3539            .unwrap()
3540            .await
3541            .unwrap();
3542        assert_eq!(
3543            active_location(&workspace, cx),
3544            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3545        );
3546
3547        // Modify file to collapse multiple nav history entries into the same location.
3548        // Ensure we don't visit the same location twice when navigating.
3549        workspace
3550            .update(cx, |_, window, cx| {
3551                editor1.update(cx, |editor, cx| {
3552                    editor.change_selections(None, window, cx, |s| {
3553                        s.select_display_ranges([DisplayPoint::new(DisplayRow(15), 0)
3554                            ..DisplayPoint::new(DisplayRow(15), 0)])
3555                    })
3556                });
3557            })
3558            .unwrap();
3559        for _ in 0..5 {
3560            workspace
3561                .update(cx, |_, window, cx| {
3562                    editor1.update(cx, |editor, cx| {
3563                        editor.change_selections(None, window, cx, |s| {
3564                            s.select_display_ranges([DisplayPoint::new(DisplayRow(3), 0)
3565                                ..DisplayPoint::new(DisplayRow(3), 0)])
3566                        });
3567                    });
3568                })
3569                .unwrap();
3570
3571            workspace
3572                .update(cx, |_, window, cx| {
3573                    editor1.update(cx, |editor, cx| {
3574                        editor.change_selections(None, window, cx, |s| {
3575                            s.select_display_ranges([DisplayPoint::new(DisplayRow(13), 0)
3576                                ..DisplayPoint::new(DisplayRow(13), 0)])
3577                        })
3578                    });
3579                })
3580                .unwrap();
3581        }
3582        workspace
3583            .update(cx, |_, window, cx| {
3584                editor1.update(cx, |editor, cx| {
3585                    editor.transact(window, cx, |editor, window, cx| {
3586                        editor.change_selections(None, window, cx, |s| {
3587                            s.select_display_ranges([DisplayPoint::new(DisplayRow(2), 0)
3588                                ..DisplayPoint::new(DisplayRow(14), 0)])
3589                        });
3590                        editor.insert("", window, cx);
3591                    })
3592                });
3593            })
3594            .unwrap();
3595
3596        workspace
3597            .update(cx, |_, window, cx| {
3598                editor1.update(cx, |editor, cx| {
3599                    editor.change_selections(None, window, cx, |s| {
3600                        s.select_display_ranges([DisplayPoint::new(DisplayRow(1), 0)
3601                            ..DisplayPoint::new(DisplayRow(1), 0)])
3602                    })
3603                });
3604            })
3605            .unwrap();
3606        workspace
3607            .update(cx, |w, window, cx| {
3608                w.go_back(w.active_pane().downgrade(), window, cx)
3609            })
3610            .unwrap()
3611            .await
3612            .unwrap();
3613        assert_eq!(
3614            active_location(&workspace, cx),
3615            (file1.clone(), DisplayPoint::new(DisplayRow(2), 0), 0.)
3616        );
3617        workspace
3618            .update(cx, |w, window, cx| {
3619                w.go_back(w.active_pane().downgrade(), window, cx)
3620            })
3621            .unwrap()
3622            .await
3623            .unwrap();
3624        assert_eq!(
3625            active_location(&workspace, cx),
3626            (file1.clone(), DisplayPoint::new(DisplayRow(3), 0), 0.)
3627        );
3628
3629        fn active_location(
3630            workspace: &WindowHandle<Workspace>,
3631            cx: &mut TestAppContext,
3632        ) -> (ProjectPath, DisplayPoint, f32) {
3633            workspace
3634                .update(cx, |workspace, _, cx| {
3635                    let item = workspace.active_item(cx).unwrap();
3636                    let editor = item.downcast::<Editor>().unwrap();
3637                    let (selections, scroll_position) = editor.update(cx, |editor, cx| {
3638                        (
3639                            editor.selections.display_ranges(cx),
3640                            editor.scroll_position(cx),
3641                        )
3642                    });
3643                    (
3644                        item.project_path(cx).unwrap(),
3645                        selections[0].start,
3646                        scroll_position.y,
3647                    )
3648                })
3649                .unwrap()
3650        }
3651    }
3652
3653    #[gpui::test]
3654    async fn test_reopening_closed_items(cx: &mut TestAppContext) {
3655        let app_state = init_test(cx);
3656        app_state
3657            .fs
3658            .as_fake()
3659            .insert_tree(
3660                path!("/root"),
3661                json!({
3662                    "a": {
3663                        "file1": "",
3664                        "file2": "",
3665                        "file3": "",
3666                        "file4": "",
3667                    },
3668                }),
3669            )
3670            .await;
3671
3672        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3673        project.update(cx, |project, _cx| {
3674            project.languages().add(markdown_language())
3675        });
3676        let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3677        let pane = workspace
3678            .read_with(cx, |workspace, _| workspace.active_pane().clone())
3679            .unwrap();
3680
3681        let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
3682        let file1 = entries[0].clone();
3683        let file2 = entries[1].clone();
3684        let file3 = entries[2].clone();
3685        let file4 = entries[3].clone();
3686
3687        let file1_item_id = workspace
3688            .update(cx, |w, window, cx| {
3689                w.open_path(file1.clone(), None, true, window, cx)
3690            })
3691            .unwrap()
3692            .await
3693            .unwrap()
3694            .item_id();
3695        let file2_item_id = workspace
3696            .update(cx, |w, window, cx| {
3697                w.open_path(file2.clone(), None, true, window, cx)
3698            })
3699            .unwrap()
3700            .await
3701            .unwrap()
3702            .item_id();
3703        let file3_item_id = workspace
3704            .update(cx, |w, window, cx| {
3705                w.open_path(file3.clone(), None, true, window, cx)
3706            })
3707            .unwrap()
3708            .await
3709            .unwrap()
3710            .item_id();
3711        let file4_item_id = workspace
3712            .update(cx, |w, window, cx| {
3713                w.open_path(file4.clone(), None, true, window, cx)
3714            })
3715            .unwrap()
3716            .await
3717            .unwrap()
3718            .item_id();
3719        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3720
3721        // Close all the pane items in some arbitrary order.
3722        workspace
3723            .update(cx, |_, window, cx| {
3724                pane.update(cx, |pane, cx| {
3725                    pane.close_item_by_id(file1_item_id, SaveIntent::Close, window, cx)
3726                })
3727            })
3728            .unwrap()
3729            .await
3730            .unwrap();
3731        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3732
3733        workspace
3734            .update(cx, |_, window, cx| {
3735                pane.update(cx, |pane, cx| {
3736                    pane.close_item_by_id(file4_item_id, SaveIntent::Close, window, cx)
3737                })
3738            })
3739            .unwrap()
3740            .await
3741            .unwrap();
3742        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3743
3744        workspace
3745            .update(cx, |_, window, cx| {
3746                pane.update(cx, |pane, cx| {
3747                    pane.close_item_by_id(file2_item_id, SaveIntent::Close, window, cx)
3748                })
3749            })
3750            .unwrap()
3751            .await
3752            .unwrap();
3753        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3754        workspace
3755            .update(cx, |_, window, cx| {
3756                pane.update(cx, |pane, cx| {
3757                    pane.close_item_by_id(file3_item_id, SaveIntent::Close, window, cx)
3758                })
3759            })
3760            .unwrap()
3761            .await
3762            .unwrap();
3763
3764        assert_eq!(active_path(&workspace, cx), None);
3765
3766        // Reopen all the closed items, ensuring they are reopened in the same order
3767        // in which they were closed.
3768        workspace
3769            .update(cx, Workspace::reopen_closed_item)
3770            .unwrap()
3771            .await
3772            .unwrap();
3773        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3774
3775        workspace
3776            .update(cx, Workspace::reopen_closed_item)
3777            .unwrap()
3778            .await
3779            .unwrap();
3780        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
3781
3782        workspace
3783            .update(cx, Workspace::reopen_closed_item)
3784            .unwrap()
3785            .await
3786            .unwrap();
3787        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3788
3789        workspace
3790            .update(cx, Workspace::reopen_closed_item)
3791            .unwrap()
3792            .await
3793            .unwrap();
3794        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
3795
3796        // Reopening past the last closed item is a no-op.
3797        workspace
3798            .update(cx, Workspace::reopen_closed_item)
3799            .unwrap()
3800            .await
3801            .unwrap();
3802        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
3803
3804        // Reopening closed items doesn't interfere with navigation history.
3805        workspace
3806            .update(cx, |workspace, window, cx| {
3807                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3808            })
3809            .unwrap()
3810            .await
3811            .unwrap();
3812        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3813
3814        workspace
3815            .update(cx, |workspace, window, cx| {
3816                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3817            })
3818            .unwrap()
3819            .await
3820            .unwrap();
3821        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
3822
3823        workspace
3824            .update(cx, |workspace, window, cx| {
3825                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3826            })
3827            .unwrap()
3828            .await
3829            .unwrap();
3830        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3831
3832        workspace
3833            .update(cx, |workspace, window, cx| {
3834                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3835            })
3836            .unwrap()
3837            .await
3838            .unwrap();
3839        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3840
3841        workspace
3842            .update(cx, |workspace, window, cx| {
3843                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3844            })
3845            .unwrap()
3846            .await
3847            .unwrap();
3848        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3849
3850        workspace
3851            .update(cx, |workspace, window, cx| {
3852                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3853            })
3854            .unwrap()
3855            .await
3856            .unwrap();
3857        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
3858
3859        workspace
3860            .update(cx, |workspace, window, cx| {
3861                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3862            })
3863            .unwrap()
3864            .await
3865            .unwrap();
3866        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
3867
3868        workspace
3869            .update(cx, |workspace, window, cx| {
3870                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3871            })
3872            .unwrap()
3873            .await
3874            .unwrap();
3875        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
3876
3877        fn active_path(
3878            workspace: &WindowHandle<Workspace>,
3879            cx: &TestAppContext,
3880        ) -> Option<ProjectPath> {
3881            workspace
3882                .read_with(cx, |workspace, cx| {
3883                    let item = workspace.active_item(cx)?;
3884                    item.project_path(cx)
3885                })
3886                .unwrap()
3887        }
3888    }
3889
3890    fn init_keymap_test(cx: &mut TestAppContext) -> Arc<AppState> {
3891        cx.update(|cx| {
3892            let app_state = AppState::test(cx);
3893
3894            theme::init(theme::LoadThemes::JustBase, cx);
3895            client::init(&app_state.client, cx);
3896            language::init(cx);
3897            workspace::init(app_state.clone(), cx);
3898            welcome::init(cx);
3899            Project::init_settings(cx);
3900            app_state
3901        })
3902    }
3903
3904    #[gpui::test]
3905    async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
3906        let executor = cx.executor();
3907        let app_state = init_keymap_test(cx);
3908        let project = Project::test(app_state.fs.clone(), [], cx).await;
3909        let workspace =
3910            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3911
3912        actions!(test1, [A, B]);
3913        // From the Atom keymap
3914        use workspace::ActivatePreviousPane;
3915        // From the JetBrains keymap
3916        use workspace::ActivatePreviousItem;
3917
3918        app_state
3919            .fs
3920            .save(
3921                "/settings.json".as_ref(),
3922                &r#"{"base_keymap": "Atom"}"#.into(),
3923                Default::default(),
3924            )
3925            .await
3926            .unwrap();
3927
3928        app_state
3929            .fs
3930            .save(
3931                "/keymap.json".as_ref(),
3932                &r#"[{"bindings": {"backspace": "test1::A"}}]"#.into(),
3933                Default::default(),
3934            )
3935            .await
3936            .unwrap();
3937        executor.run_until_parked();
3938        cx.update(|cx| {
3939            let settings_rx = watch_config_file(
3940                &executor,
3941                app_state.fs.clone(),
3942                PathBuf::from("/settings.json"),
3943            );
3944            let keymap_rx = watch_config_file(
3945                &executor,
3946                app_state.fs.clone(),
3947                PathBuf::from("/keymap.json"),
3948            );
3949            let global_settings_rx = watch_config_file(
3950                &executor,
3951                app_state.fs.clone(),
3952                PathBuf::from("/global_settings.json"),
3953            );
3954            handle_settings_file_changes(settings_rx, global_settings_rx, cx, |_, _| {});
3955            handle_keymap_file_changes(keymap_rx, cx);
3956        });
3957        workspace
3958            .update(cx, |workspace, _, cx| {
3959                workspace.register_action(|_, _: &A, _window, _cx| {});
3960                workspace.register_action(|_, _: &B, _window, _cx| {});
3961                workspace.register_action(|_, _: &ActivatePreviousPane, _window, _cx| {});
3962                workspace.register_action(|_, _: &ActivatePreviousItem, _window, _cx| {});
3963                cx.notify();
3964            })
3965            .unwrap();
3966        executor.run_until_parked();
3967        // Test loading the keymap base at all
3968        assert_key_bindings_for(
3969            workspace.into(),
3970            cx,
3971            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
3972            line!(),
3973        );
3974
3975        // Test modifying the users keymap, while retaining the base keymap
3976        app_state
3977            .fs
3978            .save(
3979                "/keymap.json".as_ref(),
3980                &r#"[{"bindings": {"backspace": "test1::B"}}]"#.into(),
3981                Default::default(),
3982            )
3983            .await
3984            .unwrap();
3985
3986        executor.run_until_parked();
3987
3988        assert_key_bindings_for(
3989            workspace.into(),
3990            cx,
3991            vec![("backspace", &B), ("k", &ActivatePreviousPane)],
3992            line!(),
3993        );
3994
3995        // Test modifying the base, while retaining the users keymap
3996        app_state
3997            .fs
3998            .save(
3999                "/settings.json".as_ref(),
4000                &r#"{"base_keymap": "JetBrains"}"#.into(),
4001                Default::default(),
4002            )
4003            .await
4004            .unwrap();
4005
4006        executor.run_until_parked();
4007
4008        assert_key_bindings_for(
4009            workspace.into(),
4010            cx,
4011            vec![("backspace", &B), ("{", &ActivatePreviousItem)],
4012            line!(),
4013        );
4014    }
4015
4016    #[gpui::test]
4017    async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
4018        let executor = cx.executor();
4019        let app_state = init_keymap_test(cx);
4020        let project = Project::test(app_state.fs.clone(), [], cx).await;
4021        let workspace =
4022            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4023
4024        actions!(test2, [A, B]);
4025        // From the Atom keymap
4026        use workspace::ActivatePreviousPane;
4027        // From the JetBrains keymap
4028        use diagnostics::Deploy;
4029
4030        workspace
4031            .update(cx, |workspace, _, _| {
4032                workspace.register_action(|_, _: &A, _window, _cx| {});
4033                workspace.register_action(|_, _: &B, _window, _cx| {});
4034                workspace.register_action(|_, _: &Deploy, _window, _cx| {});
4035            })
4036            .unwrap();
4037        app_state
4038            .fs
4039            .save(
4040                "/settings.json".as_ref(),
4041                &r#"{"base_keymap": "Atom"}"#.into(),
4042                Default::default(),
4043            )
4044            .await
4045            .unwrap();
4046        app_state
4047            .fs
4048            .save(
4049                "/keymap.json".as_ref(),
4050                &r#"[{"bindings": {"backspace": "test2::A"}}]"#.into(),
4051                Default::default(),
4052            )
4053            .await
4054            .unwrap();
4055
4056        cx.update(|cx| {
4057            let settings_rx = watch_config_file(
4058                &executor,
4059                app_state.fs.clone(),
4060                PathBuf::from("/settings.json"),
4061            );
4062            let keymap_rx = watch_config_file(
4063                &executor,
4064                app_state.fs.clone(),
4065                PathBuf::from("/keymap.json"),
4066            );
4067
4068            let global_settings_rx = watch_config_file(
4069                &executor,
4070                app_state.fs.clone(),
4071                PathBuf::from("/global_settings.json"),
4072            );
4073            handle_settings_file_changes(settings_rx, global_settings_rx, cx, |_, _| {});
4074            handle_keymap_file_changes(keymap_rx, cx);
4075        });
4076
4077        cx.background_executor.run_until_parked();
4078
4079        cx.background_executor.run_until_parked();
4080        // Test loading the keymap base at all
4081        assert_key_bindings_for(
4082            workspace.into(),
4083            cx,
4084            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
4085            line!(),
4086        );
4087
4088        // Test disabling the key binding for the base keymap
4089        app_state
4090            .fs
4091            .save(
4092                "/keymap.json".as_ref(),
4093                &r#"[{"bindings": {"backspace": null}}]"#.into(),
4094                Default::default(),
4095            )
4096            .await
4097            .unwrap();
4098
4099        cx.background_executor.run_until_parked();
4100
4101        assert_key_bindings_for(
4102            workspace.into(),
4103            cx,
4104            vec![("k", &ActivatePreviousPane)],
4105            line!(),
4106        );
4107
4108        // Test modifying the base, while retaining the users keymap
4109        app_state
4110            .fs
4111            .save(
4112                "/settings.json".as_ref(),
4113                &r#"{"base_keymap": "JetBrains"}"#.into(),
4114                Default::default(),
4115            )
4116            .await
4117            .unwrap();
4118
4119        cx.background_executor.run_until_parked();
4120
4121        assert_key_bindings_for(workspace.into(), cx, vec![("6", &Deploy)], line!());
4122    }
4123
4124    #[gpui::test]
4125    async fn test_generate_keymap_json_schema_for_registered_actions(
4126        cx: &mut gpui::TestAppContext,
4127    ) {
4128        init_keymap_test(cx);
4129        cx.update(|cx| {
4130            // Make sure it doesn't panic.
4131            KeymapFile::generate_json_schema_for_registered_actions(cx);
4132        });
4133    }
4134
4135    /// Actions that don't build from empty input won't work from command palette invocation.
4136    #[gpui::test]
4137    async fn test_actions_build_with_empty_input(cx: &mut gpui::TestAppContext) {
4138        init_keymap_test(cx);
4139        cx.update(|cx| {
4140            let all_actions = cx.all_action_names();
4141            let mut failing_names = Vec::new();
4142            let mut errors = Vec::new();
4143            for action in all_actions {
4144                match action.to_string().as_str() {
4145                    "vim::FindCommand"
4146                    | "vim::Literal"
4147                    | "vim::ResizePane"
4148                    | "vim::PushObject"
4149                    | "vim::PushFindForward"
4150                    | "vim::PushFindBackward"
4151                    | "vim::PushSneak"
4152                    | "vim::PushSneakBackward"
4153                    | "vim::PushChangeSurrounds"
4154                    | "vim::PushJump"
4155                    | "vim::PushDigraph"
4156                    | "vim::PushLiteral"
4157                    | "vim::Number"
4158                    | "vim::SelectRegister"
4159                    | "git::StageAndNext"
4160                    | "git::UnstageAndNext"
4161                    | "terminal::SendText"
4162                    | "terminal::SendKeystroke"
4163                    | "app_menu::OpenApplicationMenu"
4164                    | "picker::ConfirmInput"
4165                    | "editor::HandleInput"
4166                    | "editor::FoldAtLevel"
4167                    | "pane::ActivateItem"
4168                    | "workspace::ActivatePane"
4169                    | "workspace::MoveItemToPane"
4170                    | "workspace::MoveItemToPaneInDirection"
4171                    | "workspace::OpenTerminal"
4172                    | "workspace::SendKeystrokes"
4173                    | "zed::OpenBrowser"
4174                    | "zed::OpenZedUrl" => {}
4175                    _ => {
4176                        let result = cx.build_action(action, None);
4177                        match &result {
4178                            Ok(_) => {}
4179                            Err(err) => {
4180                                failing_names.push(action);
4181                                errors.push(format!("{action} failed to build: {err:?}"));
4182                            }
4183                        }
4184                    }
4185                }
4186            }
4187            if errors.len() > 0 {
4188                panic!(
4189                    "Failed to build actions using {{}} as input: {:?}. Errors:\n{}",
4190                    failing_names,
4191                    errors.join("\n")
4192                );
4193            }
4194        });
4195    }
4196
4197    #[gpui::test]
4198    fn test_bundled_settings_and_themes(cx: &mut App) {
4199        cx.text_system()
4200            .add_fonts(vec![
4201                Assets
4202                    .load("fonts/plex-mono/ZedPlexMono-Regular.ttf")
4203                    .unwrap()
4204                    .unwrap(),
4205                Assets
4206                    .load("fonts/plex-sans/ZedPlexSans-Regular.ttf")
4207                    .unwrap()
4208                    .unwrap(),
4209            ])
4210            .unwrap();
4211        let themes = ThemeRegistry::default();
4212        settings::init(cx);
4213        theme::init(theme::LoadThemes::JustBase, cx);
4214
4215        let mut has_default_theme = false;
4216        for theme_name in themes.list().into_iter().map(|meta| meta.name) {
4217            let theme = themes.get(&theme_name).unwrap();
4218            assert_eq!(theme.name, theme_name);
4219            if theme.name == ThemeSettings::get(None, cx).active_theme.name {
4220                has_default_theme = true;
4221            }
4222        }
4223        assert!(has_default_theme);
4224    }
4225
4226    #[gpui::test]
4227    async fn test_bundled_languages(cx: &mut TestAppContext) {
4228        env_logger::builder().is_test(true).try_init().ok();
4229        let settings = cx.update(SettingsStore::test);
4230        cx.set_global(settings);
4231        let languages = LanguageRegistry::test(cx.executor());
4232        let languages = Arc::new(languages);
4233        let node_runtime = node_runtime::NodeRuntime::unavailable();
4234        cx.update(|cx| {
4235            languages::init(languages.clone(), node_runtime, cx);
4236        });
4237        for name in languages.language_names() {
4238            languages
4239                .language_for_name(&name)
4240                .await
4241                .with_context(|| format!("language name {name}"))
4242                .unwrap();
4243        }
4244        cx.run_until_parked();
4245    }
4246
4247    pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
4248        init_test_with_state(cx, cx.update(AppState::test))
4249    }
4250
4251    fn init_test_with_state(
4252        cx: &mut TestAppContext,
4253        mut app_state: Arc<AppState>,
4254    ) -> Arc<AppState> {
4255        cx.update(move |cx| {
4256            env_logger::builder().is_test(true).try_init().ok();
4257
4258            let state = Arc::get_mut(&mut app_state).unwrap();
4259            state.build_window_options = build_window_options;
4260
4261            app_state.languages.add(markdown_language());
4262
4263            gpui_tokio::init(cx);
4264            vim_mode_setting::init(cx);
4265            theme::init(theme::LoadThemes::JustBase, cx);
4266            audio::init((), cx);
4267            channel::init(&app_state.client, app_state.user_store.clone(), cx);
4268            call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
4269            notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
4270            workspace::init(app_state.clone(), cx);
4271            Project::init_settings(cx);
4272            release_channel::init(SemanticVersion::default(), cx);
4273            command_palette::init(cx);
4274            language::init(cx);
4275            editor::init(cx);
4276            collab_ui::init(&app_state, cx);
4277            git_ui::init(cx);
4278            project_panel::init(cx);
4279            outline_panel::init(cx);
4280            terminal_view::init(cx);
4281            copilot::copilot_chat::init(app_state.fs.clone(), app_state.client.http_client(), cx);
4282            image_viewer::init(cx);
4283            language_model::init(app_state.client.clone(), cx);
4284            language_models::init(
4285                app_state.user_store.clone(),
4286                app_state.client.clone(),
4287                app_state.fs.clone(),
4288                cx,
4289            );
4290            web_search::init(cx);
4291            web_search_providers::init(app_state.client.clone(), cx);
4292            let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx);
4293            agent::init(
4294                app_state.fs.clone(),
4295                app_state.client.clone(),
4296                prompt_builder.clone(),
4297                app_state.languages.clone(),
4298                false,
4299                cx,
4300            );
4301            repl::init(app_state.fs.clone(), cx);
4302            repl::notebook::init(cx);
4303            tasks_ui::init(cx);
4304            project::debugger::breakpoint_store::BreakpointStore::init(
4305                &app_state.client.clone().into(),
4306            );
4307            project::debugger::dap_store::DapStore::init(&app_state.client.clone().into(), cx);
4308            debugger_ui::init(cx);
4309            initialize_workspace(app_state.clone(), prompt_builder, cx);
4310            search::init(cx);
4311            app_state
4312        })
4313    }
4314
4315    fn rust_lang() -> Arc<language::Language> {
4316        Arc::new(language::Language::new(
4317            language::LanguageConfig {
4318                name: "Rust".into(),
4319                matcher: LanguageMatcher {
4320                    path_suffixes: vec!["rs".to_string()],
4321                    ..Default::default()
4322                },
4323                ..Default::default()
4324            },
4325            Some(tree_sitter_rust::LANGUAGE.into()),
4326        ))
4327    }
4328
4329    fn markdown_language() -> Arc<language::Language> {
4330        Arc::new(language::Language::new(
4331            language::LanguageConfig {
4332                name: "Markdown".into(),
4333                matcher: LanguageMatcher {
4334                    path_suffixes: vec!["md".to_string()],
4335                    ..Default::default()
4336                },
4337                ..Default::default()
4338            },
4339            Some(tree_sitter_md::LANGUAGE.into()),
4340        ))
4341    }
4342
4343    #[track_caller]
4344    fn assert_key_bindings_for(
4345        window: AnyWindowHandle,
4346        cx: &TestAppContext,
4347        actions: Vec<(&'static str, &dyn Action)>,
4348        line: u32,
4349    ) {
4350        let available_actions = cx
4351            .update(|cx| window.update(cx, |_, window, cx| window.available_actions(cx)))
4352            .unwrap();
4353        for (key, action) in actions {
4354            let bindings = cx
4355                .update(|cx| window.update(cx, |_, window, _| window.bindings_for_action(action)))
4356                .unwrap();
4357            // assert that...
4358            assert!(
4359                available_actions.iter().any(|bound_action| {
4360                    // actions match...
4361                    bound_action.partial_eq(action)
4362                }),
4363                "On {} Failed to find {}",
4364                line,
4365                action.name(),
4366            );
4367            assert!(
4368                // and key strokes contain the given key
4369                bindings
4370                    .into_iter()
4371                    .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)),
4372                "On {} Failed to find {} with key binding {}",
4373                line,
4374                action.name(),
4375                key
4376            );
4377        }
4378    }
4379
4380    #[gpui::test]
4381    async fn test_opening_project_settings_when_excluded(cx: &mut gpui::TestAppContext) {
4382        // Use the proper initialization for runtime state
4383        let app_state = init_keymap_test(cx);
4384
4385        eprintln!("Running test_opening_project_settings_when_excluded");
4386
4387        // 1. Set up a project with some project settings
4388        let settings_init =
4389            r#"{ "UNIQUEVALUE": true, "git": { "inline_blame": { "enabled": false } } }"#;
4390        app_state
4391            .fs
4392            .as_fake()
4393            .insert_tree(
4394                Path::new("/root"),
4395                json!({
4396                    ".zed": {
4397                        "settings.json": settings_init
4398                    }
4399                }),
4400            )
4401            .await;
4402
4403        eprintln!("Created project with .zed/settings.json containing UNIQUEVALUE");
4404
4405        // 2. Create a project with the file system and load it
4406        let project = Project::test(app_state.fs.clone(), [Path::new("/root")], cx).await;
4407
4408        // Save original settings content for comparison
4409        let original_settings = app_state
4410            .fs
4411            .load(Path::new("/root/.zed/settings.json"))
4412            .await
4413            .unwrap();
4414
4415        let original_settings_str = original_settings.clone();
4416
4417        // Verify settings exist on disk and have expected content
4418        eprintln!("Original settings content: {}", original_settings_str);
4419        assert!(
4420            original_settings_str.contains("UNIQUEVALUE"),
4421            "Test setup failed - settings file doesn't contain our marker"
4422        );
4423
4424        // 3. Add .zed to file scan exclusions in user settings
4425        cx.update_global::<SettingsStore, _>(|store, cx| {
4426            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
4427                worktree_settings.file_scan_exclusions = Some(vec![".zed".to_string()]);
4428            });
4429        });
4430
4431        eprintln!("Added .zed to file_scan_exclusions in settings");
4432
4433        // 4. Run tasks to apply settings
4434        cx.background_executor.run_until_parked();
4435
4436        // 5. Critical: Verify .zed is actually excluded from worktree
4437        let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().clone());
4438
4439        let has_zed_entry = cx.update(|cx| worktree.read(cx).entry_for_path(".zed").is_some());
4440
4441        eprintln!(
4442            "Is .zed directory visible in worktree after exclusion: {}",
4443            has_zed_entry
4444        );
4445
4446        // This assertion verifies the test is set up correctly to show the bug
4447        // If .zed is not excluded, the test will fail here
4448        assert!(
4449            !has_zed_entry,
4450            "Test precondition failed: .zed directory should be excluded but was found in worktree"
4451        );
4452
4453        // 6. Create workspace and trigger the actual function that causes the bug
4454        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4455        window
4456            .update(cx, |workspace, window, cx| {
4457                // Call the exact function that contains the bug
4458                eprintln!("About to call open_project_settings_file");
4459                open_project_settings_file(workspace, &OpenProjectSettings, window, cx);
4460            })
4461            .unwrap();
4462
4463        // 7. Run background tasks until completion
4464        cx.background_executor.run_until_parked();
4465
4466        // 8. Verify file contents after calling function
4467        let new_content = app_state
4468            .fs
4469            .load(Path::new("/root/.zed/settings.json"))
4470            .await
4471            .unwrap();
4472
4473        let new_content_str = new_content.clone();
4474        eprintln!("New settings content: {}", new_content_str);
4475
4476        // The bug causes the settings to be overwritten with empty settings
4477        // So if the unique value is no longer present, the bug has been reproduced
4478        let bug_exists = !new_content_str.contains("UNIQUEVALUE");
4479        eprintln!("Bug reproduced: {}", bug_exists);
4480
4481        // This assertion should fail if the bug exists - showing the bug is real
4482        assert!(
4483            new_content_str.contains("UNIQUEVALUE"),
4484            "BUG FOUND: Project settings were overwritten when opening via command - original custom content was lost"
4485        );
4486    }
4487}