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, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString,
  30    Styled, Task, TitlebarOptions, UpdateGlobal, Window, WindowKind, WindowOptions, actions, point,
  31    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    // On Windows, this is set in the `update_jump_list` method of the `HistoryManager`.
1390    #[cfg(not(target_os = "windows"))]
1391    cx.set_dock_menu(vec![gpui::MenuItem::action(
1392        "New Window",
1393        workspace::NewWindow,
1394    )]);
1395}
1396
1397pub fn load_default_keymap(cx: &mut App) {
1398    let base_keymap = *BaseKeymap::get_global(cx);
1399    if base_keymap == BaseKeymap::None {
1400        return;
1401    }
1402
1403    cx.bind_keys(KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap());
1404
1405    if let Some(asset_path) = base_keymap.asset_path() {
1406        cx.bind_keys(KeymapFile::load_asset(asset_path, cx).unwrap());
1407    }
1408
1409    if VimModeSetting::get_global(cx).0 {
1410        cx.bind_keys(KeymapFile::load_asset(VIM_KEYMAP_PATH, cx).unwrap());
1411    }
1412}
1413
1414pub fn handle_settings_changed(error: Option<anyhow::Error>, cx: &mut App) {
1415    struct SettingsParseErrorNotification;
1416    let id = NotificationId::unique::<SettingsParseErrorNotification>();
1417
1418    match error {
1419        Some(error) => {
1420            if let Some(InvalidSettingsError::LocalSettings { .. }) =
1421                error.downcast_ref::<InvalidSettingsError>()
1422            {
1423                // Local settings errors are displayed by the projects
1424                return;
1425            }
1426            show_app_notification(id, cx, move |cx| {
1427                cx.new(|cx| {
1428                    MessageNotification::new(format!("Invalid user settings file\n{error}"), cx)
1429                        .primary_message("Open Settings File")
1430                        .primary_icon(IconName::Settings)
1431                        .primary_on_click(|window, cx| {
1432                            window.dispatch_action(zed_actions::OpenSettings.boxed_clone(), cx);
1433                            cx.emit(DismissEvent);
1434                        })
1435                })
1436            });
1437        }
1438        None => {
1439            dismiss_app_notification(&id, cx);
1440        }
1441    }
1442}
1443
1444pub fn open_new_ssh_project_from_project(
1445    workspace: &mut Workspace,
1446    paths: Vec<PathBuf>,
1447    window: &mut Window,
1448    cx: &mut Context<Workspace>,
1449) -> Task<anyhow::Result<()>> {
1450    let app_state = workspace.app_state().clone();
1451    let Some(ssh_client) = workspace.project().read(cx).ssh_client() else {
1452        return Task::ready(Err(anyhow::anyhow!("Not an ssh project")));
1453    };
1454    let connection_options = ssh_client.read(cx).connection_options();
1455    cx.spawn_in(window, async move |_, cx| {
1456        open_ssh_project(
1457            connection_options,
1458            paths,
1459            app_state,
1460            workspace::OpenOptions {
1461                open_new_workspace: Some(true),
1462                ..Default::default()
1463            },
1464            cx,
1465        )
1466        .await
1467    })
1468}
1469
1470fn open_project_settings_file(
1471    workspace: &mut Workspace,
1472    _: &OpenProjectSettings,
1473    window: &mut Window,
1474    cx: &mut Context<Workspace>,
1475) {
1476    open_local_file(
1477        workspace,
1478        local_settings_file_relative_path(),
1479        initial_project_settings_content(),
1480        window,
1481        cx,
1482    )
1483}
1484
1485fn open_project_tasks_file(
1486    workspace: &mut Workspace,
1487    _: &OpenProjectTasks,
1488    window: &mut Window,
1489    cx: &mut Context<Workspace>,
1490) {
1491    open_local_file(
1492        workspace,
1493        local_tasks_file_relative_path(),
1494        initial_tasks_content(),
1495        window,
1496        cx,
1497    )
1498}
1499
1500fn open_project_debug_tasks_file(
1501    workspace: &mut Workspace,
1502    _: &OpenProjectDebugTasks,
1503    window: &mut Window,
1504    cx: &mut Context<Workspace>,
1505) {
1506    open_local_file(
1507        workspace,
1508        local_debug_file_relative_path(),
1509        initial_debug_tasks_content(),
1510        window,
1511        cx,
1512    )
1513}
1514
1515fn open_local_file(
1516    workspace: &mut Workspace,
1517    settings_relative_path: &'static Path,
1518    initial_contents: Cow<'static, str>,
1519    window: &mut Window,
1520    cx: &mut Context<Workspace>,
1521) {
1522    let project = workspace.project().clone();
1523    let worktree = project
1524        .read(cx)
1525        .visible_worktrees(cx)
1526        .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
1527    if let Some(worktree) = worktree {
1528        let tree_id = worktree.read(cx).id();
1529        cx.spawn_in(window, async move |workspace, cx| {
1530            if let Some(dir_path) = settings_relative_path.parent() {
1531                if worktree.update(cx, |tree, _| tree.entry_for_path(dir_path).is_none())? {
1532                    project
1533                        .update(cx, |project, cx| {
1534                            project.create_entry((tree_id, dir_path), true, cx)
1535                        })?
1536                        .await
1537                        .context("worktree was removed")?;
1538                }
1539            }
1540
1541            if worktree.update(cx, |tree, _| {
1542                tree.entry_for_path(settings_relative_path).is_none()
1543            })? {
1544                project
1545                    .update(cx, |project, cx| {
1546                        project.create_entry((tree_id, settings_relative_path), false, cx)
1547                    })?
1548                    .await
1549                    .context("worktree was removed")?;
1550            }
1551
1552            let editor = workspace
1553                .update_in(cx, |workspace, window, cx| {
1554                    workspace.open_path((tree_id, settings_relative_path), None, true, window, cx)
1555                })?
1556                .await?
1557                .downcast::<Editor>()
1558                .context("unexpected item type: expected editor item")?;
1559
1560            editor
1561                .downgrade()
1562                .update(cx, |editor, cx| {
1563                    if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
1564                        if buffer.read(cx).is_empty() {
1565                            buffer.update(cx, |buffer, cx| {
1566                                buffer.edit([(0..0, initial_contents)], None, cx)
1567                            });
1568                        }
1569                    }
1570                })
1571                .ok();
1572
1573            anyhow::Ok(())
1574        })
1575        .detach();
1576    } else {
1577        struct NoOpenFolders;
1578
1579        workspace.show_notification(NotificationId::unique::<NoOpenFolders>(), cx, |cx| {
1580            cx.new(|cx| MessageNotification::new("This project has no folders open.", cx))
1581        })
1582    }
1583}
1584
1585fn open_telemetry_log_file(
1586    workspace: &mut Workspace,
1587    window: &mut Window,
1588    cx: &mut Context<Workspace>,
1589) {
1590    workspace.with_local_workspace(window, cx, move |workspace, window, cx| {
1591        let app_state = workspace.app_state().clone();
1592        cx.spawn_in(window, async move |workspace, cx| {
1593            async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
1594                let path = client::telemetry::Telemetry::log_file_path();
1595                app_state.fs.load(&path).await.log_err()
1596            }
1597
1598            let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string());
1599
1600            const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
1601            let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
1602            if let Some(newline_offset) = log[start_offset..].find('\n') {
1603                start_offset += newline_offset + 1;
1604            }
1605            let log_suffix = &log[start_offset..];
1606            let header = concat!(
1607                "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
1608                "// Telemetry can be disabled via the `settings.json` file.\n",
1609                "// Here is the data that has been reported for the current session:\n",
1610            );
1611            let content = format!("{}\n{}", header, log_suffix);
1612            let json = app_state.languages.language_for_name("JSON").await.log_err();
1613
1614            workspace.update_in( cx, |workspace, window, cx| {
1615                let project = workspace.project().clone();
1616                let buffer = project.update(cx, |project, cx| project.create_local_buffer(&content, json, cx));
1617                let buffer = cx.new(|cx| {
1618                    MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
1619                });
1620                workspace.add_item_to_active_pane(
1621                    Box::new(cx.new(|cx| {
1622                        let mut editor = Editor::for_multibuffer(buffer, Some(project), window, cx);
1623                        editor.set_read_only(true);
1624                        editor.set_breadcrumb_header("Telemetry Log".into());
1625                        editor
1626                    })),
1627                    None,
1628                    true,
1629                    window, cx,
1630                );
1631            }).log_err()?;
1632
1633            Some(())
1634        })
1635        .detach();
1636    }).detach();
1637}
1638
1639fn open_bundled_file(
1640    workspace: &Workspace,
1641    text: Cow<'static, str>,
1642    title: &'static str,
1643    language: &'static str,
1644    window: &mut Window,
1645    cx: &mut Context<Workspace>,
1646) {
1647    let language = workspace.app_state().languages.language_for_name(language);
1648    cx.spawn_in(window, async move |workspace, cx| {
1649        let language = language.await.log_err();
1650        workspace
1651            .update_in(cx, |workspace, window, cx| {
1652                workspace.with_local_workspace(window, cx, |workspace, window, cx| {
1653                    let project = workspace.project();
1654                    let buffer = project.update(cx, move |project, cx| {
1655                        project.create_local_buffer(text.as_ref(), language, cx)
1656                    });
1657                    let buffer =
1658                        cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into()));
1659                    workspace.add_item_to_active_pane(
1660                        Box::new(cx.new(|cx| {
1661                            let mut editor =
1662                                Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
1663                            editor.set_read_only(true);
1664                            editor.set_breadcrumb_header(title.into());
1665                            editor
1666                        })),
1667                        None,
1668                        true,
1669                        window,
1670                        cx,
1671                    );
1672                })
1673            })?
1674            .await
1675    })
1676    .detach_and_log_err(cx);
1677}
1678
1679fn open_settings_file(
1680    abs_path: &'static Path,
1681    default_content: impl FnOnce() -> Rope + Send + 'static,
1682    window: &mut Window,
1683    cx: &mut Context<Workspace>,
1684) {
1685    cx.spawn_in(window, async move |workspace, cx| {
1686        let (worktree_creation_task, settings_open_task) = workspace
1687            .update_in(cx, |workspace, window, cx| {
1688                workspace.with_local_workspace(window, cx, move |workspace, window, cx| {
1689                    let worktree_creation_task = workspace.project().update(cx, |project, cx| {
1690                        // Set up a dedicated worktree for settings, since
1691                        // otherwise we're dropping and re-starting LSP servers
1692                        // for each file inside on every settings file
1693                        // close/open
1694
1695                        // TODO: Do note that all other external files (e.g.
1696                        // drag and drop from OS) still have their worktrees
1697                        // released on file close, causing LSP servers'
1698                        // restarts.
1699                        project.find_or_create_worktree(paths::config_dir().as_path(), false, cx)
1700                    });
1701                    let settings_open_task =
1702                        create_and_open_local_file(abs_path, window, cx, default_content);
1703                    (worktree_creation_task, settings_open_task)
1704                })
1705            })?
1706            .await?;
1707        let _ = worktree_creation_task.await?;
1708        let _ = settings_open_task.await?;
1709        anyhow::Ok(())
1710    })
1711    .detach_and_log_err(cx);
1712}
1713
1714#[cfg(test)]
1715mod tests {
1716    use super::*;
1717    use assets::Assets;
1718    use collections::HashSet;
1719    use editor::{DisplayPoint, Editor, display_map::DisplayRow, scroll::Autoscroll};
1720    use gpui::{
1721        Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, SemanticVersion,
1722        TestAppContext, UpdateGlobal, VisualTestContext, WindowHandle, actions,
1723    };
1724    use language::{LanguageMatcher, LanguageRegistry};
1725    use project::{Project, ProjectPath, WorktreeSettings, project_settings::ProjectSettings};
1726    use serde_json::json;
1727    use settings::{SettingsStore, watch_config_file};
1728    use std::{
1729        path::{Path, PathBuf},
1730        time::Duration,
1731    };
1732    use theme::{ThemeRegistry, ThemeSettings};
1733    use util::{path, separator};
1734    use workspace::{
1735        NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection,
1736        WorkspaceHandle,
1737        item::{Item, ItemHandle},
1738        open_new, open_paths, pane,
1739    };
1740
1741    #[gpui::test]
1742    async fn test_open_non_existing_file(cx: &mut TestAppContext) {
1743        let app_state = init_test(cx);
1744        app_state
1745            .fs
1746            .as_fake()
1747            .insert_tree(
1748                path!("/root"),
1749                json!({
1750                    "a": {
1751                    },
1752                }),
1753            )
1754            .await;
1755
1756        cx.update(|cx| {
1757            open_paths(
1758                &[PathBuf::from(path!("/root/a/new"))],
1759                app_state.clone(),
1760                workspace::OpenOptions::default(),
1761                cx,
1762            )
1763        })
1764        .await
1765        .unwrap();
1766        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
1767
1768        let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
1769        workspace
1770            .update(cx, |workspace, _, cx| {
1771                assert!(workspace.active_item_as::<Editor>(cx).is_some())
1772            })
1773            .unwrap();
1774    }
1775
1776    #[gpui::test]
1777    async fn test_open_paths_action(cx: &mut TestAppContext) {
1778        let app_state = init_test(cx);
1779        app_state
1780            .fs
1781            .as_fake()
1782            .insert_tree(
1783                "/root",
1784                json!({
1785                    "a": {
1786                        "aa": null,
1787                        "ab": null,
1788                    },
1789                    "b": {
1790                        "ba": null,
1791                        "bb": null,
1792                    },
1793                    "c": {
1794                        "ca": null,
1795                        "cb": null,
1796                    },
1797                    "d": {
1798                        "da": null,
1799                        "db": null,
1800                    },
1801                    "e": {
1802                        "ea": null,
1803                        "eb": null,
1804                    }
1805                }),
1806            )
1807            .await;
1808
1809        cx.update(|cx| {
1810            open_paths(
1811                &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
1812                app_state.clone(),
1813                workspace::OpenOptions::default(),
1814                cx,
1815            )
1816        })
1817        .await
1818        .unwrap();
1819        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
1820
1821        cx.update(|cx| {
1822            open_paths(
1823                &[PathBuf::from("/root/a")],
1824                app_state.clone(),
1825                workspace::OpenOptions::default(),
1826                cx,
1827            )
1828        })
1829        .await
1830        .unwrap();
1831        assert_eq!(cx.read(|cx| cx.windows().len()), 1);
1832        let workspace_1 = cx
1833            .read(|cx| cx.windows()[0].downcast::<Workspace>())
1834            .unwrap();
1835        cx.run_until_parked();
1836        workspace_1
1837            .update(cx, |workspace, window, cx| {
1838                assert_eq!(workspace.worktrees(cx).count(), 2);
1839                assert!(workspace.left_dock().read(cx).is_open());
1840                assert!(
1841                    workspace
1842                        .active_pane()
1843                        .read(cx)
1844                        .focus_handle(cx)
1845                        .is_focused(window)
1846                );
1847            })
1848            .unwrap();
1849
1850        cx.update(|cx| {
1851            open_paths(
1852                &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
1853                app_state.clone(),
1854                workspace::OpenOptions::default(),
1855                cx,
1856            )
1857        })
1858        .await
1859        .unwrap();
1860        assert_eq!(cx.read(|cx| cx.windows().len()), 2);
1861
1862        // Replace existing windows
1863        let window = cx
1864            .update(|cx| cx.windows()[0].downcast::<Workspace>())
1865            .unwrap();
1866        cx.update(|cx| {
1867            open_paths(
1868                &[PathBuf::from("/root/e")],
1869                app_state,
1870                workspace::OpenOptions {
1871                    replace_window: Some(window),
1872                    ..Default::default()
1873                },
1874                cx,
1875            )
1876        })
1877        .await
1878        .unwrap();
1879        cx.background_executor.run_until_parked();
1880        assert_eq!(cx.read(|cx| cx.windows().len()), 2);
1881        let workspace_1 = cx
1882            .update(|cx| cx.windows()[0].downcast::<Workspace>())
1883            .unwrap();
1884        workspace_1
1885            .update(cx, |workspace, window, cx| {
1886                assert_eq!(
1887                    workspace
1888                        .worktrees(cx)
1889                        .map(|w| w.read(cx).abs_path())
1890                        .collect::<Vec<_>>(),
1891                    &[Path::new("/root/e").into()]
1892                );
1893                assert!(workspace.left_dock().read(cx).is_open());
1894                assert!(workspace.active_pane().focus_handle(cx).is_focused(window));
1895            })
1896            .unwrap();
1897    }
1898
1899    #[gpui::test]
1900    async fn test_open_add_new(cx: &mut TestAppContext) {
1901        let app_state = init_test(cx);
1902        app_state
1903            .fs
1904            .as_fake()
1905            .insert_tree(
1906                path!("/root"),
1907                json!({"a": "hey", "b": "", "dir": {"c": "f"}}),
1908            )
1909            .await;
1910
1911        cx.update(|cx| {
1912            open_paths(
1913                &[PathBuf::from(path!("/root/dir"))],
1914                app_state.clone(),
1915                workspace::OpenOptions::default(),
1916                cx,
1917            )
1918        })
1919        .await
1920        .unwrap();
1921        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1922
1923        cx.update(|cx| {
1924            open_paths(
1925                &[PathBuf::from(path!("/root/a"))],
1926                app_state.clone(),
1927                workspace::OpenOptions {
1928                    open_new_workspace: Some(false),
1929                    ..Default::default()
1930                },
1931                cx,
1932            )
1933        })
1934        .await
1935        .unwrap();
1936        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1937
1938        cx.update(|cx| {
1939            open_paths(
1940                &[PathBuf::from(path!("/root/dir/c"))],
1941                app_state.clone(),
1942                workspace::OpenOptions {
1943                    open_new_workspace: Some(true),
1944                    ..Default::default()
1945                },
1946                cx,
1947            )
1948        })
1949        .await
1950        .unwrap();
1951        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
1952    }
1953
1954    #[gpui::test]
1955    async fn test_open_file_in_many_spaces(cx: &mut TestAppContext) {
1956        let app_state = init_test(cx);
1957        app_state
1958            .fs
1959            .as_fake()
1960            .insert_tree(
1961                path!("/root"),
1962                json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}}),
1963            )
1964            .await;
1965
1966        cx.update(|cx| {
1967            open_paths(
1968                &[PathBuf::from(path!("/root/dir1/a"))],
1969                app_state.clone(),
1970                workspace::OpenOptions::default(),
1971                cx,
1972            )
1973        })
1974        .await
1975        .unwrap();
1976        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1977        let window1 = cx.update(|cx| cx.active_window().unwrap());
1978
1979        cx.update(|cx| {
1980            open_paths(
1981                &[PathBuf::from(path!("/root/dir2/c"))],
1982                app_state.clone(),
1983                workspace::OpenOptions::default(),
1984                cx,
1985            )
1986        })
1987        .await
1988        .unwrap();
1989        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
1990
1991        cx.update(|cx| {
1992            open_paths(
1993                &[PathBuf::from(path!("/root/dir2"))],
1994                app_state.clone(),
1995                workspace::OpenOptions::default(),
1996                cx,
1997            )
1998        })
1999        .await
2000        .unwrap();
2001        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2002        let window2 = cx.update(|cx| cx.active_window().unwrap());
2003        assert!(window1 != window2);
2004        cx.update_window(window1, |_, window, _| window.activate_window())
2005            .unwrap();
2006
2007        cx.update(|cx| {
2008            open_paths(
2009                &[PathBuf::from(path!("/root/dir2/c"))],
2010                app_state.clone(),
2011                workspace::OpenOptions::default(),
2012                cx,
2013            )
2014        })
2015        .await
2016        .unwrap();
2017        assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2018        // should have opened in window2 because that has dir2 visibly open (window1 has it open, but not in the project panel)
2019        assert!(cx.update(|cx| cx.active_window().unwrap()) == window2);
2020    }
2021
2022    #[gpui::test]
2023    async fn test_window_edit_state_restoring_disabled(cx: &mut TestAppContext) {
2024        let executor = cx.executor();
2025        let app_state = init_test(cx);
2026
2027        cx.update(|cx| {
2028            SettingsStore::update_global(cx, |store, cx| {
2029                store.update_user_settings::<ProjectSettings>(cx, |settings| {
2030                    settings.session.restore_unsaved_buffers = false
2031                });
2032            });
2033        });
2034
2035        app_state
2036            .fs
2037            .as_fake()
2038            .insert_tree(path!("/root"), json!({"a": "hey"}))
2039            .await;
2040
2041        cx.update(|cx| {
2042            open_paths(
2043                &[PathBuf::from(path!("/root/a"))],
2044                app_state.clone(),
2045                workspace::OpenOptions::default(),
2046                cx,
2047            )
2048        })
2049        .await
2050        .unwrap();
2051        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2052
2053        // When opening the workspace, the window is not in a edited state.
2054        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2055
2056        let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
2057            cx.update(|cx| window.read(cx).unwrap().is_edited())
2058        };
2059        let pane = window
2060            .read_with(cx, |workspace, _| workspace.active_pane().clone())
2061            .unwrap();
2062        let editor = window
2063            .read_with(cx, |workspace, cx| {
2064                workspace
2065                    .active_item(cx)
2066                    .unwrap()
2067                    .downcast::<Editor>()
2068                    .unwrap()
2069            })
2070            .unwrap();
2071
2072        assert!(!window_is_edited(window, cx));
2073
2074        // Editing a buffer marks the window as edited.
2075        window
2076            .update(cx, |_, window, cx| {
2077                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2078            })
2079            .unwrap();
2080
2081        assert!(window_is_edited(window, cx));
2082
2083        // Undoing the edit restores the window's edited state.
2084        window
2085            .update(cx, |_, window, cx| {
2086                editor.update(cx, |editor, cx| {
2087                    editor.undo(&Default::default(), window, cx)
2088                });
2089            })
2090            .unwrap();
2091        assert!(!window_is_edited(window, cx));
2092
2093        // Redoing the edit marks the window as edited again.
2094        window
2095            .update(cx, |_, window, cx| {
2096                editor.update(cx, |editor, cx| {
2097                    editor.redo(&Default::default(), window, cx)
2098                });
2099            })
2100            .unwrap();
2101        assert!(window_is_edited(window, cx));
2102        let weak = editor.downgrade();
2103
2104        // Closing the item restores the window's edited state.
2105        let close = window
2106            .update(cx, |_, window, cx| {
2107                pane.update(cx, |pane, cx| {
2108                    drop(editor);
2109                    pane.close_active_item(&Default::default(), window, cx)
2110                        .unwrap()
2111                })
2112            })
2113            .unwrap();
2114        executor.run_until_parked();
2115
2116        cx.simulate_prompt_answer("Don't Save");
2117        close.await.unwrap();
2118
2119        // Advance the clock to ensure that the item has been serialized and dropped from the queue
2120        cx.executor().advance_clock(Duration::from_secs(1));
2121
2122        weak.assert_released();
2123        assert!(!window_is_edited(window, cx));
2124        // Opening the buffer again doesn't impact the window's edited state.
2125        cx.update(|cx| {
2126            open_paths(
2127                &[PathBuf::from(path!("/root/a"))],
2128                app_state,
2129                workspace::OpenOptions::default(),
2130                cx,
2131            )
2132        })
2133        .await
2134        .unwrap();
2135        executor.run_until_parked();
2136
2137        window
2138            .update(cx, |workspace, _, cx| {
2139                let editor = workspace
2140                    .active_item(cx)
2141                    .unwrap()
2142                    .downcast::<Editor>()
2143                    .unwrap();
2144
2145                editor.update(cx, |editor, cx| {
2146                    assert_eq!(editor.text(cx), "hey");
2147                });
2148            })
2149            .unwrap();
2150
2151        let editor = window
2152            .read_with(cx, |workspace, cx| {
2153                workspace
2154                    .active_item(cx)
2155                    .unwrap()
2156                    .downcast::<Editor>()
2157                    .unwrap()
2158            })
2159            .unwrap();
2160        assert!(!window_is_edited(window, cx));
2161
2162        // Editing the buffer marks the window as edited.
2163        window
2164            .update(cx, |_, window, cx| {
2165                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2166            })
2167            .unwrap();
2168        executor.run_until_parked();
2169        assert!(window_is_edited(window, cx));
2170
2171        // Ensure closing the window via the mouse gets preempted due to the
2172        // buffer having unsaved changes.
2173        assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
2174        executor.run_until_parked();
2175        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2176
2177        // The window is successfully closed after the user dismisses the prompt.
2178        cx.simulate_prompt_answer("Don't Save");
2179        executor.run_until_parked();
2180        assert_eq!(cx.update(|cx| cx.windows().len()), 0);
2181    }
2182
2183    #[gpui::test]
2184    async fn test_window_edit_state_restoring_enabled(cx: &mut TestAppContext) {
2185        let app_state = init_test(cx);
2186        app_state
2187            .fs
2188            .as_fake()
2189            .insert_tree(path!("/root"), json!({"a": "hey"}))
2190            .await;
2191
2192        cx.update(|cx| {
2193            open_paths(
2194                &[PathBuf::from(path!("/root/a"))],
2195                app_state.clone(),
2196                workspace::OpenOptions::default(),
2197                cx,
2198            )
2199        })
2200        .await
2201        .unwrap();
2202
2203        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2204
2205        // When opening the workspace, the window is not in a edited state.
2206        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2207
2208        let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
2209            cx.update(|cx| window.read(cx).unwrap().is_edited())
2210        };
2211
2212        let editor = window
2213            .read_with(cx, |workspace, cx| {
2214                workspace
2215                    .active_item(cx)
2216                    .unwrap()
2217                    .downcast::<Editor>()
2218                    .unwrap()
2219            })
2220            .unwrap();
2221
2222        assert!(!window_is_edited(window, cx));
2223
2224        // Editing a buffer marks the window as edited.
2225        window
2226            .update(cx, |_, window, cx| {
2227                editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2228            })
2229            .unwrap();
2230
2231        assert!(window_is_edited(window, cx));
2232        cx.run_until_parked();
2233
2234        // Advance the clock to make sure the workspace is serialized
2235        cx.executor().advance_clock(Duration::from_secs(1));
2236
2237        // When closing the window, no prompt shows up and the window is closed.
2238        // buffer having unsaved changes.
2239        assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
2240        cx.run_until_parked();
2241        assert_eq!(cx.update(|cx| cx.windows().len()), 0);
2242
2243        // When we now reopen the window, the edited state and the edited buffer are back
2244        cx.update(|cx| {
2245            open_paths(
2246                &[PathBuf::from(path!("/root/a"))],
2247                app_state.clone(),
2248                workspace::OpenOptions::default(),
2249                cx,
2250            )
2251        })
2252        .await
2253        .unwrap();
2254
2255        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2256        assert!(cx.update(|cx| cx.active_window().is_some()));
2257
2258        cx.run_until_parked();
2259
2260        // When opening the workspace, the window is not in a edited state.
2261        let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
2262        assert!(window_is_edited(window, cx));
2263
2264        window
2265            .update(cx, |workspace, _, cx| {
2266                let editor = workspace
2267                    .active_item(cx)
2268                    .unwrap()
2269                    .downcast::<editor::Editor>()
2270                    .unwrap();
2271                editor.update(cx, |editor, cx| {
2272                    assert_eq!(editor.text(cx), "EDIThey");
2273                    assert!(editor.is_dirty(cx));
2274                });
2275
2276                editor
2277            })
2278            .unwrap();
2279    }
2280
2281    #[gpui::test]
2282    async fn test_new_empty_workspace(cx: &mut TestAppContext) {
2283        let app_state = init_test(cx);
2284        cx.update(|cx| {
2285            open_new(
2286                Default::default(),
2287                app_state.clone(),
2288                cx,
2289                |workspace, window, cx| {
2290                    Editor::new_file(workspace, &Default::default(), window, cx)
2291                },
2292            )
2293        })
2294        .await
2295        .unwrap();
2296        cx.run_until_parked();
2297
2298        let workspace = cx
2299            .update(|cx| cx.windows().first().unwrap().downcast::<Workspace>())
2300            .unwrap();
2301
2302        let editor = workspace
2303            .update(cx, |workspace, _, cx| {
2304                let editor = workspace
2305                    .active_item(cx)
2306                    .unwrap()
2307                    .downcast::<editor::Editor>()
2308                    .unwrap();
2309                editor.update(cx, |editor, cx| {
2310                    assert!(editor.text(cx).is_empty());
2311                    assert!(!editor.is_dirty(cx));
2312                });
2313
2314                editor
2315            })
2316            .unwrap();
2317
2318        let save_task = workspace
2319            .update(cx, |workspace, window, cx| {
2320                workspace.save_active_item(SaveIntent::Save, window, cx)
2321            })
2322            .unwrap();
2323        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
2324        cx.background_executor.run_until_parked();
2325        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
2326        save_task.await.unwrap();
2327        workspace
2328            .update(cx, |_, _, cx| {
2329                editor.update(cx, |editor, cx| {
2330                    assert!(!editor.is_dirty(cx));
2331                    assert_eq!(editor.title(cx), "the-new-name");
2332                });
2333            })
2334            .unwrap();
2335    }
2336
2337    #[gpui::test]
2338    async fn test_open_entry(cx: &mut TestAppContext) {
2339        let app_state = init_test(cx);
2340        app_state
2341            .fs
2342            .as_fake()
2343            .insert_tree(
2344                path!("/root"),
2345                json!({
2346                    "a": {
2347                        "file1": "contents 1",
2348                        "file2": "contents 2",
2349                        "file3": "contents 3",
2350                    },
2351                }),
2352            )
2353            .await;
2354
2355        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2356        project.update(cx, |project, _cx| {
2357            project.languages().add(markdown_language())
2358        });
2359        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2360        let workspace = window.root(cx).unwrap();
2361
2362        let entries = cx.read(|cx| workspace.file_project_paths(cx));
2363        let file1 = entries[0].clone();
2364        let file2 = entries[1].clone();
2365        let file3 = entries[2].clone();
2366
2367        // Open the first entry
2368        let entry_1 = window
2369            .update(cx, |w, window, cx| {
2370                w.open_path(file1.clone(), None, true, window, cx)
2371            })
2372            .unwrap()
2373            .await
2374            .unwrap();
2375        cx.read(|cx| {
2376            let pane = workspace.read(cx).active_pane().read(cx);
2377            assert_eq!(
2378                pane.active_item().unwrap().project_path(cx),
2379                Some(file1.clone())
2380            );
2381            assert_eq!(pane.items_len(), 1);
2382        });
2383
2384        // Open the second entry
2385        window
2386            .update(cx, |w, window, cx| {
2387                w.open_path(file2.clone(), None, true, window, cx)
2388            })
2389            .unwrap()
2390            .await
2391            .unwrap();
2392        cx.read(|cx| {
2393            let pane = workspace.read(cx).active_pane().read(cx);
2394            assert_eq!(
2395                pane.active_item().unwrap().project_path(cx),
2396                Some(file2.clone())
2397            );
2398            assert_eq!(pane.items_len(), 2);
2399        });
2400
2401        // Open the first entry again. The existing pane item is activated.
2402        let entry_1b = window
2403            .update(cx, |w, window, cx| {
2404                w.open_path(file1.clone(), None, true, window, cx)
2405            })
2406            .unwrap()
2407            .await
2408            .unwrap();
2409        assert_eq!(entry_1.item_id(), entry_1b.item_id());
2410
2411        cx.read(|cx| {
2412            let pane = workspace.read(cx).active_pane().read(cx);
2413            assert_eq!(
2414                pane.active_item().unwrap().project_path(cx),
2415                Some(file1.clone())
2416            );
2417            assert_eq!(pane.items_len(), 2);
2418        });
2419
2420        // Split the pane with the first entry, then open the second entry again.
2421        window
2422            .update(cx, |w, window, cx| {
2423                w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx);
2424                w.open_path(file2.clone(), None, true, window, cx)
2425            })
2426            .unwrap()
2427            .await
2428            .unwrap();
2429
2430        window
2431            .read_with(cx, |w, cx| {
2432                assert_eq!(
2433                    w.active_pane()
2434                        .read(cx)
2435                        .active_item()
2436                        .unwrap()
2437                        .project_path(cx),
2438                    Some(file2.clone())
2439                );
2440            })
2441            .unwrap();
2442
2443        // Open the third entry twice concurrently. Only one pane item is added.
2444        let (t1, t2) = window
2445            .update(cx, |w, window, cx| {
2446                (
2447                    w.open_path(file3.clone(), None, true, window, cx),
2448                    w.open_path(file3.clone(), None, true, window, cx),
2449                )
2450            })
2451            .unwrap();
2452        t1.await.unwrap();
2453        t2.await.unwrap();
2454        cx.read(|cx| {
2455            let pane = workspace.read(cx).active_pane().read(cx);
2456            assert_eq!(
2457                pane.active_item().unwrap().project_path(cx),
2458                Some(file3.clone())
2459            );
2460            let pane_entries = pane
2461                .items()
2462                .map(|i| i.project_path(cx).unwrap())
2463                .collect::<Vec<_>>();
2464            assert_eq!(pane_entries, &[file1, file2, file3]);
2465        });
2466    }
2467
2468    #[gpui::test]
2469    async fn test_open_paths(cx: &mut TestAppContext) {
2470        let app_state = init_test(cx);
2471
2472        app_state
2473            .fs
2474            .as_fake()
2475            .insert_tree(
2476                path!("/"),
2477                json!({
2478                    "dir1": {
2479                        "a.txt": ""
2480                    },
2481                    "dir2": {
2482                        "b.txt": ""
2483                    },
2484                    "dir3": {
2485                        "c.txt": ""
2486                    },
2487                    "d.txt": ""
2488                }),
2489            )
2490            .await;
2491
2492        cx.update(|cx| {
2493            open_paths(
2494                &[PathBuf::from(path!("/dir1/"))],
2495                app_state,
2496                workspace::OpenOptions::default(),
2497                cx,
2498            )
2499        })
2500        .await
2501        .unwrap();
2502        assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2503        let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2504        let workspace = window.root(cx).unwrap();
2505
2506        #[track_caller]
2507        fn assert_project_panel_selection(
2508            workspace: &Workspace,
2509            expected_worktree_path: &Path,
2510            expected_entry_path: &Path,
2511            cx: &App,
2512        ) {
2513            let project_panel = [
2514                workspace.left_dock().read(cx).panel::<ProjectPanel>(),
2515                workspace.right_dock().read(cx).panel::<ProjectPanel>(),
2516                workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
2517            ]
2518            .into_iter()
2519            .find_map(std::convert::identity)
2520            .expect("found no project panels")
2521            .read(cx);
2522            let (selected_worktree, selected_entry) = project_panel
2523                .selected_entry(cx)
2524                .expect("project panel should have a selected entry");
2525            assert_eq!(
2526                selected_worktree.abs_path().as_ref(),
2527                expected_worktree_path,
2528                "Unexpected project panel selected worktree path"
2529            );
2530            assert_eq!(
2531                selected_entry.path.as_ref(),
2532                expected_entry_path,
2533                "Unexpected project panel selected entry path"
2534            );
2535        }
2536
2537        // Open a file within an existing worktree.
2538        window
2539            .update(cx, |workspace, window, cx| {
2540                workspace.open_paths(
2541                    vec![path!("/dir1/a.txt").into()],
2542                    OpenOptions {
2543                        visible: Some(OpenVisible::All),
2544                        ..Default::default()
2545                    },
2546                    None,
2547                    window,
2548                    cx,
2549                )
2550            })
2551            .unwrap()
2552            .await;
2553        cx.read(|cx| {
2554            let workspace = workspace.read(cx);
2555            assert_project_panel_selection(
2556                workspace,
2557                Path::new(path!("/dir1")),
2558                Path::new("a.txt"),
2559                cx,
2560            );
2561            assert_eq!(
2562                workspace
2563                    .active_pane()
2564                    .read(cx)
2565                    .active_item()
2566                    .unwrap()
2567                    .act_as::<Editor>(cx)
2568                    .unwrap()
2569                    .read(cx)
2570                    .title(cx),
2571                "a.txt"
2572            );
2573        });
2574
2575        // Open a file outside of any existing worktree.
2576        window
2577            .update(cx, |workspace, window, cx| {
2578                workspace.open_paths(
2579                    vec![path!("/dir2/b.txt").into()],
2580                    OpenOptions {
2581                        visible: Some(OpenVisible::All),
2582                        ..Default::default()
2583                    },
2584                    None,
2585                    window,
2586                    cx,
2587                )
2588            })
2589            .unwrap()
2590            .await;
2591        cx.read(|cx| {
2592            let workspace = workspace.read(cx);
2593            assert_project_panel_selection(
2594                workspace,
2595                Path::new(path!("/dir2/b.txt")),
2596                Path::new(""),
2597                cx,
2598            );
2599            let worktree_roots = workspace
2600                .worktrees(cx)
2601                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2602                .collect::<HashSet<_>>();
2603            assert_eq!(
2604                worktree_roots,
2605                vec![path!("/dir1"), path!("/dir2/b.txt")]
2606                    .into_iter()
2607                    .map(Path::new)
2608                    .collect(),
2609            );
2610            assert_eq!(
2611                workspace
2612                    .active_pane()
2613                    .read(cx)
2614                    .active_item()
2615                    .unwrap()
2616                    .act_as::<Editor>(cx)
2617                    .unwrap()
2618                    .read(cx)
2619                    .title(cx),
2620                "b.txt"
2621            );
2622        });
2623
2624        // Ensure opening a directory and one of its children only adds one worktree.
2625        window
2626            .update(cx, |workspace, window, cx| {
2627                workspace.open_paths(
2628                    vec![path!("/dir3").into(), path!("/dir3/c.txt").into()],
2629                    OpenOptions {
2630                        visible: Some(OpenVisible::All),
2631                        ..Default::default()
2632                    },
2633                    None,
2634                    window,
2635                    cx,
2636                )
2637            })
2638            .unwrap()
2639            .await;
2640        cx.read(|cx| {
2641            let workspace = workspace.read(cx);
2642            assert_project_panel_selection(
2643                workspace,
2644                Path::new(path!("/dir3")),
2645                Path::new("c.txt"),
2646                cx,
2647            );
2648            let worktree_roots = workspace
2649                .worktrees(cx)
2650                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2651                .collect::<HashSet<_>>();
2652            assert_eq!(
2653                worktree_roots,
2654                vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
2655                    .into_iter()
2656                    .map(Path::new)
2657                    .collect(),
2658            );
2659            assert_eq!(
2660                workspace
2661                    .active_pane()
2662                    .read(cx)
2663                    .active_item()
2664                    .unwrap()
2665                    .act_as::<Editor>(cx)
2666                    .unwrap()
2667                    .read(cx)
2668                    .title(cx),
2669                "c.txt"
2670            );
2671        });
2672
2673        // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
2674        window
2675            .update(cx, |workspace, window, cx| {
2676                workspace.open_paths(
2677                    vec![path!("/d.txt").into()],
2678                    OpenOptions {
2679                        visible: Some(OpenVisible::None),
2680                        ..Default::default()
2681                    },
2682                    None,
2683                    window,
2684                    cx,
2685                )
2686            })
2687            .unwrap()
2688            .await;
2689        cx.read(|cx| {
2690            let workspace = workspace.read(cx);
2691            assert_project_panel_selection(
2692                workspace,
2693                Path::new(path!("/d.txt")),
2694                Path::new(""),
2695                cx,
2696            );
2697            let worktree_roots = workspace
2698                .worktrees(cx)
2699                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2700                .collect::<HashSet<_>>();
2701            assert_eq!(
2702                worktree_roots,
2703                vec![
2704                    path!("/dir1"),
2705                    path!("/dir2/b.txt"),
2706                    path!("/dir3"),
2707                    path!("/d.txt")
2708                ]
2709                .into_iter()
2710                .map(Path::new)
2711                .collect(),
2712            );
2713
2714            let visible_worktree_roots = workspace
2715                .visible_worktrees(cx)
2716                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2717                .collect::<HashSet<_>>();
2718            assert_eq!(
2719                visible_worktree_roots,
2720                vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
2721                    .into_iter()
2722                    .map(Path::new)
2723                    .collect(),
2724            );
2725
2726            assert_eq!(
2727                workspace
2728                    .active_pane()
2729                    .read(cx)
2730                    .active_item()
2731                    .unwrap()
2732                    .act_as::<Editor>(cx)
2733                    .unwrap()
2734                    .read(cx)
2735                    .title(cx),
2736                "d.txt"
2737            );
2738        });
2739    }
2740
2741    #[gpui::test]
2742    async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
2743        let app_state = init_test(cx);
2744        cx.update(|cx| {
2745            cx.update_global::<SettingsStore, _>(|store, cx| {
2746                store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
2747                    project_settings.file_scan_exclusions =
2748                        Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
2749                });
2750            });
2751        });
2752        app_state
2753            .fs
2754            .as_fake()
2755            .insert_tree(
2756                path!("/root"),
2757                json!({
2758                    ".gitignore": "ignored_dir\n",
2759                    ".git": {
2760                        "HEAD": "ref: refs/heads/main",
2761                    },
2762                    "regular_dir": {
2763                        "file": "regular file contents",
2764                    },
2765                    "ignored_dir": {
2766                        "ignored_subdir": {
2767                            "file": "ignored subfile contents",
2768                        },
2769                        "file": "ignored file contents",
2770                    },
2771                    "excluded_dir": {
2772                        "file": "excluded file contents",
2773                        "ignored_subdir": {
2774                            "file": "ignored subfile contents",
2775                        },
2776                    },
2777                }),
2778            )
2779            .await;
2780
2781        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2782        project.update(cx, |project, _cx| {
2783            project.languages().add(markdown_language())
2784        });
2785        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2786        let workspace = window.root(cx).unwrap();
2787
2788        let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
2789        let paths_to_open = [
2790            PathBuf::from(path!("/root/excluded_dir/file")),
2791            PathBuf::from(path!("/root/.git/HEAD")),
2792            PathBuf::from(path!("/root/excluded_dir/ignored_subdir")),
2793        ];
2794        let (opened_workspace, new_items) = cx
2795            .update(|cx| {
2796                workspace::open_paths(
2797                    &paths_to_open,
2798                    app_state,
2799                    workspace::OpenOptions::default(),
2800                    cx,
2801                )
2802            })
2803            .await
2804            .unwrap();
2805
2806        assert_eq!(
2807            opened_workspace.root(cx).unwrap().entity_id(),
2808            workspace.entity_id(),
2809            "Excluded files in subfolders of a workspace root should be opened in the workspace"
2810        );
2811        let mut opened_paths = cx.read(|cx| {
2812            assert_eq!(
2813                new_items.len(),
2814                paths_to_open.len(),
2815                "Expect to get the same number of opened items as submitted paths to open"
2816            );
2817            new_items
2818                .iter()
2819                .zip(paths_to_open.iter())
2820                .map(|(i, path)| {
2821                    match i {
2822                        Some(Ok(i)) => {
2823                            Some(i.project_path(cx).map(|p| p.path.display().to_string()))
2824                        }
2825                        Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
2826                        None => None,
2827                    }
2828                    .flatten()
2829                })
2830                .collect::<Vec<_>>()
2831        });
2832        opened_paths.sort();
2833        assert_eq!(
2834            opened_paths,
2835            vec![
2836                None,
2837                Some(separator!(".git/HEAD").to_string()),
2838                Some(separator!("excluded_dir/file").to_string()),
2839            ],
2840            "Excluded files should get opened, excluded dir should not get opened"
2841        );
2842
2843        let entries = cx.read(|cx| workspace.file_project_paths(cx));
2844        assert_eq!(
2845            initial_entries, entries,
2846            "Workspace entries should not change after opening excluded files and directories paths"
2847        );
2848
2849        cx.read(|cx| {
2850                let pane = workspace.read(cx).active_pane().read(cx);
2851                let mut opened_buffer_paths = pane
2852                    .items()
2853                    .map(|i| {
2854                        i.project_path(cx)
2855                            .expect("all excluded files that got open should have a path")
2856                            .path
2857                            .display()
2858                            .to_string()
2859                    })
2860                    .collect::<Vec<_>>();
2861                opened_buffer_paths.sort();
2862                assert_eq!(
2863                    opened_buffer_paths,
2864                    vec![separator!(".git/HEAD").to_string(), separator!("excluded_dir/file").to_string()],
2865                    "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
2866                );
2867            });
2868    }
2869
2870    #[gpui::test]
2871    async fn test_save_conflicting_item(cx: &mut TestAppContext) {
2872        let app_state = init_test(cx);
2873        app_state
2874            .fs
2875            .as_fake()
2876            .insert_tree(path!("/root"), json!({ "a.txt": "" }))
2877            .await;
2878
2879        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2880        project.update(cx, |project, _cx| {
2881            project.languages().add(markdown_language())
2882        });
2883        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2884        let workspace = window.root(cx).unwrap();
2885
2886        // Open a file within an existing worktree.
2887        window
2888            .update(cx, |workspace, window, cx| {
2889                workspace.open_paths(
2890                    vec![PathBuf::from(path!("/root/a.txt"))],
2891                    OpenOptions {
2892                        visible: Some(OpenVisible::All),
2893                        ..Default::default()
2894                    },
2895                    None,
2896                    window,
2897                    cx,
2898                )
2899            })
2900            .unwrap()
2901            .await;
2902        let editor = cx.read(|cx| {
2903            let pane = workspace.read(cx).active_pane().read(cx);
2904            let item = pane.active_item().unwrap();
2905            item.downcast::<Editor>().unwrap()
2906        });
2907
2908        window
2909            .update(cx, |_, window, cx| {
2910                editor.update(cx, |editor, cx| editor.handle_input("x", window, cx));
2911            })
2912            .unwrap();
2913
2914        app_state
2915            .fs
2916            .as_fake()
2917            .insert_file(path!("/root/a.txt"), b"changed".to_vec())
2918            .await;
2919
2920        cx.run_until_parked();
2921        cx.read(|cx| assert!(editor.is_dirty(cx)));
2922        cx.read(|cx| assert!(editor.has_conflict(cx)));
2923
2924        let save_task = window
2925            .update(cx, |workspace, window, cx| {
2926                workspace.save_active_item(SaveIntent::Save, window, cx)
2927            })
2928            .unwrap();
2929        cx.background_executor.run_until_parked();
2930        cx.simulate_prompt_answer("Overwrite");
2931        save_task.await.unwrap();
2932        window
2933            .update(cx, |_, _, cx| {
2934                editor.update(cx, |editor, cx| {
2935                    assert!(!editor.is_dirty(cx));
2936                    assert!(!editor.has_conflict(cx));
2937                });
2938            })
2939            .unwrap();
2940    }
2941
2942    #[gpui::test]
2943    async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
2944        let app_state = init_test(cx);
2945        app_state
2946            .fs
2947            .create_dir(Path::new(path!("/root")))
2948            .await
2949            .unwrap();
2950
2951        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2952        project.update(cx, |project, _| {
2953            project.languages().add(markdown_language());
2954            project.languages().add(rust_lang());
2955        });
2956        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2957        let worktree = cx.update(|cx| window.read(cx).unwrap().worktrees(cx).next().unwrap());
2958
2959        // Create a new untitled buffer
2960        cx.dispatch_action(window.into(), NewFile);
2961        let editor = window
2962            .read_with(cx, |workspace, cx| {
2963                workspace
2964                    .active_item(cx)
2965                    .unwrap()
2966                    .downcast::<Editor>()
2967                    .unwrap()
2968            })
2969            .unwrap();
2970
2971        window
2972            .update(cx, |_, window, cx| {
2973                editor.update(cx, |editor, cx| {
2974                    assert!(!editor.is_dirty(cx));
2975                    assert_eq!(editor.title(cx), "untitled");
2976                    assert!(Arc::ptr_eq(
2977                        &editor.buffer().read(cx).language_at(0, cx).unwrap(),
2978                        &languages::PLAIN_TEXT
2979                    ));
2980                    editor.handle_input("hi", window, cx);
2981                    assert!(editor.is_dirty(cx));
2982                });
2983            })
2984            .unwrap();
2985
2986        // Save the buffer. This prompts for a filename.
2987        let save_task = window
2988            .update(cx, |workspace, window, cx| {
2989                workspace.save_active_item(SaveIntent::Save, window, cx)
2990            })
2991            .unwrap();
2992        cx.background_executor.run_until_parked();
2993        cx.simulate_new_path_selection(|parent_dir| {
2994            assert_eq!(parent_dir, Path::new(path!("/root")));
2995            Some(parent_dir.join("the-new-name.rs"))
2996        });
2997        cx.read(|cx| {
2998            assert!(editor.is_dirty(cx));
2999            assert_eq!(editor.read(cx).title(cx), "untitled");
3000        });
3001
3002        // When the save completes, the buffer's title is updated and the language is assigned based
3003        // on the path.
3004        save_task.await.unwrap();
3005        window
3006            .update(cx, |_, _, cx| {
3007                editor.update(cx, |editor, cx| {
3008                    assert!(!editor.is_dirty(cx));
3009                    assert_eq!(editor.title(cx), "the-new-name.rs");
3010                    assert_eq!(
3011                        editor.buffer().read(cx).language_at(0, cx).unwrap().name(),
3012                        "Rust".into()
3013                    );
3014                });
3015            })
3016            .unwrap();
3017
3018        // Edit the file and save it again. This time, there is no filename prompt.
3019        window
3020            .update(cx, |_, window, cx| {
3021                editor.update(cx, |editor, cx| {
3022                    editor.handle_input(" there", window, cx);
3023                    assert!(editor.is_dirty(cx));
3024                });
3025            })
3026            .unwrap();
3027
3028        let save_task = window
3029            .update(cx, |workspace, window, cx| {
3030                workspace.save_active_item(SaveIntent::Save, window, cx)
3031            })
3032            .unwrap();
3033        save_task.await.unwrap();
3034
3035        assert!(!cx.did_prompt_for_new_path());
3036        window
3037            .update(cx, |_, _, cx| {
3038                editor.update(cx, |editor, cx| {
3039                    assert!(!editor.is_dirty(cx));
3040                    assert_eq!(editor.title(cx), "the-new-name.rs")
3041                });
3042            })
3043            .unwrap();
3044
3045        // Open the same newly-created file in another pane item. The new editor should reuse
3046        // the same buffer.
3047        cx.dispatch_action(window.into(), NewFile);
3048        window
3049            .update(cx, |workspace, window, cx| {
3050                workspace.split_and_clone(
3051                    workspace.active_pane().clone(),
3052                    SplitDirection::Right,
3053                    window,
3054                    cx,
3055                );
3056                workspace.open_path(
3057                    (worktree.read(cx).id(), "the-new-name.rs"),
3058                    None,
3059                    true,
3060                    window,
3061                    cx,
3062                )
3063            })
3064            .unwrap()
3065            .await
3066            .unwrap();
3067        let editor2 = window
3068            .update(cx, |workspace, _, cx| {
3069                workspace
3070                    .active_item(cx)
3071                    .unwrap()
3072                    .downcast::<Editor>()
3073                    .unwrap()
3074            })
3075            .unwrap();
3076        cx.read(|cx| {
3077            assert_eq!(
3078                editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
3079                editor.read(cx).buffer().read(cx).as_singleton().unwrap()
3080            );
3081        })
3082    }
3083
3084    #[gpui::test]
3085    async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
3086        let app_state = init_test(cx);
3087        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
3088
3089        let project = Project::test(app_state.fs.clone(), [], cx).await;
3090        project.update(cx, |project, _| {
3091            project.languages().add(rust_lang());
3092            project.languages().add(markdown_language());
3093        });
3094        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3095
3096        // Create a new untitled buffer
3097        cx.dispatch_action(window.into(), NewFile);
3098        let editor = window
3099            .read_with(cx, |workspace, cx| {
3100                workspace
3101                    .active_item(cx)
3102                    .unwrap()
3103                    .downcast::<Editor>()
3104                    .unwrap()
3105            })
3106            .unwrap();
3107        window
3108            .update(cx, |_, window, cx| {
3109                editor.update(cx, |editor, cx| {
3110                    assert!(Arc::ptr_eq(
3111                        &editor.buffer().read(cx).language_at(0, cx).unwrap(),
3112                        &languages::PLAIN_TEXT
3113                    ));
3114                    editor.handle_input("hi", window, cx);
3115                    assert!(editor.is_dirty(cx));
3116                });
3117            })
3118            .unwrap();
3119
3120        // Save the buffer. This prompts for a filename.
3121        let save_task = window
3122            .update(cx, |workspace, window, cx| {
3123                workspace.save_active_item(SaveIntent::Save, window, cx)
3124            })
3125            .unwrap();
3126        cx.background_executor.run_until_parked();
3127        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
3128        save_task.await.unwrap();
3129        // The buffer is not dirty anymore and the language is assigned based on the path.
3130        window
3131            .update(cx, |_, _, cx| {
3132                editor.update(cx, |editor, cx| {
3133                    assert!(!editor.is_dirty(cx));
3134                    assert_eq!(
3135                        editor.buffer().read(cx).language_at(0, cx).unwrap().name(),
3136                        "Rust".into()
3137                    )
3138                });
3139            })
3140            .unwrap();
3141    }
3142
3143    #[gpui::test]
3144    async fn test_pane_actions(cx: &mut TestAppContext) {
3145        let app_state = init_test(cx);
3146        app_state
3147            .fs
3148            .as_fake()
3149            .insert_tree(
3150                path!("/root"),
3151                json!({
3152                    "a": {
3153                        "file1": "contents 1",
3154                        "file2": "contents 2",
3155                        "file3": "contents 3",
3156                    },
3157                }),
3158            )
3159            .await;
3160
3161        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3162        project.update(cx, |project, _cx| {
3163            project.languages().add(markdown_language())
3164        });
3165        let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3166        let workspace = window.root(cx).unwrap();
3167
3168        let entries = cx.read(|cx| workspace.file_project_paths(cx));
3169        let file1 = entries[0].clone();
3170
3171        let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
3172
3173        window
3174            .update(cx, |w, window, cx| {
3175                w.open_path(file1.clone(), None, true, window, cx)
3176            })
3177            .unwrap()
3178            .await
3179            .unwrap();
3180
3181        let (editor_1, buffer) = window
3182            .update(cx, |_, window, cx| {
3183                pane_1.update(cx, |pane_1, cx| {
3184                    let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
3185                    assert_eq!(editor.project_path(cx), Some(file1.clone()));
3186                    let buffer = editor.update(cx, |editor, cx| {
3187                        editor.insert("dirt", window, cx);
3188                        editor.buffer().downgrade()
3189                    });
3190                    (editor.downgrade(), buffer)
3191                })
3192            })
3193            .unwrap();
3194
3195        cx.dispatch_action(window.into(), pane::SplitRight);
3196        let editor_2 = cx.update(|cx| {
3197            let pane_2 = workspace.read(cx).active_pane().clone();
3198            assert_ne!(pane_1, pane_2);
3199
3200            let pane2_item = pane_2.read(cx).active_item().unwrap();
3201            assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
3202
3203            pane2_item.downcast::<Editor>().unwrap().downgrade()
3204        });
3205        cx.dispatch_action(
3206            window.into(),
3207            workspace::CloseActiveItem {
3208                save_intent: None,
3209                close_pinned: false,
3210            },
3211        );
3212
3213        cx.background_executor.run_until_parked();
3214        window
3215            .read_with(cx, |workspace, _| {
3216                assert_eq!(workspace.panes().len(), 1);
3217                assert_eq!(workspace.active_pane(), &pane_1);
3218            })
3219            .unwrap();
3220
3221        cx.dispatch_action(
3222            window.into(),
3223            workspace::CloseActiveItem {
3224                save_intent: None,
3225                close_pinned: false,
3226            },
3227        );
3228        cx.background_executor.run_until_parked();
3229        cx.simulate_prompt_answer("Don't Save");
3230        cx.background_executor.run_until_parked();
3231
3232        window
3233            .update(cx, |workspace, _, cx| {
3234                assert_eq!(workspace.panes().len(), 1);
3235                assert!(workspace.active_item(cx).is_none());
3236            })
3237            .unwrap();
3238
3239        cx.background_executor
3240            .advance_clock(SERIALIZATION_THROTTLE_TIME);
3241        cx.update(|_| {});
3242        editor_1.assert_released();
3243        editor_2.assert_released();
3244        buffer.assert_released();
3245    }
3246
3247    #[gpui::test]
3248    async fn test_navigation(cx: &mut TestAppContext) {
3249        let app_state = init_test(cx);
3250        app_state
3251            .fs
3252            .as_fake()
3253            .insert_tree(
3254                path!("/root"),
3255                json!({
3256                    "a": {
3257                        "file1": "contents 1\n".repeat(20),
3258                        "file2": "contents 2\n".repeat(20),
3259                        "file3": "contents 3\n".repeat(20),
3260                    },
3261                }),
3262            )
3263            .await;
3264
3265        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3266        project.update(cx, |project, _cx| {
3267            project.languages().add(markdown_language())
3268        });
3269        let workspace =
3270            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3271        let pane = workspace
3272            .read_with(cx, |workspace, _| workspace.active_pane().clone())
3273            .unwrap();
3274
3275        let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
3276        let file1 = entries[0].clone();
3277        let file2 = entries[1].clone();
3278        let file3 = entries[2].clone();
3279
3280        let editor1 = workspace
3281            .update(cx, |w, window, cx| {
3282                w.open_path(file1.clone(), None, true, window, cx)
3283            })
3284            .unwrap()
3285            .await
3286            .unwrap()
3287            .downcast::<Editor>()
3288            .unwrap();
3289        workspace
3290            .update(cx, |_, window, cx| {
3291                editor1.update(cx, |editor, cx| {
3292                    editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
3293                        s.select_display_ranges([DisplayPoint::new(DisplayRow(10), 0)
3294                            ..DisplayPoint::new(DisplayRow(10), 0)])
3295                    });
3296                });
3297            })
3298            .unwrap();
3299
3300        let editor2 = workspace
3301            .update(cx, |w, window, cx| {
3302                w.open_path(file2.clone(), None, true, window, cx)
3303            })
3304            .unwrap()
3305            .await
3306            .unwrap()
3307            .downcast::<Editor>()
3308            .unwrap();
3309        let editor3 = workspace
3310            .update(cx, |w, window, cx| {
3311                w.open_path(file3.clone(), None, true, window, cx)
3312            })
3313            .unwrap()
3314            .await
3315            .unwrap()
3316            .downcast::<Editor>()
3317            .unwrap();
3318
3319        workspace
3320            .update(cx, |_, window, cx| {
3321                editor3.update(cx, |editor, cx| {
3322                    editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
3323                        s.select_display_ranges([DisplayPoint::new(DisplayRow(12), 0)
3324                            ..DisplayPoint::new(DisplayRow(12), 0)])
3325                    });
3326                    editor.newline(&Default::default(), window, cx);
3327                    editor.newline(&Default::default(), window, cx);
3328                    editor.move_down(&Default::default(), window, cx);
3329                    editor.move_down(&Default::default(), window, cx);
3330                    editor.save(true, project.clone(), window, cx)
3331                })
3332            })
3333            .unwrap()
3334            .await
3335            .unwrap();
3336        workspace
3337            .update(cx, |_, window, cx| {
3338                editor3.update(cx, |editor, cx| {
3339                    editor.set_scroll_position(point(0., 12.5), window, cx)
3340                });
3341            })
3342            .unwrap();
3343        assert_eq!(
3344            active_location(&workspace, cx),
3345            (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
3346        );
3347
3348        workspace
3349            .update(cx, |w, window, cx| {
3350                w.go_back(w.active_pane().downgrade(), window, cx)
3351            })
3352            .unwrap()
3353            .await
3354            .unwrap();
3355        assert_eq!(
3356            active_location(&workspace, cx),
3357            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3358        );
3359
3360        workspace
3361            .update(cx, |w, window, cx| {
3362                w.go_back(w.active_pane().downgrade(), window, cx)
3363            })
3364            .unwrap()
3365            .await
3366            .unwrap();
3367        assert_eq!(
3368            active_location(&workspace, cx),
3369            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3370        );
3371
3372        workspace
3373            .update(cx, |w, window, cx| {
3374                w.go_back(w.active_pane().downgrade(), window, cx)
3375            })
3376            .unwrap()
3377            .await
3378            .unwrap();
3379        assert_eq!(
3380            active_location(&workspace, cx),
3381            (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
3382        );
3383
3384        workspace
3385            .update(cx, |w, window, cx| {
3386                w.go_back(w.active_pane().downgrade(), window, cx)
3387            })
3388            .unwrap()
3389            .await
3390            .unwrap();
3391        assert_eq!(
3392            active_location(&workspace, cx),
3393            (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3394        );
3395
3396        // Go back one more time and ensure we don't navigate past the first item in the history.
3397        workspace
3398            .update(cx, |w, window, cx| {
3399                w.go_back(w.active_pane().downgrade(), window, cx)
3400            })
3401            .unwrap()
3402            .await
3403            .unwrap();
3404        assert_eq!(
3405            active_location(&workspace, cx),
3406            (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3407        );
3408
3409        workspace
3410            .update(cx, |w, window, cx| {
3411                w.go_forward(w.active_pane().downgrade(), window, cx)
3412            })
3413            .unwrap()
3414            .await
3415            .unwrap();
3416        assert_eq!(
3417            active_location(&workspace, cx),
3418            (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
3419        );
3420
3421        workspace
3422            .update(cx, |w, window, cx| {
3423                w.go_forward(w.active_pane().downgrade(), window, cx)
3424            })
3425            .unwrap()
3426            .await
3427            .unwrap();
3428        assert_eq!(
3429            active_location(&workspace, cx),
3430            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3431        );
3432
3433        // Go forward to an item that has been closed, ensuring it gets re-opened at the same
3434        // location.
3435        workspace
3436            .update(cx, |_, window, cx| {
3437                pane.update(cx, |pane, cx| {
3438                    let editor3_id = editor3.entity_id();
3439                    drop(editor3);
3440                    pane.close_item_by_id(editor3_id, SaveIntent::Close, window, cx)
3441                })
3442            })
3443            .unwrap()
3444            .await
3445            .unwrap();
3446        workspace
3447            .update(cx, |w, window, cx| {
3448                w.go_forward(w.active_pane().downgrade(), window, cx)
3449            })
3450            .unwrap()
3451            .await
3452            .unwrap();
3453        assert_eq!(
3454            active_location(&workspace, cx),
3455            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3456        );
3457
3458        workspace
3459            .update(cx, |w, window, cx| {
3460                w.go_forward(w.active_pane().downgrade(), window, cx)
3461            })
3462            .unwrap()
3463            .await
3464            .unwrap();
3465        assert_eq!(
3466            active_location(&workspace, cx),
3467            (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
3468        );
3469
3470        workspace
3471            .update(cx, |w, window, cx| {
3472                w.go_back(w.active_pane().downgrade(), window, cx)
3473            })
3474            .unwrap()
3475            .await
3476            .unwrap();
3477        assert_eq!(
3478            active_location(&workspace, cx),
3479            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3480        );
3481
3482        // Go back to an item that has been closed and removed from disk
3483        workspace
3484            .update(cx, |_, window, cx| {
3485                pane.update(cx, |pane, cx| {
3486                    let editor2_id = editor2.entity_id();
3487                    drop(editor2);
3488                    pane.close_item_by_id(editor2_id, SaveIntent::Close, window, cx)
3489                })
3490            })
3491            .unwrap()
3492            .await
3493            .unwrap();
3494        app_state
3495            .fs
3496            .remove_file(Path::new(path!("/root/a/file2")), Default::default())
3497            .await
3498            .unwrap();
3499        cx.background_executor.run_until_parked();
3500
3501        workspace
3502            .update(cx, |w, window, cx| {
3503                w.go_back(w.active_pane().downgrade(), window, cx)
3504            })
3505            .unwrap()
3506            .await
3507            .unwrap();
3508        assert_eq!(
3509            active_location(&workspace, cx),
3510            (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3511        );
3512        workspace
3513            .update(cx, |w, window, cx| {
3514                w.go_forward(w.active_pane().downgrade(), window, cx)
3515            })
3516            .unwrap()
3517            .await
3518            .unwrap();
3519        assert_eq!(
3520            active_location(&workspace, cx),
3521            (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3522        );
3523
3524        // Modify file to collapse multiple nav history entries into the same location.
3525        // Ensure we don't visit the same location twice when navigating.
3526        workspace
3527            .update(cx, |_, window, cx| {
3528                editor1.update(cx, |editor, cx| {
3529                    editor.change_selections(None, window, cx, |s| {
3530                        s.select_display_ranges([DisplayPoint::new(DisplayRow(15), 0)
3531                            ..DisplayPoint::new(DisplayRow(15), 0)])
3532                    })
3533                });
3534            })
3535            .unwrap();
3536        for _ in 0..5 {
3537            workspace
3538                .update(cx, |_, window, cx| {
3539                    editor1.update(cx, |editor, cx| {
3540                        editor.change_selections(None, window, cx, |s| {
3541                            s.select_display_ranges([DisplayPoint::new(DisplayRow(3), 0)
3542                                ..DisplayPoint::new(DisplayRow(3), 0)])
3543                        });
3544                    });
3545                })
3546                .unwrap();
3547
3548            workspace
3549                .update(cx, |_, window, cx| {
3550                    editor1.update(cx, |editor, cx| {
3551                        editor.change_selections(None, window, cx, |s| {
3552                            s.select_display_ranges([DisplayPoint::new(DisplayRow(13), 0)
3553                                ..DisplayPoint::new(DisplayRow(13), 0)])
3554                        })
3555                    });
3556                })
3557                .unwrap();
3558        }
3559        workspace
3560            .update(cx, |_, window, cx| {
3561                editor1.update(cx, |editor, cx| {
3562                    editor.transact(window, cx, |editor, window, cx| {
3563                        editor.change_selections(None, window, cx, |s| {
3564                            s.select_display_ranges([DisplayPoint::new(DisplayRow(2), 0)
3565                                ..DisplayPoint::new(DisplayRow(14), 0)])
3566                        });
3567                        editor.insert("", window, cx);
3568                    })
3569                });
3570            })
3571            .unwrap();
3572
3573        workspace
3574            .update(cx, |_, window, cx| {
3575                editor1.update(cx, |editor, cx| {
3576                    editor.change_selections(None, window, cx, |s| {
3577                        s.select_display_ranges([DisplayPoint::new(DisplayRow(1), 0)
3578                            ..DisplayPoint::new(DisplayRow(1), 0)])
3579                    })
3580                });
3581            })
3582            .unwrap();
3583        workspace
3584            .update(cx, |w, window, cx| {
3585                w.go_back(w.active_pane().downgrade(), window, cx)
3586            })
3587            .unwrap()
3588            .await
3589            .unwrap();
3590        assert_eq!(
3591            active_location(&workspace, cx),
3592            (file1.clone(), DisplayPoint::new(DisplayRow(2), 0), 0.)
3593        );
3594        workspace
3595            .update(cx, |w, window, cx| {
3596                w.go_back(w.active_pane().downgrade(), window, cx)
3597            })
3598            .unwrap()
3599            .await
3600            .unwrap();
3601        assert_eq!(
3602            active_location(&workspace, cx),
3603            (file1.clone(), DisplayPoint::new(DisplayRow(3), 0), 0.)
3604        );
3605
3606        fn active_location(
3607            workspace: &WindowHandle<Workspace>,
3608            cx: &mut TestAppContext,
3609        ) -> (ProjectPath, DisplayPoint, f32) {
3610            workspace
3611                .update(cx, |workspace, _, cx| {
3612                    let item = workspace.active_item(cx).unwrap();
3613                    let editor = item.downcast::<Editor>().unwrap();
3614                    let (selections, scroll_position) = editor.update(cx, |editor, cx| {
3615                        (
3616                            editor.selections.display_ranges(cx),
3617                            editor.scroll_position(cx),
3618                        )
3619                    });
3620                    (
3621                        item.project_path(cx).unwrap(),
3622                        selections[0].start,
3623                        scroll_position.y,
3624                    )
3625                })
3626                .unwrap()
3627        }
3628    }
3629
3630    #[gpui::test]
3631    async fn test_reopening_closed_items(cx: &mut TestAppContext) {
3632        let app_state = init_test(cx);
3633        app_state
3634            .fs
3635            .as_fake()
3636            .insert_tree(
3637                path!("/root"),
3638                json!({
3639                    "a": {
3640                        "file1": "",
3641                        "file2": "",
3642                        "file3": "",
3643                        "file4": "",
3644                    },
3645                }),
3646            )
3647            .await;
3648
3649        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3650        project.update(cx, |project, _cx| {
3651            project.languages().add(markdown_language())
3652        });
3653        let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3654        let pane = workspace
3655            .read_with(cx, |workspace, _| workspace.active_pane().clone())
3656            .unwrap();
3657
3658        let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
3659        let file1 = entries[0].clone();
3660        let file2 = entries[1].clone();
3661        let file3 = entries[2].clone();
3662        let file4 = entries[3].clone();
3663
3664        let file1_item_id = workspace
3665            .update(cx, |w, window, cx| {
3666                w.open_path(file1.clone(), None, true, window, cx)
3667            })
3668            .unwrap()
3669            .await
3670            .unwrap()
3671            .item_id();
3672        let file2_item_id = workspace
3673            .update(cx, |w, window, cx| {
3674                w.open_path(file2.clone(), None, true, window, cx)
3675            })
3676            .unwrap()
3677            .await
3678            .unwrap()
3679            .item_id();
3680        let file3_item_id = workspace
3681            .update(cx, |w, window, cx| {
3682                w.open_path(file3.clone(), None, true, window, cx)
3683            })
3684            .unwrap()
3685            .await
3686            .unwrap()
3687            .item_id();
3688        let file4_item_id = workspace
3689            .update(cx, |w, window, cx| {
3690                w.open_path(file4.clone(), None, true, window, cx)
3691            })
3692            .unwrap()
3693            .await
3694            .unwrap()
3695            .item_id();
3696        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3697
3698        // Close all the pane items in some arbitrary order.
3699        workspace
3700            .update(cx, |_, window, cx| {
3701                pane.update(cx, |pane, cx| {
3702                    pane.close_item_by_id(file1_item_id, SaveIntent::Close, window, cx)
3703                })
3704            })
3705            .unwrap()
3706            .await
3707            .unwrap();
3708        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3709
3710        workspace
3711            .update(cx, |_, window, cx| {
3712                pane.update(cx, |pane, cx| {
3713                    pane.close_item_by_id(file4_item_id, SaveIntent::Close, window, cx)
3714                })
3715            })
3716            .unwrap()
3717            .await
3718            .unwrap();
3719        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3720
3721        workspace
3722            .update(cx, |_, window, cx| {
3723                pane.update(cx, |pane, cx| {
3724                    pane.close_item_by_id(file2_item_id, SaveIntent::Close, window, cx)
3725                })
3726            })
3727            .unwrap()
3728            .await
3729            .unwrap();
3730        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3731        workspace
3732            .update(cx, |_, window, cx| {
3733                pane.update(cx, |pane, cx| {
3734                    pane.close_item_by_id(file3_item_id, SaveIntent::Close, window, cx)
3735                })
3736            })
3737            .unwrap()
3738            .await
3739            .unwrap();
3740
3741        assert_eq!(active_path(&workspace, cx), None);
3742
3743        // Reopen all the closed items, ensuring they are reopened in the same order
3744        // in which they were closed.
3745        workspace
3746            .update(cx, Workspace::reopen_closed_item)
3747            .unwrap()
3748            .await
3749            .unwrap();
3750        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3751
3752        workspace
3753            .update(cx, Workspace::reopen_closed_item)
3754            .unwrap()
3755            .await
3756            .unwrap();
3757        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
3758
3759        workspace
3760            .update(cx, Workspace::reopen_closed_item)
3761            .unwrap()
3762            .await
3763            .unwrap();
3764        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3765
3766        workspace
3767            .update(cx, Workspace::reopen_closed_item)
3768            .unwrap()
3769            .await
3770            .unwrap();
3771        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
3772
3773        // Reopening past the last closed item is a no-op.
3774        workspace
3775            .update(cx, Workspace::reopen_closed_item)
3776            .unwrap()
3777            .await
3778            .unwrap();
3779        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
3780
3781        // Reopening closed items doesn't interfere with navigation history.
3782        workspace
3783            .update(cx, |workspace, window, cx| {
3784                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3785            })
3786            .unwrap()
3787            .await
3788            .unwrap();
3789        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3790
3791        workspace
3792            .update(cx, |workspace, window, cx| {
3793                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3794            })
3795            .unwrap()
3796            .await
3797            .unwrap();
3798        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
3799
3800        workspace
3801            .update(cx, |workspace, window, cx| {
3802                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3803            })
3804            .unwrap()
3805            .await
3806            .unwrap();
3807        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3808
3809        workspace
3810            .update(cx, |workspace, window, cx| {
3811                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3812            })
3813            .unwrap()
3814            .await
3815            .unwrap();
3816        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3817
3818        workspace
3819            .update(cx, |workspace, window, cx| {
3820                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3821            })
3822            .unwrap()
3823            .await
3824            .unwrap();
3825        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3826
3827        workspace
3828            .update(cx, |workspace, window, cx| {
3829                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3830            })
3831            .unwrap()
3832            .await
3833            .unwrap();
3834        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
3835
3836        workspace
3837            .update(cx, |workspace, window, cx| {
3838                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3839            })
3840            .unwrap()
3841            .await
3842            .unwrap();
3843        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
3844
3845        workspace
3846            .update(cx, |workspace, window, cx| {
3847                workspace.go_back(workspace.active_pane().downgrade(), window, cx)
3848            })
3849            .unwrap()
3850            .await
3851            .unwrap();
3852        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
3853
3854        fn active_path(
3855            workspace: &WindowHandle<Workspace>,
3856            cx: &TestAppContext,
3857        ) -> Option<ProjectPath> {
3858            workspace
3859                .read_with(cx, |workspace, cx| {
3860                    let item = workspace.active_item(cx)?;
3861                    item.project_path(cx)
3862                })
3863                .unwrap()
3864        }
3865    }
3866
3867    fn init_keymap_test(cx: &mut TestAppContext) -> Arc<AppState> {
3868        cx.update(|cx| {
3869            let app_state = AppState::test(cx);
3870
3871            theme::init(theme::LoadThemes::JustBase, cx);
3872            client::init(&app_state.client, cx);
3873            language::init(cx);
3874            workspace::init(app_state.clone(), cx);
3875            welcome::init(cx);
3876            Project::init_settings(cx);
3877            app_state
3878        })
3879    }
3880
3881    #[gpui::test]
3882    async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
3883        let executor = cx.executor();
3884        let app_state = init_keymap_test(cx);
3885        let project = Project::test(app_state.fs.clone(), [], cx).await;
3886        let workspace =
3887            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3888
3889        actions!(test1, [A, B]);
3890        // From the Atom keymap
3891        use workspace::ActivatePreviousPane;
3892        // From the JetBrains keymap
3893        use workspace::ActivatePreviousItem;
3894
3895        app_state
3896            .fs
3897            .save(
3898                "/settings.json".as_ref(),
3899                &r#"{"base_keymap": "Atom"}"#.into(),
3900                Default::default(),
3901            )
3902            .await
3903            .unwrap();
3904
3905        app_state
3906            .fs
3907            .save(
3908                "/keymap.json".as_ref(),
3909                &r#"[{"bindings": {"backspace": "test1::A"}}]"#.into(),
3910                Default::default(),
3911            )
3912            .await
3913            .unwrap();
3914        executor.run_until_parked();
3915        cx.update(|cx| {
3916            let settings_rx = watch_config_file(
3917                &executor,
3918                app_state.fs.clone(),
3919                PathBuf::from("/settings.json"),
3920            );
3921            let keymap_rx = watch_config_file(
3922                &executor,
3923                app_state.fs.clone(),
3924                PathBuf::from("/keymap.json"),
3925            );
3926            handle_settings_file_changes(settings_rx, cx, |_, _| {});
3927            handle_keymap_file_changes(keymap_rx, cx);
3928        });
3929        workspace
3930            .update(cx, |workspace, _, cx| {
3931                workspace.register_action(|_, _: &A, _window, _cx| {});
3932                workspace.register_action(|_, _: &B, _window, _cx| {});
3933                workspace.register_action(|_, _: &ActivatePreviousPane, _window, _cx| {});
3934                workspace.register_action(|_, _: &ActivatePreviousItem, _window, _cx| {});
3935                cx.notify();
3936            })
3937            .unwrap();
3938        executor.run_until_parked();
3939        // Test loading the keymap base at all
3940        assert_key_bindings_for(
3941            workspace.into(),
3942            cx,
3943            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
3944            line!(),
3945        );
3946
3947        // Test modifying the users keymap, while retaining the base keymap
3948        app_state
3949            .fs
3950            .save(
3951                "/keymap.json".as_ref(),
3952                &r#"[{"bindings": {"backspace": "test1::B"}}]"#.into(),
3953                Default::default(),
3954            )
3955            .await
3956            .unwrap();
3957
3958        executor.run_until_parked();
3959
3960        assert_key_bindings_for(
3961            workspace.into(),
3962            cx,
3963            vec![("backspace", &B), ("k", &ActivatePreviousPane)],
3964            line!(),
3965        );
3966
3967        // Test modifying the base, while retaining the users keymap
3968        app_state
3969            .fs
3970            .save(
3971                "/settings.json".as_ref(),
3972                &r#"{"base_keymap": "JetBrains"}"#.into(),
3973                Default::default(),
3974            )
3975            .await
3976            .unwrap();
3977
3978        executor.run_until_parked();
3979
3980        assert_key_bindings_for(
3981            workspace.into(),
3982            cx,
3983            vec![("backspace", &B), ("{", &ActivatePreviousItem)],
3984            line!(),
3985        );
3986    }
3987
3988    #[gpui::test]
3989    async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
3990        let executor = cx.executor();
3991        let app_state = init_keymap_test(cx);
3992        let project = Project::test(app_state.fs.clone(), [], cx).await;
3993        let workspace =
3994            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3995
3996        actions!(test2, [A, B]);
3997        // From the Atom keymap
3998        use workspace::ActivatePreviousPane;
3999        // From the JetBrains keymap
4000        use diagnostics::Deploy;
4001
4002        workspace
4003            .update(cx, |workspace, _, _| {
4004                workspace.register_action(|_, _: &A, _window, _cx| {});
4005                workspace.register_action(|_, _: &B, _window, _cx| {});
4006                workspace.register_action(|_, _: &Deploy, _window, _cx| {});
4007            })
4008            .unwrap();
4009        app_state
4010            .fs
4011            .save(
4012                "/settings.json".as_ref(),
4013                &r#"{"base_keymap": "Atom"}"#.into(),
4014                Default::default(),
4015            )
4016            .await
4017            .unwrap();
4018        app_state
4019            .fs
4020            .save(
4021                "/keymap.json".as_ref(),
4022                &r#"[{"bindings": {"backspace": "test2::A"}}]"#.into(),
4023                Default::default(),
4024            )
4025            .await
4026            .unwrap();
4027
4028        cx.update(|cx| {
4029            let settings_rx = watch_config_file(
4030                &executor,
4031                app_state.fs.clone(),
4032                PathBuf::from("/settings.json"),
4033            );
4034            let keymap_rx = watch_config_file(
4035                &executor,
4036                app_state.fs.clone(),
4037                PathBuf::from("/keymap.json"),
4038            );
4039
4040            handle_settings_file_changes(settings_rx, cx, |_, _| {});
4041            handle_keymap_file_changes(keymap_rx, cx);
4042        });
4043
4044        cx.background_executor.run_until_parked();
4045
4046        cx.background_executor.run_until_parked();
4047        // Test loading the keymap base at all
4048        assert_key_bindings_for(
4049            workspace.into(),
4050            cx,
4051            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
4052            line!(),
4053        );
4054
4055        // Test disabling the key binding for the base keymap
4056        app_state
4057            .fs
4058            .save(
4059                "/keymap.json".as_ref(),
4060                &r#"[{"bindings": {"backspace": null}}]"#.into(),
4061                Default::default(),
4062            )
4063            .await
4064            .unwrap();
4065
4066        cx.background_executor.run_until_parked();
4067
4068        assert_key_bindings_for(
4069            workspace.into(),
4070            cx,
4071            vec![("k", &ActivatePreviousPane)],
4072            line!(),
4073        );
4074
4075        // Test modifying the base, while retaining the users keymap
4076        app_state
4077            .fs
4078            .save(
4079                "/settings.json".as_ref(),
4080                &r#"{"base_keymap": "JetBrains"}"#.into(),
4081                Default::default(),
4082            )
4083            .await
4084            .unwrap();
4085
4086        cx.background_executor.run_until_parked();
4087
4088        assert_key_bindings_for(workspace.into(), cx, vec![("6", &Deploy)], line!());
4089    }
4090
4091    #[gpui::test]
4092    async fn test_generate_keymap_json_schema_for_registered_actions(
4093        cx: &mut gpui::TestAppContext,
4094    ) {
4095        init_keymap_test(cx);
4096        cx.update(|cx| {
4097            // Make sure it doesn't panic.
4098            KeymapFile::generate_json_schema_for_registered_actions(cx);
4099        });
4100    }
4101
4102    /// Actions that don't build from empty input won't work from command palette invocation.
4103    #[gpui::test]
4104    async fn test_actions_build_with_empty_input(cx: &mut gpui::TestAppContext) {
4105        init_keymap_test(cx);
4106        cx.update(|cx| {
4107            let all_actions = cx.all_action_names();
4108            let mut failing_names = Vec::new();
4109            let mut errors = Vec::new();
4110            for action in all_actions {
4111                match action.to_string().as_str() {
4112                    "vim::FindCommand"
4113                    | "vim::Literal"
4114                    | "vim::ResizePane"
4115                    | "vim::PushObject"
4116                    | "vim::PushFindForward"
4117                    | "vim::PushFindBackward"
4118                    | "vim::PushSneak"
4119                    | "vim::PushSneakBackward"
4120                    | "vim::PushChangeSurrounds"
4121                    | "vim::PushJump"
4122                    | "vim::PushDigraph"
4123                    | "vim::PushLiteral"
4124                    | "vim::Number"
4125                    | "vim::SelectRegister"
4126                    | "git::StageAndNext"
4127                    | "git::UnstageAndNext"
4128                    | "terminal::SendText"
4129                    | "terminal::SendKeystroke"
4130                    | "app_menu::OpenApplicationMenu"
4131                    | "picker::ConfirmInput"
4132                    | "editor::HandleInput"
4133                    | "editor::FoldAtLevel"
4134                    | "pane::ActivateItem"
4135                    | "workspace::ActivatePane"
4136                    | "workspace::MoveItemToPane"
4137                    | "workspace::MoveItemToPaneInDirection"
4138                    | "workspace::OpenTerminal"
4139                    | "workspace::SendKeystrokes"
4140                    | "zed::OpenBrowser"
4141                    | "zed::OpenZedUrl" => {}
4142                    _ => {
4143                        let result = cx.build_action(action, None);
4144                        match &result {
4145                            Ok(_) => {}
4146                            Err(err) => {
4147                                failing_names.push(action);
4148                                errors.push(format!("{action} failed to build: {err:?}"));
4149                            }
4150                        }
4151                    }
4152                }
4153            }
4154            if errors.len() > 0 {
4155                panic!(
4156                    "Failed to build actions using {{}} as input: {:?}. Errors:\n{}",
4157                    failing_names,
4158                    errors.join("\n")
4159                );
4160            }
4161        });
4162    }
4163
4164    #[gpui::test]
4165    fn test_bundled_settings_and_themes(cx: &mut App) {
4166        cx.text_system()
4167            .add_fonts(vec![
4168                Assets
4169                    .load("fonts/plex-mono/ZedPlexMono-Regular.ttf")
4170                    .unwrap()
4171                    .unwrap(),
4172                Assets
4173                    .load("fonts/plex-sans/ZedPlexSans-Regular.ttf")
4174                    .unwrap()
4175                    .unwrap(),
4176            ])
4177            .unwrap();
4178        let themes = ThemeRegistry::default();
4179        settings::init(cx);
4180        theme::init(theme::LoadThemes::JustBase, cx);
4181
4182        let mut has_default_theme = false;
4183        for theme_name in themes.list().into_iter().map(|meta| meta.name) {
4184            let theme = themes.get(&theme_name).unwrap();
4185            assert_eq!(theme.name, theme_name);
4186            if theme.name == ThemeSettings::get(None, cx).active_theme.name {
4187                has_default_theme = true;
4188            }
4189        }
4190        assert!(has_default_theme);
4191    }
4192
4193    #[gpui::test]
4194    async fn test_bundled_languages(cx: &mut TestAppContext) {
4195        env_logger::builder().is_test(true).try_init().ok();
4196        let settings = cx.update(SettingsStore::test);
4197        cx.set_global(settings);
4198        let languages = LanguageRegistry::test(cx.executor());
4199        let languages = Arc::new(languages);
4200        let node_runtime = node_runtime::NodeRuntime::unavailable();
4201        cx.update(|cx| {
4202            languages::init(languages.clone(), node_runtime, cx);
4203        });
4204        for name in languages.language_names() {
4205            languages
4206                .language_for_name(&name)
4207                .await
4208                .with_context(|| format!("language name {name}"))
4209                .unwrap();
4210        }
4211        cx.run_until_parked();
4212    }
4213
4214    pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
4215        init_test_with_state(cx, cx.update(AppState::test))
4216    }
4217
4218    fn init_test_with_state(
4219        cx: &mut TestAppContext,
4220        mut app_state: Arc<AppState>,
4221    ) -> Arc<AppState> {
4222        cx.update(move |cx| {
4223            env_logger::builder().is_test(true).try_init().ok();
4224
4225            let state = Arc::get_mut(&mut app_state).unwrap();
4226            state.build_window_options = build_window_options;
4227
4228            app_state.languages.add(markdown_language());
4229
4230            gpui_tokio::init(cx);
4231            vim_mode_setting::init(cx);
4232            theme::init(theme::LoadThemes::JustBase, cx);
4233            audio::init((), cx);
4234            channel::init(&app_state.client, app_state.user_store.clone(), cx);
4235            call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
4236            notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
4237            workspace::init(app_state.clone(), cx);
4238            Project::init_settings(cx);
4239            release_channel::init(SemanticVersion::default(), cx);
4240            command_palette::init(cx);
4241            language::init(cx);
4242            editor::init(cx);
4243            collab_ui::init(&app_state, cx);
4244            git_ui::init(cx);
4245            project_panel::init(cx);
4246            outline_panel::init(cx);
4247            terminal_view::init(cx);
4248            copilot::copilot_chat::init(
4249                app_state.fs.clone(),
4250                app_state.client.http_client().clone(),
4251                cx,
4252            );
4253            image_viewer::init(cx);
4254            language_model::init(app_state.client.clone(), cx);
4255            language_models::init(
4256                app_state.user_store.clone(),
4257                app_state.client.clone(),
4258                app_state.fs.clone(),
4259                cx,
4260            );
4261            let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx);
4262            assistant::init(
4263                app_state.fs.clone(),
4264                app_state.client.clone(),
4265                prompt_builder.clone(),
4266                cx,
4267            );
4268            repl::init(app_state.fs.clone(), cx);
4269            repl::notebook::init(cx);
4270            tasks_ui::init(cx);
4271            project::debugger::breakpoint_store::BreakpointStore::init(
4272                &app_state.client.clone().into(),
4273            );
4274            project::debugger::dap_store::DapStore::init(&app_state.client.clone().into());
4275            debugger_ui::init(cx);
4276            initialize_workspace(app_state.clone(), prompt_builder, cx);
4277            search::init(cx);
4278            app_state
4279        })
4280    }
4281
4282    fn rust_lang() -> Arc<language::Language> {
4283        Arc::new(language::Language::new(
4284            language::LanguageConfig {
4285                name: "Rust".into(),
4286                matcher: LanguageMatcher {
4287                    path_suffixes: vec!["rs".to_string()],
4288                    ..Default::default()
4289                },
4290                ..Default::default()
4291            },
4292            Some(tree_sitter_rust::LANGUAGE.into()),
4293        ))
4294    }
4295
4296    fn markdown_language() -> Arc<language::Language> {
4297        Arc::new(language::Language::new(
4298            language::LanguageConfig {
4299                name: "Markdown".into(),
4300                matcher: LanguageMatcher {
4301                    path_suffixes: vec!["md".to_string()],
4302                    ..Default::default()
4303                },
4304                ..Default::default()
4305            },
4306            Some(tree_sitter_md::LANGUAGE.into()),
4307        ))
4308    }
4309
4310    #[track_caller]
4311    fn assert_key_bindings_for(
4312        window: AnyWindowHandle,
4313        cx: &TestAppContext,
4314        actions: Vec<(&'static str, &dyn Action)>,
4315        line: u32,
4316    ) {
4317        let available_actions = cx
4318            .update(|cx| window.update(cx, |_, window, cx| window.available_actions(cx)))
4319            .unwrap();
4320        for (key, action) in actions {
4321            let bindings = cx
4322                .update(|cx| window.update(cx, |_, window, _| window.bindings_for_action(action)))
4323                .unwrap();
4324            // assert that...
4325            assert!(
4326                available_actions.iter().any(|bound_action| {
4327                    // actions match...
4328                    bound_action.partial_eq(action)
4329                }),
4330                "On {} Failed to find {}",
4331                line,
4332                action.name(),
4333            );
4334            assert!(
4335                // and key strokes contain the given key
4336                bindings
4337                    .into_iter()
4338                    .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)),
4339                "On {} Failed to find {} with key binding {}",
4340                line,
4341                action.name(),
4342                key
4343            );
4344        }
4345    }
4346}