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