zed.rs

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