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