zed.rs

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