zed.rs

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