zed.rs

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