zed.rs

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