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