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