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