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