1pub mod assets;
2pub mod language;
3pub mod menus;
4#[cfg(any(test, feature = "test-support"))]
5pub mod test;
6
7use self::language::LanguageRegistry;
8use chat_panel::ChatPanel;
9pub use client;
10pub use editor;
11use gpui::{
12 action,
13 geometry::vector::vec2f,
14 keymap::Binding,
15 platform::{WindowBounds, WindowOptions},
16 ModelHandle, MutableAppContext, PathPromptOptions, Task, ViewContext,
17};
18pub use lsp;
19use parking_lot::Mutex;
20pub use people_panel;
21use people_panel::PeoplePanel;
22use postage::watch;
23pub use project::{self, fs};
24use project_panel::ProjectPanel;
25use std::{path::PathBuf, sync::Arc};
26use theme::ThemeRegistry;
27use theme_selector::ThemeSelectorParams;
28pub use workspace;
29use workspace::{OpenNew, Settings, Workspace, WorkspaceParams};
30
31action!(About);
32action!(Open, Arc<AppState>);
33action!(OpenPaths, OpenParams);
34action!(Quit);
35action!(AdjustBufferFontSize, f32);
36
37const MIN_FONT_SIZE: f32 = 6.0;
38
39pub struct AppState {
40 pub settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
41 pub settings: watch::Receiver<Settings>,
42 pub languages: Arc<LanguageRegistry>,
43 pub themes: Arc<ThemeRegistry>,
44 pub client: Arc<client::Client>,
45 pub user_store: ModelHandle<client::UserStore>,
46 pub fs: Arc<dyn fs::Fs>,
47 pub channel_list: ModelHandle<client::ChannelList>,
48 pub entry_openers: Arc<[Box<dyn workspace::EntryOpener>]>,
49}
50
51#[derive(Clone)]
52pub struct OpenParams {
53 pub paths: Vec<PathBuf>,
54 pub app_state: Arc<AppState>,
55}
56
57pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
58 cx.add_global_action(open);
59 cx.add_global_action(|action: &OpenPaths, cx: &mut MutableAppContext| {
60 open_paths(action, cx).detach()
61 });
62 cx.add_global_action(open_new);
63 cx.add_global_action(quit);
64
65 cx.add_global_action({
66 let settings_tx = app_state.settings_tx.clone();
67
68 move |action: &AdjustBufferFontSize, cx| {
69 let mut settings_tx = settings_tx.lock();
70 let new_size = (settings_tx.borrow().buffer_font_size + action.0).max(MIN_FONT_SIZE);
71 settings_tx.borrow_mut().buffer_font_size = new_size;
72 cx.refresh_windows();
73 }
74 });
75
76 cx.add_bindings(vec![
77 Binding::new("cmd-=", AdjustBufferFontSize(1.), None),
78 Binding::new("cmd--", AdjustBufferFontSize(-1.), None),
79 ])
80}
81
82fn open(action: &Open, cx: &mut MutableAppContext) {
83 let app_state = action.0.clone();
84 cx.prompt_for_paths(
85 PathPromptOptions {
86 files: true,
87 directories: true,
88 multiple: true,
89 },
90 move |paths, cx| {
91 if let Some(paths) = paths {
92 cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state }));
93 }
94 },
95 );
96}
97
98fn open_paths(action: &OpenPaths, cx: &mut MutableAppContext) -> Task<()> {
99 log::info!("open paths {:?}", action.0.paths);
100
101 // Open paths in existing workspace if possible
102 for window_id in cx.window_ids().collect::<Vec<_>>() {
103 if let Some(handle) = cx.root_view::<Workspace>(window_id) {
104 let task = handle.update(cx, |view, cx| {
105 if view.contains_paths(&action.0.paths, cx.as_ref()) {
106 log::info!("open paths on existing workspace");
107 Some(view.open_paths(&action.0.paths, cx))
108 } else {
109 None
110 }
111 });
112
113 if let Some(task) = task {
114 return task;
115 }
116 }
117 }
118
119 log::info!("open new workspace");
120
121 // Add a new workspace if necessary
122 let app_state = &action.0.app_state;
123 let (_, workspace) = cx.add_window(window_options(), |cx| {
124 build_workspace(&WorkspaceParams::from(app_state.as_ref()), cx)
125 });
126 // cx.resize_window(window_id);
127 workspace.update(cx, |workspace, cx| {
128 workspace.open_paths(&action.0.paths, cx)
129 })
130}
131
132fn open_new(action: &OpenNew, cx: &mut MutableAppContext) {
133 let (window_id, workspace) =
134 cx.add_window(window_options(), |cx| build_workspace(&action.0, cx));
135 cx.dispatch_action(window_id, vec![workspace.id()], action);
136}
137
138fn build_workspace(params: &WorkspaceParams, cx: &mut ViewContext<Workspace>) -> Workspace {
139 let mut workspace = Workspace::new(params, cx);
140 let project = workspace.project().clone();
141 workspace.left_sidebar_mut().add_item(
142 "icons/folder-tree-16.svg",
143 ProjectPanel::new(project, params.settings.clone(), cx).into(),
144 );
145 workspace.right_sidebar_mut().add_item(
146 "icons/user-16.svg",
147 cx.add_view(|cx| PeoplePanel::new(params.user_store.clone(), params.settings.clone(), cx))
148 .into(),
149 );
150 workspace.right_sidebar_mut().add_item(
151 "icons/comment-16.svg",
152 cx.add_view(|cx| {
153 ChatPanel::new(
154 params.client.clone(),
155 params.channel_list.clone(),
156 params.settings.clone(),
157 cx,
158 )
159 })
160 .into(),
161 );
162
163 let diagnostic =
164 cx.add_view(|_| editor::items::DiagnosticMessage::new(params.settings.clone()));
165 let cursor_position =
166 cx.add_view(|_| editor::items::CursorPosition::new(params.settings.clone()));
167 workspace.status_bar().update(cx, |status_bar, cx| {
168 status_bar.add_left_item(diagnostic, cx);
169 status_bar.add_right_item(cursor_position, cx);
170 });
171
172 workspace
173}
174
175fn window_options() -> WindowOptions<'static> {
176 WindowOptions {
177 bounds: WindowBounds::Maximized,
178 title: None,
179 titlebar_appears_transparent: true,
180 traffic_light_position: Some(vec2f(8., 8.)),
181 }
182}
183
184fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
185 cx.platform().quit();
186}
187
188impl<'a> From<&'a AppState> for WorkspaceParams {
189 fn from(state: &'a AppState) -> Self {
190 Self {
191 client: state.client.clone(),
192 fs: state.fs.clone(),
193 languages: state.languages.clone(),
194 settings: state.settings.clone(),
195 user_store: state.user_store.clone(),
196 channel_list: state.channel_list.clone(),
197 entry_openers: state.entry_openers.clone(),
198 }
199 }
200}
201
202impl<'a> From<&'a AppState> for ThemeSelectorParams {
203 fn from(state: &'a AppState) -> Self {
204 Self {
205 settings_tx: state.settings_tx.clone(),
206 settings: state.settings.clone(),
207 themes: state.themes.clone(),
208 }
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use editor::Editor;
216 use project::ProjectPath;
217 use serde_json::json;
218 use std::{collections::HashSet, path::Path};
219 use test::test_app_state;
220 use theme::DEFAULT_THEME_NAME;
221 use util::test::temp_tree;
222 use workspace::{pane, ItemView, ItemViewHandle, SplitDirection, WorkspaceHandle};
223
224 #[gpui::test]
225 async fn test_open_paths_action(mut cx: gpui::TestAppContext) {
226 let app_state = cx.update(test_app_state);
227 let dir = temp_tree(json!({
228 "a": {
229 "aa": null,
230 "ab": null,
231 },
232 "b": {
233 "ba": null,
234 "bb": null,
235 },
236 "c": {
237 "ca": null,
238 "cb": null,
239 },
240 }));
241
242 cx.update(|cx| {
243 open_paths(
244 &OpenPaths(OpenParams {
245 paths: vec![
246 dir.path().join("a").to_path_buf(),
247 dir.path().join("b").to_path_buf(),
248 ],
249 app_state: app_state.clone(),
250 }),
251 cx,
252 )
253 })
254 .await;
255 assert_eq!(cx.window_ids().len(), 1);
256
257 cx.update(|cx| {
258 open_paths(
259 &OpenPaths(OpenParams {
260 paths: vec![dir.path().join("a").to_path_buf()],
261 app_state: app_state.clone(),
262 }),
263 cx,
264 )
265 })
266 .await;
267 assert_eq!(cx.window_ids().len(), 1);
268 let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
269 workspace_1.read_with(&cx, |workspace, cx| {
270 assert_eq!(workspace.worktrees(cx).len(), 2)
271 });
272
273 cx.update(|cx| {
274 open_paths(
275 &OpenPaths(OpenParams {
276 paths: vec![
277 dir.path().join("b").to_path_buf(),
278 dir.path().join("c").to_path_buf(),
279 ],
280 app_state: app_state.clone(),
281 }),
282 cx,
283 )
284 })
285 .await;
286 assert_eq!(cx.window_ids().len(), 2);
287 }
288
289 #[gpui::test]
290 async fn test_new_empty_workspace(mut cx: gpui::TestAppContext) {
291 let app_state = cx.update(test_app_state);
292 cx.update(|cx| init(&app_state, cx));
293 cx.dispatch_global_action(workspace::OpenNew(app_state.as_ref().into()));
294 let window_id = *cx.window_ids().first().unwrap();
295 let workspace = cx.root_view::<Workspace>(window_id).unwrap();
296 let editor = workspace.update(&mut cx, |workspace, cx| {
297 workspace
298 .active_item(cx)
299 .unwrap()
300 .to_any()
301 .downcast::<editor::Editor>()
302 .unwrap()
303 });
304
305 editor.update(&mut cx, |editor, cx| {
306 assert!(editor.text(cx).is_empty());
307 });
308
309 workspace.update(&mut cx, |workspace, cx| {
310 workspace.save_active_item(&workspace::Save, cx)
311 });
312
313 app_state.fs.as_fake().insert_dir("/root").await.unwrap();
314 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
315
316 editor
317 .condition(&cx, |editor, cx| editor.title(cx) == "the-new-name")
318 .await;
319 editor.update(&mut cx, |editor, cx| {
320 assert!(!editor.is_dirty(cx));
321 });
322 }
323
324 #[gpui::test]
325 async fn test_open_entry(mut cx: gpui::TestAppContext) {
326 let app_state = cx.update(test_app_state);
327 app_state
328 .fs
329 .as_fake()
330 .insert_tree(
331 "/root",
332 json!({
333 "a": {
334 "file1": "contents 1",
335 "file2": "contents 2",
336 "file3": "contents 3",
337 },
338 }),
339 )
340 .await;
341
342 let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx));
343 workspace
344 .update(&mut cx, |workspace, cx| {
345 workspace.add_worktree(Path::new("/root"), cx)
346 })
347 .await
348 .unwrap();
349
350 cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
351 .await;
352 let entries = cx.read(|cx| workspace.file_project_paths(cx));
353 let file1 = entries[0].clone();
354 let file2 = entries[1].clone();
355 let file3 = entries[2].clone();
356
357 // Open the first entry
358 workspace
359 .update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx))
360 .unwrap()
361 .await;
362 cx.read(|cx| {
363 let pane = workspace.read(cx).active_pane().read(cx);
364 assert_eq!(
365 pane.active_item().unwrap().project_path(cx),
366 Some(file1.clone())
367 );
368 assert_eq!(pane.items().len(), 1);
369 });
370
371 // Open the second entry
372 workspace
373 .update(&mut cx, |w, cx| w.open_entry(file2.clone(), cx))
374 .unwrap()
375 .await;
376 cx.read(|cx| {
377 let pane = workspace.read(cx).active_pane().read(cx);
378 assert_eq!(
379 pane.active_item().unwrap().project_path(cx),
380 Some(file2.clone())
381 );
382 assert_eq!(pane.items().len(), 2);
383 });
384
385 // Open the first entry again. The existing pane item is activated.
386 workspace.update(&mut cx, |w, cx| {
387 assert!(w.open_entry(file1.clone(), cx).is_none())
388 });
389 cx.read(|cx| {
390 let pane = workspace.read(cx).active_pane().read(cx);
391 assert_eq!(
392 pane.active_item().unwrap().project_path(cx),
393 Some(file1.clone())
394 );
395 assert_eq!(pane.items().len(), 2);
396 });
397
398 // Split the pane with the first entry, then open the second entry again.
399 workspace.update(&mut cx, |w, cx| {
400 w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
401 assert!(w.open_entry(file2.clone(), cx).is_none());
402 assert_eq!(
403 w.active_pane()
404 .read(cx)
405 .active_item()
406 .unwrap()
407 .project_path(cx.as_ref()),
408 Some(file2.clone())
409 );
410 });
411
412 // Open the third entry twice concurrently. Only one pane item is added.
413 let (t1, t2) = workspace.update(&mut cx, |w, cx| {
414 (
415 w.open_entry(file3.clone(), cx).unwrap(),
416 w.open_entry(file3.clone(), cx).unwrap(),
417 )
418 });
419 t1.await;
420 t2.await;
421 cx.read(|cx| {
422 let pane = workspace.read(cx).active_pane().read(cx);
423 assert_eq!(
424 pane.active_item().unwrap().project_path(cx),
425 Some(file3.clone())
426 );
427 let pane_entries = pane
428 .items()
429 .iter()
430 .map(|i| i.project_path(cx).unwrap())
431 .collect::<Vec<_>>();
432 assert_eq!(pane_entries, &[file1, file2, file3]);
433 });
434 }
435
436 #[gpui::test]
437 async fn test_open_paths(mut cx: gpui::TestAppContext) {
438 let app_state = cx.update(test_app_state);
439 let fs = app_state.fs.as_fake();
440 fs.insert_dir("/dir1").await.unwrap();
441 fs.insert_dir("/dir2").await.unwrap();
442 fs.insert_file("/dir1/a.txt", "".into()).await.unwrap();
443 fs.insert_file("/dir2/b.txt", "".into()).await.unwrap();
444
445 let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx));
446 workspace
447 .update(&mut cx, |workspace, cx| {
448 workspace.add_worktree("/dir1".as_ref(), cx)
449 })
450 .await
451 .unwrap();
452 cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
453 .await;
454
455 // Open a file within an existing worktree.
456 cx.update(|cx| {
457 workspace.update(cx, |view, cx| view.open_paths(&["/dir1/a.txt".into()], cx))
458 })
459 .await;
460 cx.read(|cx| {
461 assert_eq!(
462 workspace
463 .read(cx)
464 .active_pane()
465 .read(cx)
466 .active_item()
467 .unwrap()
468 .title(cx),
469 "a.txt"
470 );
471 });
472
473 // Open a file outside of any existing worktree.
474 cx.update(|cx| {
475 workspace.update(cx, |view, cx| view.open_paths(&["/dir2/b.txt".into()], cx))
476 })
477 .await;
478 cx.read(|cx| {
479 let worktree_roots = workspace
480 .read(cx)
481 .worktrees(cx)
482 .iter()
483 .map(|w| w.read(cx).as_local().unwrap().abs_path())
484 .collect::<HashSet<_>>();
485 assert_eq!(
486 worktree_roots,
487 vec!["/dir1", "/dir2/b.txt"]
488 .into_iter()
489 .map(Path::new)
490 .collect(),
491 );
492 assert_eq!(
493 workspace
494 .read(cx)
495 .active_pane()
496 .read(cx)
497 .active_item()
498 .unwrap()
499 .title(cx),
500 "b.txt"
501 );
502 });
503 }
504
505 #[gpui::test]
506 async fn test_save_conflicting_item(mut cx: gpui::TestAppContext) {
507 let app_state = cx.update(test_app_state);
508 let fs = app_state.fs.as_fake();
509 fs.insert_tree("/root", json!({ "a.txt": "" })).await;
510
511 let (window_id, workspace) =
512 cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx));
513 workspace
514 .update(&mut cx, |workspace, cx| {
515 workspace.add_worktree(Path::new("/root"), cx)
516 })
517 .await
518 .unwrap();
519
520 // Open a file within an existing worktree.
521 cx.update(|cx| {
522 workspace.update(cx, |view, cx| {
523 view.open_paths(&[PathBuf::from("/root/a.txt")], cx)
524 })
525 })
526 .await;
527 let editor = cx.read(|cx| {
528 let pane = workspace.read(cx).active_pane().read(cx);
529 let item = pane.active_item().unwrap();
530 item.to_any().downcast::<Editor>().unwrap()
531 });
532
533 cx.update(|cx| {
534 editor.update(cx, |editor, cx| {
535 editor.handle_input(&editor::Input("x".into()), cx)
536 })
537 });
538 fs.insert_file("/root/a.txt", "changed".to_string())
539 .await
540 .unwrap();
541 editor
542 .condition(&cx, |editor, cx| editor.has_conflict(cx))
543 .await;
544 cx.read(|cx| assert!(editor.is_dirty(cx)));
545
546 cx.update(|cx| workspace.update(cx, |w, cx| w.save_active_item(&workspace::Save, cx)));
547 cx.simulate_prompt_answer(window_id, 0);
548 editor
549 .condition(&cx, |editor, cx| !editor.is_dirty(cx))
550 .await;
551 cx.read(|cx| assert!(!editor.has_conflict(cx)));
552 }
553
554 #[gpui::test]
555 async fn test_open_and_save_new_file(mut cx: gpui::TestAppContext) {
556 let app_state = cx.update(test_app_state);
557 app_state.fs.as_fake().insert_dir("/root").await.unwrap();
558 let params = app_state.as_ref().into();
559 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
560 workspace
561 .update(&mut cx, |workspace, cx| {
562 workspace.add_worktree(Path::new("/root"), cx)
563 })
564 .await
565 .unwrap();
566 let worktree = cx.read(|cx| {
567 workspace
568 .read(cx)
569 .worktrees(cx)
570 .iter()
571 .next()
572 .unwrap()
573 .clone()
574 });
575
576 // Create a new untitled buffer
577 cx.dispatch_action(window_id, vec![workspace.id()], OpenNew(params.clone()));
578 let editor = workspace.read_with(&cx, |workspace, cx| {
579 workspace
580 .active_item(cx)
581 .unwrap()
582 .to_any()
583 .downcast::<Editor>()
584 .unwrap()
585 });
586
587 editor.update(&mut cx, |editor, cx| {
588 assert!(!editor.is_dirty(cx.as_ref()));
589 assert_eq!(editor.title(cx.as_ref()), "untitled");
590 assert!(editor.language(cx).is_none());
591 editor.handle_input(&editor::Input("hi".into()), cx);
592 assert!(editor.is_dirty(cx.as_ref()));
593 });
594
595 // Save the buffer. This prompts for a filename.
596 workspace.update(&mut cx, |workspace, cx| {
597 workspace.save_active_item(&workspace::Save, cx)
598 });
599 cx.simulate_new_path_selection(|parent_dir| {
600 assert_eq!(parent_dir, Path::new("/root"));
601 Some(parent_dir.join("the-new-name.rs"))
602 });
603 cx.read(|cx| {
604 assert!(editor.is_dirty(cx));
605 assert_eq!(editor.title(cx), "untitled");
606 });
607
608 // When the save completes, the buffer's title is updated.
609 editor
610 .condition(&cx, |editor, cx| !editor.is_dirty(cx))
611 .await;
612 cx.read(|cx| {
613 assert!(!editor.is_dirty(cx));
614 assert_eq!(editor.title(cx), "the-new-name.rs");
615 });
616 // The language is assigned based on the path
617 editor.read_with(&cx, |editor, cx| {
618 assert_eq!(editor.language(cx).unwrap().name(), "Rust")
619 });
620
621 // Edit the file and save it again. This time, there is no filename prompt.
622 editor.update(&mut cx, |editor, cx| {
623 editor.handle_input(&editor::Input(" there".into()), cx);
624 assert_eq!(editor.is_dirty(cx.as_ref()), true);
625 });
626 workspace.update(&mut cx, |workspace, cx| {
627 workspace.save_active_item(&workspace::Save, cx)
628 });
629 assert!(!cx.did_prompt_for_new_path());
630 editor
631 .condition(&cx, |editor, cx| !editor.is_dirty(cx))
632 .await;
633 cx.read(|cx| assert_eq!(editor.title(cx), "the-new-name.rs"));
634
635 // Open the same newly-created file in another pane item. The new editor should reuse
636 // the same buffer.
637 cx.dispatch_action(window_id, vec![workspace.id()], OpenNew(params.clone()));
638 workspace.update(&mut cx, |workspace, cx| {
639 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
640 assert!(workspace
641 .open_entry(
642 ProjectPath {
643 worktree_id: worktree.id(),
644 path: Path::new("the-new-name.rs").into()
645 },
646 cx
647 )
648 .is_none());
649 });
650 let editor2 = workspace.update(&mut cx, |workspace, cx| {
651 workspace
652 .active_item(cx)
653 .unwrap()
654 .to_any()
655 .downcast::<Editor>()
656 .unwrap()
657 });
658 cx.read(|cx| {
659 assert_eq!(editor2.read(cx).buffer(), editor.read(cx).buffer());
660 })
661 }
662
663 #[gpui::test]
664 async fn test_setting_language_when_saving_as_single_file_worktree(
665 mut cx: gpui::TestAppContext,
666 ) {
667 let app_state = cx.update(test_app_state);
668 app_state.fs.as_fake().insert_dir("/root").await.unwrap();
669 let params = app_state.as_ref().into();
670 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
671
672 // Create a new untitled buffer
673 cx.dispatch_action(window_id, vec![workspace.id()], OpenNew(params.clone()));
674 let editor = workspace.read_with(&cx, |workspace, cx| {
675 workspace
676 .active_item(cx)
677 .unwrap()
678 .to_any()
679 .downcast::<Editor>()
680 .unwrap()
681 });
682
683 editor.update(&mut cx, |editor, cx| {
684 assert!(editor.language(cx).is_none());
685 editor.handle_input(&editor::Input("hi".into()), cx);
686 assert!(editor.is_dirty(cx.as_ref()));
687 });
688
689 // Save the buffer. This prompts for a filename.
690 workspace.update(&mut cx, |workspace, cx| {
691 workspace.save_active_item(&workspace::Save, cx)
692 });
693 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
694
695 editor
696 .condition(&cx, |editor, cx| !editor.is_dirty(cx))
697 .await;
698
699 // The language is assigned based on the path
700 editor.read_with(&cx, |editor, cx| {
701 assert_eq!(editor.language(cx).unwrap().name(), "Rust")
702 });
703 }
704
705 #[gpui::test]
706 async fn test_pane_actions(mut cx: gpui::TestAppContext) {
707 cx.update(|cx| pane::init(cx));
708 let app_state = cx.update(test_app_state);
709 app_state
710 .fs
711 .as_fake()
712 .insert_tree(
713 "/root",
714 json!({
715 "a": {
716 "file1": "contents 1",
717 "file2": "contents 2",
718 "file3": "contents 3",
719 },
720 }),
721 )
722 .await;
723
724 let (window_id, workspace) =
725 cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx));
726 workspace
727 .update(&mut cx, |workspace, cx| {
728 workspace.add_worktree(Path::new("/root"), cx)
729 })
730 .await
731 .unwrap();
732 cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
733 .await;
734 let entries = cx.read(|cx| workspace.file_project_paths(cx));
735 let file1 = entries[0].clone();
736
737 let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
738
739 workspace
740 .update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx))
741 .unwrap()
742 .await;
743 cx.read(|cx| {
744 assert_eq!(
745 pane_1.read(cx).active_item().unwrap().project_path(cx),
746 Some(file1.clone())
747 );
748 });
749
750 cx.dispatch_action(
751 window_id,
752 vec![pane_1.id()],
753 pane::Split(SplitDirection::Right),
754 );
755 cx.update(|cx| {
756 let pane_2 = workspace.read(cx).active_pane().clone();
757 assert_ne!(pane_1, pane_2);
758
759 let pane2_item = pane_2.read(cx).active_item().unwrap();
760 assert_eq!(pane2_item.project_path(cx.as_ref()), Some(file1.clone()));
761
762 cx.dispatch_action(window_id, vec![pane_2.id()], &workspace::CloseActiveItem);
763 let workspace = workspace.read(cx);
764 assert_eq!(workspace.panes().len(), 1);
765 assert_eq!(workspace.active_pane(), &pane_1);
766 });
767 }
768
769 #[gpui::test]
770 fn test_bundled_themes(cx: &mut MutableAppContext) {
771 let app_state = test_app_state(cx);
772 let mut has_default_theme = false;
773 for theme_name in app_state.themes.list() {
774 let theme = app_state.themes.get(&theme_name).unwrap();
775 if theme.name == DEFAULT_THEME_NAME {
776 has_default_theme = true;
777 }
778 assert_eq!(theme.name, theme_name);
779 }
780 assert!(has_default_theme);
781 }
782}