lib.rs

  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(&params, 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(&params, 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}