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