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