zed.rs

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