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