zed.rs

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