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