zed.rs

   1pub mod assets;
   2pub mod languages;
   3pub mod menus;
   4pub mod only_instance;
   5#[cfg(any(test, feature = "test-support"))]
   6pub mod test;
   7
   8use ai::AssistantPanel;
   9use anyhow::Context;
  10use assets::Assets;
  11use breadcrumbs::Breadcrumbs;
  12pub use client;
  13use collab_ui::CollabTitlebarItem; // TODO: Add back toggle collab ui shortcut
  14use collections::VecDeque;
  15pub use editor;
  16use editor::{Editor, MultiBuffer};
  17
  18use anyhow::anyhow;
  19use feedback::{
  20    feedback_info_text::FeedbackInfoText, submit_feedback_button::SubmitFeedbackButton,
  21};
  22use futures::{channel::mpsc, StreamExt};
  23use gpui::{
  24    anyhow::{self, Result},
  25    geometry::vector::vec2f,
  26    impl_actions,
  27    platform::{Platform, PromptLevel, TitlebarOptions, WindowBounds, WindowKind, WindowOptions},
  28    AppContext, AsyncAppContext, Task, ViewContext, WeakViewHandle,
  29};
  30pub use lsp;
  31pub use project;
  32use project_panel::ProjectPanel;
  33use quick_action_bar::QuickActionBar;
  34use search::{BufferSearchBar, ProjectSearchBar};
  35use serde::Deserialize;
  36use serde_json::to_string_pretty;
  37use settings::{initial_local_settings_content, KeymapFile, SettingsStore};
  38use std::{borrow::Cow, str, sync::Arc};
  39use terminal_view::terminal_panel::{self, TerminalPanel};
  40use util::{
  41    asset_str,
  42    channel::ReleaseChannel,
  43    paths::{self, LOCAL_SETTINGS_RELATIVE_PATH},
  44    ResultExt,
  45};
  46use uuid::Uuid;
  47use welcome::BaseKeymap;
  48pub use workspace;
  49use workspace::{
  50    create_and_open_local_file, dock::PanelHandle,
  51    notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile,
  52    NewWindow, Workspace, WorkspaceSettings,
  53};
  54use zed_actions::*;
  55
  56#[derive(Deserialize, Clone, PartialEq)]
  57pub struct OpenBrowser {
  58    url: Arc<str>,
  59}
  60
  61impl_actions!(zed, [OpenBrowser]);
  62
  63pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
  64    cx.add_action(about);
  65    cx.add_global_action(|_: &Hide, cx: &mut gpui::AppContext| {
  66        cx.platform().hide();
  67    });
  68    cx.add_global_action(|_: &HideOthers, cx: &mut gpui::AppContext| {
  69        cx.platform().hide_other_apps();
  70    });
  71    cx.add_global_action(|_: &ShowAll, cx: &mut gpui::AppContext| {
  72        cx.platform().unhide_other_apps();
  73    });
  74    cx.add_action(
  75        |_: &mut Workspace, _: &Minimize, cx: &mut ViewContext<Workspace>| {
  76            cx.minimize_window();
  77        },
  78    );
  79    cx.add_action(
  80        |_: &mut Workspace, _: &Zoom, cx: &mut ViewContext<Workspace>| {
  81            cx.zoom_window();
  82        },
  83    );
  84    cx.add_action(
  85        |_: &mut Workspace, _: &ToggleFullScreen, cx: &mut ViewContext<Workspace>| {
  86            cx.toggle_full_screen();
  87        },
  88    );
  89    cx.add_global_action(quit);
  90    cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
  91    cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
  92        theme::adjust_font_size(cx, |size| *size += 1.0)
  93    });
  94    cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| {
  95        theme::adjust_font_size(cx, |size| *size -= 1.0)
  96    });
  97    cx.add_global_action(move |_: &ResetBufferFontSize, cx| theme::reset_font_size(cx));
  98    cx.add_global_action(move |_: &install_cli::Install, cx| {
  99        cx.spawn(|cx| async move {
 100            install_cli::install_cli(&cx)
 101                .await
 102                .context("error creating CLI symlink")
 103        })
 104        .detach_and_log_err(cx);
 105    });
 106    cx.add_action(
 107        move |workspace: &mut Workspace, _: &OpenLog, cx: &mut ViewContext<Workspace>| {
 108            open_log_file(workspace, cx);
 109        },
 110    );
 111    cx.add_action(
 112        move |workspace: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext<Workspace>| {
 113            open_bundled_file(
 114                workspace,
 115                asset_str::<Assets>("licenses.md"),
 116                "Open Source License Attribution",
 117                "Markdown",
 118                cx,
 119            );
 120        },
 121    );
 122    cx.add_action(
 123        move |workspace: &mut Workspace, _: &OpenTelemetryLog, cx: &mut ViewContext<Workspace>| {
 124            open_telemetry_log_file(workspace, cx);
 125        },
 126    );
 127    cx.add_action(
 128        move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
 129            create_and_open_local_file(&paths::KEYMAP, cx, Default::default).detach_and_log_err(cx);
 130        },
 131    );
 132    cx.add_action(
 133        move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
 134            create_and_open_local_file(&paths::SETTINGS, cx, || {
 135                settings::initial_user_settings_content().as_ref().into()
 136            })
 137            .detach_and_log_err(cx);
 138        },
 139    );
 140    cx.add_action(open_local_settings_file);
 141    cx.add_action(
 142        move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext<Workspace>| {
 143            open_bundled_file(
 144                workspace,
 145                settings::default_keymap(),
 146                "Default Key Bindings",
 147                "JSON",
 148                cx,
 149            );
 150        },
 151    );
 152    cx.add_action(
 153        move |workspace: &mut Workspace,
 154              _: &OpenDefaultSettings,
 155              cx: &mut ViewContext<Workspace>| {
 156            open_bundled_file(
 157                workspace,
 158                settings::default_settings(),
 159                "Default Settings",
 160                "JSON",
 161                cx,
 162            );
 163        },
 164    );
 165    cx.add_action({
 166        move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
 167            let app_state = workspace.app_state().clone();
 168            let markdown = app_state.languages.language_for_name("JSON");
 169            let window = cx.window();
 170            cx.spawn(|workspace, mut cx| async move {
 171                let markdown = markdown.await.log_err();
 172                let content = to_string_pretty(&window.debug_elements(&cx).ok_or_else(|| {
 173                    anyhow!("could not debug elements for window {}", window.id())
 174                })?)
 175                .unwrap();
 176                workspace
 177                    .update(&mut cx, |workspace, cx| {
 178                        workspace.with_local_workspace(cx, move |workspace, cx| {
 179                            let project = workspace.project().clone();
 180
 181                            let buffer = project
 182                                .update(cx, |project, cx| {
 183                                    project.create_buffer(&content, markdown, cx)
 184                                })
 185                                .expect("creating buffers on a local workspace always succeeds");
 186                            let buffer = cx.add_model(|cx| {
 187                                MultiBuffer::singleton(buffer, cx)
 188                                    .with_title("Debug Elements".into())
 189                            });
 190                            workspace.add_item(
 191                                Box::new(cx.add_view(|cx| {
 192                                    Editor::for_multibuffer(buffer, Some(project.clone()), cx)
 193                                })),
 194                                cx,
 195                            );
 196                        })
 197                    })?
 198                    .await
 199            })
 200            .detach_and_log_err(cx);
 201        }
 202    });
 203    cx.add_action(
 204        |workspace: &mut Workspace,
 205         _: &project_panel::ToggleFocus,
 206         cx: &mut ViewContext<Workspace>| {
 207            workspace.toggle_panel_focus::<ProjectPanel>(cx);
 208        },
 209    );
 210    cx.add_action(
 211        |workspace: &mut Workspace,
 212         _: &collab_ui::collab_panel::ToggleFocus,
 213         cx: &mut ViewContext<Workspace>| {
 214            workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(cx);
 215        },
 216    );
 217    cx.add_action(
 218        |workspace: &mut Workspace,
 219         _: &terminal_panel::ToggleFocus,
 220         cx: &mut ViewContext<Workspace>| {
 221            workspace.toggle_panel_focus::<TerminalPanel>(cx);
 222        },
 223    );
 224    cx.add_global_action({
 225        let app_state = Arc::downgrade(&app_state);
 226        move |_: &NewWindow, cx: &mut AppContext| {
 227            if let Some(app_state) = app_state.upgrade() {
 228                open_new(&app_state, cx, |workspace, cx| {
 229                    Editor::new_file(workspace, &Default::default(), cx)
 230                })
 231                .detach();
 232            }
 233        }
 234    });
 235    cx.add_global_action({
 236        let app_state = Arc::downgrade(&app_state);
 237        move |_: &NewFile, cx: &mut AppContext| {
 238            if let Some(app_state) = app_state.upgrade() {
 239                open_new(&app_state, cx, |workspace, cx| {
 240                    Editor::new_file(workspace, &Default::default(), cx)
 241                })
 242                .detach();
 243            }
 244        }
 245    });
 246    load_default_keymap(cx);
 247}
 248
 249pub fn initialize_workspace(
 250    workspace_handle: WeakViewHandle<Workspace>,
 251    was_deserialized: bool,
 252    app_state: Arc<AppState>,
 253    cx: AsyncAppContext,
 254) -> Task<Result<()>> {
 255    cx.spawn(|mut cx| async move {
 256        workspace_handle.update(&mut cx, |workspace, cx| {
 257            let workspace_handle = cx.handle();
 258            cx.subscribe(&workspace_handle, {
 259                move |workspace, _, event, cx| {
 260                    if let workspace::Event::PaneAdded(pane) = event {
 261                        pane.update(cx, |pane, cx| {
 262                            pane.toolbar().update(cx, |toolbar, cx| {
 263                                let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
 264                                toolbar.add_item(breadcrumbs, cx);
 265                                let buffer_search_bar = cx.add_view(BufferSearchBar::new);
 266                                toolbar.add_item(buffer_search_bar.clone(), cx);
 267                                let quick_action_bar = cx.add_view(|_| {
 268                                    QuickActionBar::new(buffer_search_bar, workspace)
 269                                });
 270                                toolbar.add_item(quick_action_bar, cx);
 271                                let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
 272                                toolbar.add_item(project_search_bar, cx);
 273                                let submit_feedback_button =
 274                                    cx.add_view(|_| SubmitFeedbackButton::new());
 275                                toolbar.add_item(submit_feedback_button, cx);
 276                                let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
 277                                toolbar.add_item(feedback_info_text, cx);
 278                                let lsp_log_item =
 279                                    cx.add_view(|_| language_tools::LspLogToolbarItemView::new());
 280                                toolbar.add_item(lsp_log_item, cx);
 281                                let syntax_tree_item = cx
 282                                    .add_view(|_| language_tools::SyntaxTreeToolbarItemView::new());
 283                                toolbar.add_item(syntax_tree_item, cx);
 284                            })
 285                        });
 286                    }
 287                }
 288            })
 289            .detach();
 290
 291            cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
 292
 293            let collab_titlebar_item =
 294                cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
 295            workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
 296
 297            let copilot =
 298                cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
 299            let diagnostic_summary =
 300                cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
 301            let activity_indicator = activity_indicator::ActivityIndicator::new(
 302                workspace,
 303                app_state.languages.clone(),
 304                cx,
 305            );
 306            let active_buffer_language =
 307                cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
 308            let vim_mode_indicator = cx.add_view(|cx| vim::ModeIndicator::new(cx));
 309            let feedback_button = cx.add_view(|_| {
 310                feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)
 311            });
 312            let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
 313            workspace.status_bar().update(cx, |status_bar, cx| {
 314                status_bar.add_left_item(diagnostic_summary, cx);
 315                status_bar.add_left_item(activity_indicator, cx);
 316
 317                status_bar.add_right_item(feedback_button, cx);
 318                status_bar.add_right_item(copilot, cx);
 319                status_bar.add_right_item(active_buffer_language, cx);
 320                status_bar.add_right_item(vim_mode_indicator, cx);
 321                status_bar.add_right_item(cursor_position, cx);
 322            });
 323
 324            auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
 325
 326            vim::observe_keystrokes(cx);
 327
 328            cx.on_window_should_close(|workspace, cx| {
 329                if let Some(task) = workspace.close(&Default::default(), cx) {
 330                    task.detach_and_log_err(cx);
 331                }
 332                false
 333            });
 334        })?;
 335
 336        let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
 337        let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
 338        let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
 339        let channels_panel =
 340            collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
 341        let (project_panel, terminal_panel, assistant_panel, channels_panel) = futures::try_join!(
 342            project_panel,
 343            terminal_panel,
 344            assistant_panel,
 345            channels_panel
 346        )?;
 347        workspace_handle.update(&mut cx, |workspace, cx| {
 348            let project_panel_position = project_panel.position(cx);
 349            workspace.add_panel_with_extra_event_handler(
 350                project_panel,
 351                cx,
 352                |workspace, _, event, cx| match event {
 353                    project_panel::Event::NewSearchInDirectory { dir_entry } => {
 354                        search::ProjectSearchView::new_search_in_directory(workspace, dir_entry, cx)
 355                    }
 356                    project_panel::Event::ActivatePanel => {
 357                        workspace.focus_panel::<ProjectPanel>(cx);
 358                    }
 359                    _ => {}
 360                },
 361            );
 362            workspace.add_panel(terminal_panel, cx);
 363            workspace.add_panel(assistant_panel, cx);
 364            workspace.add_panel(channels_panel, cx);
 365
 366            if !was_deserialized
 367                && workspace
 368                    .project()
 369                    .read(cx)
 370                    .visible_worktrees(cx)
 371                    .any(|tree| {
 372                        tree.read(cx)
 373                            .root_entry()
 374                            .map_or(false, |entry| entry.is_dir())
 375                    })
 376            {
 377                workspace.toggle_dock(project_panel_position, cx);
 378            }
 379            cx.focus_self();
 380        })?;
 381        Ok(())
 382    })
 383}
 384
 385pub fn build_window_options(
 386    bounds: Option<WindowBounds>,
 387    display: Option<Uuid>,
 388    platform: &dyn Platform,
 389) -> WindowOptions<'static> {
 390    let bounds = bounds.unwrap_or(WindowBounds::Maximized);
 391    let screen = display.and_then(|display| platform.screen_by_id(display));
 392
 393    WindowOptions {
 394        titlebar: Some(TitlebarOptions {
 395            title: None,
 396            appears_transparent: true,
 397            traffic_light_position: Some(vec2f(8., 8.)),
 398        }),
 399        center: false,
 400        focus: false,
 401        show: false,
 402        kind: WindowKind::Normal,
 403        is_movable: true,
 404        bounds,
 405        screen,
 406    }
 407}
 408
 409fn quit(_: &Quit, cx: &mut gpui::AppContext) {
 410    let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
 411    cx.spawn(|mut cx| async move {
 412        let mut workspace_windows = cx
 413            .windows()
 414            .into_iter()
 415            .filter_map(|window| window.downcast::<Workspace>())
 416            .collect::<Vec<_>>();
 417
 418        // If multiple windows have unsaved changes, and need a save prompt,
 419        // prompt in the active window before switching to a different window.
 420        workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
 421
 422        if let (true, Some(window)) = (should_confirm, workspace_windows.first().copied()) {
 423            let answer = window.prompt(
 424                PromptLevel::Info,
 425                "Are you sure you want to quit?",
 426                &["Quit", "Cancel"],
 427                &mut cx,
 428            );
 429
 430            if let Some(mut answer) = answer {
 431                let answer = answer.next().await;
 432                if answer != Some(0) {
 433                    return Ok(());
 434                }
 435            }
 436        }
 437
 438        // If the user cancels any save prompt, then keep the app open.
 439        for window in workspace_windows {
 440            if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| {
 441                workspace.prepare_to_close(true, cx)
 442            }) {
 443                if !should_close.await? {
 444                    return Ok(());
 445                }
 446            }
 447        }
 448        cx.platform().quit();
 449        anyhow::Ok(())
 450    })
 451    .detach_and_log_err(cx);
 452}
 453
 454fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
 455    let app_name = cx.global::<ReleaseChannel>().display_name();
 456    let version = env!("CARGO_PKG_VERSION");
 457    cx.prompt(PromptLevel::Info, &format!("{app_name} {version}"), &["OK"]);
 458}
 459
 460fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
 461    const MAX_LINES: usize = 1000;
 462
 463    workspace
 464        .with_local_workspace(cx, move |workspace, cx| {
 465            let fs = workspace.app_state().fs.clone();
 466            cx.spawn(|workspace, mut cx| async move {
 467                let (old_log, new_log) =
 468                    futures::join!(fs.load(&paths::OLD_LOG), fs.load(&paths::LOG));
 469
 470                let mut lines = VecDeque::with_capacity(MAX_LINES);
 471                for line in old_log
 472                    .iter()
 473                    .flat_map(|log| log.lines())
 474                    .chain(new_log.iter().flat_map(|log| log.lines()))
 475                {
 476                    if lines.len() == MAX_LINES {
 477                        lines.pop_front();
 478                    }
 479                    lines.push_back(line);
 480                }
 481                let log = lines
 482                    .into_iter()
 483                    .flat_map(|line| [line, "\n"])
 484                    .collect::<String>();
 485
 486                workspace
 487                    .update(&mut cx, |workspace, cx| {
 488                        let project = workspace.project().clone();
 489                        let buffer = project
 490                            .update(cx, |project, cx| project.create_buffer("", None, cx))
 491                            .expect("creating buffers on a local workspace always succeeds");
 492                        buffer.update(cx, |buffer, cx| buffer.edit([(0..0, log)], None, cx));
 493
 494                        let buffer = cx.add_model(|cx| {
 495                            MultiBuffer::singleton(buffer, cx).with_title("Log".into())
 496                        });
 497                        workspace.add_item(
 498                            Box::new(
 499                                cx.add_view(|cx| {
 500                                    Editor::for_multibuffer(buffer, Some(project), cx)
 501                                }),
 502                            ),
 503                            cx,
 504                        );
 505                    })
 506                    .log_err();
 507            })
 508            .detach();
 509        })
 510        .detach();
 511}
 512
 513pub fn load_default_keymap(cx: &mut AppContext) {
 514    for path in ["keymaps/default.json", "keymaps/vim.json"] {
 515        KeymapFile::load_asset(path, cx).unwrap();
 516    }
 517
 518    if let Some(asset_path) = settings::get::<BaseKeymap>(cx).asset_path() {
 519        KeymapFile::load_asset(asset_path, cx).unwrap();
 520    }
 521}
 522
 523pub fn handle_keymap_file_changes(
 524    mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
 525    cx: &mut AppContext,
 526) {
 527    cx.spawn(move |mut cx| async move {
 528        let mut settings_subscription = None;
 529        while let Some(user_keymap_content) = user_keymap_file_rx.next().await {
 530            if let Ok(keymap_content) = KeymapFile::parse(&user_keymap_content) {
 531                cx.update(|cx| reload_keymaps(cx, &keymap_content));
 532
 533                let mut old_base_keymap = cx.read(|cx| *settings::get::<BaseKeymap>(cx));
 534                drop(settings_subscription);
 535                settings_subscription = Some(cx.update(|cx| {
 536                    cx.observe_global::<SettingsStore, _>(move |cx| {
 537                        let new_base_keymap = *settings::get::<BaseKeymap>(cx);
 538                        if new_base_keymap != old_base_keymap {
 539                            old_base_keymap = new_base_keymap.clone();
 540                            reload_keymaps(cx, &keymap_content);
 541                        }
 542                    })
 543                }));
 544            }
 545        }
 546    })
 547    .detach();
 548}
 549
 550fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) {
 551    cx.clear_bindings();
 552    load_default_keymap(cx);
 553    keymap_content.clone().add_to_cx(cx).log_err();
 554    cx.set_menus(menus::menus());
 555}
 556
 557fn open_local_settings_file(
 558    workspace: &mut Workspace,
 559    _: &OpenLocalSettings,
 560    cx: &mut ViewContext<Workspace>,
 561) {
 562    let project = workspace.project().clone();
 563    let worktree = project
 564        .read(cx)
 565        .visible_worktrees(cx)
 566        .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
 567    if let Some(worktree) = worktree {
 568        let tree_id = worktree.read(cx).id();
 569        cx.spawn(|workspace, mut cx| async move {
 570            let file_path = &*LOCAL_SETTINGS_RELATIVE_PATH;
 571
 572            if let Some(dir_path) = file_path.parent() {
 573                if worktree.read_with(&cx, |tree, _| tree.entry_for_path(dir_path).is_none()) {
 574                    project
 575                        .update(&mut cx, |project, cx| {
 576                            project.create_entry((tree_id, dir_path), true, cx)
 577                        })
 578                        .ok_or_else(|| anyhow!("worktree was removed"))?
 579                        .await?;
 580                }
 581            }
 582
 583            if worktree.read_with(&cx, |tree, _| tree.entry_for_path(file_path).is_none()) {
 584                project
 585                    .update(&mut cx, |project, cx| {
 586                        project.create_entry((tree_id, file_path), false, cx)
 587                    })
 588                    .ok_or_else(|| anyhow!("worktree was removed"))?
 589                    .await?;
 590            }
 591
 592            let editor = workspace
 593                .update(&mut cx, |workspace, cx| {
 594                    workspace.open_path((tree_id, file_path), None, true, cx)
 595                })?
 596                .await?
 597                .downcast::<Editor>()
 598                .ok_or_else(|| anyhow!("unexpected item type"))?;
 599
 600            editor
 601                .downgrade()
 602                .update(&mut cx, |editor, cx| {
 603                    if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
 604                        if buffer.read(cx).is_empty() {
 605                            buffer.update(cx, |buffer, cx| {
 606                                buffer.edit([(0..0, initial_local_settings_content())], None, cx)
 607                            });
 608                        }
 609                    }
 610                })
 611                .ok();
 612
 613            anyhow::Ok(())
 614        })
 615        .detach();
 616    } else {
 617        workspace.show_notification(0, cx, |cx| {
 618            cx.add_view(|_| MessageNotification::new("This project has no folders open."))
 619        })
 620    }
 621}
 622
 623fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
 624    workspace.with_local_workspace(cx, move |workspace, cx| {
 625        let app_state = workspace.app_state().clone();
 626        cx.spawn(|workspace, mut cx| async move {
 627            async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
 628                let path = app_state.client.telemetry().log_file_path()?;
 629                app_state.fs.load(&path).await.log_err()
 630            }
 631
 632            let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string());
 633
 634            const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
 635            let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
 636            if let Some(newline_offset) = log[start_offset..].find('\n') {
 637                start_offset += newline_offset + 1;
 638            }
 639            let log_suffix = &log[start_offset..];
 640            let json = app_state.languages.language_for_name("JSON").await.log_err();
 641
 642            workspace.update(&mut cx, |workspace, cx| {
 643                let project = workspace.project().clone();
 644                let buffer = project
 645                    .update(cx, |project, cx| project.create_buffer("", None, cx))
 646                    .expect("creating buffers on a local workspace always succeeds");
 647                buffer.update(cx, |buffer, cx| {
 648                    buffer.set_language(json, cx);
 649                    buffer.edit(
 650                        [(
 651                            0..0,
 652                            concat!(
 653                                "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
 654                                "// Telemetry can be disabled via the `settings.json` file.\n",
 655                                "// Here is the data that has been reported for the current session:\n",
 656                                "\n"
 657                            ),
 658                        )],
 659                        None,
 660                        cx,
 661                    );
 662                    buffer.edit([(buffer.len()..buffer.len(), log_suffix)], None, cx);
 663                });
 664
 665                let buffer = cx.add_model(|cx| {
 666                    MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
 667                });
 668                workspace.add_item(
 669                    Box::new(cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
 670                    cx,
 671                );
 672            }).log_err()?;
 673
 674            Some(())
 675        })
 676        .detach();
 677    }).detach();
 678}
 679
 680fn open_bundled_file(
 681    workspace: &mut Workspace,
 682    text: Cow<'static, str>,
 683    title: &'static str,
 684    language: &'static str,
 685    cx: &mut ViewContext<Workspace>,
 686) {
 687    let language = workspace.app_state().languages.language_for_name(language);
 688    cx.spawn(|workspace, mut cx| async move {
 689        let language = language.await.log_err();
 690        workspace
 691            .update(&mut cx, |workspace, cx| {
 692                workspace.with_local_workspace(cx, |workspace, cx| {
 693                    let project = workspace.project();
 694                    let buffer = project.update(cx, move |project, cx| {
 695                        project
 696                            .create_buffer(text.as_ref(), language, cx)
 697                            .expect("creating buffers on a local workspace always succeeds")
 698                    });
 699                    let buffer = cx.add_model(|cx| {
 700                        MultiBuffer::singleton(buffer, cx).with_title(title.into())
 701                    });
 702                    workspace.add_item(
 703                        Box::new(cx.add_view(|cx| {
 704                            Editor::for_multibuffer(buffer, Some(project.clone()), cx)
 705                        })),
 706                        cx,
 707                    );
 708                })
 709            })?
 710            .await
 711    })
 712    .detach_and_log_err(cx);
 713}
 714
 715#[cfg(test)]
 716mod tests {
 717    use super::*;
 718    use assets::Assets;
 719    use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
 720    use fs::{FakeFs, Fs};
 721    use gpui::{
 722        actions, elements::Empty, executor::Deterministic, Action, AnyElement, AnyWindowHandle,
 723        AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle,
 724    };
 725    use language::LanguageRegistry;
 726    use project::{Project, ProjectPath};
 727    use serde_json::json;
 728    use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
 729    use std::{
 730        collections::HashSet,
 731        path::{Path, PathBuf},
 732    };
 733    use theme::{ThemeRegistry, ThemeSettings};
 734    use workspace::{
 735        item::{Item, ItemHandle},
 736        open_new, open_paths, pane, NewFile, SaveBehavior, SplitDirection, WorkspaceHandle,
 737    };
 738
 739    #[gpui::test]
 740    async fn test_open_paths_action(cx: &mut TestAppContext) {
 741        let app_state = init_test(cx);
 742        app_state
 743            .fs
 744            .as_fake()
 745            .insert_tree(
 746                "/root",
 747                json!({
 748                    "a": {
 749                        "aa": null,
 750                        "ab": null,
 751                    },
 752                    "b": {
 753                        "ba": null,
 754                        "bb": null,
 755                    },
 756                    "c": {
 757                        "ca": null,
 758                        "cb": null,
 759                    },
 760                    "d": {
 761                        "da": null,
 762                        "db": null,
 763                    },
 764                }),
 765            )
 766            .await;
 767
 768        cx.update(|cx| {
 769            open_paths(
 770                &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
 771                &app_state,
 772                None,
 773                cx,
 774            )
 775        })
 776        .await
 777        .unwrap();
 778        assert_eq!(cx.windows().len(), 1);
 779
 780        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
 781            .await
 782            .unwrap();
 783        assert_eq!(cx.windows().len(), 1);
 784        let workspace_1 = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
 785        workspace_1.update(cx, |workspace, cx| {
 786            assert_eq!(workspace.worktrees(cx).count(), 2);
 787            assert!(workspace.left_dock().read(cx).is_open());
 788            assert!(workspace.active_pane().is_focused(cx));
 789        });
 790
 791        cx.update(|cx| {
 792            open_paths(
 793                &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
 794                &app_state,
 795                None,
 796                cx,
 797            )
 798        })
 799        .await
 800        .unwrap();
 801        assert_eq!(cx.windows().len(), 2);
 802
 803        // Replace existing windows
 804        let window = cx.windows()[0].downcast::<Workspace>().unwrap();
 805        cx.update(|cx| {
 806            open_paths(
 807                &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
 808                &app_state,
 809                Some(window),
 810                cx,
 811            )
 812        })
 813        .await
 814        .unwrap();
 815        assert_eq!(cx.windows().len(), 2);
 816        let workspace_1 = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
 817        workspace_1.update(cx, |workspace, cx| {
 818            assert_eq!(
 819                workspace
 820                    .worktrees(cx)
 821                    .map(|w| w.read(cx).abs_path())
 822                    .collect::<Vec<_>>(),
 823                &[Path::new("/root/c").into(), Path::new("/root/d").into()]
 824            );
 825            assert!(workspace.left_dock().read(cx).is_open());
 826            assert!(workspace.active_pane().is_focused(cx));
 827        });
 828    }
 829
 830    #[gpui::test]
 831    async fn test_window_edit_state(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
 832        let app_state = init_test(cx);
 833        app_state
 834            .fs
 835            .as_fake()
 836            .insert_tree("/root", json!({"a": "hey"}))
 837            .await;
 838
 839        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
 840            .await
 841            .unwrap();
 842        assert_eq!(cx.windows().len(), 1);
 843
 844        // When opening the workspace, the window is not in a edited state.
 845        let window = cx.windows()[0].downcast::<Workspace>().unwrap();
 846        let workspace = window.root(cx);
 847        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 848        let editor = workspace.read_with(cx, |workspace, cx| {
 849            workspace
 850                .active_item(cx)
 851                .unwrap()
 852                .downcast::<Editor>()
 853                .unwrap()
 854        });
 855        assert!(!window.is_edited(cx));
 856
 857        // Editing a buffer marks the window as edited.
 858        editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
 859        assert!(window.is_edited(cx));
 860
 861        // Undoing the edit restores the window's edited state.
 862        editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
 863        assert!(!window.is_edited(cx));
 864
 865        // Redoing the edit marks the window as edited again.
 866        editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
 867        assert!(window.is_edited(cx));
 868
 869        // Closing the item restores the window's edited state.
 870        let close = pane.update(cx, |pane, cx| {
 871            drop(editor);
 872            pane.close_active_item(&Default::default(), cx).unwrap()
 873        });
 874        executor.run_until_parked();
 875
 876        window.simulate_prompt_answer(1, cx);
 877        close.await.unwrap();
 878        assert!(!window.is_edited(cx));
 879
 880        // Opening the buffer again doesn't impact the window's edited state.
 881        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
 882            .await
 883            .unwrap();
 884        let editor = workspace.read_with(cx, |workspace, cx| {
 885            workspace
 886                .active_item(cx)
 887                .unwrap()
 888                .downcast::<Editor>()
 889                .unwrap()
 890        });
 891        assert!(!window.is_edited(cx));
 892
 893        // Editing the buffer marks the window as edited.
 894        editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
 895        assert!(window.is_edited(cx));
 896
 897        // Ensure closing the window via the mouse gets preempted due to the
 898        // buffer having unsaved changes.
 899        assert!(!window.simulate_close(cx));
 900        executor.run_until_parked();
 901        assert_eq!(cx.windows().len(), 1);
 902
 903        // The window is successfully closed after the user dismisses the prompt.
 904        window.simulate_prompt_answer(1, cx);
 905        executor.run_until_parked();
 906        assert_eq!(cx.windows().len(), 0);
 907    }
 908
 909    #[gpui::test]
 910    async fn test_new_empty_workspace(cx: &mut TestAppContext) {
 911        let app_state = init_test(cx);
 912        cx.update(|cx| {
 913            open_new(&app_state, cx, |workspace, cx| {
 914                Editor::new_file(workspace, &Default::default(), cx)
 915            })
 916        })
 917        .await;
 918
 919        let window = cx
 920            .windows()
 921            .first()
 922            .unwrap()
 923            .downcast::<Workspace>()
 924            .unwrap();
 925        let workspace = window.root(cx);
 926
 927        let editor = workspace.update(cx, |workspace, cx| {
 928            workspace
 929                .active_item(cx)
 930                .unwrap()
 931                .downcast::<editor::Editor>()
 932                .unwrap()
 933        });
 934
 935        editor.update(cx, |editor, cx| {
 936            assert!(editor.text(cx).is_empty());
 937        });
 938
 939        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
 940        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
 941        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
 942        save_task.await.unwrap();
 943        editor.read_with(cx, |editor, cx| {
 944            assert!(!editor.is_dirty(cx));
 945            assert_eq!(editor.title(cx), "the-new-name");
 946        });
 947    }
 948
 949    #[gpui::test]
 950    async fn test_open_entry(cx: &mut TestAppContext) {
 951        let app_state = init_test(cx);
 952        app_state
 953            .fs
 954            .as_fake()
 955            .insert_tree(
 956                "/root",
 957                json!({
 958                    "a": {
 959                        "file1": "contents 1",
 960                        "file2": "contents 2",
 961                        "file3": "contents 3",
 962                    },
 963                }),
 964            )
 965            .await;
 966
 967        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
 968        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
 969        let workspace = window.root(cx);
 970
 971        let entries = cx.read(|cx| workspace.file_project_paths(cx));
 972        let file1 = entries[0].clone();
 973        let file2 = entries[1].clone();
 974        let file3 = entries[2].clone();
 975
 976        // Open the first entry
 977        let entry_1 = workspace
 978            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
 979            .await
 980            .unwrap();
 981        cx.read(|cx| {
 982            let pane = workspace.read(cx).active_pane().read(cx);
 983            assert_eq!(
 984                pane.active_item().unwrap().project_path(cx),
 985                Some(file1.clone())
 986            );
 987            assert_eq!(pane.items_len(), 1);
 988        });
 989
 990        // Open the second entry
 991        workspace
 992            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
 993            .await
 994            .unwrap();
 995        cx.read(|cx| {
 996            let pane = workspace.read(cx).active_pane().read(cx);
 997            assert_eq!(
 998                pane.active_item().unwrap().project_path(cx),
 999                Some(file2.clone())
1000            );
1001            assert_eq!(pane.items_len(), 2);
1002        });
1003
1004        // Open the first entry again. The existing pane item is activated.
1005        let entry_1b = workspace
1006            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1007            .await
1008            .unwrap();
1009        assert_eq!(entry_1.id(), entry_1b.id());
1010
1011        cx.read(|cx| {
1012            let pane = workspace.read(cx).active_pane().read(cx);
1013            assert_eq!(
1014                pane.active_item().unwrap().project_path(cx),
1015                Some(file1.clone())
1016            );
1017            assert_eq!(pane.items_len(), 2);
1018        });
1019
1020        // Split the pane with the first entry, then open the second entry again.
1021        workspace
1022            .update(cx, |w, cx| {
1023                w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx);
1024                w.open_path(file2.clone(), None, true, cx)
1025            })
1026            .await
1027            .unwrap();
1028
1029        workspace.read_with(cx, |w, cx| {
1030            assert_eq!(
1031                w.active_pane()
1032                    .read(cx)
1033                    .active_item()
1034                    .unwrap()
1035                    .project_path(cx),
1036                Some(file2.clone())
1037            );
1038        });
1039
1040        // Open the third entry twice concurrently. Only one pane item is added.
1041        let (t1, t2) = workspace.update(cx, |w, cx| {
1042            (
1043                w.open_path(file3.clone(), None, true, cx),
1044                w.open_path(file3.clone(), None, true, cx),
1045            )
1046        });
1047        t1.await.unwrap();
1048        t2.await.unwrap();
1049        cx.read(|cx| {
1050            let pane = workspace.read(cx).active_pane().read(cx);
1051            assert_eq!(
1052                pane.active_item().unwrap().project_path(cx),
1053                Some(file3.clone())
1054            );
1055            let pane_entries = pane
1056                .items()
1057                .map(|i| i.project_path(cx).unwrap())
1058                .collect::<Vec<_>>();
1059            assert_eq!(pane_entries, &[file1, file2, file3]);
1060        });
1061    }
1062
1063    #[gpui::test]
1064    async fn test_open_paths(cx: &mut TestAppContext) {
1065        let app_state = init_test(cx);
1066
1067        app_state
1068            .fs
1069            .as_fake()
1070            .insert_tree(
1071                "/",
1072                json!({
1073                    "dir1": {
1074                        "a.txt": ""
1075                    },
1076                    "dir2": {
1077                        "b.txt": ""
1078                    },
1079                    "dir3": {
1080                        "c.txt": ""
1081                    },
1082                    "d.txt": ""
1083                }),
1084            )
1085            .await;
1086
1087        cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx))
1088            .await
1089            .unwrap();
1090        assert_eq!(cx.windows().len(), 1);
1091        let workspace = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
1092
1093        #[track_caller]
1094        fn assert_project_panel_selection(
1095            workspace: &Workspace,
1096            expected_worktree_path: &Path,
1097            expected_entry_path: &Path,
1098            cx: &AppContext,
1099        ) {
1100            let project_panel = [
1101                workspace.left_dock().read(cx).panel::<ProjectPanel>(),
1102                workspace.right_dock().read(cx).panel::<ProjectPanel>(),
1103                workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
1104            ]
1105            .into_iter()
1106            .find_map(std::convert::identity)
1107            .expect("found no project panels")
1108            .read(cx);
1109            let (selected_worktree, selected_entry) = project_panel
1110                .selected_entry(cx)
1111                .expect("project panel should have a selected entry");
1112            assert_eq!(
1113                selected_worktree.abs_path().as_ref(),
1114                expected_worktree_path,
1115                "Unexpected project panel selected worktree path"
1116            );
1117            assert_eq!(
1118                selected_entry.path.as_ref(),
1119                expected_entry_path,
1120                "Unexpected project panel selected entry path"
1121            );
1122        }
1123
1124        // Open a file within an existing worktree.
1125        workspace
1126            .update(cx, |view, cx| {
1127                view.open_paths(vec!["/dir1/a.txt".into()], true, cx)
1128            })
1129            .await;
1130        cx.read(|cx| {
1131            let workspace = workspace.read(cx);
1132            assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx);
1133            assert_eq!(
1134                workspace
1135                    .active_pane()
1136                    .read(cx)
1137                    .active_item()
1138                    .unwrap()
1139                    .as_any()
1140                    .downcast_ref::<Editor>()
1141                    .unwrap()
1142                    .read(cx)
1143                    .title(cx),
1144                "a.txt"
1145            );
1146        });
1147
1148        // Open a file outside of any existing worktree.
1149        workspace
1150            .update(cx, |view, cx| {
1151                view.open_paths(vec!["/dir2/b.txt".into()], true, cx)
1152            })
1153            .await;
1154        cx.read(|cx| {
1155            let workspace = workspace.read(cx);
1156            assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx);
1157            let worktree_roots = workspace
1158                .worktrees(cx)
1159                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1160                .collect::<HashSet<_>>();
1161            assert_eq!(
1162                worktree_roots,
1163                vec!["/dir1", "/dir2/b.txt"]
1164                    .into_iter()
1165                    .map(Path::new)
1166                    .collect(),
1167            );
1168            assert_eq!(
1169                workspace
1170                    .active_pane()
1171                    .read(cx)
1172                    .active_item()
1173                    .unwrap()
1174                    .as_any()
1175                    .downcast_ref::<Editor>()
1176                    .unwrap()
1177                    .read(cx)
1178                    .title(cx),
1179                "b.txt"
1180            );
1181        });
1182
1183        // Ensure opening a directory and one of its children only adds one worktree.
1184        workspace
1185            .update(cx, |view, cx| {
1186                view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], true, cx)
1187            })
1188            .await;
1189        cx.read(|cx| {
1190            let workspace = workspace.read(cx);
1191            assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx);
1192            let worktree_roots = workspace
1193                .worktrees(cx)
1194                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1195                .collect::<HashSet<_>>();
1196            assert_eq!(
1197                worktree_roots,
1198                vec!["/dir1", "/dir2/b.txt", "/dir3"]
1199                    .into_iter()
1200                    .map(Path::new)
1201                    .collect(),
1202            );
1203            assert_eq!(
1204                workspace
1205                    .active_pane()
1206                    .read(cx)
1207                    .active_item()
1208                    .unwrap()
1209                    .as_any()
1210                    .downcast_ref::<Editor>()
1211                    .unwrap()
1212                    .read(cx)
1213                    .title(cx),
1214                "c.txt"
1215            );
1216        });
1217
1218        // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
1219        workspace
1220            .update(cx, |view, cx| {
1221                view.open_paths(vec!["/d.txt".into()], false, cx)
1222            })
1223            .await;
1224        cx.read(|cx| {
1225            let workspace = workspace.read(cx);
1226            assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx);
1227            let worktree_roots = workspace
1228                .worktrees(cx)
1229                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1230                .collect::<HashSet<_>>();
1231            assert_eq!(
1232                worktree_roots,
1233                vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
1234                    .into_iter()
1235                    .map(Path::new)
1236                    .collect(),
1237            );
1238
1239            let visible_worktree_roots = workspace
1240                .visible_worktrees(cx)
1241                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1242                .collect::<HashSet<_>>();
1243            assert_eq!(
1244                visible_worktree_roots,
1245                vec!["/dir1", "/dir2/b.txt", "/dir3"]
1246                    .into_iter()
1247                    .map(Path::new)
1248                    .collect(),
1249            );
1250
1251            assert_eq!(
1252                workspace
1253                    .active_pane()
1254                    .read(cx)
1255                    .active_item()
1256                    .unwrap()
1257                    .as_any()
1258                    .downcast_ref::<Editor>()
1259                    .unwrap()
1260                    .read(cx)
1261                    .title(cx),
1262                "d.txt"
1263            );
1264        });
1265    }
1266
1267    #[gpui::test]
1268    async fn test_save_conflicting_item(cx: &mut TestAppContext) {
1269        let app_state = init_test(cx);
1270        app_state
1271            .fs
1272            .as_fake()
1273            .insert_tree("/root", json!({ "a.txt": "" }))
1274            .await;
1275
1276        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1277        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
1278        let workspace = window.root(cx);
1279
1280        // Open a file within an existing worktree.
1281        workspace
1282            .update(cx, |view, cx| {
1283                view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx)
1284            })
1285            .await;
1286        let editor = cx.read(|cx| {
1287            let pane = workspace.read(cx).active_pane().read(cx);
1288            let item = pane.active_item().unwrap();
1289            item.downcast::<Editor>().unwrap()
1290        });
1291
1292        editor.update(cx, |editor, cx| editor.handle_input("x", cx));
1293        app_state
1294            .fs
1295            .as_fake()
1296            .insert_file("/root/a.txt", "changed".to_string())
1297            .await;
1298        editor
1299            .condition(cx, |editor, cx| editor.has_conflict(cx))
1300            .await;
1301        cx.read(|cx| assert!(editor.is_dirty(cx)));
1302
1303        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1304        window.simulate_prompt_answer(0, cx);
1305        save_task.await.unwrap();
1306        editor.read_with(cx, |editor, cx| {
1307            assert!(!editor.is_dirty(cx));
1308            assert!(!editor.has_conflict(cx));
1309        });
1310    }
1311
1312    #[gpui::test]
1313    async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
1314        let app_state = init_test(cx);
1315        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1316
1317        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1318        project.update(cx, |project, _| project.languages().add(rust_lang()));
1319        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
1320        let workspace = window.root(cx);
1321        let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
1322
1323        // Create a new untitled buffer
1324        cx.dispatch_action(window.into(), NewFile);
1325        let editor = workspace.read_with(cx, |workspace, cx| {
1326            workspace
1327                .active_item(cx)
1328                .unwrap()
1329                .downcast::<Editor>()
1330                .unwrap()
1331        });
1332
1333        editor.update(cx, |editor, cx| {
1334            assert!(!editor.is_dirty(cx));
1335            assert_eq!(editor.title(cx), "untitled");
1336            assert!(Arc::ptr_eq(
1337                &editor.language_at(0, cx).unwrap(),
1338                &languages::PLAIN_TEXT
1339            ));
1340            editor.handle_input("hi", cx);
1341            assert!(editor.is_dirty(cx));
1342        });
1343
1344        // Save the buffer. This prompts for a filename.
1345        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1346        cx.simulate_new_path_selection(|parent_dir| {
1347            assert_eq!(parent_dir, Path::new("/root"));
1348            Some(parent_dir.join("the-new-name.rs"))
1349        });
1350        cx.read(|cx| {
1351            assert!(editor.is_dirty(cx));
1352            assert_eq!(editor.read(cx).title(cx), "untitled");
1353        });
1354
1355        // When the save completes, the buffer's title is updated and the language is assigned based
1356        // on the path.
1357        save_task.await.unwrap();
1358        editor.read_with(cx, |editor, cx| {
1359            assert!(!editor.is_dirty(cx));
1360            assert_eq!(editor.title(cx), "the-new-name.rs");
1361            assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
1362        });
1363
1364        // Edit the file and save it again. This time, there is no filename prompt.
1365        editor.update(cx, |editor, cx| {
1366            editor.handle_input(" there", cx);
1367            assert!(editor.is_dirty(cx));
1368        });
1369        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1370        save_task.await.unwrap();
1371        assert!(!cx.did_prompt_for_new_path());
1372        editor.read_with(cx, |editor, cx| {
1373            assert!(!editor.is_dirty(cx));
1374            assert_eq!(editor.title(cx), "the-new-name.rs")
1375        });
1376
1377        // Open the same newly-created file in another pane item. The new editor should reuse
1378        // the same buffer.
1379        cx.dispatch_action(window.into(), NewFile);
1380        workspace
1381            .update(cx, |workspace, cx| {
1382                workspace.split_and_clone(
1383                    workspace.active_pane().clone(),
1384                    SplitDirection::Right,
1385                    cx,
1386                );
1387                workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
1388            })
1389            .await
1390            .unwrap();
1391        let editor2 = workspace.update(cx, |workspace, cx| {
1392            workspace
1393                .active_item(cx)
1394                .unwrap()
1395                .downcast::<Editor>()
1396                .unwrap()
1397        });
1398        cx.read(|cx| {
1399            assert_eq!(
1400                editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
1401                editor.read(cx).buffer().read(cx).as_singleton().unwrap()
1402            );
1403        })
1404    }
1405
1406    #[gpui::test]
1407    async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
1408        let app_state = init_test(cx);
1409        app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1410
1411        let project = Project::test(app_state.fs.clone(), [], cx).await;
1412        project.update(cx, |project, _| project.languages().add(rust_lang()));
1413        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
1414        let workspace = window.root(cx);
1415
1416        // Create a new untitled buffer
1417        cx.dispatch_action(window.into(), NewFile);
1418        let editor = workspace.read_with(cx, |workspace, cx| {
1419            workspace
1420                .active_item(cx)
1421                .unwrap()
1422                .downcast::<Editor>()
1423                .unwrap()
1424        });
1425
1426        editor.update(cx, |editor, cx| {
1427            assert!(Arc::ptr_eq(
1428                &editor.language_at(0, cx).unwrap(),
1429                &languages::PLAIN_TEXT
1430            ));
1431            editor.handle_input("hi", cx);
1432            assert!(editor.is_dirty(cx));
1433        });
1434
1435        // Save the buffer. This prompts for a filename.
1436        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1437        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
1438        save_task.await.unwrap();
1439        // The buffer is not dirty anymore and the language is assigned based on the path.
1440        editor.read_with(cx, |editor, cx| {
1441            assert!(!editor.is_dirty(cx));
1442            assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
1443        });
1444    }
1445
1446    #[gpui::test]
1447    async fn test_pane_actions(cx: &mut TestAppContext) {
1448        let app_state = init_test(cx);
1449        app_state
1450            .fs
1451            .as_fake()
1452            .insert_tree(
1453                "/root",
1454                json!({
1455                    "a": {
1456                        "file1": "contents 1",
1457                        "file2": "contents 2",
1458                        "file3": "contents 3",
1459                    },
1460                }),
1461            )
1462            .await;
1463
1464        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1465        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
1466        let workspace = window.root(cx);
1467
1468        let entries = cx.read(|cx| workspace.file_project_paths(cx));
1469        let file1 = entries[0].clone();
1470
1471        let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
1472
1473        workspace
1474            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1475            .await
1476            .unwrap();
1477
1478        let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| {
1479            let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
1480            assert_eq!(editor.project_path(cx), Some(file1.clone()));
1481            let buffer = editor.update(cx, |editor, cx| {
1482                editor.insert("dirt", cx);
1483                editor.buffer().downgrade()
1484            });
1485            (editor.downgrade(), buffer)
1486        });
1487
1488        cx.dispatch_action(window.into(), pane::SplitRight);
1489        let editor_2 = cx.update(|cx| {
1490            let pane_2 = workspace.read(cx).active_pane().clone();
1491            assert_ne!(pane_1, pane_2);
1492
1493            let pane2_item = pane_2.read(cx).active_item().unwrap();
1494            assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
1495
1496            pane2_item.downcast::<Editor>().unwrap().downgrade()
1497        });
1498        cx.dispatch_action(
1499            window.into(),
1500            workspace::CloseActiveItem {
1501                save_behavior: None,
1502            },
1503        );
1504
1505        cx.foreground().run_until_parked();
1506        workspace.read_with(cx, |workspace, _| {
1507            assert_eq!(workspace.panes().len(), 1);
1508            assert_eq!(workspace.active_pane(), &pane_1);
1509        });
1510
1511        cx.dispatch_action(
1512            window.into(),
1513            workspace::CloseActiveItem {
1514                save_behavior: None,
1515            },
1516        );
1517        cx.foreground().run_until_parked();
1518        window.simulate_prompt_answer(1, cx);
1519        cx.foreground().run_until_parked();
1520
1521        workspace.read_with(cx, |workspace, cx| {
1522            assert_eq!(workspace.panes().len(), 1);
1523            assert!(workspace.active_item(cx).is_none());
1524        });
1525
1526        cx.assert_dropped(editor_1);
1527        cx.assert_dropped(editor_2);
1528        cx.assert_dropped(buffer);
1529    }
1530
1531    #[gpui::test]
1532    async fn test_navigation(cx: &mut TestAppContext) {
1533        let app_state = init_test(cx);
1534        app_state
1535            .fs
1536            .as_fake()
1537            .insert_tree(
1538                "/root",
1539                json!({
1540                    "a": {
1541                        "file1": "contents 1\n".repeat(20),
1542                        "file2": "contents 2\n".repeat(20),
1543                        "file3": "contents 3\n".repeat(20),
1544                    },
1545                }),
1546            )
1547            .await;
1548
1549        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1550        let workspace = cx
1551            .add_window(|cx| Workspace::test_new(project.clone(), cx))
1552            .root(cx);
1553        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1554
1555        let entries = cx.read(|cx| workspace.file_project_paths(cx));
1556        let file1 = entries[0].clone();
1557        let file2 = entries[1].clone();
1558        let file3 = entries[2].clone();
1559
1560        let editor1 = workspace
1561            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1562            .await
1563            .unwrap()
1564            .downcast::<Editor>()
1565            .unwrap();
1566        editor1.update(cx, |editor, cx| {
1567            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
1568                s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)])
1569            });
1570        });
1571        let editor2 = workspace
1572            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
1573            .await
1574            .unwrap()
1575            .downcast::<Editor>()
1576            .unwrap();
1577        let editor3 = workspace
1578            .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
1579            .await
1580            .unwrap()
1581            .downcast::<Editor>()
1582            .unwrap();
1583
1584        editor3
1585            .update(cx, |editor, cx| {
1586                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
1587                    s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)])
1588                });
1589                editor.newline(&Default::default(), cx);
1590                editor.newline(&Default::default(), cx);
1591                editor.move_down(&Default::default(), cx);
1592                editor.move_down(&Default::default(), cx);
1593                editor.save(project.clone(), cx)
1594            })
1595            .await
1596            .unwrap();
1597        editor3.update(cx, |editor, cx| {
1598            editor.set_scroll_position(vec2f(0., 12.5), cx)
1599        });
1600        assert_eq!(
1601            active_location(&workspace, cx),
1602            (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1603        );
1604
1605        workspace
1606            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1607            .await
1608            .unwrap();
1609        assert_eq!(
1610            active_location(&workspace, cx),
1611            (file3.clone(), DisplayPoint::new(0, 0), 0.)
1612        );
1613
1614        workspace
1615            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1616            .await
1617            .unwrap();
1618        assert_eq!(
1619            active_location(&workspace, cx),
1620            (file2.clone(), DisplayPoint::new(0, 0), 0.)
1621        );
1622
1623        workspace
1624            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1625            .await
1626            .unwrap();
1627        assert_eq!(
1628            active_location(&workspace, cx),
1629            (file1.clone(), DisplayPoint::new(10, 0), 0.)
1630        );
1631
1632        workspace
1633            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1634            .await
1635            .unwrap();
1636        assert_eq!(
1637            active_location(&workspace, cx),
1638            (file1.clone(), DisplayPoint::new(0, 0), 0.)
1639        );
1640
1641        // Go back one more time and ensure we don't navigate past the first item in the history.
1642        workspace
1643            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1644            .await
1645            .unwrap();
1646        assert_eq!(
1647            active_location(&workspace, cx),
1648            (file1.clone(), DisplayPoint::new(0, 0), 0.)
1649        );
1650
1651        workspace
1652            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
1653            .await
1654            .unwrap();
1655        assert_eq!(
1656            active_location(&workspace, cx),
1657            (file1.clone(), DisplayPoint::new(10, 0), 0.)
1658        );
1659
1660        workspace
1661            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
1662            .await
1663            .unwrap();
1664        assert_eq!(
1665            active_location(&workspace, cx),
1666            (file2.clone(), DisplayPoint::new(0, 0), 0.)
1667        );
1668
1669        // Go forward to an item that has been closed, ensuring it gets re-opened at the same
1670        // location.
1671        pane.update(cx, |pane, cx| {
1672            let editor3_id = editor3.id();
1673            drop(editor3);
1674            pane.close_item_by_id(editor3_id, SaveBehavior::PromptOnWrite, cx)
1675        })
1676        .await
1677        .unwrap();
1678        workspace
1679            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
1680            .await
1681            .unwrap();
1682        assert_eq!(
1683            active_location(&workspace, cx),
1684            (file3.clone(), DisplayPoint::new(0, 0), 0.)
1685        );
1686
1687        workspace
1688            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
1689            .await
1690            .unwrap();
1691        assert_eq!(
1692            active_location(&workspace, cx),
1693            (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1694        );
1695
1696        workspace
1697            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1698            .await
1699            .unwrap();
1700        assert_eq!(
1701            active_location(&workspace, cx),
1702            (file3.clone(), DisplayPoint::new(0, 0), 0.)
1703        );
1704
1705        // Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
1706        pane.update(cx, |pane, cx| {
1707            let editor2_id = editor2.id();
1708            drop(editor2);
1709            pane.close_item_by_id(editor2_id, SaveBehavior::PromptOnWrite, cx)
1710        })
1711        .await
1712        .unwrap();
1713        app_state
1714            .fs
1715            .remove_file(Path::new("/root/a/file2"), Default::default())
1716            .await
1717            .unwrap();
1718        cx.foreground().run_until_parked();
1719
1720        workspace
1721            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1722            .await
1723            .unwrap();
1724        assert_eq!(
1725            active_location(&workspace, cx),
1726            (file1.clone(), DisplayPoint::new(10, 0), 0.)
1727        );
1728        workspace
1729            .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
1730            .await
1731            .unwrap();
1732        assert_eq!(
1733            active_location(&workspace, cx),
1734            (file3.clone(), DisplayPoint::new(0, 0), 0.)
1735        );
1736
1737        // Modify file to collapse multiple nav history entries into the same location.
1738        // Ensure we don't visit the same location twice when navigating.
1739        editor1.update(cx, |editor, cx| {
1740            editor.change_selections(None, cx, |s| {
1741                s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)])
1742            })
1743        });
1744
1745        for _ in 0..5 {
1746            editor1.update(cx, |editor, cx| {
1747                editor.change_selections(None, cx, |s| {
1748                    s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
1749                });
1750            });
1751            editor1.update(cx, |editor, cx| {
1752                editor.change_selections(None, cx, |s| {
1753                    s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)])
1754                })
1755            });
1756        }
1757
1758        editor1.update(cx, |editor, cx| {
1759            editor.transact(cx, |editor, cx| {
1760                editor.change_selections(None, cx, |s| {
1761                    s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)])
1762                });
1763                editor.insert("", cx);
1764            })
1765        });
1766
1767        editor1.update(cx, |editor, cx| {
1768            editor.change_selections(None, cx, |s| {
1769                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1770            })
1771        });
1772        workspace
1773            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1774            .await
1775            .unwrap();
1776        assert_eq!(
1777            active_location(&workspace, cx),
1778            (file1.clone(), DisplayPoint::new(2, 0), 0.)
1779        );
1780        workspace
1781            .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1782            .await
1783            .unwrap();
1784        assert_eq!(
1785            active_location(&workspace, cx),
1786            (file1.clone(), DisplayPoint::new(3, 0), 0.)
1787        );
1788
1789        fn active_location(
1790            workspace: &ViewHandle<Workspace>,
1791            cx: &mut TestAppContext,
1792        ) -> (ProjectPath, DisplayPoint, f32) {
1793            workspace.update(cx, |workspace, cx| {
1794                let item = workspace.active_item(cx).unwrap();
1795                let editor = item.downcast::<Editor>().unwrap();
1796                let (selections, scroll_position) = editor.update(cx, |editor, cx| {
1797                    (
1798                        editor.selections.display_ranges(cx),
1799                        editor.scroll_position(cx),
1800                    )
1801                });
1802                (
1803                    item.project_path(cx).unwrap(),
1804                    selections[0].start,
1805                    scroll_position.y(),
1806                )
1807            })
1808        }
1809    }
1810
1811    #[gpui::test]
1812    async fn test_reopening_closed_items(cx: &mut TestAppContext) {
1813        let app_state = init_test(cx);
1814        app_state
1815            .fs
1816            .as_fake()
1817            .insert_tree(
1818                "/root",
1819                json!({
1820                    "a": {
1821                        "file1": "",
1822                        "file2": "",
1823                        "file3": "",
1824                        "file4": "",
1825                    },
1826                }),
1827            )
1828            .await;
1829
1830        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1831        let workspace = cx
1832            .add_window(|cx| Workspace::test_new(project, cx))
1833            .root(cx);
1834        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1835
1836        let entries = cx.read(|cx| workspace.file_project_paths(cx));
1837        let file1 = entries[0].clone();
1838        let file2 = entries[1].clone();
1839        let file3 = entries[2].clone();
1840        let file4 = entries[3].clone();
1841
1842        let file1_item_id = workspace
1843            .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1844            .await
1845            .unwrap()
1846            .id();
1847        let file2_item_id = workspace
1848            .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
1849            .await
1850            .unwrap()
1851            .id();
1852        let file3_item_id = workspace
1853            .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
1854            .await
1855            .unwrap()
1856            .id();
1857        let file4_item_id = workspace
1858            .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx))
1859            .await
1860            .unwrap()
1861            .id();
1862        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1863
1864        // Close all the pane items in some arbitrary order.
1865        pane.update(cx, |pane, cx| {
1866            pane.close_item_by_id(file1_item_id, SaveBehavior::PromptOnWrite, cx)
1867        })
1868        .await
1869        .unwrap();
1870        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1871
1872        pane.update(cx, |pane, cx| {
1873            pane.close_item_by_id(file4_item_id, SaveBehavior::PromptOnWrite, cx)
1874        })
1875        .await
1876        .unwrap();
1877        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1878
1879        pane.update(cx, |pane, cx| {
1880            pane.close_item_by_id(file2_item_id, SaveBehavior::PromptOnWrite, cx)
1881        })
1882        .await
1883        .unwrap();
1884        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1885
1886        pane.update(cx, |pane, cx| {
1887            pane.close_item_by_id(file3_item_id, SaveBehavior::PromptOnWrite, cx)
1888        })
1889        .await
1890        .unwrap();
1891        assert_eq!(active_path(&workspace, cx), None);
1892
1893        // Reopen all the closed items, ensuring they are reopened in the same order
1894        // in which they were closed.
1895        workspace
1896            .update(cx, Workspace::reopen_closed_item)
1897            .await
1898            .unwrap();
1899        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1900
1901        workspace
1902            .update(cx, Workspace::reopen_closed_item)
1903            .await
1904            .unwrap();
1905        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1906
1907        workspace
1908            .update(cx, Workspace::reopen_closed_item)
1909            .await
1910            .unwrap();
1911        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1912
1913        workspace
1914            .update(cx, Workspace::reopen_closed_item)
1915            .await
1916            .unwrap();
1917        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1918
1919        // Reopening past the last closed item is a no-op.
1920        workspace
1921            .update(cx, Workspace::reopen_closed_item)
1922            .await
1923            .unwrap();
1924        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1925
1926        // Reopening closed items doesn't interfere with navigation history.
1927        workspace
1928            .update(cx, |workspace, cx| {
1929                workspace.go_back(workspace.active_pane().downgrade(), cx)
1930            })
1931            .await
1932            .unwrap();
1933        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1934
1935        workspace
1936            .update(cx, |workspace, cx| {
1937                workspace.go_back(workspace.active_pane().downgrade(), cx)
1938            })
1939            .await
1940            .unwrap();
1941        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1942
1943        workspace
1944            .update(cx, |workspace, cx| {
1945                workspace.go_back(workspace.active_pane().downgrade(), cx)
1946            })
1947            .await
1948            .unwrap();
1949        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1950
1951        workspace
1952            .update(cx, |workspace, cx| {
1953                workspace.go_back(workspace.active_pane().downgrade(), cx)
1954            })
1955            .await
1956            .unwrap();
1957        assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1958
1959        workspace
1960            .update(cx, |workspace, cx| {
1961                workspace.go_back(workspace.active_pane().downgrade(), cx)
1962            })
1963            .await
1964            .unwrap();
1965        assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1966
1967        workspace
1968            .update(cx, |workspace, cx| {
1969                workspace.go_back(workspace.active_pane().downgrade(), cx)
1970            })
1971            .await
1972            .unwrap();
1973        assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1974
1975        workspace
1976            .update(cx, |workspace, cx| {
1977                workspace.go_back(workspace.active_pane().downgrade(), cx)
1978            })
1979            .await
1980            .unwrap();
1981        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1982
1983        workspace
1984            .update(cx, |workspace, cx| {
1985                workspace.go_back(workspace.active_pane().downgrade(), cx)
1986            })
1987            .await
1988            .unwrap();
1989        assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1990
1991        fn active_path(
1992            workspace: &ViewHandle<Workspace>,
1993            cx: &TestAppContext,
1994        ) -> Option<ProjectPath> {
1995            workspace.read_with(cx, |workspace, cx| {
1996                let item = workspace.active_item(cx)?;
1997                item.project_path(cx)
1998            })
1999        }
2000    }
2001
2002    #[gpui::test]
2003    async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
2004        struct TestView;
2005
2006        impl Entity for TestView {
2007            type Event = ();
2008        }
2009
2010        impl View for TestView {
2011            fn ui_name() -> &'static str {
2012                "TestView"
2013            }
2014
2015            fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
2016                Empty::new().into_any()
2017            }
2018        }
2019
2020        let executor = cx.background();
2021        let fs = FakeFs::new(executor.clone());
2022
2023        actions!(test, [A, B]);
2024        // From the Atom keymap
2025        actions!(workspace, [ActivatePreviousPane]);
2026        // From the JetBrains keymap
2027        actions!(pane, [ActivatePrevItem]);
2028
2029        fs.save(
2030            "/settings.json".as_ref(),
2031            &r#"
2032            {
2033                "base_keymap": "Atom"
2034            }
2035            "#
2036            .into(),
2037            Default::default(),
2038        )
2039        .await
2040        .unwrap();
2041
2042        fs.save(
2043            "/keymap.json".as_ref(),
2044            &r#"
2045            [
2046                {
2047                    "bindings": {
2048                        "backspace": "test::A"
2049                    }
2050                }
2051            ]
2052            "#
2053            .into(),
2054            Default::default(),
2055        )
2056        .await
2057        .unwrap();
2058
2059        cx.update(|cx| {
2060            cx.set_global(SettingsStore::test(cx));
2061            theme::init(Assets, cx);
2062            welcome::init(cx);
2063
2064            cx.add_global_action(|_: &A, _cx| {});
2065            cx.add_global_action(|_: &B, _cx| {});
2066            cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
2067            cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
2068
2069            let settings_rx = watch_config_file(
2070                executor.clone(),
2071                fs.clone(),
2072                PathBuf::from("/settings.json"),
2073            );
2074            let keymap_rx =
2075                watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
2076
2077            handle_keymap_file_changes(keymap_rx, cx);
2078            handle_settings_file_changes(settings_rx, cx);
2079        });
2080
2081        cx.foreground().run_until_parked();
2082
2083        let window = cx.add_window(|_| TestView);
2084
2085        // Test loading the keymap base at all
2086        assert_key_bindings_for(
2087            window.into(),
2088            cx,
2089            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
2090            line!(),
2091        );
2092
2093        // Test modifying the users keymap, while retaining the base keymap
2094        fs.save(
2095            "/keymap.json".as_ref(),
2096            &r#"
2097            [
2098                {
2099                    "bindings": {
2100                        "backspace": "test::B"
2101                    }
2102                }
2103            ]
2104            "#
2105            .into(),
2106            Default::default(),
2107        )
2108        .await
2109        .unwrap();
2110
2111        cx.foreground().run_until_parked();
2112
2113        assert_key_bindings_for(
2114            window.into(),
2115            cx,
2116            vec![("backspace", &B), ("k", &ActivatePreviousPane)],
2117            line!(),
2118        );
2119
2120        // Test modifying the base, while retaining the users keymap
2121        fs.save(
2122            "/settings.json".as_ref(),
2123            &r#"
2124            {
2125                "base_keymap": "JetBrains"
2126            }
2127            "#
2128            .into(),
2129            Default::default(),
2130        )
2131        .await
2132        .unwrap();
2133
2134        cx.foreground().run_until_parked();
2135
2136        assert_key_bindings_for(
2137            window.into(),
2138            cx,
2139            vec![("backspace", &B), ("[", &ActivatePrevItem)],
2140            line!(),
2141        );
2142
2143        #[track_caller]
2144        fn assert_key_bindings_for<'a>(
2145            window: AnyWindowHandle,
2146            cx: &TestAppContext,
2147            actions: Vec<(&'static str, &'a dyn Action)>,
2148            line: u32,
2149        ) {
2150            for (key, action) in actions {
2151                // assert that...
2152                assert!(
2153                    cx.available_actions(window, 0)
2154                        .into_iter()
2155                        .any(|(_, bound_action, b)| {
2156                            // action names match...
2157                            bound_action.name() == action.name()
2158                        && bound_action.namespace() == action.namespace()
2159                        // and key strokes contain the given key
2160                        && b.iter()
2161                            .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
2162                        }),
2163                    "On {} Failed to find {} with key binding {}",
2164                    line,
2165                    action.name(),
2166                    key
2167                );
2168            }
2169        }
2170    }
2171
2172    #[gpui::test]
2173    async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
2174        struct TestView;
2175
2176        impl Entity for TestView {
2177            type Event = ();
2178        }
2179
2180        impl View for TestView {
2181            fn ui_name() -> &'static str {
2182                "TestView"
2183            }
2184
2185            fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
2186                Empty::new().into_any()
2187            }
2188        }
2189
2190        let executor = cx.background();
2191        let fs = FakeFs::new(executor.clone());
2192
2193        actions!(test, [A, B]);
2194        // From the Atom keymap
2195        actions!(workspace, [ActivatePreviousPane]);
2196        // From the JetBrains keymap
2197        actions!(pane, [ActivatePrevItem]);
2198
2199        fs.save(
2200            "/settings.json".as_ref(),
2201            &r#"
2202            {
2203                "base_keymap": "Atom"
2204            }
2205            "#
2206            .into(),
2207            Default::default(),
2208        )
2209        .await
2210        .unwrap();
2211
2212        fs.save(
2213            "/keymap.json".as_ref(),
2214            &r#"
2215            [
2216                {
2217                    "bindings": {
2218                        "backspace": "test::A"
2219                    }
2220                }
2221            ]
2222            "#
2223            .into(),
2224            Default::default(),
2225        )
2226        .await
2227        .unwrap();
2228
2229        cx.update(|cx| {
2230            cx.set_global(SettingsStore::test(cx));
2231            theme::init(Assets, cx);
2232            welcome::init(cx);
2233
2234            cx.add_global_action(|_: &A, _cx| {});
2235            cx.add_global_action(|_: &B, _cx| {});
2236            cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
2237            cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
2238
2239            let settings_rx = watch_config_file(
2240                executor.clone(),
2241                fs.clone(),
2242                PathBuf::from("/settings.json"),
2243            );
2244            let keymap_rx =
2245                watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
2246
2247            handle_keymap_file_changes(keymap_rx, cx);
2248            handle_settings_file_changes(settings_rx, cx);
2249        });
2250
2251        cx.foreground().run_until_parked();
2252
2253        let window = cx.add_window(|_| TestView);
2254
2255        // Test loading the keymap base at all
2256        assert_key_bindings_for(
2257            window.into(),
2258            cx,
2259            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
2260            line!(),
2261        );
2262
2263        // Test disabling the key binding for the base keymap
2264        fs.save(
2265            "/keymap.json".as_ref(),
2266            &r#"
2267            [
2268                {
2269                    "bindings": {
2270                        "backspace": null
2271                    }
2272                }
2273            ]
2274            "#
2275            .into(),
2276            Default::default(),
2277        )
2278        .await
2279        .unwrap();
2280
2281        cx.foreground().run_until_parked();
2282
2283        assert_key_bindings_for(
2284            window.into(),
2285            cx,
2286            vec![("k", &ActivatePreviousPane)],
2287            line!(),
2288        );
2289
2290        // Test modifying the base, while retaining the users keymap
2291        fs.save(
2292            "/settings.json".as_ref(),
2293            &r#"
2294            {
2295                "base_keymap": "JetBrains"
2296            }
2297            "#
2298            .into(),
2299            Default::default(),
2300        )
2301        .await
2302        .unwrap();
2303
2304        cx.foreground().run_until_parked();
2305
2306        assert_key_bindings_for(window.into(), cx, vec![("[", &ActivatePrevItem)], line!());
2307
2308        #[track_caller]
2309        fn assert_key_bindings_for<'a>(
2310            window: AnyWindowHandle,
2311            cx: &TestAppContext,
2312            actions: Vec<(&'static str, &'a dyn Action)>,
2313            line: u32,
2314        ) {
2315            for (key, action) in actions {
2316                // assert that...
2317                assert!(
2318                    cx.available_actions(window, 0)
2319                        .into_iter()
2320                        .any(|(_, bound_action, b)| {
2321                            // action names match...
2322                            bound_action.name() == action.name()
2323                        && bound_action.namespace() == action.namespace()
2324                        // and key strokes contain the given key
2325                        && b.iter()
2326                            .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
2327                        }),
2328                    "On {} Failed to find {} with key binding {}",
2329                    line,
2330                    action.name(),
2331                    key
2332                );
2333            }
2334        }
2335    }
2336
2337    #[gpui::test]
2338    fn test_bundled_settings_and_themes(cx: &mut AppContext) {
2339        cx.platform()
2340            .fonts()
2341            .add_fonts(&[
2342                Assets
2343                    .load("fonts/zed-sans/zed-sans-extended.ttf")
2344                    .unwrap()
2345                    .to_vec()
2346                    .into(),
2347                Assets
2348                    .load("fonts/zed-mono/zed-mono-extended.ttf")
2349                    .unwrap()
2350                    .to_vec()
2351                    .into(),
2352                Assets
2353                    .load("fonts/plex/IBMPlexSans-Regular.ttf")
2354                    .unwrap()
2355                    .to_vec()
2356                    .into(),
2357            ])
2358            .unwrap();
2359        let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
2360        let mut settings = SettingsStore::default();
2361        settings
2362            .set_default_settings(&settings::default_settings(), cx)
2363            .unwrap();
2364        cx.set_global(settings);
2365        theme::init(Assets, cx);
2366
2367        let mut has_default_theme = false;
2368        for theme_name in themes.list(false).map(|meta| meta.name) {
2369            let theme = themes.get(&theme_name).unwrap();
2370            assert_eq!(theme.meta.name, theme_name);
2371            if theme.meta.name == settings::get::<ThemeSettings>(cx).theme.meta.name {
2372                has_default_theme = true;
2373            }
2374        }
2375        assert!(has_default_theme);
2376    }
2377
2378    #[gpui::test]
2379    fn test_bundled_languages(cx: &mut AppContext) {
2380        let mut languages = LanguageRegistry::test();
2381        languages.set_executor(cx.background().clone());
2382        let languages = Arc::new(languages);
2383        let node_runtime = node_runtime::FakeNodeRuntime::new();
2384        languages::init(languages.clone(), node_runtime);
2385        for name in languages.language_names() {
2386            languages.language_for_name(&name);
2387        }
2388        cx.foreground().run_until_parked();
2389    }
2390
2391    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
2392        cx.foreground().forbid_parking();
2393        cx.update(|cx| {
2394            let mut app_state = AppState::test(cx);
2395            let state = Arc::get_mut(&mut app_state).unwrap();
2396            state.initialize_workspace = initialize_workspace;
2397            state.build_window_options = build_window_options;
2398            theme::init((), cx);
2399            audio::init((), cx);
2400            call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
2401            workspace::init(app_state.clone(), cx);
2402            Project::init_settings(cx);
2403            language::init(cx);
2404            editor::init(cx);
2405            project_panel::init_settings(cx);
2406            collab_ui::init(&app_state, cx);
2407            pane::init(cx);
2408            project_panel::init((), cx);
2409            terminal_view::init(cx);
2410            ai::init(cx);
2411            app_state
2412        })
2413    }
2414
2415    fn rust_lang() -> Arc<language::Language> {
2416        Arc::new(language::Language::new(
2417            language::LanguageConfig {
2418                name: "Rust".into(),
2419                path_suffixes: vec!["rs".to_string()],
2420                ..Default::default()
2421            },
2422            Some(tree_sitter_rust::language()),
2423        ))
2424    }
2425}