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