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