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(¶ms, 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(¶ms, 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(¶ms, 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(¶ms, 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(¶ms, 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(¶ms, 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(¶ms, 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}