zed.rs

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