1mod items;
2pub mod pane;
3pub mod pane_group;
4pub mod settings;
5pub mod sidebar;
6
7use anyhow::Result;
8use buffer::{Buffer, LanguageRegistry};
9use client::{Authenticate, ChannelList, Client, UserStore};
10use gpui::{
11 action, elements::*, json::to_string_pretty, keymap::Binding, platform::CursorStyle,
12 AnyViewHandle, AppContext, ClipboardItem, Entity, ModelHandle, MutableAppContext, PromptLevel,
13 RenderContext, Task, View, ViewContext, ViewHandle, WeakModelHandle,
14};
15use log::error;
16pub use pane::*;
17pub use pane_group::*;
18use postage::{prelude::Stream, watch};
19use project::{Fs, Project, ProjectPath, Worktree};
20pub use settings::Settings;
21use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus};
22use std::{
23 collections::{hash_map::Entry, HashMap},
24 future::Future,
25 path::{Path, PathBuf},
26 sync::Arc,
27};
28
29action!(OpenNew, WorkspaceParams);
30action!(Save);
31action!(DebugElements);
32
33pub fn init(cx: &mut MutableAppContext) {
34 cx.add_action(Workspace::save_active_item);
35 cx.add_action(Workspace::debug_elements);
36 cx.add_action(Workspace::open_new_file);
37 cx.add_action(Workspace::toggle_sidebar_item);
38 cx.add_action(Workspace::toggle_sidebar_item_focus);
39 cx.add_bindings(vec![
40 Binding::new("cmd-s", Save, None),
41 Binding::new("cmd-alt-i", DebugElements, None),
42 Binding::new(
43 "cmd-shift-!",
44 ToggleSidebarItem(SidebarItemId {
45 side: Side::Left,
46 item_index: 0,
47 }),
48 None,
49 ),
50 Binding::new(
51 "cmd-1",
52 ToggleSidebarItemFocus(SidebarItemId {
53 side: Side::Left,
54 item_index: 0,
55 }),
56 None,
57 ),
58 ]);
59 pane::init(cx);
60}
61
62pub trait Item: Entity + Sized {
63 type View: ItemView;
64
65 fn build_view(
66 handle: ModelHandle<Self>,
67 settings: watch::Receiver<Settings>,
68 cx: &mut ViewContext<Self::View>,
69 ) -> Self::View;
70
71 fn project_path(&self) -> Option<ProjectPath>;
72}
73
74pub trait ItemView: View {
75 fn title(&self, cx: &AppContext) -> String;
76 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
77 fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
78 where
79 Self: Sized,
80 {
81 None
82 }
83 fn is_dirty(&self, _: &AppContext) -> bool {
84 false
85 }
86 fn has_conflict(&self, _: &AppContext) -> bool {
87 false
88 }
89 fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>>;
90 fn save_as(
91 &mut self,
92 worktree: ModelHandle<Worktree>,
93 path: &Path,
94 cx: &mut ViewContext<Self>,
95 ) -> Task<anyhow::Result<()>>;
96 fn should_activate_item_on_event(_: &Self::Event) -> bool {
97 false
98 }
99 fn should_close_item_on_event(_: &Self::Event) -> bool {
100 false
101 }
102 fn should_update_tab_on_event(_: &Self::Event) -> bool {
103 false
104 }
105}
106
107pub trait ItemHandle: Send + Sync {
108 fn boxed_clone(&self) -> Box<dyn ItemHandle>;
109 fn downgrade(&self) -> Box<dyn WeakItemHandle>;
110}
111
112pub trait WeakItemHandle {
113 fn add_view(
114 &self,
115 window_id: usize,
116 settings: watch::Receiver<Settings>,
117 cx: &mut MutableAppContext,
118 ) -> Option<Box<dyn ItemViewHandle>>;
119 fn alive(&self, cx: &AppContext) -> bool;
120 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
121}
122
123pub trait ItemViewHandle {
124 fn title(&self, cx: &AppContext) -> String;
125 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
126 fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
127 fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>>;
128 fn set_parent_pane(&self, pane: &ViewHandle<Pane>, cx: &mut MutableAppContext);
129 fn id(&self) -> usize;
130 fn to_any(&self) -> AnyViewHandle;
131 fn is_dirty(&self, cx: &AppContext) -> bool;
132 fn has_conflict(&self, cx: &AppContext) -> bool;
133 fn save(&self, cx: &mut MutableAppContext) -> Result<Task<Result<()>>>;
134 fn save_as(
135 &self,
136 worktree: ModelHandle<Worktree>,
137 path: &Path,
138 cx: &mut MutableAppContext,
139 ) -> Task<anyhow::Result<()>>;
140}
141
142impl<T: Item> ItemHandle for ModelHandle<T> {
143 fn boxed_clone(&self) -> Box<dyn ItemHandle> {
144 Box::new(self.clone())
145 }
146
147 fn downgrade(&self) -> Box<dyn WeakItemHandle> {
148 Box::new(self.downgrade())
149 }
150}
151
152impl<T: Item> WeakItemHandle for WeakModelHandle<T> {
153 fn add_view(
154 &self,
155 window_id: usize,
156 settings: watch::Receiver<Settings>,
157 cx: &mut MutableAppContext,
158 ) -> Option<Box<dyn ItemViewHandle>> {
159 if let Some(handle) = self.upgrade(cx.as_ref()) {
160 Some(Box::new(cx.add_view(window_id, |cx| {
161 T::build_view(handle, settings, cx)
162 })))
163 } else {
164 None
165 }
166 }
167
168 fn alive(&self, cx: &AppContext) -> bool {
169 self.upgrade(cx).is_some()
170 }
171
172 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
173 self.upgrade(cx).and_then(|h| h.read(cx).project_path())
174 }
175}
176
177impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
178 fn title(&self, cx: &AppContext) -> String {
179 self.read(cx).title(cx)
180 }
181
182 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
183 self.read(cx).project_path(cx)
184 }
185
186 fn boxed_clone(&self) -> Box<dyn ItemViewHandle> {
187 Box::new(self.clone())
188 }
189
190 fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>> {
191 self.update(cx, |item, cx| {
192 cx.add_option_view(|cx| item.clone_on_split(cx))
193 })
194 .map(|handle| Box::new(handle) as Box<dyn ItemViewHandle>)
195 }
196
197 fn set_parent_pane(&self, pane: &ViewHandle<Pane>, cx: &mut MutableAppContext) {
198 pane.update(cx, |_, cx| {
199 cx.subscribe(self, |pane, item, event, cx| {
200 if T::should_close_item_on_event(event) {
201 pane.close_item(item.id(), cx);
202 return;
203 }
204 if T::should_activate_item_on_event(event) {
205 if let Some(ix) = pane.item_index(&item) {
206 pane.activate_item(ix, cx);
207 pane.activate(cx);
208 }
209 }
210 if T::should_update_tab_on_event(event) {
211 cx.notify()
212 }
213 })
214 .detach();
215 });
216 }
217
218 fn save(&self, cx: &mut MutableAppContext) -> Result<Task<Result<()>>> {
219 self.update(cx, |item, cx| item.save(cx))
220 }
221
222 fn save_as(
223 &self,
224 worktree: ModelHandle<Worktree>,
225 path: &Path,
226 cx: &mut MutableAppContext,
227 ) -> Task<anyhow::Result<()>> {
228 self.update(cx, |item, cx| item.save_as(worktree, path, cx))
229 }
230
231 fn is_dirty(&self, cx: &AppContext) -> bool {
232 self.read(cx).is_dirty(cx)
233 }
234
235 fn has_conflict(&self, cx: &AppContext) -> bool {
236 self.read(cx).has_conflict(cx)
237 }
238
239 fn id(&self) -> usize {
240 self.id()
241 }
242
243 fn to_any(&self) -> AnyViewHandle {
244 self.into()
245 }
246}
247
248impl Clone for Box<dyn ItemViewHandle> {
249 fn clone(&self) -> Box<dyn ItemViewHandle> {
250 self.boxed_clone()
251 }
252}
253
254impl Clone for Box<dyn ItemHandle> {
255 fn clone(&self) -> Box<dyn ItemHandle> {
256 self.boxed_clone()
257 }
258}
259
260#[derive(Clone)]
261pub struct WorkspaceParams {
262 pub client: Arc<Client>,
263 pub fs: Arc<dyn Fs>,
264 pub languages: Arc<LanguageRegistry>,
265 pub settings: watch::Receiver<Settings>,
266 pub user_store: ModelHandle<UserStore>,
267 pub channel_list: ModelHandle<ChannelList>,
268}
269
270impl WorkspaceParams {
271 #[cfg(any(test, feature = "test-support"))]
272 pub fn test(cx: &mut MutableAppContext) -> Self {
273 let mut languages = LanguageRegistry::new();
274 languages.add(Arc::new(buffer::Language::new(
275 buffer::LanguageConfig {
276 name: "Rust".to_string(),
277 path_suffixes: vec!["rs".to_string()],
278 ..Default::default()
279 },
280 tree_sitter_rust::language(),
281 )));
282
283 let client = Client::new();
284 let http_client = client::test::FakeHttpClient::new(|_| async move {
285 Ok(client::http::ServerResponse::new(404))
286 });
287 let theme =
288 gpui::fonts::with_font_cache(cx.font_cache().clone(), || theme::Theme::default());
289 let settings = Settings::new("Courier", cx.font_cache(), Arc::new(theme)).unwrap();
290 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
291 Self {
292 channel_list: cx
293 .add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)),
294 client,
295 fs: Arc::new(project::FakeFs::new()),
296 languages: Arc::new(languages),
297 settings: watch::channel_with(settings).1,
298 user_store,
299 }
300 }
301}
302
303pub struct Workspace {
304 pub settings: watch::Receiver<Settings>,
305 client: Arc<Client>,
306 user_store: ModelHandle<client::UserStore>,
307 fs: Arc<dyn Fs>,
308 modal: Option<AnyViewHandle>,
309 center: PaneGroup,
310 left_sidebar: Sidebar,
311 right_sidebar: Sidebar,
312 panes: Vec<ViewHandle<Pane>>,
313 active_pane: ViewHandle<Pane>,
314 project: ModelHandle<Project>,
315 items: Vec<Box<dyn WeakItemHandle>>,
316 loading_items: HashMap<
317 ProjectPath,
318 postage::watch::Receiver<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
319 >,
320 _observe_current_user: Task<()>,
321}
322
323impl Workspace {
324 pub fn new(params: &WorkspaceParams, cx: &mut ViewContext<Self>) -> Self {
325 let project = cx.add_model(|cx| {
326 Project::new(
327 params.languages.clone(),
328 params.client.clone(),
329 params.fs.clone(),
330 cx.background(),
331 )
332 });
333 cx.observe(&project, |_, _, cx| cx.notify()).detach();
334
335 let pane = cx.add_view(|_| Pane::new(params.settings.clone()));
336 let pane_id = pane.id();
337 cx.observe(&pane, move |me, _, cx| {
338 let active_entry = me.active_project_path(cx);
339 me.project
340 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
341 })
342 .detach();
343 cx.subscribe(&pane, move |me, _, event, cx| {
344 me.handle_pane_event(pane_id, event, cx)
345 })
346 .detach();
347 cx.focus(&pane);
348
349 let mut current_user = params.user_store.read(cx).watch_current_user().clone();
350 let mut connection_status = params.client.status().clone();
351 let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
352 current_user.recv().await;
353 connection_status.recv().await;
354 let mut stream =
355 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
356
357 while stream.recv().await.is_some() {
358 cx.update(|cx| {
359 if let Some(this) = this.upgrade(&cx) {
360 this.update(cx, |_, cx| cx.notify());
361 }
362 })
363 }
364 });
365
366 Workspace {
367 modal: None,
368 center: PaneGroup::new(pane.id()),
369 panes: vec![pane.clone()],
370 active_pane: pane.clone(),
371 settings: params.settings.clone(),
372 client: params.client.clone(),
373 user_store: params.user_store.clone(),
374 fs: params.fs.clone(),
375 left_sidebar: Sidebar::new(Side::Left),
376 right_sidebar: Sidebar::new(Side::Right),
377 project,
378 items: Default::default(),
379 loading_items: Default::default(),
380 _observe_current_user,
381 }
382 }
383
384 pub fn left_sidebar_mut(&mut self) -> &mut Sidebar {
385 &mut self.left_sidebar
386 }
387
388 pub fn right_sidebar_mut(&mut self) -> &mut Sidebar {
389 &mut self.right_sidebar
390 }
391
392 pub fn project(&self) -> &ModelHandle<Project> {
393 &self.project
394 }
395
396 pub fn worktrees<'a>(&self, cx: &'a AppContext) -> &'a [ModelHandle<Worktree>] {
397 &self.project.read(cx).worktrees()
398 }
399
400 pub fn contains_paths(&self, paths: &[PathBuf], cx: &AppContext) -> bool {
401 paths.iter().all(|path| self.contains_path(&path, cx))
402 }
403
404 pub fn contains_path(&self, path: &Path, cx: &AppContext) -> bool {
405 for worktree in self.worktrees(cx) {
406 let worktree = worktree.read(cx).as_local();
407 if worktree.map_or(false, |w| w.contains_abs_path(path)) {
408 return true;
409 }
410 }
411 false
412 }
413
414 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
415 let futures = self
416 .worktrees(cx)
417 .iter()
418 .filter_map(|worktree| worktree.read(cx).as_local())
419 .map(|worktree| worktree.scan_complete())
420 .collect::<Vec<_>>();
421 async move {
422 for future in futures {
423 future.await;
424 }
425 }
426 }
427
428 pub fn open_paths(&mut self, abs_paths: &[PathBuf], cx: &mut ViewContext<Self>) -> Task<()> {
429 let entries = abs_paths
430 .iter()
431 .cloned()
432 .map(|path| self.project_path_for_path(&path, cx))
433 .collect::<Vec<_>>();
434
435 let fs = self.fs.clone();
436 let tasks = abs_paths
437 .iter()
438 .cloned()
439 .zip(entries.into_iter())
440 .map(|(abs_path, project_path)| {
441 cx.spawn(|this, mut cx| {
442 let fs = fs.clone();
443 async move {
444 let project_path = project_path.await?;
445 if fs.is_file(&abs_path).await {
446 if let Some(entry) =
447 this.update(&mut cx, |this, cx| this.open_entry(project_path, cx))
448 {
449 entry.await;
450 }
451 }
452 Ok(())
453 }
454 })
455 })
456 .collect::<Vec<Task<Result<()>>>>();
457
458 cx.foreground().spawn(async move {
459 for task in tasks {
460 if let Err(error) = task.await {
461 log::error!("error opening paths {}", error);
462 }
463 }
464 })
465 }
466
467 fn worktree_for_abs_path(
468 &self,
469 abs_path: &Path,
470 cx: &mut ViewContext<Self>,
471 ) -> Task<Result<(ModelHandle<Worktree>, PathBuf)>> {
472 let abs_path: Arc<Path> = Arc::from(abs_path);
473 cx.spawn(|this, mut cx| async move {
474 let mut entry_id = None;
475 this.read_with(&cx, |this, cx| {
476 for tree in this.worktrees(cx) {
477 if let Some(relative_path) = tree
478 .read(cx)
479 .as_local()
480 .and_then(|t| abs_path.strip_prefix(t.abs_path()).ok())
481 {
482 entry_id = Some((tree.clone(), relative_path.into()));
483 break;
484 }
485 }
486 });
487
488 if let Some(entry_id) = entry_id {
489 Ok(entry_id)
490 } else {
491 let worktree = this
492 .update(&mut cx, |this, cx| this.add_worktree(&abs_path, cx))
493 .await?;
494 Ok((worktree, PathBuf::new()))
495 }
496 })
497 }
498
499 fn project_path_for_path(
500 &self,
501 abs_path: &Path,
502 cx: &mut ViewContext<Self>,
503 ) -> Task<Result<ProjectPath>> {
504 let entry = self.worktree_for_abs_path(abs_path, cx);
505 cx.spawn(|_, _| async move {
506 let (worktree, path) = entry.await?;
507 Ok(ProjectPath {
508 worktree_id: worktree.id(),
509 path: path.into(),
510 })
511 })
512 }
513
514 pub fn add_worktree(
515 &self,
516 path: &Path,
517 cx: &mut ViewContext<Self>,
518 ) -> Task<Result<ModelHandle<Worktree>>> {
519 self.project
520 .update(cx, |project, cx| project.add_local_worktree(path, cx))
521 }
522
523 pub fn toggle_modal<V, F>(&mut self, cx: &mut ViewContext<Self>, add_view: F)
524 where
525 V: 'static + View,
526 F: FnOnce(&mut ViewContext<Self>, &mut Self) -> ViewHandle<V>,
527 {
528 if self.modal.as_ref().map_or(false, |modal| modal.is::<V>()) {
529 self.modal.take();
530 cx.focus_self();
531 } else {
532 let modal = add_view(cx, self);
533 cx.focus(&modal);
534 self.modal = Some(modal.into());
535 }
536 cx.notify();
537 }
538
539 pub fn modal(&self) -> Option<&AnyViewHandle> {
540 self.modal.as_ref()
541 }
542
543 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
544 if self.modal.take().is_some() {
545 cx.focus(&self.active_pane);
546 cx.notify();
547 }
548 }
549
550 pub fn open_new_file(&mut self, _: &OpenNew, cx: &mut ViewContext<Self>) {
551 let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
552 let item_handle = ItemHandle::downgrade(&buffer);
553 let view = item_handle
554 .add_view(cx.window_id(), self.settings.clone(), cx)
555 .unwrap();
556 self.items.push(item_handle);
557 self.active_pane().add_item_view(view, cx.as_mut());
558 }
559
560 #[must_use]
561 pub fn open_entry(
562 &mut self,
563 project_path: ProjectPath,
564 cx: &mut ViewContext<Self>,
565 ) -> Option<Task<()>> {
566 let pane = self.active_pane().clone();
567 if self.activate_or_open_existing_entry(project_path.clone(), &pane, cx) {
568 return None;
569 }
570
571 // let (worktree_id, path) = project_path.clone();
572
573 let worktree = match self
574 .project
575 .read(cx)
576 .worktree_for_id(project_path.worktree_id)
577 {
578 Some(worktree) => worktree,
579 None => {
580 log::error!("worktree {} does not exist", project_path.worktree_id);
581 return None;
582 }
583 };
584
585 if let Entry::Vacant(entry) = self.loading_items.entry(project_path.clone()) {
586 let (mut tx, rx) = postage::watch::channel();
587 entry.insert(rx);
588
589 let project_path = project_path.clone();
590 cx.as_mut()
591 .spawn(|mut cx| async move {
592 let buffer = worktree
593 .update(&mut cx, |worktree, cx| {
594 worktree.open_buffer(project_path.path.as_ref(), cx)
595 })
596 .await;
597 *tx.borrow_mut() = Some(
598 buffer
599 .map(|buffer| Box::new(buffer) as Box<dyn ItemHandle>)
600 .map_err(Arc::new),
601 );
602 })
603 .detach();
604 }
605
606 let pane = pane.downgrade();
607 let settings = self.settings.clone();
608 let mut watch = self.loading_items.get(&project_path).unwrap().clone();
609
610 Some(cx.spawn(|this, mut cx| async move {
611 let load_result = loop {
612 if let Some(load_result) = watch.borrow().as_ref() {
613 break load_result.clone();
614 }
615 watch.recv().await;
616 };
617
618 this.update(&mut cx, |this, cx| {
619 this.loading_items.remove(&project_path);
620 if let Some(pane) = pane.upgrade(&cx) {
621 match load_result {
622 Ok(item) => {
623 // By the time loading finishes, the entry could have been already added
624 // to the pane. If it was, we activate it, otherwise we'll store the
625 // item and add a new view for it.
626 if !this.activate_or_open_existing_entry(project_path, &pane, cx) {
627 let weak_item = item.downgrade();
628 let view = weak_item
629 .add_view(cx.window_id(), settings, cx.as_mut())
630 .unwrap();
631 this.items.push(weak_item);
632 pane.add_item_view(view, cx.as_mut());
633 }
634 }
635 Err(error) => {
636 log::error!("error opening item: {}", error);
637 }
638 }
639 }
640 })
641 }))
642 }
643
644 fn activate_or_open_existing_entry(
645 &mut self,
646 project_path: ProjectPath,
647 pane: &ViewHandle<Pane>,
648 cx: &mut ViewContext<Self>,
649 ) -> bool {
650 // If the pane contains a view for this file, then activate
651 // that item view.
652 if pane.update(cx, |pane, cx| pane.activate_entry(project_path.clone(), cx)) {
653 return true;
654 }
655
656 // Otherwise, if this file is already open somewhere in the workspace,
657 // then add another view for it.
658 let settings = self.settings.clone();
659 let mut view_for_existing_item = None;
660 self.items.retain(|item| {
661 if item.alive(cx.as_ref()) {
662 if view_for_existing_item.is_none()
663 && item
664 .project_path(cx)
665 .map_or(false, |item_project_path| item_project_path == project_path)
666 {
667 view_for_existing_item = Some(
668 item.add_view(cx.window_id(), settings.clone(), cx.as_mut())
669 .unwrap(),
670 );
671 }
672 true
673 } else {
674 false
675 }
676 });
677 if let Some(view) = view_for_existing_item {
678 pane.add_item_view(view, cx.as_mut());
679 true
680 } else {
681 false
682 }
683 }
684
685 pub fn active_item(&self, cx: &ViewContext<Self>) -> Option<Box<dyn ItemViewHandle>> {
686 self.active_pane().read(cx).active_item()
687 }
688
689 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
690 self.active_item(cx).and_then(|item| item.project_path(cx))
691 }
692
693 pub fn save_active_item(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
694 if let Some(item) = self.active_item(cx) {
695 let handle = cx.handle();
696 if item.project_path(cx.as_ref()).is_none() {
697 let worktree = self.worktrees(cx).first();
698 let start_abs_path = worktree
699 .and_then(|w| w.read(cx).as_local())
700 .map_or(Path::new(""), |w| w.abs_path())
701 .to_path_buf();
702 cx.prompt_for_new_path(&start_abs_path, move |abs_path, cx| {
703 if let Some(abs_path) = abs_path {
704 cx.spawn(|mut cx| async move {
705 let result = match handle
706 .update(&mut cx, |this, cx| {
707 this.worktree_for_abs_path(&abs_path, cx)
708 })
709 .await
710 {
711 Ok((worktree, path)) => {
712 handle
713 .update(&mut cx, |_, cx| {
714 item.save_as(worktree, &path, cx.as_mut())
715 })
716 .await
717 }
718 Err(error) => Err(error),
719 };
720
721 if let Err(error) = result {
722 error!("failed to save item: {:?}, ", error);
723 }
724 })
725 .detach()
726 }
727 });
728 return;
729 } else if item.has_conflict(cx.as_ref()) {
730 const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
731
732 cx.prompt(
733 PromptLevel::Warning,
734 CONFLICT_MESSAGE,
735 &["Overwrite", "Cancel"],
736 move |answer, cx| {
737 if answer == 0 {
738 cx.spawn(|mut cx| async move {
739 if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await {
740 error!("failed to save item: {:?}, ", error);
741 }
742 })
743 .detach();
744 }
745 },
746 );
747 } else {
748 cx.spawn(|_, mut cx| async move {
749 if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await {
750 error!("failed to save item: {:?}, ", error);
751 }
752 })
753 .detach();
754 }
755 }
756 }
757
758 pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
759 let sidebar = match action.0.side {
760 Side::Left => &mut self.left_sidebar,
761 Side::Right => &mut self.right_sidebar,
762 };
763 sidebar.toggle_item(action.0.item_index);
764 if let Some(active_item) = sidebar.active_item() {
765 cx.focus(active_item);
766 } else {
767 cx.focus_self();
768 }
769 cx.notify();
770 }
771
772 pub fn toggle_sidebar_item_focus(
773 &mut self,
774 action: &ToggleSidebarItemFocus,
775 cx: &mut ViewContext<Self>,
776 ) {
777 let sidebar = match action.0.side {
778 Side::Left => &mut self.left_sidebar,
779 Side::Right => &mut self.right_sidebar,
780 };
781 sidebar.activate_item(action.0.item_index);
782 if let Some(active_item) = sidebar.active_item() {
783 if active_item.is_focused(cx) {
784 cx.focus_self();
785 } else {
786 cx.focus(active_item);
787 }
788 }
789 cx.notify();
790 }
791
792 pub fn debug_elements(&mut self, _: &DebugElements, cx: &mut ViewContext<Self>) {
793 match to_string_pretty(&cx.debug_elements()) {
794 Ok(json) => {
795 let kib = json.len() as f32 / 1024.;
796 cx.as_mut().write_to_clipboard(ClipboardItem::new(json));
797 log::info!(
798 "copied {:.1} KiB of element debug JSON to the clipboard",
799 kib
800 );
801 }
802 Err(error) => {
803 log::error!("error debugging elements: {}", error);
804 }
805 };
806 }
807
808 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
809 let pane = cx.add_view(|_| Pane::new(self.settings.clone()));
810 let pane_id = pane.id();
811 cx.observe(&pane, move |me, _, cx| {
812 let active_entry = me.active_project_path(cx);
813 me.project
814 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
815 })
816 .detach();
817 cx.subscribe(&pane, move |me, _, event, cx| {
818 me.handle_pane_event(pane_id, event, cx)
819 })
820 .detach();
821 self.panes.push(pane.clone());
822 self.activate_pane(pane.clone(), cx);
823 pane
824 }
825
826 fn activate_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
827 self.active_pane = pane;
828 cx.focus(&self.active_pane);
829 cx.notify();
830 }
831
832 fn handle_pane_event(
833 &mut self,
834 pane_id: usize,
835 event: &pane::Event,
836 cx: &mut ViewContext<Self>,
837 ) {
838 if let Some(pane) = self.pane(pane_id) {
839 match event {
840 pane::Event::Split(direction) => {
841 self.split_pane(pane, *direction, cx);
842 }
843 pane::Event::Remove => {
844 self.remove_pane(pane, cx);
845 }
846 pane::Event::Activate => {
847 self.activate_pane(pane, cx);
848 }
849 }
850 } else {
851 error!("pane {} not found", pane_id);
852 }
853 }
854
855 fn split_pane(
856 &mut self,
857 pane: ViewHandle<Pane>,
858 direction: SplitDirection,
859 cx: &mut ViewContext<Self>,
860 ) -> ViewHandle<Pane> {
861 let new_pane = self.add_pane(cx);
862 self.activate_pane(new_pane.clone(), cx);
863 if let Some(item) = pane.read(cx).active_item() {
864 if let Some(clone) = item.clone_on_split(cx.as_mut()) {
865 new_pane.add_item_view(clone, cx.as_mut());
866 }
867 }
868 self.center
869 .split(pane.id(), new_pane.id(), direction)
870 .unwrap();
871 cx.notify();
872 new_pane
873 }
874
875 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
876 if self.center.remove(pane.id()).unwrap() {
877 self.panes.retain(|p| p != &pane);
878 self.activate_pane(self.panes.last().unwrap().clone(), cx);
879 }
880 }
881
882 fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
883 self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
884 }
885
886 pub fn active_pane(&self) -> &ViewHandle<Pane> {
887 &self.active_pane
888 }
889
890 fn render_connection_status(&self) -> Option<ElementBox> {
891 let theme = &self.settings.borrow().theme;
892 match &*self.client.status().borrow() {
893 client::Status::ConnectionError
894 | client::Status::ConnectionLost
895 | client::Status::Reauthenticating
896 | client::Status::Reconnecting { .. }
897 | client::Status::ReconnectionError { .. } => Some(
898 Container::new(
899 Align::new(
900 ConstrainedBox::new(
901 Svg::new("icons/offline-14.svg")
902 .with_color(theme.workspace.titlebar.icon_color)
903 .boxed(),
904 )
905 .with_width(theme.workspace.titlebar.offline_icon.width)
906 .boxed(),
907 )
908 .boxed(),
909 )
910 .with_style(theme.workspace.titlebar.offline_icon.container)
911 .boxed(),
912 ),
913 client::Status::UpgradeRequired => Some(
914 Label::new(
915 "Please update Zed to collaborate".to_string(),
916 theme.workspace.titlebar.outdated_warning.text.clone(),
917 )
918 .contained()
919 .with_style(theme.workspace.titlebar.outdated_warning.container)
920 .aligned()
921 .boxed(),
922 ),
923 _ => None,
924 }
925 }
926
927 fn render_avatar(&self, cx: &mut RenderContext<Self>) -> ElementBox {
928 let theme = &self.settings.borrow().theme;
929 let avatar = if let Some(avatar) = self
930 .user_store
931 .read(cx)
932 .current_user()
933 .and_then(|user| user.avatar.clone())
934 {
935 Image::new(avatar)
936 .with_style(theme.workspace.titlebar.avatar)
937 .boxed()
938 } else {
939 MouseEventHandler::new::<Authenticate, _, _, _>(0, cx, |_, _| {
940 Svg::new("icons/signed-out-12.svg")
941 .with_color(theme.workspace.titlebar.icon_color)
942 .boxed()
943 })
944 .on_click(|cx| cx.dispatch_action(Authenticate))
945 .with_cursor_style(CursorStyle::PointingHand)
946 .boxed()
947 };
948
949 ConstrainedBox::new(
950 Align::new(
951 ConstrainedBox::new(avatar)
952 .with_width(theme.workspace.titlebar.avatar_width)
953 .boxed(),
954 )
955 .boxed(),
956 )
957 .with_width(theme.workspace.right_sidebar.width)
958 .boxed()
959 }
960}
961
962impl Entity for Workspace {
963 type Event = ();
964}
965
966impl View for Workspace {
967 fn ui_name() -> &'static str {
968 "Workspace"
969 }
970
971 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
972 let settings = self.settings.borrow();
973 let theme = &settings.theme;
974 Container::new(
975 Flex::column()
976 .with_child(
977 ConstrainedBox::new(
978 Container::new(
979 Stack::new()
980 .with_child(
981 Align::new(
982 Label::new(
983 "zed".into(),
984 theme.workspace.titlebar.title.clone(),
985 )
986 .boxed(),
987 )
988 .boxed(),
989 )
990 .with_child(
991 Align::new(
992 Flex::row()
993 .with_children(self.render_connection_status())
994 .with_child(self.render_avatar(cx))
995 .boxed(),
996 )
997 .right()
998 .boxed(),
999 )
1000 .boxed(),
1001 )
1002 .with_style(theme.workspace.titlebar.container)
1003 .boxed(),
1004 )
1005 .with_height(32.)
1006 .named("titlebar"),
1007 )
1008 .with_child(
1009 Expanded::new(
1010 1.0,
1011 Stack::new()
1012 .with_child({
1013 let mut content = Flex::row();
1014 content.add_child(self.left_sidebar.render(&settings, cx));
1015 if let Some(element) =
1016 self.left_sidebar.render_active_item(&settings, cx)
1017 {
1018 content.add_child(Flexible::new(0.8, element).boxed());
1019 }
1020 content.add_child(
1021 Expanded::new(1.0, self.center.render(&settings.theme)).boxed(),
1022 );
1023 if let Some(element) =
1024 self.right_sidebar.render_active_item(&settings, cx)
1025 {
1026 content.add_child(Flexible::new(0.8, element).boxed());
1027 }
1028 content.add_child(self.right_sidebar.render(&settings, cx));
1029 content.boxed()
1030 })
1031 .with_children(
1032 self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()),
1033 )
1034 .boxed(),
1035 )
1036 .boxed(),
1037 )
1038 .boxed(),
1039 )
1040 .with_background_color(settings.theme.workspace.background)
1041 .named("workspace")
1042 }
1043
1044 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
1045 cx.focus(&self.active_pane);
1046 }
1047}
1048
1049#[cfg(test)]
1050pub trait WorkspaceHandle {
1051 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
1052}
1053
1054#[cfg(test)]
1055impl WorkspaceHandle for ViewHandle<Workspace> {
1056 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
1057 self.read(cx)
1058 .worktrees(cx)
1059 .iter()
1060 .flat_map(|worktree| {
1061 let worktree_id = worktree.id();
1062 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
1063 worktree_id,
1064 path: f.path.clone(),
1065 })
1066 })
1067 .collect::<Vec<_>>()
1068 }
1069}
1070
1071#[cfg(test)]
1072mod tests {
1073 use super::*;
1074 use editor::{Editor, Input};
1075 use serde_json::json;
1076 use std::collections::HashSet;
1077
1078 #[gpui::test]
1079 async fn test_open_entry(mut cx: gpui::TestAppContext) {
1080 let params = cx.update(WorkspaceParams::test);
1081 params
1082 .fs
1083 .as_fake()
1084 .insert_tree(
1085 "/root",
1086 json!({
1087 "a": {
1088 "file1": "contents 1",
1089 "file2": "contents 2",
1090 "file3": "contents 3",
1091 },
1092 }),
1093 )
1094 .await;
1095
1096 let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
1097 workspace
1098 .update(&mut cx, |workspace, cx| {
1099 workspace.add_worktree(Path::new("/root"), cx)
1100 })
1101 .await
1102 .unwrap();
1103
1104 cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
1105 .await;
1106 let entries = cx.read(|cx| workspace.file_project_paths(cx));
1107 let file1 = entries[0].clone();
1108 let file2 = entries[1].clone();
1109 let file3 = entries[2].clone();
1110
1111 // Open the first entry
1112 workspace
1113 .update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx))
1114 .unwrap()
1115 .await;
1116 cx.read(|cx| {
1117 let pane = workspace.read(cx).active_pane().read(cx);
1118 assert_eq!(
1119 pane.active_item().unwrap().project_path(cx),
1120 Some(file1.clone())
1121 );
1122 assert_eq!(pane.items().len(), 1);
1123 });
1124
1125 // Open the second entry
1126 workspace
1127 .update(&mut cx, |w, cx| w.open_entry(file2.clone(), cx))
1128 .unwrap()
1129 .await;
1130 cx.read(|cx| {
1131 let pane = workspace.read(cx).active_pane().read(cx);
1132 assert_eq!(
1133 pane.active_item().unwrap().project_path(cx),
1134 Some(file2.clone())
1135 );
1136 assert_eq!(pane.items().len(), 2);
1137 });
1138
1139 // Open the first entry again. The existing pane item is activated.
1140 workspace.update(&mut cx, |w, cx| {
1141 assert!(w.open_entry(file1.clone(), cx).is_none())
1142 });
1143 cx.read(|cx| {
1144 let pane = workspace.read(cx).active_pane().read(cx);
1145 assert_eq!(
1146 pane.active_item().unwrap().project_path(cx),
1147 Some(file1.clone())
1148 );
1149 assert_eq!(pane.items().len(), 2);
1150 });
1151
1152 // Split the pane with the first entry, then open the second entry again.
1153 workspace.update(&mut cx, |w, cx| {
1154 w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
1155 assert!(w.open_entry(file2.clone(), cx).is_none());
1156 assert_eq!(
1157 w.active_pane()
1158 .read(cx)
1159 .active_item()
1160 .unwrap()
1161 .project_path(cx.as_ref()),
1162 Some(file2.clone())
1163 );
1164 });
1165
1166 // Open the third entry twice concurrently. Only one pane item is added.
1167 let (t1, t2) = workspace.update(&mut cx, |w, cx| {
1168 (
1169 w.open_entry(file3.clone(), cx).unwrap(),
1170 w.open_entry(file3.clone(), cx).unwrap(),
1171 )
1172 });
1173 t1.await;
1174 t2.await;
1175 cx.read(|cx| {
1176 let pane = workspace.read(cx).active_pane().read(cx);
1177 assert_eq!(
1178 pane.active_item().unwrap().project_path(cx),
1179 Some(file3.clone())
1180 );
1181 let pane_entries = pane
1182 .items()
1183 .iter()
1184 .map(|i| i.project_path(cx).unwrap())
1185 .collect::<Vec<_>>();
1186 assert_eq!(pane_entries, &[file1, file2, file3]);
1187 });
1188 }
1189
1190 #[gpui::test]
1191 async fn test_open_paths(mut cx: gpui::TestAppContext) {
1192 let params = cx.update(WorkspaceParams::test);
1193 let fs = params.fs.as_fake();
1194 fs.insert_dir("/dir1").await.unwrap();
1195 fs.insert_dir("/dir2").await.unwrap();
1196 fs.insert_file("/dir1/a.txt", "".into()).await.unwrap();
1197 fs.insert_file("/dir2/b.txt", "".into()).await.unwrap();
1198
1199 let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
1200 workspace
1201 .update(&mut cx, |workspace, cx| {
1202 workspace.add_worktree("/dir1".as_ref(), cx)
1203 })
1204 .await
1205 .unwrap();
1206 cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
1207 .await;
1208
1209 // Open a file within an existing worktree.
1210 cx.update(|cx| {
1211 workspace.update(cx, |view, cx| view.open_paths(&["/dir1/a.txt".into()], cx))
1212 })
1213 .await;
1214 cx.read(|cx| {
1215 assert_eq!(
1216 workspace
1217 .read(cx)
1218 .active_pane()
1219 .read(cx)
1220 .active_item()
1221 .unwrap()
1222 .title(cx),
1223 "a.txt"
1224 );
1225 });
1226
1227 // Open a file outside of any existing worktree.
1228 cx.update(|cx| {
1229 workspace.update(cx, |view, cx| view.open_paths(&["/dir2/b.txt".into()], cx))
1230 })
1231 .await;
1232 cx.read(|cx| {
1233 let worktree_roots = workspace
1234 .read(cx)
1235 .worktrees(cx)
1236 .iter()
1237 .map(|w| w.read(cx).as_local().unwrap().abs_path())
1238 .collect::<HashSet<_>>();
1239 assert_eq!(
1240 worktree_roots,
1241 vec!["/dir1", "/dir2/b.txt"]
1242 .into_iter()
1243 .map(Path::new)
1244 .collect(),
1245 );
1246 assert_eq!(
1247 workspace
1248 .read(cx)
1249 .active_pane()
1250 .read(cx)
1251 .active_item()
1252 .unwrap()
1253 .title(cx),
1254 "b.txt"
1255 );
1256 });
1257 }
1258
1259 #[gpui::test]
1260 async fn test_save_conflicting_item(mut cx: gpui::TestAppContext) {
1261 let params = cx.update(WorkspaceParams::test);
1262 let fs = params.fs.as_fake();
1263 fs.insert_tree("/root", json!({ "a.txt": "" })).await;
1264
1265 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
1266 workspace
1267 .update(&mut cx, |workspace, cx| {
1268 workspace.add_worktree(Path::new("/root"), cx)
1269 })
1270 .await
1271 .unwrap();
1272
1273 // Open a file within an existing worktree.
1274 cx.update(|cx| {
1275 workspace.update(cx, |view, cx| {
1276 view.open_paths(&[PathBuf::from("/root/a.txt")], cx)
1277 })
1278 })
1279 .await;
1280 let editor = cx.read(|cx| {
1281 let pane = workspace.read(cx).active_pane().read(cx);
1282 let item = pane.active_item().unwrap();
1283 item.to_any().downcast::<Editor>().unwrap()
1284 });
1285
1286 cx.update(|cx| editor.update(cx, |editor, cx| editor.handle_input(&Input("x".into()), cx)));
1287 fs.insert_file("/root/a.txt", "changed".to_string())
1288 .await
1289 .unwrap();
1290 editor
1291 .condition(&cx, |editor, cx| editor.has_conflict(cx))
1292 .await;
1293 cx.read(|cx| assert!(editor.is_dirty(cx)));
1294
1295 cx.update(|cx| workspace.update(cx, |w, cx| w.save_active_item(&Save, cx)));
1296 cx.simulate_prompt_answer(window_id, 0);
1297 editor
1298 .condition(&cx, |editor, cx| !editor.is_dirty(cx))
1299 .await;
1300 cx.read(|cx| assert!(!editor.has_conflict(cx)));
1301 }
1302
1303 #[gpui::test]
1304 async fn test_open_and_save_new_file(mut cx: gpui::TestAppContext) {
1305 let params = cx.update(WorkspaceParams::test);
1306 params.fs.as_fake().insert_dir("/root").await.unwrap();
1307 let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
1308 workspace
1309 .update(&mut cx, |workspace, cx| {
1310 workspace.add_worktree(Path::new("/root"), cx)
1311 })
1312 .await
1313 .unwrap();
1314 let worktree = cx.read(|cx| {
1315 workspace
1316 .read(cx)
1317 .worktrees(cx)
1318 .iter()
1319 .next()
1320 .unwrap()
1321 .clone()
1322 });
1323
1324 // Create a new untitled buffer
1325 let editor = workspace.update(&mut cx, |workspace, cx| {
1326 workspace.open_new_file(&OpenNew(params.clone()), cx);
1327 workspace
1328 .active_item(cx)
1329 .unwrap()
1330 .to_any()
1331 .downcast::<Editor>()
1332 .unwrap()
1333 });
1334
1335 editor.update(&mut cx, |editor, cx| {
1336 assert!(!editor.is_dirty(cx.as_ref()));
1337 assert_eq!(editor.title(cx.as_ref()), "untitled");
1338 assert!(editor.language(cx).is_none());
1339 editor.handle_input(&Input("hi".into()), cx);
1340 assert!(editor.is_dirty(cx.as_ref()));
1341 });
1342
1343 // Save the buffer. This prompts for a filename.
1344 workspace.update(&mut cx, |workspace, cx| {
1345 workspace.save_active_item(&Save, cx)
1346 });
1347 cx.simulate_new_path_selection(|parent_dir| {
1348 assert_eq!(parent_dir, Path::new("/root"));
1349 Some(parent_dir.join("the-new-name.rs"))
1350 });
1351 cx.read(|cx| {
1352 assert!(editor.is_dirty(cx));
1353 assert_eq!(editor.title(cx), "untitled");
1354 });
1355
1356 // When the save completes, the buffer's title is updated.
1357 editor
1358 .condition(&cx, |editor, cx| !editor.is_dirty(cx))
1359 .await;
1360 cx.read(|cx| {
1361 assert!(!editor.is_dirty(cx));
1362 assert_eq!(editor.title(cx), "the-new-name.rs");
1363 });
1364 // The language is assigned based on the path
1365 editor.read_with(&cx, |editor, cx| {
1366 assert_eq!(editor.language(cx).unwrap().name(), "Rust")
1367 });
1368
1369 // Edit the file and save it again. This time, there is no filename prompt.
1370 editor.update(&mut cx, |editor, cx| {
1371 editor.handle_input(&Input(" there".into()), cx);
1372 assert_eq!(editor.is_dirty(cx.as_ref()), true);
1373 });
1374 workspace.update(&mut cx, |workspace, cx| {
1375 workspace.save_active_item(&Save, cx)
1376 });
1377 assert!(!cx.did_prompt_for_new_path());
1378 editor
1379 .condition(&cx, |editor, cx| !editor.is_dirty(cx))
1380 .await;
1381 cx.read(|cx| assert_eq!(editor.title(cx), "the-new-name.rs"));
1382
1383 // Open the same newly-created file in another pane item. The new editor should reuse
1384 // the same buffer.
1385 workspace.update(&mut cx, |workspace, cx| {
1386 workspace.open_new_file(&OpenNew(params.clone()), cx);
1387 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
1388 assert!(workspace
1389 .open_entry(
1390 ProjectPath {
1391 worktree_id: worktree.id(),
1392 path: Path::new("the-new-name.rs").into()
1393 },
1394 cx
1395 )
1396 .is_none());
1397 });
1398 let editor2 = workspace.update(&mut cx, |workspace, cx| {
1399 workspace
1400 .active_item(cx)
1401 .unwrap()
1402 .to_any()
1403 .downcast::<Editor>()
1404 .unwrap()
1405 });
1406 cx.read(|cx| {
1407 assert_eq!(editor2.read(cx).buffer(), editor.read(cx).buffer());
1408 })
1409 }
1410
1411 #[gpui::test]
1412 async fn test_setting_language_when_saving_as_single_file_worktree(
1413 mut cx: gpui::TestAppContext,
1414 ) {
1415 let params = cx.update(WorkspaceParams::test);
1416 params.fs.as_fake().insert_dir("/root").await.unwrap();
1417 let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
1418
1419 // Create a new untitled buffer
1420 let editor = workspace.update(&mut cx, |workspace, cx| {
1421 workspace.open_new_file(&OpenNew(params.clone()), cx);
1422 workspace
1423 .active_item(cx)
1424 .unwrap()
1425 .to_any()
1426 .downcast::<Editor>()
1427 .unwrap()
1428 });
1429
1430 editor.update(&mut cx, |editor, cx| {
1431 assert!(editor.language(cx).is_none());
1432 editor.handle_input(&Input("hi".into()), cx);
1433 assert!(editor.is_dirty(cx.as_ref()));
1434 });
1435
1436 // Save the buffer. This prompts for a filename.
1437 workspace.update(&mut cx, |workspace, cx| {
1438 workspace.save_active_item(&Save, cx)
1439 });
1440 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
1441
1442 editor
1443 .condition(&cx, |editor, cx| !editor.is_dirty(cx))
1444 .await;
1445
1446 // The language is assigned based on the path
1447 editor.read_with(&cx, |editor, cx| {
1448 assert_eq!(editor.language(cx).unwrap().name(), "Rust")
1449 });
1450 }
1451
1452 #[gpui::test]
1453 async fn test_pane_actions(mut cx: gpui::TestAppContext) {
1454 cx.update(|cx| pane::init(cx));
1455 let params = cx.update(WorkspaceParams::test);
1456 params
1457 .fs
1458 .as_fake()
1459 .insert_tree(
1460 "/root",
1461 json!({
1462 "a": {
1463 "file1": "contents 1",
1464 "file2": "contents 2",
1465 "file3": "contents 3",
1466 },
1467 }),
1468 )
1469 .await;
1470
1471 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
1472 workspace
1473 .update(&mut cx, |workspace, cx| {
1474 workspace.add_worktree(Path::new("/root"), cx)
1475 })
1476 .await
1477 .unwrap();
1478 cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
1479 .await;
1480 let entries = cx.read(|cx| workspace.file_project_paths(cx));
1481 let file1 = entries[0].clone();
1482
1483 let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
1484
1485 workspace
1486 .update(&mut cx, |w, cx| w.open_entry(file1.clone(), cx))
1487 .unwrap()
1488 .await;
1489 cx.read(|cx| {
1490 assert_eq!(
1491 pane_1.read(cx).active_item().unwrap().project_path(cx),
1492 Some(file1.clone())
1493 );
1494 });
1495
1496 cx.dispatch_action(
1497 window_id,
1498 vec![pane_1.id()],
1499 pane::Split(SplitDirection::Right),
1500 );
1501 cx.update(|cx| {
1502 let pane_2 = workspace.read(cx).active_pane().clone();
1503 assert_ne!(pane_1, pane_2);
1504
1505 let pane2_item = pane_2.read(cx).active_item().unwrap();
1506 assert_eq!(pane2_item.project_path(cx.as_ref()), Some(file1.clone()));
1507
1508 cx.dispatch_action(window_id, vec![pane_2.id()], &CloseActiveItem);
1509 let workspace = workspace.read(cx);
1510 assert_eq!(workspace.panes.len(), 1);
1511 assert_eq!(workspace.active_pane(), &pane_1);
1512 });
1513 }
1514}