zed.rs

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