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