1mod feedback;
2pub mod languages;
3pub mod menus;
4pub mod settings_file;
5#[cfg(any(test, feature = "test-support"))]
6pub mod test;
7
8use anyhow::{anyhow, Context, Result};
9use breadcrumbs::Breadcrumbs;
10pub use client;
11pub use contacts_panel;
12use contacts_panel::ContactsPanel;
13pub use editor;
14use editor::Editor;
15use gpui::{
16 actions,
17 geometry::vector::vec2f,
18 impl_actions,
19 platform::{WindowBounds, WindowOptions},
20 AsyncAppContext, ViewContext,
21};
22use lazy_static::lazy_static;
23pub use lsp;
24use project::Project;
25pub use project::{self, fs};
26use project_panel::ProjectPanel;
27use search::{BufferSearchBar, ProjectSearchBar};
28use serde::Deserialize;
29use serde_json::to_string_pretty;
30use settings::{keymap_file_json_schema, settings_file_json_schema, Settings};
31use std::{
32 path::{Path, PathBuf},
33 sync::Arc,
34};
35use util::ResultExt;
36pub use workspace;
37use workspace::{AppState, Workspace};
38
39#[derive(Deserialize, Clone, PartialEq)]
40struct OpenBrowser {
41 url: Arc<str>,
42}
43
44impl_actions!(zed, [OpenBrowser]);
45
46actions!(
47 zed,
48 [
49 About,
50 Quit,
51 DebugElements,
52 OpenSettings,
53 OpenKeymap,
54 IncreaseBufferFontSize,
55 DecreaseBufferFontSize,
56 ResetBufferFontSize,
57 InstallCommandLineInterface,
58 ]
59);
60
61const MIN_FONT_SIZE: f32 = 6.0;
62
63lazy_static! {
64 pub static ref ROOT_PATH: PathBuf = dirs::home_dir()
65 .expect("failed to determine home directory")
66 .join(".zed");
67 pub static ref SETTINGS_PATH: PathBuf = ROOT_PATH.join("settings.json");
68 pub static ref KEYMAP_PATH: PathBuf = ROOT_PATH.join("keymap.json");
69}
70
71pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
72 cx.add_action(about);
73 cx.add_global_action(quit);
74 cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
75 cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
76 cx.update_global::<Settings, _, _>(|settings, cx| {
77 settings.buffer_font_size = (settings.buffer_font_size + 1.0).max(MIN_FONT_SIZE);
78 cx.refresh_windows();
79 });
80 });
81 cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| {
82 cx.update_global::<Settings, _, _>(|settings, cx| {
83 settings.buffer_font_size = (settings.buffer_font_size - 1.0).max(MIN_FONT_SIZE);
84 cx.refresh_windows();
85 });
86 });
87 cx.add_global_action(move |_: &ResetBufferFontSize, cx| {
88 cx.update_global::<Settings, _, _>(|settings, cx| {
89 settings.buffer_font_size = settings.default_buffer_font_size;
90 cx.refresh_windows();
91 });
92 });
93 cx.add_global_action(move |_: &InstallCommandLineInterface, cx| {
94 cx.spawn(|cx| async move { install_cli(&cx).await.context("error creating CLI symlink") })
95 .detach_and_log_err(cx);
96 });
97 cx.add_action({
98 let app_state = app_state.clone();
99 move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
100 open_config_file(&SETTINGS_PATH, app_state.clone(), cx);
101 }
102 });
103 cx.add_action({
104 let app_state = app_state.clone();
105 move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
106 open_config_file(&KEYMAP_PATH, app_state.clone(), cx);
107 }
108 });
109 cx.add_action(
110 |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
111 let content = to_string_pretty(&cx.debug_elements()).unwrap();
112 let project = workspace.project().clone();
113 let json_language = project.read(cx).languages().get_language("JSON").unwrap();
114 if project.read(cx).is_remote() {
115 cx.propagate_action();
116 } else if let Some(buffer) = project
117 .update(cx, |project, cx| {
118 project.create_buffer(&content, Some(json_language), cx)
119 })
120 .log_err()
121 {
122 workspace.add_item(
123 Box::new(
124 cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
125 ),
126 cx,
127 );
128 }
129 },
130 );
131
132 lsp_status::init(cx);
133 settings::KeymapFileContent::load_defaults(cx);
134}
135
136pub fn initialize_workspace(
137 workspace: &mut Workspace,
138 app_state: &Arc<AppState>,
139 cx: &mut ViewContext<Workspace>,
140) {
141 cx.subscribe(&cx.handle(), {
142 let project = workspace.project().clone();
143 move |_, _, event, cx| {
144 if let workspace::Event::PaneAdded(pane) = event {
145 pane.update(cx, |pane, cx| {
146 pane.toolbar().update(cx, |toolbar, cx| {
147 let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(project.clone()));
148 toolbar.add_item(breadcrumbs, cx);
149 let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx));
150 toolbar.add_item(buffer_search_bar, cx);
151 let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
152 toolbar.add_item(project_search_bar, cx);
153 })
154 });
155 }
156 }
157 })
158 .detach();
159
160 cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
161
162 let theme_names = app_state.themes.list().collect();
163 let language_names = app_state.languages.language_names();
164
165 workspace.project().update(cx, |project, cx| {
166 let action_names = cx.all_action_names().collect::<Vec<_>>();
167 project.set_language_server_settings(serde_json::json!({
168 "json": {
169 "format": {
170 "enable": true,
171 },
172 "schemas": [
173 {
174 "fileMatch": [".zed/settings.json"],
175 "schema": settings_file_json_schema(theme_names, language_names),
176 },
177 {
178 "fileMatch": [".zed/keymap.json"],
179 "schema": keymap_file_json_schema(&action_names),
180 }
181 ]
182 }
183 }));
184 });
185
186 let project_panel = ProjectPanel::new(workspace.project().clone(), cx);
187 let contact_panel = cx.add_view(|cx| {
188 ContactsPanel::new(
189 app_state.user_store.clone(),
190 app_state.project_store.clone(),
191 workspace.weak_handle(),
192 cx,
193 )
194 });
195
196 workspace.left_sidebar().update(cx, |sidebar, cx| {
197 sidebar.add_item(
198 "icons/folder-tree-solid-14.svg",
199 "Project Panel".to_string(),
200 project_panel.into(),
201 cx,
202 )
203 });
204 workspace.right_sidebar().update(cx, |sidebar, cx| {
205 sidebar.add_item(
206 "icons/contacts-solid-14.svg",
207 "Contacts Panel".to_string(),
208 contact_panel.into(),
209 cx,
210 )
211 });
212
213 let diagnostic_summary =
214 cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
215 let lsp_status = lsp_status::LspStatusItem::new(workspace, app_state.languages.clone(), cx);
216 let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
217 let auto_update = cx.add_view(|cx| auto_update::AutoUpdateIndicator::new(cx));
218 let feedback_link = cx.add_view(|_| feedback::FeedbackLink);
219 workspace.status_bar().update(cx, |status_bar, cx| {
220 status_bar.add_left_item(diagnostic_summary, cx);
221 status_bar.add_left_item(lsp_status, cx);
222 status_bar.add_right_item(cursor_position, cx);
223 status_bar.add_right_item(auto_update, cx);
224 status_bar.add_right_item(feedback_link, cx);
225 });
226
227 auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
228
229 cx.on_window_should_close(|workspace, cx| {
230 if let Some(task) = workspace.close(&Default::default(), cx) {
231 task.detach_and_log_err(cx);
232 }
233 false
234 });
235}
236
237pub fn build_window_options() -> WindowOptions<'static> {
238 WindowOptions {
239 bounds: WindowBounds::Maximized,
240 title: None,
241 titlebar_appears_transparent: true,
242 traffic_light_position: Some(vec2f(8., 8.)),
243 }
244}
245
246fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
247 let mut workspaces = cx
248 .window_ids()
249 .filter_map(|window_id| cx.root_view::<Workspace>(window_id))
250 .collect::<Vec<_>>();
251
252 // If multiple windows have unsaved changes, and need a save prompt,
253 // prompt in the active window before switching to a different window.
254 workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
255
256 cx.spawn(|mut cx| async move {
257 // If the user cancels any save prompt, then keep the app open.
258 for workspace in workspaces {
259 if !workspace
260 .update(&mut cx, |workspace, cx| workspace.prepare_to_close(cx))
261 .await?
262 {
263 return Ok(());
264 }
265 }
266 cx.platform().quit();
267 anyhow::Ok(())
268 })
269 .detach_and_log_err(cx);
270}
271
272fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
273 cx.prompt(
274 gpui::PromptLevel::Info,
275 &format!("Zed {}", env!("CARGO_PKG_VERSION")),
276 &["OK"],
277 );
278}
279
280async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
281 let cli_path = cx.platform().path_for_auxiliary_executable("cli")?;
282 let link_path = Path::new("/usr/local/bin/zed");
283 let bin_dir_path = link_path.parent().unwrap();
284
285 // Don't re-create symlink if it points to the same CLI binary.
286 if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) {
287 return Ok(());
288 }
289
290 // If the symlink is not there or is outdated, first try replacing it
291 // without escalating.
292 smol::fs::remove_file(link_path).await.log_err();
293 if smol::fs::unix::symlink(&cli_path, link_path)
294 .await
295 .log_err()
296 .is_some()
297 {
298 return Ok(());
299 }
300
301 // The symlink could not be created, so use osascript with admin privileges
302 // to create it.
303 let status = smol::process::Command::new("osascript")
304 .args([
305 "-e",
306 &format!(
307 "do shell script \" \
308 mkdir -p \'{}\' && \
309 ln -sf \'{}\' \'{}\' \
310 \" with administrator privileges",
311 bin_dir_path.to_string_lossy(),
312 cli_path.to_string_lossy(),
313 link_path.to_string_lossy(),
314 ),
315 ])
316 .stdout(smol::process::Stdio::inherit())
317 .stderr(smol::process::Stdio::inherit())
318 .output()
319 .await?
320 .status;
321 if status.success() {
322 Ok(())
323 } else {
324 Err(anyhow!("error running osascript"))
325 }
326}
327
328fn open_config_file(
329 path: &'static Path,
330 app_state: Arc<AppState>,
331 cx: &mut ViewContext<Workspace>,
332) {
333 cx.spawn(|workspace, mut cx| async move {
334 let fs = &app_state.fs;
335 if !fs.is_file(path).await {
336 fs.create_dir(&ROOT_PATH).await?;
337 fs.create_file(path, Default::default()).await?;
338 }
339
340 workspace
341 .update(&mut cx, |workspace, cx| {
342 if workspace.project().read(cx).is_local() {
343 workspace.open_paths(vec![path.to_path_buf()], false, cx)
344 } else {
345 let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
346 let mut workspace = Workspace::new(
347 Project::local(
348 false,
349 app_state.client.clone(),
350 app_state.user_store.clone(),
351 app_state.project_store.clone(),
352 app_state.languages.clone(),
353 app_state.fs.clone(),
354 cx,
355 ),
356 cx,
357 );
358 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
359 workspace
360 });
361 workspace.update(cx, |workspace, cx| {
362 workspace.open_paths(vec![path.to_path_buf()], false, cx)
363 })
364 }
365 })
366 .await;
367 Ok::<_, anyhow::Error>(())
368 })
369 .detach_and_log_err(cx)
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375 use assets::Assets;
376 use editor::{Autoscroll, DisplayPoint, Editor};
377 use gpui::{
378 executor::Deterministic, AssetSource, MutableAppContext, TestAppContext, ViewHandle,
379 };
380 use project::ProjectPath;
381 use serde_json::json;
382 use std::{
383 collections::HashSet,
384 path::{Path, PathBuf},
385 };
386 use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME};
387 use workspace::{
388 open_paths, pane, Item, ItemHandle, NewFile, Pane, SplitDirection, WorkspaceHandle,
389 };
390
391 #[gpui::test]
392 async fn test_open_paths_action(cx: &mut TestAppContext) {
393 let app_state = init(cx);
394 app_state
395 .fs
396 .as_fake()
397 .insert_tree(
398 "/root",
399 json!({
400 "a": {
401 "aa": null,
402 "ab": null,
403 },
404 "b": {
405 "ba": null,
406 "bb": null,
407 },
408 "c": {
409 "ca": null,
410 "cb": null,
411 },
412 }),
413 )
414 .await;
415
416 cx.update(|cx| {
417 open_paths(
418 &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
419 &app_state,
420 cx,
421 )
422 })
423 .await;
424 assert_eq!(cx.window_ids().len(), 1);
425
426 cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
427 .await;
428 assert_eq!(cx.window_ids().len(), 1);
429 let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
430 workspace_1.update(cx, |workspace, cx| {
431 assert_eq!(workspace.worktrees(cx).count(), 2);
432 assert!(workspace.left_sidebar().read(cx).active_item().is_some());
433 assert!(workspace.active_pane().is_focused(cx));
434 });
435
436 cx.update(|cx| {
437 open_paths(
438 &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
439 &app_state,
440 cx,
441 )
442 })
443 .await;
444 assert_eq!(cx.window_ids().len(), 2);
445 }
446
447 #[gpui::test]
448 async fn test_window_edit_state(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
449 let app_state = init(cx);
450 app_state
451 .fs
452 .as_fake()
453 .insert_tree("/root", json!({"a": "hey"}))
454 .await;
455
456 cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
457 .await;
458 assert_eq!(cx.window_ids().len(), 1);
459
460 // When opening the workspace, the window is not in a edited state.
461 let workspace = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
462 let editor = workspace.read_with(cx, |workspace, cx| {
463 workspace
464 .active_item(cx)
465 .unwrap()
466 .downcast::<Editor>()
467 .unwrap()
468 });
469 assert!(!cx.is_window_edited(workspace.window_id()));
470
471 // Editing a buffer marks the window as edited.
472 editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
473 assert!(cx.is_window_edited(workspace.window_id()));
474
475 // Undoing the edit restores the window's edited state.
476 editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
477 assert!(!cx.is_window_edited(workspace.window_id()));
478
479 // Redoing the edit marks the window as edited again.
480 editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
481 assert!(cx.is_window_edited(workspace.window_id()));
482
483 // Closing the item restores the window's edited state.
484 let close = workspace.update(cx, |workspace, cx| {
485 drop(editor);
486 Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
487 });
488 executor.run_until_parked();
489 cx.simulate_prompt_answer(workspace.window_id(), 1);
490 close.await.unwrap();
491 assert!(!cx.is_window_edited(workspace.window_id()));
492
493 // Opening the buffer again doesn't impact the window's edited state.
494 cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
495 .await;
496 let editor = workspace.read_with(cx, |workspace, cx| {
497 workspace
498 .active_item(cx)
499 .unwrap()
500 .downcast::<Editor>()
501 .unwrap()
502 });
503 assert!(!cx.is_window_edited(workspace.window_id()));
504
505 // Editing the buffer marks the window as edited.
506 editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
507 assert!(cx.is_window_edited(workspace.window_id()));
508
509 // Ensure closing the window via the mouse gets preempted due to the
510 // buffer having unsaved changes.
511 assert!(!cx.simulate_window_close(workspace.window_id()));
512 executor.run_until_parked();
513 assert_eq!(cx.window_ids().len(), 1);
514
515 // The window is successfully closed after the user dismisses the prompt.
516 cx.simulate_prompt_answer(workspace.window_id(), 1);
517 executor.run_until_parked();
518 assert_eq!(cx.window_ids().len(), 0);
519 }
520
521 #[gpui::test]
522 async fn test_new_empty_workspace(cx: &mut TestAppContext) {
523 let app_state = init(cx);
524 cx.dispatch_global_action(workspace::NewFile);
525 let window_id = *cx.window_ids().first().unwrap();
526 let workspace = cx.root_view::<Workspace>(window_id).unwrap();
527 let editor = workspace.update(cx, |workspace, cx| {
528 workspace
529 .active_item(cx)
530 .unwrap()
531 .downcast::<editor::Editor>()
532 .unwrap()
533 });
534
535 editor.update(cx, |editor, cx| {
536 assert!(editor.text(cx).is_empty());
537 });
538
539 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
540 app_state.fs.as_fake().insert_dir("/root").await;
541 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
542 save_task.await.unwrap();
543 editor.read_with(cx, |editor, cx| {
544 assert!(!editor.is_dirty(cx));
545 assert_eq!(editor.title(cx), "the-new-name");
546 });
547 }
548
549 #[gpui::test]
550 async fn test_open_entry(cx: &mut TestAppContext) {
551 let app_state = init(cx);
552 app_state
553 .fs
554 .as_fake()
555 .insert_tree(
556 "/root",
557 json!({
558 "a": {
559 "file1": "contents 1",
560 "file2": "contents 2",
561 "file3": "contents 3",
562 },
563 }),
564 )
565 .await;
566
567 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
568 let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
569
570 let entries = cx.read(|cx| workspace.file_project_paths(cx));
571 let file1 = entries[0].clone();
572 let file2 = entries[1].clone();
573 let file3 = entries[2].clone();
574
575 // Open the first entry
576 let entry_1 = workspace
577 .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
578 .await
579 .unwrap();
580 cx.read(|cx| {
581 let pane = workspace.read(cx).active_pane().read(cx);
582 assert_eq!(
583 pane.active_item().unwrap().project_path(cx),
584 Some(file1.clone())
585 );
586 assert_eq!(pane.items().count(), 1);
587 });
588
589 // Open the second entry
590 workspace
591 .update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
592 .await
593 .unwrap();
594 cx.read(|cx| {
595 let pane = workspace.read(cx).active_pane().read(cx);
596 assert_eq!(
597 pane.active_item().unwrap().project_path(cx),
598 Some(file2.clone())
599 );
600 assert_eq!(pane.items().count(), 2);
601 });
602
603 // Open the first entry again. The existing pane item is activated.
604 let entry_1b = workspace
605 .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
606 .await
607 .unwrap();
608 assert_eq!(entry_1.id(), entry_1b.id());
609
610 cx.read(|cx| {
611 let pane = workspace.read(cx).active_pane().read(cx);
612 assert_eq!(
613 pane.active_item().unwrap().project_path(cx),
614 Some(file1.clone())
615 );
616 assert_eq!(pane.items().count(), 2);
617 });
618
619 // Split the pane with the first entry, then open the second entry again.
620 workspace
621 .update(cx, |w, cx| {
622 w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
623 w.open_path(file2.clone(), true, cx)
624 })
625 .await
626 .unwrap();
627
628 workspace.read_with(cx, |w, cx| {
629 assert_eq!(
630 w.active_pane()
631 .read(cx)
632 .active_item()
633 .unwrap()
634 .project_path(cx.as_ref()),
635 Some(file2.clone())
636 );
637 });
638
639 // Open the third entry twice concurrently. Only one pane item is added.
640 let (t1, t2) = workspace.update(cx, |w, cx| {
641 (
642 w.open_path(file3.clone(), true, cx),
643 w.open_path(file3.clone(), true, cx),
644 )
645 });
646 t1.await.unwrap();
647 t2.await.unwrap();
648 cx.read(|cx| {
649 let pane = workspace.read(cx).active_pane().read(cx);
650 assert_eq!(
651 pane.active_item().unwrap().project_path(cx),
652 Some(file3.clone())
653 );
654 let pane_entries = pane
655 .items()
656 .map(|i| i.project_path(cx).unwrap())
657 .collect::<Vec<_>>();
658 assert_eq!(pane_entries, &[file1, file2, file3]);
659 });
660 }
661
662 #[gpui::test]
663 async fn test_open_paths(cx: &mut TestAppContext) {
664 let app_state = init(cx);
665
666 let fs = app_state.fs.as_fake();
667 fs.insert_dir("/dir1").await;
668 fs.insert_dir("/dir2").await;
669 fs.insert_dir("/dir3").await;
670 fs.insert_file("/dir1/a.txt", "".into()).await;
671 fs.insert_file("/dir2/b.txt", "".into()).await;
672 fs.insert_file("/dir3/c.txt", "".into()).await;
673 fs.insert_file("/d.txt", "".into()).await;
674
675 let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
676 let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
677
678 // Open a file within an existing worktree.
679 cx.update(|cx| {
680 workspace.update(cx, |view, cx| {
681 view.open_paths(vec!["/dir1/a.txt".into()], true, cx)
682 })
683 })
684 .await;
685 cx.read(|cx| {
686 assert_eq!(
687 workspace
688 .read(cx)
689 .active_pane()
690 .read(cx)
691 .active_item()
692 .unwrap()
693 .to_any()
694 .downcast::<Editor>()
695 .unwrap()
696 .read(cx)
697 .title(cx),
698 "a.txt"
699 );
700 });
701
702 // Open a file outside of any existing worktree.
703 cx.update(|cx| {
704 workspace.update(cx, |view, cx| {
705 view.open_paths(vec!["/dir2/b.txt".into()], true, cx)
706 })
707 })
708 .await;
709 cx.read(|cx| {
710 let worktree_roots = workspace
711 .read(cx)
712 .worktrees(cx)
713 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
714 .collect::<HashSet<_>>();
715 assert_eq!(
716 worktree_roots,
717 vec!["/dir1", "/dir2/b.txt"]
718 .into_iter()
719 .map(Path::new)
720 .collect(),
721 );
722 assert_eq!(
723 workspace
724 .read(cx)
725 .active_pane()
726 .read(cx)
727 .active_item()
728 .unwrap()
729 .to_any()
730 .downcast::<Editor>()
731 .unwrap()
732 .read(cx)
733 .title(cx),
734 "b.txt"
735 );
736 });
737
738 // Ensure opening a directory and one of its children only adds one worktree.
739 cx.update(|cx| {
740 workspace.update(cx, |view, cx| {
741 view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], true, cx)
742 })
743 })
744 .await;
745 cx.read(|cx| {
746 let worktree_roots = workspace
747 .read(cx)
748 .worktrees(cx)
749 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
750 .collect::<HashSet<_>>();
751 assert_eq!(
752 worktree_roots,
753 vec!["/dir1", "/dir2/b.txt", "/dir3"]
754 .into_iter()
755 .map(Path::new)
756 .collect(),
757 );
758 assert_eq!(
759 workspace
760 .read(cx)
761 .active_pane()
762 .read(cx)
763 .active_item()
764 .unwrap()
765 .to_any()
766 .downcast::<Editor>()
767 .unwrap()
768 .read(cx)
769 .title(cx),
770 "c.txt"
771 );
772 });
773
774 // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
775 cx.update(|cx| {
776 workspace.update(cx, |view, cx| {
777 view.open_paths(vec!["/d.txt".into()], false, cx)
778 })
779 })
780 .await;
781 cx.read(|cx| {
782 let worktree_roots = workspace
783 .read(cx)
784 .worktrees(cx)
785 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
786 .collect::<HashSet<_>>();
787 assert_eq!(
788 worktree_roots,
789 vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
790 .into_iter()
791 .map(Path::new)
792 .collect(),
793 );
794
795 let visible_worktree_roots = workspace
796 .read(cx)
797 .visible_worktrees(cx)
798 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
799 .collect::<HashSet<_>>();
800 assert_eq!(
801 visible_worktree_roots,
802 vec!["/dir1", "/dir2/b.txt", "/dir3"]
803 .into_iter()
804 .map(Path::new)
805 .collect(),
806 );
807
808 assert_eq!(
809 workspace
810 .read(cx)
811 .active_pane()
812 .read(cx)
813 .active_item()
814 .unwrap()
815 .to_any()
816 .downcast::<Editor>()
817 .unwrap()
818 .read(cx)
819 .title(cx),
820 "d.txt"
821 );
822 });
823 }
824
825 #[gpui::test]
826 async fn test_save_conflicting_item(cx: &mut TestAppContext) {
827 let app_state = init(cx);
828 app_state
829 .fs
830 .as_fake()
831 .insert_tree("/root", json!({ "a.txt": "" }))
832 .await;
833
834 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
835 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
836
837 // Open a file within an existing worktree.
838 cx.update(|cx| {
839 workspace.update(cx, |view, cx| {
840 view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx)
841 })
842 })
843 .await;
844 let editor = cx.read(|cx| {
845 let pane = workspace.read(cx).active_pane().read(cx);
846 let item = pane.active_item().unwrap();
847 item.downcast::<Editor>().unwrap()
848 });
849
850 cx.update(|cx| {
851 editor.update(cx, |editor, cx| {
852 editor.handle_input(&editor::Input("x".into()), cx)
853 })
854 });
855 app_state
856 .fs
857 .as_fake()
858 .insert_file("/root/a.txt", "changed".to_string())
859 .await;
860 editor
861 .condition(&cx, |editor, cx| editor.has_conflict(cx))
862 .await;
863 cx.read(|cx| assert!(editor.is_dirty(cx)));
864
865 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
866 cx.simulate_prompt_answer(window_id, 0);
867 save_task.await.unwrap();
868 editor.read_with(cx, |editor, cx| {
869 assert!(!editor.is_dirty(cx));
870 assert!(!editor.has_conflict(cx));
871 });
872 }
873
874 #[gpui::test]
875 async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
876 let app_state = init(cx);
877 app_state.fs.as_fake().insert_dir("/root").await;
878
879 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
880 project.update(cx, |project, _| project.languages().add(rust_lang()));
881 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
882 let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
883
884 // Create a new untitled buffer
885 cx.dispatch_action(window_id, NewFile);
886 let editor = workspace.read_with(cx, |workspace, cx| {
887 workspace
888 .active_item(cx)
889 .unwrap()
890 .downcast::<Editor>()
891 .unwrap()
892 });
893
894 editor.update(cx, |editor, cx| {
895 assert!(!editor.is_dirty(cx));
896 assert_eq!(editor.title(cx), "untitled");
897 assert!(Arc::ptr_eq(
898 editor.language_at(0, cx).unwrap(),
899 &languages::PLAIN_TEXT
900 ));
901 editor.handle_input(&editor::Input("hi".into()), cx);
902 assert!(editor.is_dirty(cx));
903 });
904
905 // Save the buffer. This prompts for a filename.
906 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
907 cx.simulate_new_path_selection(|parent_dir| {
908 assert_eq!(parent_dir, Path::new("/root"));
909 Some(parent_dir.join("the-new-name.rs"))
910 });
911 cx.read(|cx| {
912 assert!(editor.is_dirty(cx));
913 assert_eq!(editor.read(cx).title(cx), "untitled");
914 });
915
916 // When the save completes, the buffer's title is updated and the language is assigned based
917 // on the path.
918 save_task.await.unwrap();
919 editor.read_with(cx, |editor, cx| {
920 assert!(!editor.is_dirty(cx));
921 assert_eq!(editor.title(cx), "the-new-name.rs");
922 assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
923 });
924
925 // Edit the file and save it again. This time, there is no filename prompt.
926 editor.update(cx, |editor, cx| {
927 editor.handle_input(&editor::Input(" there".into()), cx);
928 assert_eq!(editor.is_dirty(cx.as_ref()), true);
929 });
930 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
931 save_task.await.unwrap();
932 assert!(!cx.did_prompt_for_new_path());
933 editor.read_with(cx, |editor, cx| {
934 assert!(!editor.is_dirty(cx));
935 assert_eq!(editor.title(cx), "the-new-name.rs")
936 });
937
938 // Open the same newly-created file in another pane item. The new editor should reuse
939 // the same buffer.
940 cx.dispatch_action(window_id, NewFile);
941 workspace
942 .update(cx, |workspace, cx| {
943 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
944 workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), true, cx)
945 })
946 .await
947 .unwrap();
948 let editor2 = workspace.update(cx, |workspace, cx| {
949 workspace
950 .active_item(cx)
951 .unwrap()
952 .downcast::<Editor>()
953 .unwrap()
954 });
955 cx.read(|cx| {
956 assert_eq!(
957 editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
958 editor.read(cx).buffer().read(cx).as_singleton().unwrap()
959 );
960 })
961 }
962
963 #[gpui::test]
964 async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
965 let app_state = init(cx);
966 app_state.fs.as_fake().insert_dir("/root").await;
967
968 let project = Project::test(app_state.fs.clone(), [], cx).await;
969 project.update(cx, |project, _| project.languages().add(rust_lang()));
970 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
971
972 // Create a new untitled buffer
973 cx.dispatch_action(window_id, NewFile);
974 let editor = workspace.read_with(cx, |workspace, cx| {
975 workspace
976 .active_item(cx)
977 .unwrap()
978 .downcast::<Editor>()
979 .unwrap()
980 });
981
982 editor.update(cx, |editor, cx| {
983 assert!(Arc::ptr_eq(
984 editor.language_at(0, cx).unwrap(),
985 &languages::PLAIN_TEXT
986 ));
987 editor.handle_input(&editor::Input("hi".into()), cx);
988 assert!(editor.is_dirty(cx.as_ref()));
989 });
990
991 // Save the buffer. This prompts for a filename.
992 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
993 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
994 save_task.await.unwrap();
995 // The buffer is not dirty anymore and the language is assigned based on the path.
996 editor.read_with(cx, |editor, cx| {
997 assert!(!editor.is_dirty(cx));
998 assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
999 });
1000 }
1001
1002 #[gpui::test]
1003 async fn test_pane_actions(cx: &mut TestAppContext) {
1004 init(cx);
1005
1006 let app_state = cx.update(AppState::test);
1007 app_state
1008 .fs
1009 .as_fake()
1010 .insert_tree(
1011 "/root",
1012 json!({
1013 "a": {
1014 "file1": "contents 1",
1015 "file2": "contents 2",
1016 "file3": "contents 3",
1017 },
1018 }),
1019 )
1020 .await;
1021
1022 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1023 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
1024
1025 let entries = cx.read(|cx| workspace.file_project_paths(cx));
1026 let file1 = entries[0].clone();
1027
1028 let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
1029
1030 workspace
1031 .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
1032 .await
1033 .unwrap();
1034
1035 let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| {
1036 let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
1037 assert_eq!(editor.project_path(cx), Some(file1.clone()));
1038 let buffer = editor.update(cx, |editor, cx| {
1039 editor.insert("dirt", cx);
1040 editor.buffer().downgrade()
1041 });
1042 (editor.downgrade(), buffer)
1043 });
1044
1045 cx.dispatch_action(window_id, pane::SplitRight);
1046 let editor_2 = cx.update(|cx| {
1047 let pane_2 = workspace.read(cx).active_pane().clone();
1048 assert_ne!(pane_1, pane_2);
1049
1050 let pane2_item = pane_2.read(cx).active_item().unwrap();
1051 assert_eq!(pane2_item.project_path(cx.as_ref()), Some(file1.clone()));
1052
1053 pane2_item.downcast::<Editor>().unwrap().downgrade()
1054 });
1055 cx.dispatch_action(window_id, workspace::CloseActiveItem);
1056
1057 cx.foreground().run_until_parked();
1058 workspace.read_with(cx, |workspace, _| {
1059 assert_eq!(workspace.panes().len(), 1);
1060 assert_eq!(workspace.active_pane(), &pane_1);
1061 });
1062
1063 cx.dispatch_action(window_id, workspace::CloseActiveItem);
1064 cx.foreground().run_until_parked();
1065 cx.simulate_prompt_answer(window_id, 1);
1066 cx.foreground().run_until_parked();
1067
1068 workspace.read_with(cx, |workspace, cx| {
1069 assert!(workspace.active_item(cx).is_none());
1070 });
1071
1072 cx.assert_dropped(editor_1);
1073 cx.assert_dropped(editor_2);
1074 cx.assert_dropped(buffer);
1075 }
1076
1077 #[gpui::test]
1078 async fn test_navigation(cx: &mut TestAppContext) {
1079 let app_state = init(cx);
1080 app_state
1081 .fs
1082 .as_fake()
1083 .insert_tree(
1084 "/root",
1085 json!({
1086 "a": {
1087 "file1": "contents 1\n".repeat(20),
1088 "file2": "contents 2\n".repeat(20),
1089 "file3": "contents 3\n".repeat(20),
1090 },
1091 }),
1092 )
1093 .await;
1094
1095 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1096 let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
1097
1098 let entries = cx.read(|cx| workspace.file_project_paths(cx));
1099 let file1 = entries[0].clone();
1100 let file2 = entries[1].clone();
1101 let file3 = entries[2].clone();
1102
1103 let editor1 = workspace
1104 .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
1105 .await
1106 .unwrap()
1107 .downcast::<Editor>()
1108 .unwrap();
1109 editor1.update(cx, |editor, cx| {
1110 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
1111 s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)])
1112 });
1113 });
1114 let editor2 = workspace
1115 .update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
1116 .await
1117 .unwrap()
1118 .downcast::<Editor>()
1119 .unwrap();
1120 let editor3 = workspace
1121 .update(cx, |w, cx| w.open_path(file3.clone(), true, cx))
1122 .await
1123 .unwrap()
1124 .downcast::<Editor>()
1125 .unwrap();
1126
1127 editor3
1128 .update(cx, |editor, cx| {
1129 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
1130 s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)])
1131 });
1132 editor.newline(&Default::default(), cx);
1133 editor.newline(&Default::default(), cx);
1134 editor.move_down(&Default::default(), cx);
1135 editor.move_down(&Default::default(), cx);
1136 editor.save(project.clone(), cx)
1137 })
1138 .await
1139 .unwrap();
1140 editor3.update(cx, |editor, cx| {
1141 editor.set_scroll_position(vec2f(0., 12.5), cx)
1142 });
1143 assert_eq!(
1144 active_location(&workspace, cx),
1145 (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1146 );
1147
1148 workspace
1149 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1150 .await;
1151 assert_eq!(
1152 active_location(&workspace, cx),
1153 (file3.clone(), DisplayPoint::new(0, 0), 0.)
1154 );
1155
1156 workspace
1157 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1158 .await;
1159 assert_eq!(
1160 active_location(&workspace, cx),
1161 (file2.clone(), DisplayPoint::new(0, 0), 0.)
1162 );
1163
1164 workspace
1165 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1166 .await;
1167 assert_eq!(
1168 active_location(&workspace, cx),
1169 (file1.clone(), DisplayPoint::new(10, 0), 0.)
1170 );
1171
1172 workspace
1173 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1174 .await;
1175 assert_eq!(
1176 active_location(&workspace, cx),
1177 (file1.clone(), DisplayPoint::new(0, 0), 0.)
1178 );
1179
1180 // Go back one more time and ensure we don't navigate past the first item in the history.
1181 workspace
1182 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1183 .await;
1184 assert_eq!(
1185 active_location(&workspace, cx),
1186 (file1.clone(), DisplayPoint::new(0, 0), 0.)
1187 );
1188
1189 workspace
1190 .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1191 .await;
1192 assert_eq!(
1193 active_location(&workspace, cx),
1194 (file1.clone(), DisplayPoint::new(10, 0), 0.)
1195 );
1196
1197 workspace
1198 .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1199 .await;
1200 assert_eq!(
1201 active_location(&workspace, cx),
1202 (file2.clone(), DisplayPoint::new(0, 0), 0.)
1203 );
1204
1205 // Go forward to an item that has been closed, ensuring it gets re-opened at the same
1206 // location.
1207 workspace
1208 .update(cx, |workspace, cx| {
1209 let editor3_id = editor3.id();
1210 drop(editor3);
1211 Pane::close_item(workspace, workspace.active_pane().clone(), editor3_id, cx)
1212 })
1213 .await
1214 .unwrap();
1215 workspace
1216 .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1217 .await;
1218 assert_eq!(
1219 active_location(&workspace, cx),
1220 (file3.clone(), DisplayPoint::new(0, 0), 0.)
1221 );
1222
1223 workspace
1224 .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1225 .await;
1226 assert_eq!(
1227 active_location(&workspace, cx),
1228 (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1229 );
1230
1231 workspace
1232 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1233 .await;
1234 assert_eq!(
1235 active_location(&workspace, cx),
1236 (file3.clone(), DisplayPoint::new(0, 0), 0.)
1237 );
1238
1239 // Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
1240 workspace
1241 .update(cx, |workspace, cx| {
1242 let editor2_id = editor2.id();
1243 drop(editor2);
1244 Pane::close_item(workspace, workspace.active_pane().clone(), editor2_id, cx)
1245 })
1246 .await
1247 .unwrap();
1248 app_state
1249 .fs
1250 .remove_file(Path::new("/root/a/file2"), Default::default())
1251 .await
1252 .unwrap();
1253 workspace
1254 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1255 .await;
1256 assert_eq!(
1257 active_location(&workspace, cx),
1258 (file1.clone(), DisplayPoint::new(10, 0), 0.)
1259 );
1260 workspace
1261 .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1262 .await;
1263 assert_eq!(
1264 active_location(&workspace, cx),
1265 (file3.clone(), DisplayPoint::new(0, 0), 0.)
1266 );
1267
1268 // Modify file to collapse multiple nav history entries into the same location.
1269 // Ensure we don't visit the same location twice when navigating.
1270 editor1.update(cx, |editor, cx| {
1271 editor.change_selections(None, cx, |s| {
1272 s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)])
1273 })
1274 });
1275
1276 for _ in 0..5 {
1277 editor1.update(cx, |editor, cx| {
1278 editor.change_selections(None, cx, |s| {
1279 s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
1280 });
1281 });
1282 editor1.update(cx, |editor, cx| {
1283 editor.change_selections(None, cx, |s| {
1284 s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)])
1285 })
1286 });
1287 }
1288
1289 editor1.update(cx, |editor, cx| {
1290 editor.transact(cx, |editor, cx| {
1291 editor.change_selections(None, cx, |s| {
1292 s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)])
1293 });
1294 editor.insert("", cx);
1295 })
1296 });
1297
1298 editor1.update(cx, |editor, cx| {
1299 editor.change_selections(None, cx, |s| {
1300 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1301 })
1302 });
1303 workspace
1304 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1305 .await;
1306 assert_eq!(
1307 active_location(&workspace, cx),
1308 (file1.clone(), DisplayPoint::new(2, 0), 0.)
1309 );
1310 workspace
1311 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1312 .await;
1313 assert_eq!(
1314 active_location(&workspace, cx),
1315 (file1.clone(), DisplayPoint::new(3, 0), 0.)
1316 );
1317
1318 fn active_location(
1319 workspace: &ViewHandle<Workspace>,
1320 cx: &mut TestAppContext,
1321 ) -> (ProjectPath, DisplayPoint, f32) {
1322 workspace.update(cx, |workspace, cx| {
1323 let item = workspace.active_item(cx).unwrap();
1324 let editor = item.downcast::<Editor>().unwrap();
1325 let (selections, scroll_position) = editor.update(cx, |editor, cx| {
1326 (
1327 editor.selections.display_ranges(cx),
1328 editor.scroll_position(cx),
1329 )
1330 });
1331 (
1332 item.project_path(cx).unwrap(),
1333 selections[0].start,
1334 scroll_position.y(),
1335 )
1336 })
1337 }
1338 }
1339
1340 #[gpui::test]
1341 async fn test_reopening_closed_items(cx: &mut TestAppContext) {
1342 let app_state = init(cx);
1343 app_state
1344 .fs
1345 .as_fake()
1346 .insert_tree(
1347 "/root",
1348 json!({
1349 "a": {
1350 "file1": "",
1351 "file2": "",
1352 "file3": "",
1353 "file4": "",
1354 },
1355 }),
1356 )
1357 .await;
1358
1359 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1360 let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
1361 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1362
1363 let entries = cx.read(|cx| workspace.file_project_paths(cx));
1364 let file1 = entries[0].clone();
1365 let file2 = entries[1].clone();
1366 let file3 = entries[2].clone();
1367 let file4 = entries[3].clone();
1368
1369 let file1_item_id = workspace
1370 .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
1371 .await
1372 .unwrap()
1373 .id();
1374 let file2_item_id = workspace
1375 .update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
1376 .await
1377 .unwrap()
1378 .id();
1379 let file3_item_id = workspace
1380 .update(cx, |w, cx| w.open_path(file3.clone(), true, cx))
1381 .await
1382 .unwrap()
1383 .id();
1384 let file4_item_id = workspace
1385 .update(cx, |w, cx| w.open_path(file4.clone(), true, cx))
1386 .await
1387 .unwrap()
1388 .id();
1389 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1390
1391 // Close all the pane items in some arbitrary order.
1392 workspace
1393 .update(cx, |workspace, cx| {
1394 Pane::close_item(workspace, pane.clone(), file1_item_id, cx)
1395 })
1396 .await
1397 .unwrap();
1398 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1399
1400 workspace
1401 .update(cx, |workspace, cx| {
1402 Pane::close_item(workspace, pane.clone(), file4_item_id, cx)
1403 })
1404 .await
1405 .unwrap();
1406 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1407
1408 workspace
1409 .update(cx, |workspace, cx| {
1410 Pane::close_item(workspace, pane.clone(), file2_item_id, cx)
1411 })
1412 .await
1413 .unwrap();
1414 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1415
1416 workspace
1417 .update(cx, |workspace, cx| {
1418 Pane::close_item(workspace, pane.clone(), file3_item_id, cx)
1419 })
1420 .await
1421 .unwrap();
1422 assert_eq!(active_path(&workspace, cx), None);
1423
1424 // Reopen all the closed items, ensuring they are reopened in the same order
1425 // in which they were closed.
1426 workspace
1427 .update(cx, |workspace, cx| Pane::reopen_closed_item(workspace, cx))
1428 .await;
1429 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1430
1431 workspace
1432 .update(cx, |workspace, cx| Pane::reopen_closed_item(workspace, cx))
1433 .await;
1434 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1435
1436 workspace
1437 .update(cx, |workspace, cx| Pane::reopen_closed_item(workspace, cx))
1438 .await;
1439 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1440
1441 workspace
1442 .update(cx, |workspace, cx| Pane::reopen_closed_item(workspace, cx))
1443 .await;
1444 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1445
1446 // Reopening past the last closed item is a no-op.
1447 workspace
1448 .update(cx, |workspace, cx| Pane::reopen_closed_item(workspace, cx))
1449 .await;
1450 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1451
1452 // Reopening closed items doesn't interfere with navigation history.
1453 workspace
1454 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1455 .await;
1456 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1457
1458 workspace
1459 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1460 .await;
1461 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1462
1463 workspace
1464 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1465 .await;
1466 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1467
1468 workspace
1469 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1470 .await;
1471 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1472
1473 workspace
1474 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1475 .await;
1476 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1477
1478 workspace
1479 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1480 .await;
1481 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1482
1483 workspace
1484 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1485 .await;
1486 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1487
1488 workspace
1489 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1490 .await;
1491 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1492
1493 fn active_path(
1494 workspace: &ViewHandle<Workspace>,
1495 cx: &TestAppContext,
1496 ) -> Option<ProjectPath> {
1497 workspace.read_with(cx, |workspace, cx| {
1498 let item = workspace.active_item(cx)?;
1499 item.project_path(cx)
1500 })
1501 }
1502 }
1503
1504 #[gpui::test]
1505 fn test_bundled_themes(cx: &mut MutableAppContext) {
1506 let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
1507
1508 lazy_static::lazy_static! {
1509 static ref DEFAULT_THEME: parking_lot::Mutex<Option<Arc<Theme>>> = Default::default();
1510 static ref FONTS: Vec<Arc<Vec<u8>>> = vec![
1511 Assets.load("fonts/zed-sans/zed-sans-extended.ttf").unwrap().to_vec().into(),
1512 Assets.load("fonts/zed-mono/zed-mono-extended.ttf").unwrap().to_vec().into(),
1513 ];
1514 }
1515
1516 cx.platform().fonts().add_fonts(&FONTS).unwrap();
1517
1518 let mut has_default_theme = false;
1519 for theme_name in themes.list() {
1520 let theme = themes.get(&theme_name).unwrap();
1521 if theme.name == DEFAULT_THEME_NAME {
1522 has_default_theme = true;
1523 }
1524 assert_eq!(theme.name, theme_name);
1525 }
1526 assert!(has_default_theme);
1527 }
1528
1529 fn init(cx: &mut TestAppContext) -> Arc<AppState> {
1530 cx.foreground().forbid_parking();
1531 cx.update(|cx| {
1532 let mut app_state = AppState::test(cx);
1533 let state = Arc::get_mut(&mut app_state).unwrap();
1534 state.initialize_workspace = initialize_workspace;
1535 state.build_window_options = build_window_options;
1536 workspace::init(app_state.clone(), cx);
1537 editor::init(cx);
1538 pane::init(cx);
1539 app_state
1540 })
1541 }
1542
1543 fn rust_lang() -> Arc<language::Language> {
1544 Arc::new(language::Language::new(
1545 language::LanguageConfig {
1546 name: "Rust".into(),
1547 path_suffixes: vec!["rs".to_string()],
1548 ..Default::default()
1549 },
1550 Some(tree_sitter_rust::language()),
1551 ))
1552 }
1553}