zed.rs

   1mod feedback;
   2pub mod languages;
   3pub mod menus;
   4pub mod settings_file;
   5#[cfg(any(test, feature = "test-support"))]
   6pub mod test;
   7
   8use anyhow::{anyhow, Context, Result};
   9use breadcrumbs::Breadcrumbs;
  10pub use client;
  11pub use contacts_panel;
  12use contacts_panel::ContactsPanel;
  13pub use editor;
  14use editor::Editor;
  15use gpui::{
  16    actions,
  17    geometry::vector::vec2f,
  18    impl_actions,
  19    platform::{WindowBounds, WindowOptions},
  20    AsyncAppContext, ViewContext,
  21};
  22use lazy_static::lazy_static;
  23pub use lsp;
  24use project::Project;
  25pub use project::{self, fs};
  26use project_panel::ProjectPanel;
  27use search::{BufferSearchBar, ProjectSearchBar};
  28use serde::Deserialize;
  29use serde_json::to_string_pretty;
  30use settings::{keymap_file_json_schema, settings_file_json_schema, Settings};
  31use std::{
  32    path::{Path, PathBuf},
  33    sync::Arc,
  34};
  35use util::ResultExt;
  36pub use workspace;
  37use workspace::{AppState, Workspace};
  38
  39#[derive(Deserialize, Clone, PartialEq)]
  40struct OpenBrowser {
  41    url: Arc<str>,
  42}
  43
  44impl_actions!(zed, [OpenBrowser]);
  45
  46actions!(
  47    zed,
  48    [
  49        About,
  50        Quit,
  51        DebugElements,
  52        OpenSettings,
  53        OpenKeymap,
  54        IncreaseBufferFontSize,
  55        DecreaseBufferFontSize,
  56        ResetBufferFontSize,
  57        InstallCommandLineInterface,
  58    ]
  59);
  60
  61const MIN_FONT_SIZE: f32 = 6.0;
  62
  63lazy_static! {
  64    pub static ref ROOT_PATH: PathBuf = dirs::home_dir()
  65        .expect("failed to determine home directory")
  66        .join(".zed");
  67    pub static ref SETTINGS_PATH: PathBuf = ROOT_PATH.join("settings.json");
  68    pub static ref KEYMAP_PATH: PathBuf = ROOT_PATH.join("keymap.json");
  69}
  70
  71pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
  72    cx.add_action(about);
  73    cx.add_global_action(quit);
  74    cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
  75    cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
  76        cx.update_global::<Settings, _, _>(|settings, cx| {
  77            settings.buffer_font_size = (settings.buffer_font_size + 1.0).max(MIN_FONT_SIZE);
  78            cx.refresh_windows();
  79        });
  80    });
  81    cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| {
  82        cx.update_global::<Settings, _, _>(|settings, cx| {
  83            settings.buffer_font_size = (settings.buffer_font_size - 1.0).max(MIN_FONT_SIZE);
  84            cx.refresh_windows();
  85        });
  86    });
  87    cx.add_global_action(move |_: &ResetBufferFontSize, cx| {
  88        cx.update_global::<Settings, _, _>(|settings, cx| {
  89            settings.buffer_font_size = settings.default_buffer_font_size;
  90            cx.refresh_windows();
  91        });
  92    });
  93    cx.add_global_action(move |_: &InstallCommandLineInterface, cx| {
  94        cx.spawn(|cx| async move { install_cli(&cx).await.context("error creating CLI symlink") })
  95            .detach_and_log_err(cx);
  96    });
  97    cx.add_action({
  98        let app_state = app_state.clone();
  99        move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
 100            open_config_file(&SETTINGS_PATH, app_state.clone(), cx);
 101        }
 102    });
 103    cx.add_action({
 104        let app_state = app_state.clone();
 105        move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
 106            open_config_file(&KEYMAP_PATH, app_state.clone(), cx);
 107        }
 108    });
 109    cx.add_action(
 110        |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
 111            let content = to_string_pretty(&cx.debug_elements()).unwrap();
 112            let project = workspace.project().clone();
 113            let json_language = project.read(cx).languages().get_language("JSON").unwrap();
 114            if project.read(cx).is_remote() {
 115                cx.propagate_action();
 116            } else if let Some(buffer) = project
 117                .update(cx, |project, cx| {
 118                    project.create_buffer(&content, Some(json_language), cx)
 119                })
 120                .log_err()
 121            {
 122                workspace.add_item(
 123                    Box::new(
 124                        cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
 125                    ),
 126                    cx,
 127                );
 128            }
 129        },
 130    );
 131
 132    lsp_status::init(cx);
 133    settings::KeymapFileContent::load_defaults(cx);
 134}
 135
 136pub fn initialize_workspace(
 137    workspace: &mut Workspace,
 138    app_state: &Arc<AppState>,
 139    cx: &mut ViewContext<Workspace>,
 140) {
 141    cx.subscribe(&cx.handle(), {
 142        let project = workspace.project().clone();
 143        move |_, _, event, cx| {
 144            if let workspace::Event::PaneAdded(pane) = event {
 145                pane.update(cx, |pane, cx| {
 146                    pane.toolbar().update(cx, |toolbar, cx| {
 147                        let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(project.clone()));
 148                        toolbar.add_item(breadcrumbs, cx);
 149                        let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx));
 150                        toolbar.add_item(buffer_search_bar, cx);
 151                        let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
 152                        toolbar.add_item(project_search_bar, cx);
 153                    })
 154                });
 155            }
 156        }
 157    })
 158    .detach();
 159
 160    cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
 161
 162    let theme_names = app_state.themes.list().collect();
 163    let language_names = app_state.languages.language_names();
 164
 165    workspace.project().update(cx, |project, cx| {
 166        let action_names = cx.all_action_names().collect::<Vec<_>>();
 167        project.set_language_server_settings(serde_json::json!({
 168            "json": {
 169                "schemas": [
 170                    {
 171                        "fileMatch": [".zed/settings.json"],
 172                        "schema": settings_file_json_schema(theme_names, language_names),
 173                    },
 174                    {
 175                        "fileMatch": [".zed/keymap.json"],
 176                        "schema": keymap_file_json_schema(&action_names),
 177                    }
 178                ]
 179            }
 180        }));
 181    });
 182
 183    let project_panel = ProjectPanel::new(workspace.project().clone(), cx);
 184    let contact_panel = cx.add_view(|cx| {
 185        ContactsPanel::new(
 186            app_state.user_store.clone(),
 187            app_state.project_store.clone(),
 188            workspace.weak_handle(),
 189            cx,
 190        )
 191    });
 192
 193    workspace.left_sidebar().update(cx, |sidebar, cx| {
 194        sidebar.add_item(
 195            "icons/folder-tree-solid-14.svg",
 196            "Project Panel".to_string(),
 197            project_panel.into(),
 198            cx,
 199        )
 200    });
 201    workspace.right_sidebar().update(cx, |sidebar, cx| {
 202        sidebar.add_item(
 203            "icons/contacts-solid-14.svg",
 204            "Contacts Panel".to_string(),
 205            contact_panel.into(),
 206            cx,
 207        )
 208    });
 209
 210    let diagnostic_summary =
 211        cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
 212    let lsp_status = lsp_status::LspStatusItem::new(workspace, app_state.languages.clone(), cx);
 213    let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
 214    let auto_update = cx.add_view(|cx| auto_update::AutoUpdateIndicator::new(cx));
 215    let feedback_link = cx.add_view(|_| feedback::FeedbackLink);
 216    workspace.status_bar().update(cx, |status_bar, cx| {
 217        status_bar.add_left_item(diagnostic_summary, cx);
 218        status_bar.add_left_item(lsp_status, cx);
 219        status_bar.add_right_item(cursor_position, cx);
 220        status_bar.add_right_item(auto_update, cx);
 221        status_bar.add_right_item(feedback_link, cx);
 222    });
 223
 224    auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
 225}
 226
 227pub fn build_window_options() -> WindowOptions<'static> {
 228    WindowOptions {
 229        bounds: WindowBounds::Maximized,
 230        title: None,
 231        titlebar_appears_transparent: true,
 232        traffic_light_position: Some(vec2f(8., 8.)),
 233    }
 234}
 235
 236fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
 237    let mut workspaces = cx
 238        .window_ids()
 239        .filter_map(|window_id| cx.root_view::<Workspace>(window_id))
 240        .collect::<Vec<_>>();
 241
 242    // If multiple windows have unsaved changes, and need a save prompt,
 243    // prompt in the active window before switching to a different window.
 244    workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
 245
 246    cx.spawn(|mut cx| async move {
 247        // If the user cancels any save prompt, then keep the app open.
 248        for workspace in workspaces {
 249            if !workspace
 250                .update(&mut cx, |workspace, cx| workspace.prepare_to_close(cx))
 251                .await?
 252            {
 253                return Ok(());
 254            }
 255        }
 256        cx.platform().quit();
 257        anyhow::Ok(())
 258    })
 259    .detach_and_log_err(cx);
 260}
 261
 262fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
 263    cx.prompt(
 264        gpui::PromptLevel::Info,
 265        &format!("Zed {}", env!("CARGO_PKG_VERSION")),
 266        &["OK"],
 267    );
 268}
 269
 270async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
 271    let cli_path = cx.platform().path_for_auxiliary_executable("cli")?;
 272    let link_path = Path::new("/usr/local/bin/zed");
 273    let bin_dir_path = link_path.parent().unwrap();
 274
 275    // Don't re-create symlink if it points to the same CLI binary.
 276    if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) {
 277        return Ok(());
 278    }
 279
 280    // If the symlink is not there or is outdated, first try replacing it
 281    // without escalating.
 282    smol::fs::remove_file(link_path).await.log_err();
 283    if smol::fs::unix::symlink(&cli_path, link_path)
 284        .await
 285        .log_err()
 286        .is_some()
 287    {
 288        return Ok(());
 289    }
 290
 291    // The symlink could not be created, so use osascript with admin privileges
 292    // to create it.
 293    let status = smol::process::Command::new("osascript")
 294        .args([
 295            "-e",
 296            &format!(
 297                "do shell script \" \
 298                    mkdir -p \'{}\' && \
 299                    ln -sf \'{}\' \'{}\' \
 300                \" with administrator privileges",
 301                bin_dir_path.to_string_lossy(),
 302                cli_path.to_string_lossy(),
 303                link_path.to_string_lossy(),
 304            ),
 305        ])
 306        .stdout(smol::process::Stdio::inherit())
 307        .stderr(smol::process::Stdio::inherit())
 308        .output()
 309        .await?
 310        .status;
 311    if status.success() {
 312        Ok(())
 313    } else {
 314        Err(anyhow!("error running osascript"))
 315    }
 316}
 317
 318fn open_config_file(
 319    path: &'static Path,
 320    app_state: Arc<AppState>,
 321    cx: &mut ViewContext<Workspace>,
 322) {
 323    cx.spawn(|workspace, mut cx| async move {
 324        let fs = &app_state.fs;
 325        if !fs.is_file(path).await {
 326            fs.create_dir(&ROOT_PATH).await?;
 327            fs.create_file(path, Default::default()).await?;
 328        }
 329
 330        workspace
 331            .update(&mut cx, |workspace, cx| {
 332                if workspace.project().read(cx).is_local() {
 333                    workspace.open_paths(vec![path.to_path_buf()], false, cx)
 334                } else {
 335                    let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
 336                        let mut workspace = Workspace::new(
 337                            Project::local(
 338                                false,
 339                                app_state.client.clone(),
 340                                app_state.user_store.clone(),
 341                                app_state.project_store.clone(),
 342                                app_state.languages.clone(),
 343                                app_state.fs.clone(),
 344                                cx,
 345                            ),
 346                            cx,
 347                        );
 348                        (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
 349                        workspace
 350                    });
 351                    workspace.update(cx, |workspace, cx| {
 352                        workspace.open_paths(vec![path.to_path_buf()], false, cx)
 353                    })
 354                }
 355            })
 356            .await;
 357        Ok::<_, anyhow::Error>(())
 358    })
 359    .detach_and_log_err(cx)
 360}
 361
 362#[cfg(test)]
 363mod tests {
 364    use super::*;
 365    use assets::Assets;
 366    use editor::{Autoscroll, DisplayPoint, Editor};
 367    use gpui::{AssetSource, MutableAppContext, TestAppContext, ViewHandle};
 368    use project::ProjectPath;
 369    use serde_json::json;
 370    use std::{
 371        collections::HashSet,
 372        path::{Path, PathBuf},
 373    };
 374    use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME};
 375    use workspace::{
 376        open_paths, pane, Item, ItemHandle, NewFile, Pane, SplitDirection, WorkspaceHandle,
 377    };
 378
 379    #[gpui::test]
 380    async fn test_open_paths_action(cx: &mut TestAppContext) {
 381        let app_state = init(cx);
 382        app_state
 383            .fs
 384            .as_fake()
 385            .insert_tree(
 386                "/root",
 387                json!({
 388                    "a": {
 389                        "aa": null,
 390                        "ab": null,
 391                    },
 392                    "b": {
 393                        "ba": null,
 394                        "bb": null,
 395                    },
 396                    "c": {
 397                        "ca": null,
 398                        "cb": null,
 399                    },
 400                }),
 401            )
 402            .await;
 403
 404        cx.update(|cx| {
 405            open_paths(
 406                &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
 407                &app_state,
 408                cx,
 409            )
 410        })
 411        .await;
 412        assert_eq!(cx.window_ids().len(), 1);
 413
 414        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
 415            .await;
 416        assert_eq!(cx.window_ids().len(), 1);
 417        let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
 418        workspace_1.update(cx, |workspace, cx| {
 419            assert_eq!(workspace.worktrees(cx).count(), 2);
 420            assert!(workspace.left_sidebar().read(cx).active_item().is_some());
 421            assert!(workspace.active_pane().is_focused(cx));
 422        });
 423
 424        cx.update(|cx| {
 425            open_paths(
 426                &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
 427                &app_state,
 428                cx,
 429            )
 430        })
 431        .await;
 432        assert_eq!(cx.window_ids().len(), 2);
 433    }
 434
 435    #[gpui::test]
 436    async fn test_new_empty_workspace(cx: &mut TestAppContext) {
 437        let app_state = init(cx);
 438        cx.dispatch_global_action(workspace::NewFile);
 439        let window_id = *cx.window_ids().first().unwrap();
 440        let workspace = cx.root_view::<Workspace>(window_id).unwrap();
 441        let editor = workspace.update(cx, |workspace, cx| {
 442            workspace
 443                .active_item(cx)
 444                .unwrap()
 445                .downcast::<editor::Editor>()
 446                .unwrap()
 447        });
 448
 449        editor.update(cx, |editor, cx| {
 450            assert!(editor.text(cx).is_empty());
 451        });
 452
 453        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
 454        app_state.fs.as_fake().insert_dir("/root").await;
 455        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
 456        save_task.await.unwrap();
 457        editor.read_with(cx, |editor, cx| {
 458            assert!(!editor.is_dirty(cx));
 459            assert_eq!(editor.title(cx), "the-new-name");
 460        });
 461    }
 462
 463    #[gpui::test]
 464    async fn test_open_entry(cx: &mut TestAppContext) {
 465        let app_state = init(cx);
 466        app_state
 467            .fs
 468            .as_fake()
 469            .insert_tree(
 470                "/root",
 471                json!({
 472                    "a": {
 473                        "file1": "contents 1",
 474                        "file2": "contents 2",
 475                        "file3": "contents 3",
 476                    },
 477                }),
 478            )
 479            .await;
 480
 481        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
 482        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
 483
 484        let entries = cx.read(|cx| workspace.file_project_paths(cx));
 485        let file1 = entries[0].clone();
 486        let file2 = entries[1].clone();
 487        let file3 = entries[2].clone();
 488
 489        // Open the first entry
 490        let entry_1 = workspace
 491            .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
 492            .await
 493            .unwrap();
 494        cx.read(|cx| {
 495            let pane = workspace.read(cx).active_pane().read(cx);
 496            assert_eq!(
 497                pane.active_item().unwrap().project_path(cx),
 498                Some(file1.clone())
 499            );
 500            assert_eq!(pane.items().count(), 1);
 501        });
 502
 503        // Open the second entry
 504        workspace
 505            .update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
 506            .await
 507            .unwrap();
 508        cx.read(|cx| {
 509            let pane = workspace.read(cx).active_pane().read(cx);
 510            assert_eq!(
 511                pane.active_item().unwrap().project_path(cx),
 512                Some(file2.clone())
 513            );
 514            assert_eq!(pane.items().count(), 2);
 515        });
 516
 517        // Open the first entry again. The existing pane item is activated.
 518        let entry_1b = workspace
 519            .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
 520            .await
 521            .unwrap();
 522        assert_eq!(entry_1.id(), entry_1b.id());
 523
 524        cx.read(|cx| {
 525            let pane = workspace.read(cx).active_pane().read(cx);
 526            assert_eq!(
 527                pane.active_item().unwrap().project_path(cx),
 528                Some(file1.clone())
 529            );
 530            assert_eq!(pane.items().count(), 2);
 531        });
 532
 533        // Split the pane with the first entry, then open the second entry again.
 534        workspace
 535            .update(cx, |w, cx| {
 536                w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
 537                w.open_path(file2.clone(), true, cx)
 538            })
 539            .await
 540            .unwrap();
 541
 542        workspace.read_with(cx, |w, cx| {
 543            assert_eq!(
 544                w.active_pane()
 545                    .read(cx)
 546                    .active_item()
 547                    .unwrap()
 548                    .project_path(cx.as_ref()),
 549                Some(file2.clone())
 550            );
 551        });
 552
 553        // Open the third entry twice concurrently. Only one pane item is added.
 554        let (t1, t2) = workspace.update(cx, |w, cx| {
 555            (
 556                w.open_path(file3.clone(), true, cx),
 557                w.open_path(file3.clone(), true, cx),
 558            )
 559        });
 560        t1.await.unwrap();
 561        t2.await.unwrap();
 562        cx.read(|cx| {
 563            let pane = workspace.read(cx).active_pane().read(cx);
 564            assert_eq!(
 565                pane.active_item().unwrap().project_path(cx),
 566                Some(file3.clone())
 567            );
 568            let pane_entries = pane
 569                .items()
 570                .map(|i| i.project_path(cx).unwrap())
 571                .collect::<Vec<_>>();
 572            assert_eq!(pane_entries, &[file1, file2, file3]);
 573        });
 574    }
 575
 576    #[gpui::test]
 577    async fn test_open_paths(cx: &mut TestAppContext) {
 578        let app_state = init(cx);
 579
 580        let fs = app_state.fs.as_fake();
 581        fs.insert_dir("/dir1").await;
 582        fs.insert_dir("/dir2").await;
 583        fs.insert_dir("/dir3").await;
 584        fs.insert_file("/dir1/a.txt", "".into()).await;
 585        fs.insert_file("/dir2/b.txt", "".into()).await;
 586        fs.insert_file("/dir3/c.txt", "".into()).await;
 587        fs.insert_file("/d.txt", "".into()).await;
 588
 589        let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
 590        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
 591
 592        // Open a file within an existing worktree.
 593        cx.update(|cx| {
 594            workspace.update(cx, |view, cx| {
 595                view.open_paths(vec!["/dir1/a.txt".into()], true, cx)
 596            })
 597        })
 598        .await;
 599        cx.read(|cx| {
 600            assert_eq!(
 601                workspace
 602                    .read(cx)
 603                    .active_pane()
 604                    .read(cx)
 605                    .active_item()
 606                    .unwrap()
 607                    .to_any()
 608                    .downcast::<Editor>()
 609                    .unwrap()
 610                    .read(cx)
 611                    .title(cx),
 612                "a.txt"
 613            );
 614        });
 615
 616        // Open a file outside of any existing worktree.
 617        cx.update(|cx| {
 618            workspace.update(cx, |view, cx| {
 619                view.open_paths(vec!["/dir2/b.txt".into()], true, cx)
 620            })
 621        })
 622        .await;
 623        cx.read(|cx| {
 624            let worktree_roots = workspace
 625                .read(cx)
 626                .worktrees(cx)
 627                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
 628                .collect::<HashSet<_>>();
 629            assert_eq!(
 630                worktree_roots,
 631                vec!["/dir1", "/dir2/b.txt"]
 632                    .into_iter()
 633                    .map(Path::new)
 634                    .collect(),
 635            );
 636            assert_eq!(
 637                workspace
 638                    .read(cx)
 639                    .active_pane()
 640                    .read(cx)
 641                    .active_item()
 642                    .unwrap()
 643                    .to_any()
 644                    .downcast::<Editor>()
 645                    .unwrap()
 646                    .read(cx)
 647                    .title(cx),
 648                "b.txt"
 649            );
 650        });
 651
 652        // Ensure opening a directory and one of its children only adds one worktree.
 653        cx.update(|cx| {
 654            workspace.update(cx, |view, cx| {
 655                view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], true, cx)
 656            })
 657        })
 658        .await;
 659        cx.read(|cx| {
 660            let worktree_roots = workspace
 661                .read(cx)
 662                .worktrees(cx)
 663                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
 664                .collect::<HashSet<_>>();
 665            assert_eq!(
 666                worktree_roots,
 667                vec!["/dir1", "/dir2/b.txt", "/dir3"]
 668                    .into_iter()
 669                    .map(Path::new)
 670                    .collect(),
 671            );
 672            assert_eq!(
 673                workspace
 674                    .read(cx)
 675                    .active_pane()
 676                    .read(cx)
 677                    .active_item()
 678                    .unwrap()
 679                    .to_any()
 680                    .downcast::<Editor>()
 681                    .unwrap()
 682                    .read(cx)
 683                    .title(cx),
 684                "c.txt"
 685            );
 686        });
 687
 688        // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
 689        cx.update(|cx| {
 690            workspace.update(cx, |view, cx| {
 691                view.open_paths(vec!["/d.txt".into()], false, cx)
 692            })
 693        })
 694        .await;
 695        cx.read(|cx| {
 696            let worktree_roots = workspace
 697                .read(cx)
 698                .worktrees(cx)
 699                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
 700                .collect::<HashSet<_>>();
 701            assert_eq!(
 702                worktree_roots,
 703                vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
 704                    .into_iter()
 705                    .map(Path::new)
 706                    .collect(),
 707            );
 708
 709            let visible_worktree_roots = workspace
 710                .read(cx)
 711                .visible_worktrees(cx)
 712                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
 713                .collect::<HashSet<_>>();
 714            assert_eq!(
 715                visible_worktree_roots,
 716                vec!["/dir1", "/dir2/b.txt", "/dir3"]
 717                    .into_iter()
 718                    .map(Path::new)
 719                    .collect(),
 720            );
 721
 722            assert_eq!(
 723                workspace
 724                    .read(cx)
 725                    .active_pane()
 726                    .read(cx)
 727                    .active_item()
 728                    .unwrap()
 729                    .to_any()
 730                    .downcast::<Editor>()
 731                    .unwrap()
 732                    .read(cx)
 733                    .title(cx),
 734                "d.txt"
 735            );
 736        });
 737    }
 738
 739    #[gpui::test]
 740    async fn test_save_conflicting_item(cx: &mut TestAppContext) {
 741        let app_state = init(cx);
 742        app_state
 743            .fs
 744            .as_fake()
 745            .insert_tree("/root", json!({ "a.txt": "" }))
 746            .await;
 747
 748        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
 749        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
 750
 751        // Open a file within an existing worktree.
 752        cx.update(|cx| {
 753            workspace.update(cx, |view, cx| {
 754                view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx)
 755            })
 756        })
 757        .await;
 758        let editor = cx.read(|cx| {
 759            let pane = workspace.read(cx).active_pane().read(cx);
 760            let item = pane.active_item().unwrap();
 761            item.downcast::<Editor>().unwrap()
 762        });
 763
 764        cx.update(|cx| {
 765            editor.update(cx, |editor, cx| {
 766                editor.handle_input(&editor::Input("x".into()), cx)
 767            })
 768        });
 769        app_state
 770            .fs
 771            .as_fake()
 772            .insert_file("/root/a.txt", "changed".to_string())
 773            .await;
 774        editor
 775            .condition(&cx, |editor, cx| editor.has_conflict(cx))
 776            .await;
 777        cx.read(|cx| assert!(editor.is_dirty(cx)));
 778
 779        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
 780        cx.simulate_prompt_answer(window_id, 0);
 781        save_task.await.unwrap();
 782        editor.read_with(cx, |editor, cx| {
 783            assert!(!editor.is_dirty(cx));
 784            assert!(!editor.has_conflict(cx));
 785        });
 786    }
 787
 788    #[gpui::test]
 789    async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
 790        let app_state = init(cx);
 791        app_state.fs.as_fake().insert_dir("/root").await;
 792
 793        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
 794        project.update(cx, |project, _| project.languages().add(rust_lang()));
 795        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
 796        let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
 797
 798        // Create a new untitled buffer
 799        cx.dispatch_action(window_id, NewFile);
 800        let editor = workspace.read_with(cx, |workspace, cx| {
 801            workspace
 802                .active_item(cx)
 803                .unwrap()
 804                .downcast::<Editor>()
 805                .unwrap()
 806        });
 807
 808        editor.update(cx, |editor, cx| {
 809            assert!(!editor.is_dirty(cx));
 810            assert_eq!(editor.title(cx), "untitled");
 811            assert!(Arc::ptr_eq(
 812                editor.language_at(0, cx).unwrap(),
 813                &languages::PLAIN_TEXT
 814            ));
 815            editor.handle_input(&editor::Input("hi".into()), cx);
 816            assert!(editor.is_dirty(cx));
 817        });
 818
 819        // Save the buffer. This prompts for a filename.
 820        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
 821        cx.simulate_new_path_selection(|parent_dir| {
 822            assert_eq!(parent_dir, Path::new("/root"));
 823            Some(parent_dir.join("the-new-name.rs"))
 824        });
 825        cx.read(|cx| {
 826            assert!(editor.is_dirty(cx));
 827            assert_eq!(editor.read(cx).title(cx), "untitled");
 828        });
 829
 830        // When the save completes, the buffer's title is updated and the language is assigned based
 831        // on the path.
 832        save_task.await.unwrap();
 833        editor.read_with(cx, |editor, cx| {
 834            assert!(!editor.is_dirty(cx));
 835            assert_eq!(editor.title(cx), "the-new-name.rs");
 836            assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
 837        });
 838
 839        // Edit the file and save it again. This time, there is no filename prompt.
 840        editor.update(cx, |editor, cx| {
 841            editor.handle_input(&editor::Input(" there".into()), cx);
 842            assert_eq!(editor.is_dirty(cx.as_ref()), true);
 843        });
 844        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
 845        save_task.await.unwrap();
 846        assert!(!cx.did_prompt_for_new_path());
 847        editor.read_with(cx, |editor, cx| {
 848            assert!(!editor.is_dirty(cx));
 849            assert_eq!(editor.title(cx), "the-new-name.rs")
 850        });
 851
 852        // Open the same newly-created file in another pane item. The new editor should reuse
 853        // the same buffer.
 854        cx.dispatch_action(window_id, NewFile);
 855        workspace
 856            .update(cx, |workspace, cx| {
 857                workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
 858                workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), true, cx)
 859            })
 860            .await
 861            .unwrap();
 862        let editor2 = workspace.update(cx, |workspace, cx| {
 863            workspace
 864                .active_item(cx)
 865                .unwrap()
 866                .downcast::<Editor>()
 867                .unwrap()
 868        });
 869        cx.read(|cx| {
 870            assert_eq!(
 871                editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
 872                editor.read(cx).buffer().read(cx).as_singleton().unwrap()
 873            );
 874        })
 875    }
 876
 877    #[gpui::test]
 878    async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
 879        let app_state = init(cx);
 880        app_state.fs.as_fake().insert_dir("/root").await;
 881
 882        let project = Project::test(app_state.fs.clone(), [], cx).await;
 883        project.update(cx, |project, _| project.languages().add(rust_lang()));
 884        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
 885
 886        // Create a new untitled buffer
 887        cx.dispatch_action(window_id, NewFile);
 888        let editor = workspace.read_with(cx, |workspace, cx| {
 889            workspace
 890                .active_item(cx)
 891                .unwrap()
 892                .downcast::<Editor>()
 893                .unwrap()
 894        });
 895
 896        editor.update(cx, |editor, cx| {
 897            assert!(Arc::ptr_eq(
 898                editor.language_at(0, cx).unwrap(),
 899                &languages::PLAIN_TEXT
 900            ));
 901            editor.handle_input(&editor::Input("hi".into()), cx);
 902            assert!(editor.is_dirty(cx.as_ref()));
 903        });
 904
 905        // Save the buffer. This prompts for a filename.
 906        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
 907        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
 908        save_task.await.unwrap();
 909        // The buffer is not dirty anymore and the language is assigned based on the path.
 910        editor.read_with(cx, |editor, cx| {
 911            assert!(!editor.is_dirty(cx));
 912            assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
 913        });
 914    }
 915
 916    #[gpui::test]
 917    async fn test_pane_actions(cx: &mut TestAppContext) {
 918        init(cx);
 919
 920        let app_state = cx.update(AppState::test);
 921        app_state
 922            .fs
 923            .as_fake()
 924            .insert_tree(
 925                "/root",
 926                json!({
 927                    "a": {
 928                        "file1": "contents 1",
 929                        "file2": "contents 2",
 930                        "file3": "contents 3",
 931                    },
 932                }),
 933            )
 934            .await;
 935
 936        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
 937        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
 938
 939        let entries = cx.read(|cx| workspace.file_project_paths(cx));
 940        let file1 = entries[0].clone();
 941
 942        let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
 943
 944        workspace
 945            .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
 946            .await
 947            .unwrap();
 948
 949        let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| {
 950            let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
 951            assert_eq!(editor.project_path(cx), Some(file1.clone()));
 952            let buffer = editor.update(cx, |editor, cx| {
 953                editor.insert("dirt", cx);
 954                editor.buffer().downgrade()
 955            });
 956            (editor.downgrade(), buffer)
 957        });
 958
 959        cx.dispatch_action(window_id, pane::SplitRight);
 960        let editor_2 = cx.update(|cx| {
 961            let pane_2 = workspace.read(cx).active_pane().clone();
 962            assert_ne!(pane_1, pane_2);
 963
 964            let pane2_item = pane_2.read(cx).active_item().unwrap();
 965            assert_eq!(pane2_item.project_path(cx.as_ref()), Some(file1.clone()));
 966
 967            pane2_item.downcast::<Editor>().unwrap().downgrade()
 968        });
 969        cx.dispatch_action(window_id, workspace::CloseActiveItem);
 970
 971        cx.foreground().run_until_parked();
 972        workspace.read_with(cx, |workspace, _| {
 973            assert_eq!(workspace.panes().len(), 1);
 974            assert_eq!(workspace.active_pane(), &pane_1);
 975        });
 976
 977        cx.dispatch_action(window_id, workspace::CloseActiveItem);
 978        cx.foreground().run_until_parked();
 979        cx.simulate_prompt_answer(window_id, 1);
 980        cx.foreground().run_until_parked();
 981
 982        workspace.read_with(cx, |workspace, cx| {
 983            assert!(workspace.active_item(cx).is_none());
 984        });
 985
 986        cx.assert_dropped(editor_1);
 987        cx.assert_dropped(editor_2);
 988        cx.assert_dropped(buffer);
 989    }
 990
 991    #[gpui::test]
 992    async fn test_navigation(cx: &mut TestAppContext) {
 993        let app_state = init(cx);
 994        app_state
 995            .fs
 996            .as_fake()
 997            .insert_tree(
 998                "/root",
 999                json!({
1000                    "a": {
1001                        "file1": "contents 1\n".repeat(20),
1002                        "file2": "contents 2\n".repeat(20),
1003                        "file3": "contents 3\n".repeat(20),
1004                    },
1005                }),
1006            )
1007            .await;
1008
1009        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1010        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
1011
1012        let entries = cx.read(|cx| workspace.file_project_paths(cx));
1013        let file1 = entries[0].clone();
1014        let file2 = entries[1].clone();
1015        let file3 = entries[2].clone();
1016
1017        let editor1 = workspace
1018            .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
1019            .await
1020            .unwrap()
1021            .downcast::<Editor>()
1022            .unwrap();
1023        editor1.update(cx, |editor, cx| {
1024            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
1025                s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)])
1026            });
1027        });
1028        let editor2 = workspace
1029            .update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
1030            .await
1031            .unwrap()
1032            .downcast::<Editor>()
1033            .unwrap();
1034        let editor3 = workspace
1035            .update(cx, |w, cx| w.open_path(file3.clone(), true, cx))
1036            .await
1037            .unwrap()
1038            .downcast::<Editor>()
1039            .unwrap();
1040
1041        editor3
1042            .update(cx, |editor, cx| {
1043                editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
1044                    s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)])
1045                });
1046                editor.newline(&Default::default(), cx);
1047                editor.newline(&Default::default(), cx);
1048                editor.move_down(&Default::default(), cx);
1049                editor.move_down(&Default::default(), cx);
1050                editor.save(project.clone(), cx)
1051            })
1052            .await
1053            .unwrap();
1054        editor3.update(cx, |editor, cx| {
1055            editor.set_scroll_position(vec2f(0., 12.5), cx)
1056        });
1057        assert_eq!(
1058            active_location(&workspace, cx),
1059            (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1060        );
1061
1062        workspace
1063            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1064            .await;
1065        assert_eq!(
1066            active_location(&workspace, cx),
1067            (file3.clone(), DisplayPoint::new(0, 0), 0.)
1068        );
1069
1070        workspace
1071            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1072            .await;
1073        assert_eq!(
1074            active_location(&workspace, cx),
1075            (file2.clone(), DisplayPoint::new(0, 0), 0.)
1076        );
1077
1078        workspace
1079            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1080            .await;
1081        assert_eq!(
1082            active_location(&workspace, cx),
1083            (file1.clone(), DisplayPoint::new(10, 0), 0.)
1084        );
1085
1086        workspace
1087            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1088            .await;
1089        assert_eq!(
1090            active_location(&workspace, cx),
1091            (file1.clone(), DisplayPoint::new(0, 0), 0.)
1092        );
1093
1094        // Go back one more time and ensure we don't navigate past the first item in the history.
1095        workspace
1096            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1097            .await;
1098        assert_eq!(
1099            active_location(&workspace, cx),
1100            (file1.clone(), DisplayPoint::new(0, 0), 0.)
1101        );
1102
1103        workspace
1104            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1105            .await;
1106        assert_eq!(
1107            active_location(&workspace, cx),
1108            (file1.clone(), DisplayPoint::new(10, 0), 0.)
1109        );
1110
1111        workspace
1112            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1113            .await;
1114        assert_eq!(
1115            active_location(&workspace, cx),
1116            (file2.clone(), DisplayPoint::new(0, 0), 0.)
1117        );
1118
1119        // Go forward to an item that has been closed, ensuring it gets re-opened at the same
1120        // location.
1121        workspace
1122            .update(cx, |workspace, cx| {
1123                let editor3_id = editor3.id();
1124                drop(editor3);
1125                Pane::close_item(workspace, workspace.active_pane().clone(), editor3_id, cx)
1126            })
1127            .await
1128            .unwrap();
1129        workspace
1130            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1131            .await;
1132        assert_eq!(
1133            active_location(&workspace, cx),
1134            (file3.clone(), DisplayPoint::new(0, 0), 0.)
1135        );
1136
1137        workspace
1138            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1139            .await;
1140        assert_eq!(
1141            active_location(&workspace, cx),
1142            (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1143        );
1144
1145        workspace
1146            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1147            .await;
1148        assert_eq!(
1149            active_location(&workspace, cx),
1150            (file3.clone(), DisplayPoint::new(0, 0), 0.)
1151        );
1152
1153        // Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
1154        workspace
1155            .update(cx, |workspace, cx| {
1156                let editor2_id = editor2.id();
1157                drop(editor2);
1158                Pane::close_item(workspace, workspace.active_pane().clone(), editor2_id, cx)
1159            })
1160            .await
1161            .unwrap();
1162        app_state
1163            .fs
1164            .remove_file(Path::new("/root/a/file2"), Default::default())
1165            .await
1166            .unwrap();
1167        workspace
1168            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1169            .await;
1170        assert_eq!(
1171            active_location(&workspace, cx),
1172            (file1.clone(), DisplayPoint::new(10, 0), 0.)
1173        );
1174        workspace
1175            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1176            .await;
1177        assert_eq!(
1178            active_location(&workspace, cx),
1179            (file3.clone(), DisplayPoint::new(0, 0), 0.)
1180        );
1181
1182        // Modify file to collapse multiple nav history entries into the same location.
1183        // Ensure we don't visit the same location twice when navigating.
1184        editor1.update(cx, |editor, cx| {
1185            editor.change_selections(None, cx, |s| {
1186                s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)])
1187            })
1188        });
1189
1190        for _ in 0..5 {
1191            editor1.update(cx, |editor, cx| {
1192                editor.change_selections(None, cx, |s| {
1193                    s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
1194                });
1195            });
1196            editor1.update(cx, |editor, cx| {
1197                editor.change_selections(None, cx, |s| {
1198                    s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)])
1199                })
1200            });
1201        }
1202
1203        editor1.update(cx, |editor, cx| {
1204            editor.transact(cx, |editor, cx| {
1205                editor.change_selections(None, cx, |s| {
1206                    s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)])
1207                });
1208                editor.insert("", cx);
1209            })
1210        });
1211
1212        editor1.update(cx, |editor, cx| {
1213            editor.change_selections(None, cx, |s| {
1214                s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1215            })
1216        });
1217        workspace
1218            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1219            .await;
1220        assert_eq!(
1221            active_location(&workspace, cx),
1222            (file1.clone(), DisplayPoint::new(2, 0), 0.)
1223        );
1224        workspace
1225            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1226            .await;
1227        assert_eq!(
1228            active_location(&workspace, cx),
1229            (file1.clone(), DisplayPoint::new(3, 0), 0.)
1230        );
1231
1232        fn active_location(
1233            workspace: &ViewHandle<Workspace>,
1234            cx: &mut TestAppContext,
1235        ) -> (ProjectPath, DisplayPoint, f32) {
1236            workspace.update(cx, |workspace, cx| {
1237                let item = workspace.active_item(cx).unwrap();
1238                let editor = item.downcast::<Editor>().unwrap();
1239                let (selections, scroll_position) = editor.update(cx, |editor, cx| {
1240                    (
1241                        editor.selections.display_ranges(cx),
1242                        editor.scroll_position(cx),
1243                    )
1244                });
1245                (
1246                    item.project_path(cx).unwrap(),
1247                    selections[0].start,
1248                    scroll_position.y(),
1249                )
1250            })
1251        }
1252    }
1253
1254    #[gpui::test]
1255    fn test_bundled_themes(cx: &mut MutableAppContext) {
1256        let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
1257
1258        lazy_static::lazy_static! {
1259            static ref DEFAULT_THEME: parking_lot::Mutex<Option<Arc<Theme>>> = Default::default();
1260            static ref FONTS: Vec<Arc<Vec<u8>>> = vec![
1261                Assets.load("fonts/zed-sans/zed-sans-extended.ttf").unwrap().to_vec().into(),
1262                Assets.load("fonts/zed-mono/zed-mono-extended.ttf").unwrap().to_vec().into(),
1263            ];
1264        }
1265
1266        cx.platform().fonts().add_fonts(&FONTS).unwrap();
1267
1268        let mut has_default_theme = false;
1269        for theme_name in themes.list() {
1270            let theme = themes.get(&theme_name).unwrap();
1271            if theme.name == DEFAULT_THEME_NAME {
1272                has_default_theme = true;
1273            }
1274            assert_eq!(theme.name, theme_name);
1275        }
1276        assert!(has_default_theme);
1277    }
1278
1279    fn init(cx: &mut TestAppContext) -> Arc<AppState> {
1280        cx.foreground().forbid_parking();
1281        cx.update(|cx| {
1282            let mut app_state = AppState::test(cx);
1283            let state = Arc::get_mut(&mut app_state).unwrap();
1284            state.initialize_workspace = initialize_workspace;
1285            state.build_window_options = build_window_options;
1286            workspace::init(app_state.clone(), cx);
1287            editor::init(cx);
1288            pane::init(cx);
1289            app_state
1290        })
1291    }
1292
1293    fn rust_lang() -> Arc<language::Language> {
1294        Arc::new(language::Language::new(
1295            language::LanguageConfig {
1296                name: "Rust".into(),
1297                path_suffixes: vec!["rs".to_string()],
1298                ..Default::default()
1299            },
1300            Some(tree_sitter_rust::language()),
1301        ))
1302    }
1303}