zed.rs

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