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