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