zed.rs

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