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