zed.rs

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