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