zed.rs

   1pub mod languages;
   2pub mod menus;
   3pub mod settings_file;
   4#[cfg(any(test, feature = "test-support"))]
   5pub mod test;
   6
   7use anyhow::Context;
   8use breadcrumbs::Breadcrumbs;
   9use chat_panel::ChatPanel;
  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    platform::{WindowBounds, WindowOptions},
  19    ModelHandle, ViewContext,
  20};
  21use lazy_static::lazy_static;
  22pub use lsp;
  23use project::Project;
  24pub use project::{self, fs};
  25use project_panel::ProjectPanel;
  26use search::{BufferSearchBar, ProjectSearchBar};
  27use serde_json::to_string_pretty;
  28use settings::Settings;
  29use std::{path::PathBuf, sync::Arc};
  30use util::ResultExt;
  31pub use workspace;
  32use workspace::{AppState, Workspace, WorkspaceParams};
  33
  34actions!(
  35    zed,
  36    [
  37        About,
  38        Quit,
  39        DebugElements,
  40        OpenSettings,
  41        IncreaseBufferFontSize,
  42        DecreaseBufferFontSize,
  43        InstallCommandLineTool,
  44    ]
  45);
  46
  47const MIN_FONT_SIZE: f32 = 6.0;
  48
  49lazy_static! {
  50    pub static ref ROOT_PATH: PathBuf = dirs::home_dir()
  51        .expect("failed to determine home directory")
  52        .join(".zed");
  53    pub static ref SETTINGS_PATH: PathBuf = ROOT_PATH.join("settings.json");
  54    pub static ref KEYMAP_PATH: PathBuf = ROOT_PATH.join("keymap.json");
  55}
  56
  57pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
  58    cx.add_global_action(quit);
  59    cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
  60        cx.update_global::<Settings, _, _>(|settings, cx| {
  61            settings.buffer_font_size = (settings.buffer_font_size + 1.0).max(MIN_FONT_SIZE);
  62            cx.refresh_windows();
  63        });
  64    });
  65    cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| {
  66        cx.update_global::<Settings, _, _>(|settings, cx| {
  67            settings.buffer_font_size = (settings.buffer_font_size - 1.0).max(MIN_FONT_SIZE);
  68            cx.refresh_windows();
  69        });
  70    });
  71    cx.add_global_action(move |_: &InstallCommandLineTool, cx| {
  72        cx.spawn(|cx| async move {
  73            log::info!("installing command line launcher");
  74            let cli_path = cx
  75                .platform()
  76                .path_for_auxiliary_executable("cli")
  77                .log_err()?;
  78            let link_path = "/opt/homebrew/bin/zed";
  79            smol::fs::unix::symlink(cli_path.as_path(), link_path)
  80                .await
  81                .context("failed to install cli symlink")
  82                .log_err()?;
  83            log::info!(
  84                "created symlink {} -> {}",
  85                link_path,
  86                cli_path.to_string_lossy()
  87            );
  88            Some(())
  89        })
  90        .detach();
  91    });
  92    cx.add_action({
  93        let app_state = app_state.clone();
  94        move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
  95            let app_state = app_state.clone();
  96            cx.spawn(move |workspace, mut cx| async move {
  97                let fs = &app_state.fs;
  98                if !fs.is_file(&SETTINGS_PATH).await {
  99                    fs.create_dir(&ROOT_PATH).await?;
 100                    fs.create_file(&SETTINGS_PATH, Default::default()).await?;
 101                }
 102
 103                workspace
 104                    .update(&mut cx, |workspace, cx| {
 105                        if workspace.project().read(cx).is_local() {
 106                            workspace.open_paths(&[SETTINGS_PATH.clone()], cx)
 107                        } else {
 108                            let (_, workspace) =
 109                                cx.add_window((app_state.build_window_options)(), |cx| {
 110                                    let project = Project::local(
 111                                        app_state.client.clone(),
 112                                        app_state.user_store.clone(),
 113                                        app_state.languages.clone(),
 114                                        app_state.fs.clone(),
 115                                        cx,
 116                                    );
 117                                    (app_state.build_workspace)(project, &app_state, cx)
 118                                });
 119                            workspace.update(cx, |workspace, cx| {
 120                                workspace.open_paths(&[SETTINGS_PATH.clone()], cx)
 121                            })
 122                        }
 123                    })
 124                    .await;
 125                Ok::<_, anyhow::Error>(())
 126            })
 127            .detach_and_log_err(cx);
 128        }
 129    });
 130    cx.add_action(
 131        |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
 132            let content = to_string_pretty(&cx.debug_elements()).unwrap();
 133            let project = workspace.project().clone();
 134            let json_language = project.read(cx).languages().get_language("JSON").unwrap();
 135            if project.read(cx).is_remote() {
 136                cx.propagate_action();
 137            } else if let Some(buffer) = project
 138                .update(cx, |project, cx| {
 139                    project.create_buffer(&content, Some(json_language), cx)
 140                })
 141                .log_err()
 142            {
 143                workspace.add_item(
 144                    Box::new(
 145                        cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
 146                    ),
 147                    cx,
 148                );
 149            }
 150        },
 151    );
 152
 153    workspace::lsp_status::init(cx);
 154
 155    settings::KeymapFile::load_defaults(cx);
 156}
 157
 158pub fn build_workspace(
 159    project: ModelHandle<Project>,
 160    app_state: &Arc<AppState>,
 161    cx: &mut ViewContext<Workspace>,
 162) -> Workspace {
 163    cx.subscribe(&cx.handle(), {
 164        let project = project.clone();
 165        move |_, _, event, cx| {
 166            let workspace::Event::PaneAdded(pane) = event;
 167            pane.update(cx, |pane, cx| {
 168                pane.toolbar().update(cx, |toolbar, cx| {
 169                    let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(project.clone()));
 170                    toolbar.add_item(breadcrumbs, cx);
 171                    let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx));
 172                    toolbar.add_item(buffer_search_bar, cx);
 173                    let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
 174                    toolbar.add_item(project_search_bar, cx);
 175                })
 176            });
 177        }
 178    })
 179    .detach();
 180
 181    let workspace_params = WorkspaceParams {
 182        project,
 183        client: app_state.client.clone(),
 184        fs: app_state.fs.clone(),
 185        languages: app_state.languages.clone(),
 186        themes: app_state.themes.clone(),
 187        user_store: app_state.user_store.clone(),
 188        channel_list: app_state.channel_list.clone(),
 189    };
 190    let mut workspace = Workspace::new(&workspace_params, cx);
 191    let project = workspace.project().clone();
 192
 193    let theme_names = app_state.themes.list().collect();
 194    let language_names = app_state.languages.language_names();
 195
 196    project.update(cx, |project, _| {
 197        project.set_language_server_settings(serde_json::json!({
 198            "json": {
 199                "schemas": [
 200                    {
 201                        "fileMatch": "**/.zed/settings.json",
 202                        "schema": Settings::file_json_schema(theme_names, language_names),
 203                    }
 204                ]
 205            }
 206        }));
 207    });
 208
 209    workspace.left_sidebar_mut().add_item(
 210        "icons/folder-tree-16.svg",
 211        ProjectPanel::new(project, cx).into(),
 212    );
 213    workspace.right_sidebar_mut().add_item(
 214        "icons/user-16.svg",
 215        cx.add_view(|cx| ContactsPanel::new(app_state.clone(), cx))
 216            .into(),
 217    );
 218    workspace.right_sidebar_mut().add_item(
 219        "icons/comment-16.svg",
 220        cx.add_view(|cx| {
 221            ChatPanel::new(app_state.client.clone(), app_state.channel_list.clone(), cx)
 222        })
 223        .into(),
 224    );
 225
 226    let diagnostic_message = cx.add_view(|_| editor::items::DiagnosticMessage::new());
 227    let diagnostic_summary =
 228        cx.add_view(|cx| diagnostics::items::DiagnosticSummary::new(workspace.project(), cx));
 229    let lsp_status = cx.add_view(|cx| {
 230        workspace::lsp_status::LspStatus::new(workspace.project(), app_state.languages.clone(), cx)
 231    });
 232    let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
 233    workspace.status_bar().update(cx, |status_bar, cx| {
 234        status_bar.add_left_item(diagnostic_summary, cx);
 235        status_bar.add_left_item(diagnostic_message, cx);
 236        status_bar.add_left_item(lsp_status, cx);
 237        status_bar.add_right_item(cursor_position, cx);
 238    });
 239
 240    workspace
 241}
 242
 243pub fn build_window_options() -> WindowOptions<'static> {
 244    WindowOptions {
 245        bounds: WindowBounds::Maximized,
 246        title: None,
 247        titlebar_appears_transparent: true,
 248        traffic_light_position: Some(vec2f(8., 8.)),
 249    }
 250}
 251
 252fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
 253    cx.platform().quit();
 254}
 255
 256#[cfg(test)]
 257mod tests {
 258    use super::*;
 259    use assets::Assets;
 260    use editor::{DisplayPoint, Editor};
 261    use gpui::{AssetSource, MutableAppContext, TestAppContext, ViewHandle};
 262    use project::{Fs, ProjectPath};
 263    use serde_json::json;
 264    use std::{
 265        collections::HashSet,
 266        path::{Path, PathBuf},
 267    };
 268    use test::test_app_state;
 269    use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME};
 270    use util::test::temp_tree;
 271    use workspace::{
 272        open_paths, pane, Item, ItemHandle, OpenNew, Pane, SplitDirection, WorkspaceHandle,
 273    };
 274
 275    #[gpui::test]
 276    async fn test_open_paths_action(cx: &mut TestAppContext) {
 277        let app_state = cx.update(test_app_state);
 278        let dir = temp_tree(json!({
 279            "a": {
 280                "aa": null,
 281                "ab": null,
 282            },
 283            "b": {
 284                "ba": null,
 285                "bb": null,
 286            },
 287            "c": {
 288                "ca": null,
 289                "cb": null,
 290            },
 291        }));
 292
 293        cx.update(|cx| {
 294            open_paths(
 295                &[
 296                    dir.path().join("a").to_path_buf(),
 297                    dir.path().join("b").to_path_buf(),
 298                ],
 299                &app_state,
 300                cx,
 301            )
 302        })
 303        .await;
 304        assert_eq!(cx.window_ids().len(), 1);
 305
 306        cx.update(|cx| open_paths(&[dir.path().join("a").to_path_buf()], &app_state, cx))
 307            .await;
 308        assert_eq!(cx.window_ids().len(), 1);
 309        let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
 310        workspace_1.read_with(cx, |workspace, cx| {
 311            assert_eq!(workspace.worktrees(cx).count(), 2)
 312        });
 313
 314        cx.update(|cx| {
 315            open_paths(
 316                &[
 317                    dir.path().join("b").to_path_buf(),
 318                    dir.path().join("c").to_path_buf(),
 319                ],
 320                &app_state,
 321                cx,
 322            )
 323        })
 324        .await;
 325        assert_eq!(cx.window_ids().len(), 2);
 326    }
 327
 328    #[gpui::test]
 329    async fn test_new_empty_workspace(cx: &mut TestAppContext) {
 330        let app_state = cx.update(test_app_state);
 331        cx.update(|cx| {
 332            workspace::init(&app_state.client, cx);
 333        });
 334        cx.dispatch_global_action(workspace::OpenNew(app_state.clone()));
 335        let window_id = *cx.window_ids().first().unwrap();
 336        let workspace = cx.root_view::<Workspace>(window_id).unwrap();
 337        let editor = workspace.update(cx, |workspace, cx| {
 338            workspace
 339                .active_item(cx)
 340                .unwrap()
 341                .downcast::<editor::Editor>()
 342                .unwrap()
 343        });
 344
 345        editor.update(cx, |editor, cx| {
 346            assert!(editor.text(cx).is_empty());
 347        });
 348
 349        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(cx));
 350        app_state.fs.as_fake().insert_dir("/root").await;
 351        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
 352        save_task.await.unwrap();
 353        editor.read_with(cx, |editor, cx| {
 354            assert!(!editor.is_dirty(cx));
 355            assert_eq!(editor.title(cx), "the-new-name");
 356        });
 357    }
 358
 359    #[gpui::test]
 360    async fn test_open_entry(cx: &mut TestAppContext) {
 361        let app_state = cx.update(test_app_state);
 362        app_state
 363            .fs
 364            .as_fake()
 365            .insert_tree(
 366                "/root",
 367                json!({
 368                    "a": {
 369                        "file1": "contents 1",
 370                        "file2": "contents 2",
 371                        "file3": "contents 3",
 372                    },
 373                }),
 374            )
 375            .await;
 376        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
 377        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
 378        params
 379            .project
 380            .update(cx, |project, cx| {
 381                project.find_or_create_local_worktree("/root", true, cx)
 382            })
 383            .await
 384            .unwrap();
 385
 386        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
 387            .await;
 388        let entries = cx.read(|cx| workspace.file_project_paths(cx));
 389        let file1 = entries[0].clone();
 390        let file2 = entries[1].clone();
 391        let file3 = entries[2].clone();
 392
 393        // Open the first entry
 394        let entry_1 = workspace
 395            .update(cx, |w, cx| w.open_path(file1.clone(), cx))
 396            .await
 397            .unwrap();
 398        cx.read(|cx| {
 399            let pane = workspace.read(cx).active_pane().read(cx);
 400            assert_eq!(
 401                pane.active_item().unwrap().project_path(cx),
 402                Some(file1.clone())
 403            );
 404            assert_eq!(pane.items().count(), 1);
 405        });
 406
 407        // Open the second entry
 408        workspace
 409            .update(cx, |w, cx| w.open_path(file2.clone(), cx))
 410            .await
 411            .unwrap();
 412        cx.read(|cx| {
 413            let pane = workspace.read(cx).active_pane().read(cx);
 414            assert_eq!(
 415                pane.active_item().unwrap().project_path(cx),
 416                Some(file2.clone())
 417            );
 418            assert_eq!(pane.items().count(), 2);
 419        });
 420
 421        // Open the first entry again. The existing pane item is activated.
 422        let entry_1b = workspace
 423            .update(cx, |w, cx| w.open_path(file1.clone(), cx))
 424            .await
 425            .unwrap();
 426        assert_eq!(entry_1.id(), entry_1b.id());
 427
 428        cx.read(|cx| {
 429            let pane = workspace.read(cx).active_pane().read(cx);
 430            assert_eq!(
 431                pane.active_item().unwrap().project_path(cx),
 432                Some(file1.clone())
 433            );
 434            assert_eq!(pane.items().count(), 2);
 435        });
 436
 437        // Split the pane with the first entry, then open the second entry again.
 438        workspace
 439            .update(cx, |w, cx| {
 440                w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
 441                w.open_path(file2.clone(), cx)
 442            })
 443            .await
 444            .unwrap();
 445
 446        workspace.read_with(cx, |w, cx| {
 447            assert_eq!(
 448                w.active_pane()
 449                    .read(cx)
 450                    .active_item()
 451                    .unwrap()
 452                    .project_path(cx.as_ref()),
 453                Some(file2.clone())
 454            );
 455        });
 456
 457        // Open the third entry twice concurrently. Only one pane item is added.
 458        let (t1, t2) = workspace.update(cx, |w, cx| {
 459            (
 460                w.open_path(file3.clone(), cx),
 461                w.open_path(file3.clone(), cx),
 462            )
 463        });
 464        t1.await.unwrap();
 465        t2.await.unwrap();
 466        cx.read(|cx| {
 467            let pane = workspace.read(cx).active_pane().read(cx);
 468            assert_eq!(
 469                pane.active_item().unwrap().project_path(cx),
 470                Some(file3.clone())
 471            );
 472            let pane_entries = pane
 473                .items()
 474                .map(|i| i.project_path(cx).unwrap())
 475                .collect::<Vec<_>>();
 476            assert_eq!(pane_entries, &[file1, file2, file3]);
 477        });
 478    }
 479
 480    #[gpui::test]
 481    async fn test_open_paths(cx: &mut TestAppContext) {
 482        let app_state = cx.update(test_app_state);
 483        let fs = app_state.fs.as_fake();
 484        fs.insert_dir("/dir1").await;
 485        fs.insert_dir("/dir2").await;
 486        fs.insert_file("/dir1/a.txt", "".into()).await;
 487        fs.insert_file("/dir2/b.txt", "".into()).await;
 488
 489        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
 490        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
 491        params
 492            .project
 493            .update(cx, |project, cx| {
 494                project.find_or_create_local_worktree("/dir1", true, cx)
 495            })
 496            .await
 497            .unwrap();
 498        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
 499            .await;
 500
 501        // Open a file within an existing worktree.
 502        cx.update(|cx| {
 503            workspace.update(cx, |view, cx| view.open_paths(&["/dir1/a.txt".into()], cx))
 504        })
 505        .await;
 506        cx.read(|cx| {
 507            assert_eq!(
 508                workspace
 509                    .read(cx)
 510                    .active_pane()
 511                    .read(cx)
 512                    .active_item()
 513                    .unwrap()
 514                    .to_any()
 515                    .downcast::<Editor>()
 516                    .unwrap()
 517                    .read(cx)
 518                    .title(cx),
 519                "a.txt"
 520            );
 521        });
 522
 523        // Open a file outside of any existing worktree.
 524        cx.update(|cx| {
 525            workspace.update(cx, |view, cx| view.open_paths(&["/dir2/b.txt".into()], cx))
 526        })
 527        .await;
 528        cx.read(|cx| {
 529            let worktree_roots = workspace
 530                .read(cx)
 531                .worktrees(cx)
 532                .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
 533                .collect::<HashSet<_>>();
 534            assert_eq!(
 535                worktree_roots,
 536                vec!["/dir1", "/dir2/b.txt"]
 537                    .into_iter()
 538                    .map(Path::new)
 539                    .collect(),
 540            );
 541            assert_eq!(
 542                workspace
 543                    .read(cx)
 544                    .active_pane()
 545                    .read(cx)
 546                    .active_item()
 547                    .unwrap()
 548                    .to_any()
 549                    .downcast::<Editor>()
 550                    .unwrap()
 551                    .read(cx)
 552                    .title(cx),
 553                "b.txt"
 554            );
 555        });
 556    }
 557
 558    #[gpui::test]
 559    async fn test_save_conflicting_item(cx: &mut TestAppContext) {
 560        let app_state = cx.update(test_app_state);
 561        let fs = app_state.fs.as_fake();
 562        fs.insert_tree("/root", json!({ "a.txt": "" })).await;
 563
 564        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
 565        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
 566        params
 567            .project
 568            .update(cx, |project, cx| {
 569                project.find_or_create_local_worktree("/root", true, cx)
 570            })
 571            .await
 572            .unwrap();
 573
 574        // Open a file within an existing worktree.
 575        cx.update(|cx| {
 576            workspace.update(cx, |view, cx| {
 577                view.open_paths(&[PathBuf::from("/root/a.txt")], cx)
 578            })
 579        })
 580        .await;
 581        let editor = cx.read(|cx| {
 582            let pane = workspace.read(cx).active_pane().read(cx);
 583            let item = pane.active_item().unwrap();
 584            item.downcast::<Editor>().unwrap()
 585        });
 586
 587        cx.update(|cx| {
 588            editor.update(cx, |editor, cx| {
 589                editor.handle_input(&editor::Input("x".into()), cx)
 590            })
 591        });
 592        fs.insert_file("/root/a.txt", "changed".to_string()).await;
 593        editor
 594            .condition(&cx, |editor, cx| editor.has_conflict(cx))
 595            .await;
 596        cx.read(|cx| assert!(editor.is_dirty(cx)));
 597
 598        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(cx));
 599        cx.simulate_prompt_answer(window_id, 0);
 600        save_task.await.unwrap();
 601        editor.read_with(cx, |editor, cx| {
 602            assert!(!editor.is_dirty(cx));
 603            assert!(!editor.has_conflict(cx));
 604        });
 605    }
 606
 607    #[gpui::test]
 608    async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
 609        let app_state = cx.update(test_app_state);
 610        app_state.fs.as_fake().insert_dir("/root").await;
 611        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
 612        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
 613        params
 614            .project
 615            .update(cx, |project, cx| {
 616                project.find_or_create_local_worktree("/root", true, cx)
 617            })
 618            .await
 619            .unwrap();
 620        let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
 621
 622        // Create a new untitled buffer
 623        cx.dispatch_action(window_id, OpenNew(app_state.clone()));
 624        let editor = workspace.read_with(cx, |workspace, cx| {
 625            workspace
 626                .active_item(cx)
 627                .unwrap()
 628                .downcast::<Editor>()
 629                .unwrap()
 630        });
 631
 632        editor.update(cx, |editor, cx| {
 633            assert!(!editor.is_dirty(cx));
 634            assert_eq!(editor.title(cx), "untitled");
 635            assert!(Arc::ptr_eq(
 636                editor.language_at(0, cx).unwrap(),
 637                &languages::PLAIN_TEXT
 638            ));
 639            editor.handle_input(&editor::Input("hi".into()), cx);
 640            assert!(editor.is_dirty(cx));
 641        });
 642
 643        // Save the buffer. This prompts for a filename.
 644        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(cx));
 645        cx.simulate_new_path_selection(|parent_dir| {
 646            assert_eq!(parent_dir, Path::new("/root"));
 647            Some(parent_dir.join("the-new-name.rs"))
 648        });
 649        cx.read(|cx| {
 650            assert!(editor.is_dirty(cx));
 651            assert_eq!(editor.read(cx).title(cx), "untitled");
 652        });
 653
 654        // When the save completes, the buffer's title is updated and the language is assigned based
 655        // on the path.
 656        save_task.await.unwrap();
 657        editor.read_with(cx, |editor, cx| {
 658            assert!(!editor.is_dirty(cx));
 659            assert_eq!(editor.title(cx), "the-new-name.rs");
 660            assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
 661        });
 662
 663        // Edit the file and save it again. This time, there is no filename prompt.
 664        editor.update(cx, |editor, cx| {
 665            editor.handle_input(&editor::Input(" there".into()), cx);
 666            assert_eq!(editor.is_dirty(cx.as_ref()), true);
 667        });
 668        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(cx));
 669        save_task.await.unwrap();
 670        assert!(!cx.did_prompt_for_new_path());
 671        editor.read_with(cx, |editor, cx| {
 672            assert!(!editor.is_dirty(cx));
 673            assert_eq!(editor.title(cx), "the-new-name.rs")
 674        });
 675
 676        // Open the same newly-created file in another pane item. The new editor should reuse
 677        // the same buffer.
 678        cx.dispatch_action(window_id, OpenNew(app_state.clone()));
 679        workspace
 680            .update(cx, |workspace, cx| {
 681                workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
 682                workspace.open_path(
 683                    ProjectPath {
 684                        worktree_id: worktree.read(cx).id(),
 685                        path: Path::new("the-new-name.rs").into(),
 686                    },
 687                    cx,
 688                )
 689            })
 690            .await
 691            .unwrap();
 692        let editor2 = workspace.update(cx, |workspace, cx| {
 693            workspace
 694                .active_item(cx)
 695                .unwrap()
 696                .downcast::<Editor>()
 697                .unwrap()
 698        });
 699        cx.read(|cx| {
 700            assert_eq!(
 701                editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
 702                editor.read(cx).buffer().read(cx).as_singleton().unwrap()
 703            );
 704        })
 705    }
 706
 707    #[gpui::test]
 708    async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
 709        let app_state = cx.update(test_app_state);
 710        app_state.fs.as_fake().insert_dir("/root").await;
 711        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
 712        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
 713
 714        // Create a new untitled buffer
 715        cx.dispatch_action(window_id, OpenNew(app_state.clone()));
 716        let editor = workspace.read_with(cx, |workspace, cx| {
 717            workspace
 718                .active_item(cx)
 719                .unwrap()
 720                .downcast::<Editor>()
 721                .unwrap()
 722        });
 723
 724        editor.update(cx, |editor, cx| {
 725            assert!(Arc::ptr_eq(
 726                editor.language_at(0, cx).unwrap(),
 727                &languages::PLAIN_TEXT
 728            ));
 729            editor.handle_input(&editor::Input("hi".into()), cx);
 730            assert!(editor.is_dirty(cx.as_ref()));
 731        });
 732
 733        // Save the buffer. This prompts for a filename.
 734        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(cx));
 735        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
 736        save_task.await.unwrap();
 737        // The buffer is not dirty anymore and the language is assigned based on the path.
 738        editor.read_with(cx, |editor, cx| {
 739            assert!(!editor.is_dirty(cx));
 740            assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
 741        });
 742    }
 743
 744    #[gpui::test]
 745    async fn test_pane_actions(cx: &mut TestAppContext) {
 746        cx.foreground().forbid_parking();
 747
 748        cx.update(|cx| pane::init(cx));
 749        let app_state = cx.update(test_app_state);
 750        app_state
 751            .fs
 752            .as_fake()
 753            .insert_tree(
 754                "/root",
 755                json!({
 756                    "a": {
 757                        "file1": "contents 1",
 758                        "file2": "contents 2",
 759                        "file3": "contents 3",
 760                    },
 761                }),
 762            )
 763            .await;
 764
 765        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
 766        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
 767        params
 768            .project
 769            .update(cx, |project, cx| {
 770                project.find_or_create_local_worktree("/root", true, cx)
 771            })
 772            .await
 773            .unwrap();
 774        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
 775            .await;
 776        let entries = cx.read(|cx| workspace.file_project_paths(cx));
 777        let file1 = entries[0].clone();
 778
 779        let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
 780
 781        workspace
 782            .update(cx, |w, cx| w.open_path(file1.clone(), cx))
 783            .await
 784            .unwrap();
 785
 786        let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| {
 787            let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
 788            assert_eq!(editor.project_path(cx), Some(file1.clone()));
 789            let buffer = editor.update(cx, |editor, cx| {
 790                editor.insert("dirt", cx);
 791                editor.buffer().downgrade()
 792            });
 793            (editor.downgrade(), buffer)
 794        });
 795
 796        cx.dispatch_action(window_id, pane::Split(SplitDirection::Right));
 797        let editor_2 = cx.update(|cx| {
 798            let pane_2 = workspace.read(cx).active_pane().clone();
 799            assert_ne!(pane_1, pane_2);
 800
 801            let pane2_item = pane_2.read(cx).active_item().unwrap();
 802            assert_eq!(pane2_item.project_path(cx.as_ref()), Some(file1.clone()));
 803
 804            pane2_item.downcast::<Editor>().unwrap().downgrade()
 805        });
 806        cx.dispatch_action(window_id, workspace::CloseActiveItem);
 807
 808        cx.foreground().run_until_parked();
 809        workspace.read_with(cx, |workspace, _| {
 810            assert_eq!(workspace.panes().len(), 1);
 811            assert_eq!(workspace.active_pane(), &pane_1);
 812        });
 813
 814        cx.dispatch_action(window_id, workspace::CloseActiveItem);
 815        cx.foreground().run_until_parked();
 816        cx.simulate_prompt_answer(window_id, 1);
 817        cx.foreground().run_until_parked();
 818
 819        workspace.read_with(cx, |workspace, cx| {
 820            assert!(workspace.active_item(cx).is_none());
 821        });
 822
 823        cx.assert_dropped(editor_1);
 824        cx.assert_dropped(editor_2);
 825        cx.assert_dropped(buffer);
 826    }
 827
 828    #[gpui::test]
 829    async fn test_navigation(cx: &mut TestAppContext) {
 830        let app_state = cx.update(test_app_state);
 831        app_state
 832            .fs
 833            .as_fake()
 834            .insert_tree(
 835                "/root",
 836                json!({
 837                    "a": {
 838                        "file1": "contents 1\n".repeat(20),
 839                        "file2": "contents 2\n".repeat(20),
 840                        "file3": "contents 3\n".repeat(20),
 841                    },
 842                }),
 843            )
 844            .await;
 845        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
 846        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
 847        params
 848            .project
 849            .update(cx, |project, cx| {
 850                project.find_or_create_local_worktree("/root", true, cx)
 851            })
 852            .await
 853            .unwrap();
 854        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
 855            .await;
 856        let entries = cx.read(|cx| workspace.file_project_paths(cx));
 857        let file1 = entries[0].clone();
 858        let file2 = entries[1].clone();
 859        let file3 = entries[2].clone();
 860
 861        let editor1 = workspace
 862            .update(cx, |w, cx| w.open_path(file1.clone(), cx))
 863            .await
 864            .unwrap()
 865            .downcast::<Editor>()
 866            .unwrap();
 867        editor1.update(cx, |editor, cx| {
 868            editor.select_display_ranges(&[DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)], cx);
 869        });
 870        let editor2 = workspace
 871            .update(cx, |w, cx| w.open_path(file2.clone(), cx))
 872            .await
 873            .unwrap()
 874            .downcast::<Editor>()
 875            .unwrap();
 876        let editor3 = workspace
 877            .update(cx, |w, cx| w.open_path(file3.clone(), cx))
 878            .await
 879            .unwrap()
 880            .downcast::<Editor>()
 881            .unwrap();
 882        editor3.update(cx, |editor, cx| {
 883            editor.select_display_ranges(&[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)], cx);
 884        });
 885        assert_eq!(
 886            active_location(&workspace, cx),
 887            (file3.clone(), DisplayPoint::new(15, 0))
 888        );
 889
 890        workspace
 891            .update(cx, |w, cx| Pane::go_back(w, None, cx))
 892            .await;
 893        assert_eq!(
 894            active_location(&workspace, cx),
 895            (file3.clone(), DisplayPoint::new(0, 0))
 896        );
 897
 898        workspace
 899            .update(cx, |w, cx| Pane::go_back(w, None, cx))
 900            .await;
 901        assert_eq!(
 902            active_location(&workspace, cx),
 903            (file2.clone(), DisplayPoint::new(0, 0))
 904        );
 905
 906        workspace
 907            .update(cx, |w, cx| Pane::go_back(w, None, cx))
 908            .await;
 909        assert_eq!(
 910            active_location(&workspace, cx),
 911            (file1.clone(), DisplayPoint::new(10, 0))
 912        );
 913
 914        workspace
 915            .update(cx, |w, cx| Pane::go_back(w, None, cx))
 916            .await;
 917        assert_eq!(
 918            active_location(&workspace, cx),
 919            (file1.clone(), DisplayPoint::new(0, 0))
 920        );
 921
 922        // Go back one more time and ensure we don't navigate past the first item in the history.
 923        workspace
 924            .update(cx, |w, cx| Pane::go_back(w, None, cx))
 925            .await;
 926        assert_eq!(
 927            active_location(&workspace, cx),
 928            (file1.clone(), DisplayPoint::new(0, 0))
 929        );
 930
 931        workspace
 932            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
 933            .await;
 934        assert_eq!(
 935            active_location(&workspace, cx),
 936            (file1.clone(), DisplayPoint::new(10, 0))
 937        );
 938
 939        workspace
 940            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
 941            .await;
 942        assert_eq!(
 943            active_location(&workspace, cx),
 944            (file2.clone(), DisplayPoint::new(0, 0))
 945        );
 946
 947        // Go forward to an item that has been closed, ensuring it gets re-opened at the same
 948        // location.
 949        workspace
 950            .update(cx, |workspace, cx| {
 951                let editor3_id = editor3.id();
 952                drop(editor3);
 953                Pane::close_item(workspace, workspace.active_pane().clone(), editor3_id, cx)
 954            })
 955            .await
 956            .unwrap();
 957        workspace
 958            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
 959            .await;
 960        assert_eq!(
 961            active_location(&workspace, cx),
 962            (file3.clone(), DisplayPoint::new(0, 0))
 963        );
 964
 965        // Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
 966        workspace
 967            .update(cx, |workspace, cx| {
 968                let editor2_id = editor2.id();
 969                drop(editor2);
 970                Pane::close_item(workspace, workspace.active_pane().clone(), editor2_id, cx)
 971            })
 972            .await
 973            .unwrap();
 974        app_state
 975            .fs
 976            .as_fake()
 977            .remove_file(Path::new("/root/a/file2"), Default::default())
 978            .await
 979            .unwrap();
 980        workspace
 981            .update(cx, |w, cx| Pane::go_back(w, None, cx))
 982            .await;
 983        assert_eq!(
 984            active_location(&workspace, cx),
 985            (file1.clone(), DisplayPoint::new(10, 0))
 986        );
 987        workspace
 988            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
 989            .await;
 990        assert_eq!(
 991            active_location(&workspace, cx),
 992            (file3.clone(), DisplayPoint::new(0, 0))
 993        );
 994
 995        // Modify file to remove nav history location, and ensure duplicates are skipped
 996        editor1.update(cx, |editor, cx| {
 997            editor.select_display_ranges(&[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)], cx)
 998        });
 999
1000        for _ in 0..5 {
1001            editor1.update(cx, |editor, cx| {
1002                editor
1003                    .select_display_ranges(&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)], cx);
1004            });
1005            editor1.update(cx, |editor, cx| {
1006                editor.select_display_ranges(
1007                    &[DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)],
1008                    cx,
1009                )
1010            });
1011        }
1012
1013        editor1.update(cx, |editor, cx| {
1014            editor.transact(cx, |editor, cx| {
1015                editor.select_display_ranges(
1016                    &[DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)],
1017                    cx,
1018                );
1019                editor.insert("", cx);
1020            })
1021        });
1022
1023        editor1.update(cx, |editor, cx| {
1024            editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx)
1025        });
1026        workspace
1027            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1028            .await;
1029        assert_eq!(
1030            active_location(&workspace, cx),
1031            (file1.clone(), DisplayPoint::new(2, 0))
1032        );
1033        workspace
1034            .update(cx, |w, cx| Pane::go_back(w, None, cx))
1035            .await;
1036        assert_eq!(
1037            active_location(&workspace, cx),
1038            (file1.clone(), DisplayPoint::new(3, 0))
1039        );
1040
1041        fn active_location(
1042            workspace: &ViewHandle<Workspace>,
1043            cx: &mut TestAppContext,
1044        ) -> (ProjectPath, DisplayPoint) {
1045            workspace.update(cx, |workspace, cx| {
1046                let item = workspace.active_item(cx).unwrap();
1047                let editor = item.downcast::<Editor>().unwrap();
1048                let selections = editor.update(cx, |editor, cx| editor.selected_display_ranges(cx));
1049                (item.project_path(cx).unwrap(), selections[0].start)
1050            })
1051        }
1052    }
1053
1054    #[gpui::test]
1055    fn test_bundled_themes(cx: &mut MutableAppContext) {
1056        let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
1057
1058        lazy_static::lazy_static! {
1059            static ref DEFAULT_THEME: parking_lot::Mutex<Option<Arc<Theme>>> = Default::default();
1060            static ref FONTS: Vec<Arc<Vec<u8>>> = vec![
1061                Assets.load("fonts/zed-sans/zed-sans-extended.ttf").unwrap().to_vec().into(),
1062                Assets.load("fonts/zed-mono/zed-mono-extended.ttf").unwrap().to_vec().into(),
1063            ];
1064        }
1065
1066        cx.platform().fonts().add_fonts(&FONTS).unwrap();
1067
1068        let mut has_default_theme = false;
1069        for theme_name in themes.list() {
1070            let theme = themes.get(&theme_name).unwrap();
1071            if theme.name == DEFAULT_THEME_NAME {
1072                has_default_theme = true;
1073            }
1074            assert_eq!(theme.name, theme_name);
1075        }
1076        assert!(has_default_theme);
1077    }
1078}