1pub mod dock;
2/// NOTE: Focus only 'takes' after an update has flushed_effects.
3///
4/// This may cause issues when you're trying to write tests that use workspace focus to add items at
5/// specific locations.
6pub mod item;
7pub mod notifications;
8pub mod pane;
9pub mod pane_group;
10mod persistence;
11pub mod searchable;
12pub mod shared_screen;
13mod status_bar;
14mod toolbar;
15
16use anyhow::{anyhow, Context, Result};
17use assets::Assets;
18use call::ActiveCall;
19use client::{
20 proto::{self, PeerId},
21 Client, TypedEnvelope, UserStore,
22};
23use collections::{hash_map, HashMap, HashSet};
24use drag_and_drop::DragAndDrop;
25use futures::{
26 channel::{mpsc, oneshot},
27 future::try_join_all,
28 FutureExt, StreamExt,
29};
30use gpui::{
31 actions,
32 elements::*,
33 geometry::{
34 rect::RectF,
35 vector::{vec2f, Vector2F},
36 },
37 impl_actions,
38 platform::{
39 CursorStyle, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds,
40 WindowOptions,
41 },
42 AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
43 SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
44 WindowContext,
45};
46use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
47use language::{LanguageRegistry, Rope};
48use std::{
49 any::TypeId,
50 borrow::Cow,
51 cmp, env,
52 future::Future,
53 path::{Path, PathBuf},
54 str,
55 sync::Arc,
56 time::Duration,
57};
58
59use crate::{
60 notifications::simple_message_notification::MessageNotification,
61 persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace},
62};
63use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle, TogglePanel};
64use lazy_static::lazy_static;
65use notifications::{NotificationHandle, NotifyResultExt};
66pub use pane::*;
67pub use pane_group::*;
68use persistence::{model::SerializedItem, DB};
69pub use persistence::{
70 model::{ItemId, WorkspaceLocation},
71 WorkspaceDb, DB as WORKSPACE_DB,
72};
73use postage::prelude::Stream;
74use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
75use serde::Deserialize;
76use settings::{Autosave, Settings};
77use shared_screen::SharedScreen;
78use status_bar::StatusBar;
79pub use status_bar::StatusItemView;
80use theme::{Theme, ThemeRegistry};
81pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
82use util::{paths, ResultExt};
83
84lazy_static! {
85 static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
86 .ok()
87 .as_deref()
88 .and_then(parse_pixel_position_env_var);
89 static ref ZED_WINDOW_POSITION: Option<Vector2F> = env::var("ZED_WINDOW_POSITION")
90 .ok()
91 .as_deref()
92 .and_then(parse_pixel_position_env_var);
93}
94
95pub trait Modal: View {
96 fn dismiss_on_event(event: &Self::Event) -> bool;
97}
98
99#[derive(Clone, PartialEq)]
100pub struct RemoveWorktreeFromProject(pub WorktreeId);
101
102actions!(
103 workspace,
104 [
105 Open,
106 NewFile,
107 NewWindow,
108 CloseWindow,
109 AddFolderToProject,
110 Unfollow,
111 Save,
112 SaveAs,
113 SaveAll,
114 ActivatePreviousPane,
115 ActivateNextPane,
116 FollowNextCollaborator,
117 ToggleLeftDock,
118 NewTerminal,
119 ToggleTerminalFocus,
120 NewSearch,
121 Feedback,
122 Restart,
123 Welcome,
124 ToggleZoom,
125 ]
126);
127
128actions!(zed, [OpenSettings]);
129
130#[derive(Clone, PartialEq)]
131pub struct OpenPaths {
132 pub paths: Vec<PathBuf>,
133}
134
135#[derive(Clone, Deserialize, PartialEq)]
136pub struct ActivatePane(pub usize);
137
138pub struct Toast {
139 id: usize,
140 msg: Cow<'static, str>,
141 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
142}
143
144impl Toast {
145 pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
146 Toast {
147 id,
148 msg: msg.into(),
149 on_click: None,
150 }
151 }
152
153 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
154 where
155 M: Into<Cow<'static, str>>,
156 F: Fn(&mut WindowContext) + 'static,
157 {
158 self.on_click = Some((message.into(), Arc::new(on_click)));
159 self
160 }
161}
162
163impl PartialEq for Toast {
164 fn eq(&self, other: &Self) -> bool {
165 self.id == other.id
166 && self.msg == other.msg
167 && self.on_click.is_some() == other.on_click.is_some()
168 }
169}
170
171impl Clone for Toast {
172 fn clone(&self) -> Self {
173 Toast {
174 id: self.id,
175 msg: self.msg.to_owned(),
176 on_click: self.on_click.clone(),
177 }
178 }
179}
180
181pub type WorkspaceId = i64;
182
183impl_actions!(workspace, [ActivatePane]);
184
185pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
186 pane::init(cx);
187 notifications::init(cx);
188
189 cx.add_global_action({
190 let app_state = Arc::downgrade(&app_state);
191 move |_: &Open, cx: &mut AppContext| {
192 let mut paths = cx.prompt_for_paths(PathPromptOptions {
193 files: true,
194 directories: true,
195 multiple: true,
196 });
197
198 if let Some(app_state) = app_state.upgrade() {
199 cx.spawn(move |mut cx| async move {
200 if let Some(paths) = paths.recv().await.flatten() {
201 cx.update(|cx| {
202 open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
203 });
204 }
205 })
206 .detach();
207 }
208 }
209 });
210 cx.add_async_action(Workspace::open);
211
212 cx.add_async_action(Workspace::follow_next_collaborator);
213 cx.add_async_action(Workspace::close);
214 cx.add_global_action(Workspace::close_global);
215 cx.add_global_action(restart);
216 cx.add_async_action(Workspace::save_all);
217 cx.add_action(Workspace::add_folder_to_project);
218 cx.add_action(
219 |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
220 let pane = workspace.active_pane().clone();
221 workspace.unfollow(&pane, cx);
222 },
223 );
224 cx.add_action(
225 |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
226 workspace.save_active_item(false, cx).detach_and_log_err(cx);
227 },
228 );
229 cx.add_action(
230 |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
231 workspace.save_active_item(true, cx).detach_and_log_err(cx);
232 },
233 );
234 cx.add_action(Workspace::toggle_panel);
235 cx.add_action(Workspace::focus_center);
236 cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
237 workspace.activate_previous_pane(cx)
238 });
239 cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
240 workspace.activate_next_pane(cx)
241 });
242 cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
243 workspace.toggle_dock(DockPosition::Left, cx);
244 });
245 cx.add_action(Workspace::activate_pane_at_index);
246
247 cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
248 cx.spawn(|workspace, mut cx| async move {
249 let err = install_cli::install_cli(&cx)
250 .await
251 .context("Failed to create CLI symlink");
252
253 workspace.update(&mut cx, |workspace, cx| {
254 if matches!(err, Err(_)) {
255 err.notify_err(workspace, cx);
256 } else {
257 workspace.show_notification(1, cx, |cx| {
258 cx.add_view(|_| {
259 MessageNotification::new("Successfully installed the `zed` binary")
260 })
261 });
262 }
263 })
264 })
265 .detach();
266 });
267
268 cx.add_action(
269 move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
270 create_and_open_local_file(&paths::SETTINGS, cx, || {
271 Settings::initial_user_settings_content(&Assets)
272 .as_ref()
273 .into()
274 })
275 .detach_and_log_err(cx);
276 },
277 );
278
279 let client = &app_state.client;
280 client.add_view_request_handler(Workspace::handle_follow);
281 client.add_view_message_handler(Workspace::handle_unfollow);
282 client.add_view_message_handler(Workspace::handle_update_followers);
283}
284
285type ProjectItemBuilders = HashMap<
286 TypeId,
287 fn(ModelHandle<Project>, AnyModelHandle, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
288>;
289pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
290 cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
291 builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
292 let item = model.downcast::<I::Item>().unwrap();
293 Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx)))
294 });
295 });
296}
297
298type FollowableItemBuilder = fn(
299 ViewHandle<Pane>,
300 ModelHandle<Project>,
301 ViewId,
302 &mut Option<proto::view::Variant>,
303 &mut AppContext,
304) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
305type FollowableItemBuilders = HashMap<
306 TypeId,
307 (
308 FollowableItemBuilder,
309 fn(&AnyViewHandle) -> Box<dyn FollowableItemHandle>,
310 ),
311>;
312pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
313 cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
314 builders.insert(
315 TypeId::of::<I>(),
316 (
317 |pane, project, id, state, cx| {
318 I::from_state_proto(pane, project, id, state, cx).map(|task| {
319 cx.foreground()
320 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
321 })
322 },
323 |this| Box::new(this.clone().downcast::<I>().unwrap()),
324 ),
325 );
326 });
327}
328
329type ItemDeserializers = HashMap<
330 Arc<str>,
331 fn(
332 ModelHandle<Project>,
333 WeakViewHandle<Workspace>,
334 WorkspaceId,
335 ItemId,
336 &mut ViewContext<Pane>,
337 ) -> Task<Result<Box<dyn ItemHandle>>>,
338>;
339pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
340 cx.update_default_global(|deserializers: &mut ItemDeserializers, _cx| {
341 if let Some(serialized_item_kind) = I::serialized_item_kind() {
342 deserializers.insert(
343 Arc::from(serialized_item_kind),
344 |project, workspace, workspace_id, item_id, cx| {
345 let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
346 cx.foreground()
347 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
348 },
349 );
350 }
351 });
352}
353
354pub struct AppState {
355 pub languages: Arc<LanguageRegistry>,
356 pub themes: Arc<ThemeRegistry>,
357 pub client: Arc<client::Client>,
358 pub user_store: ModelHandle<client::UserStore>,
359 pub fs: Arc<dyn fs::Fs>,
360 pub build_window_options:
361 fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
362 pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
363 pub background_actions: BackgroundActions,
364}
365
366impl AppState {
367 #[cfg(any(test, feature = "test-support"))]
368 pub fn test(cx: &mut AppContext) -> Arc<Self> {
369 let settings = Settings::test(cx);
370 cx.set_global(settings);
371
372 let fs = fs::FakeFs::new(cx.background().clone());
373 let languages = Arc::new(LanguageRegistry::test());
374 let http_client = util::http::FakeHttpClient::with_404_response();
375 let client = Client::new(http_client.clone(), cx);
376 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
377 let themes = ThemeRegistry::new((), cx.font_cache().clone());
378 Arc::new(Self {
379 client,
380 themes,
381 fs,
382 languages,
383 user_store,
384 initialize_workspace: |_, _, _| {},
385 build_window_options: |_, _, _| Default::default(),
386 background_actions: || &[],
387 })
388 }
389}
390
391struct DelayedDebouncedEditAction {
392 task: Option<Task<()>>,
393 cancel_channel: Option<oneshot::Sender<()>>,
394}
395
396impl DelayedDebouncedEditAction {
397 fn new() -> DelayedDebouncedEditAction {
398 DelayedDebouncedEditAction {
399 task: None,
400 cancel_channel: None,
401 }
402 }
403
404 fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, f: F)
405 where
406 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
407 {
408 if let Some(channel) = self.cancel_channel.take() {
409 _ = channel.send(());
410 }
411
412 let (sender, mut receiver) = oneshot::channel::<()>();
413 self.cancel_channel = Some(sender);
414
415 let previous_task = self.task.take();
416 self.task = Some(cx.spawn(|workspace, mut cx| async move {
417 let mut timer = cx.background().timer(delay).fuse();
418 if let Some(previous_task) = previous_task {
419 previous_task.await;
420 }
421
422 futures::select_biased! {
423 _ = receiver => return,
424 _ = timer => {}
425 }
426
427 if let Some(result) = workspace
428 .update(&mut cx, |workspace, cx| (f)(workspace, cx))
429 .log_err()
430 {
431 result.await.log_err();
432 }
433 }));
434 }
435}
436
437pub enum Event {
438 PaneAdded(ViewHandle<Pane>),
439 ContactRequestedJoin(u64),
440}
441
442pub struct Workspace {
443 weak_self: WeakViewHandle<Self>,
444 remote_entity_subscription: Option<client::Subscription>,
445 modal: Option<AnyViewHandle>,
446 center: PaneGroup,
447 left_dock: ViewHandle<Dock>,
448 bottom_dock: ViewHandle<Dock>,
449 right_dock: ViewHandle<Dock>,
450 panes: Vec<ViewHandle<Pane>>,
451 panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
452 active_pane: ViewHandle<Pane>,
453 last_active_center_pane: Option<WeakViewHandle<Pane>>,
454 status_bar: ViewHandle<StatusBar>,
455 titlebar_item: Option<AnyViewHandle>,
456 notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
457 project: ModelHandle<Project>,
458 leader_state: LeaderState,
459 follower_states_by_leader: FollowerStatesByLeader,
460 last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
461 window_edited: bool,
462 active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
463 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
464 database_id: WorkspaceId,
465 app_state: Arc<AppState>,
466 _subscriptions: Vec<Subscription>,
467 _apply_leader_updates: Task<Result<()>>,
468 _observe_current_user: Task<Result<()>>,
469}
470
471#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
472pub struct ViewId {
473 pub creator: PeerId,
474 pub id: u64,
475}
476
477#[derive(Default)]
478struct LeaderState {
479 followers: HashSet<PeerId>,
480}
481
482type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
483
484#[derive(Default)]
485struct FollowerState {
486 active_view_id: Option<ViewId>,
487 items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
488}
489
490impl Workspace {
491 pub fn new(
492 serialized_workspace: Option<SerializedWorkspace>,
493 workspace_id: WorkspaceId,
494 project: ModelHandle<Project>,
495 app_state: Arc<AppState>,
496 cx: &mut ViewContext<Self>,
497 ) -> Self {
498 cx.observe(&project, |_, _, cx| cx.notify()).detach();
499 cx.subscribe(&project, move |this, _, event, cx| {
500 match event {
501 project::Event::RemoteIdChanged(remote_id) => {
502 this.update_window_title(cx);
503 this.project_remote_id_changed(*remote_id, cx);
504 }
505
506 project::Event::CollaboratorLeft(peer_id) => {
507 this.collaborator_left(*peer_id, cx);
508 }
509
510 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
511 this.update_window_title(cx);
512 this.serialize_workspace(cx);
513 }
514
515 project::Event::DisconnectedFromHost => {
516 this.update_window_edited(cx);
517 cx.blur();
518 }
519
520 project::Event::Closed => {
521 cx.remove_window();
522 }
523
524 _ => {}
525 }
526 cx.notify()
527 })
528 .detach();
529
530 let weak_handle = cx.weak_handle();
531
532 let center_pane =
533 cx.add_view(|cx| Pane::new(weak_handle.clone(), app_state.background_actions, cx));
534 cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
535 cx.focus(¢er_pane);
536 cx.emit(Event::PaneAdded(center_pane.clone()));
537
538 let mut current_user = app_state.user_store.read(cx).watch_current_user();
539 let mut connection_status = app_state.client.status();
540 let _observe_current_user = cx.spawn(|this, mut cx| async move {
541 current_user.recv().await;
542 connection_status.recv().await;
543 let mut stream =
544 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
545
546 while stream.recv().await.is_some() {
547 this.update(&mut cx, |_, cx| cx.notify())?;
548 }
549 anyhow::Ok(())
550 });
551
552 // All leader updates are enqueued and then processed in a single task, so
553 // that each asynchronous operation can be run in order.
554 let (leader_updates_tx, mut leader_updates_rx) =
555 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
556 let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
557 while let Some((leader_id, update)) = leader_updates_rx.next().await {
558 Self::process_leader_update(&this, leader_id, update, &mut cx)
559 .await
560 .log_err();
561 }
562
563 Ok(())
564 });
565
566 cx.emit_global(WorkspaceCreated(weak_handle.clone()));
567
568 let left_dock = cx.add_view(|_| Dock::new(DockPosition::Left));
569 let bottom_dock = cx.add_view(|_| Dock::new(DockPosition::Bottom));
570 let right_dock = cx.add_view(|_| Dock::new(DockPosition::Right));
571 let left_dock_buttons =
572 cx.add_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx));
573 let bottom_dock_buttons =
574 cx.add_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx));
575 let right_dock_buttons =
576 cx.add_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx));
577 let status_bar = cx.add_view(|cx| {
578 let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
579 status_bar.add_left_item(left_dock_buttons, cx);
580 status_bar.add_right_item(right_dock_buttons, cx);
581 status_bar.add_right_item(bottom_dock_buttons, cx);
582 status_bar
583 });
584
585 cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
586 drag_and_drop.register_container(weak_handle.clone());
587 });
588
589 let mut active_call = None;
590 if cx.has_global::<ModelHandle<ActiveCall>>() {
591 let call = cx.global::<ModelHandle<ActiveCall>>().clone();
592 let mut subscriptions = Vec::new();
593 subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
594 active_call = Some((call, subscriptions));
595 }
596
597 let subscriptions = vec![
598 cx.observe_fullscreen(|_, _, cx| cx.notify()),
599 cx.observe_window_activation(Self::on_window_activation_changed),
600 cx.observe_window_bounds(move |_, mut bounds, display, cx| {
601 // Transform fixed bounds to be stored in terms of the containing display
602 if let WindowBounds::Fixed(mut window_bounds) = bounds {
603 if let Some(screen) = cx.platform().screen_by_id(display) {
604 let screen_bounds = screen.bounds();
605 window_bounds
606 .set_origin_x(window_bounds.origin_x() - screen_bounds.origin_x());
607 window_bounds
608 .set_origin_y(window_bounds.origin_y() - screen_bounds.origin_y());
609 bounds = WindowBounds::Fixed(window_bounds);
610 }
611 }
612
613 cx.background()
614 .spawn(DB.set_window_bounds(workspace_id, bounds, display))
615 .detach_and_log_err(cx);
616 }),
617 cx.observe(&left_dock, |this, _, cx| {
618 this.serialize_workspace(cx);
619 cx.notify();
620 }),
621 cx.observe(&bottom_dock, |this, _, cx| {
622 this.serialize_workspace(cx);
623 cx.notify();
624 }),
625 cx.observe(&right_dock, |this, _, cx| {
626 this.serialize_workspace(cx);
627 cx.notify();
628 }),
629 ];
630
631 let mut this = Workspace {
632 weak_self: weak_handle.clone(),
633 modal: None,
634 center: PaneGroup::new(center_pane.clone()),
635 panes: vec![center_pane.clone()],
636 panes_by_item: Default::default(),
637 active_pane: center_pane.clone(),
638 last_active_center_pane: Some(center_pane.downgrade()),
639 status_bar,
640 titlebar_item: None,
641 notifications: Default::default(),
642 remote_entity_subscription: None,
643 left_dock,
644 bottom_dock,
645 right_dock,
646 project: project.clone(),
647 leader_state: Default::default(),
648 follower_states_by_leader: Default::default(),
649 last_leaders_by_pane: Default::default(),
650 window_edited: false,
651 active_call,
652 database_id: workspace_id,
653 app_state,
654 _observe_current_user,
655 _apply_leader_updates,
656 leader_updates_tx,
657 _subscriptions: subscriptions,
658 };
659 this.project_remote_id_changed(project.read(cx).remote_id(), cx);
660 cx.defer(|this, cx| this.update_window_title(cx));
661
662 if let Some(serialized_workspace) = serialized_workspace {
663 cx.defer(move |_, cx| {
664 Self::load_from_serialized_workspace(weak_handle, serialized_workspace, cx)
665 });
666 }
667
668 this
669 }
670
671 fn new_local(
672 abs_paths: Vec<PathBuf>,
673 app_state: Arc<AppState>,
674 requesting_window_id: Option<usize>,
675 cx: &mut AppContext,
676 ) -> Task<(
677 WeakViewHandle<Workspace>,
678 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
679 )> {
680 let project_handle = Project::local(
681 app_state.client.clone(),
682 app_state.user_store.clone(),
683 app_state.languages.clone(),
684 app_state.fs.clone(),
685 cx,
686 );
687
688 cx.spawn(|mut cx| async move {
689 let mut serialized_workspace =
690 persistence::DB.workspace_for_roots(&abs_paths.as_slice());
691
692 let paths_to_open = serialized_workspace
693 .as_ref()
694 .map(|workspace| workspace.location.paths())
695 .unwrap_or(Arc::new(abs_paths));
696
697 // Get project paths for all of the abs_paths
698 let mut worktree_roots: HashSet<Arc<Path>> = Default::default();
699 let mut project_paths = Vec::new();
700 for path in paths_to_open.iter() {
701 if let Some((worktree, project_entry)) = cx
702 .update(|cx| {
703 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
704 })
705 .await
706 .log_err()
707 {
708 worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path()));
709 project_paths.push(Some(project_entry));
710 } else {
711 project_paths.push(None);
712 }
713 }
714
715 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
716 serialized_workspace.id
717 } else {
718 DB.next_id().await.unwrap_or(0)
719 };
720
721 let window_bounds_override =
722 ZED_WINDOW_POSITION
723 .zip(*ZED_WINDOW_SIZE)
724 .map(|(position, size)| {
725 WindowBounds::Fixed(RectF::new(
726 cx.platform().screens()[0].bounds().origin() + position,
727 size,
728 ))
729 });
730
731 let build_workspace =
732 |cx: &mut ViewContext<Workspace>,
733 serialized_workspace: Option<SerializedWorkspace>| {
734 let mut workspace = Workspace::new(
735 serialized_workspace,
736 workspace_id,
737 project_handle.clone(),
738 app_state.clone(),
739 cx,
740 );
741 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
742 workspace
743 };
744
745 let workspace = requesting_window_id
746 .and_then(|window_id| {
747 cx.update(|cx| {
748 cx.replace_root_view(window_id, |cx| {
749 build_workspace(cx, serialized_workspace.take())
750 })
751 })
752 })
753 .unwrap_or_else(|| {
754 let (bounds, display) = if let Some(bounds) = window_bounds_override {
755 (Some(bounds), None)
756 } else {
757 serialized_workspace
758 .as_ref()
759 .and_then(|serialized_workspace| {
760 let display = serialized_workspace.display?;
761 let mut bounds = serialized_workspace.bounds?;
762
763 // Stored bounds are relative to the containing display.
764 // So convert back to global coordinates if that screen still exists
765 if let WindowBounds::Fixed(mut window_bounds) = bounds {
766 if let Some(screen) = cx.platform().screen_by_id(display) {
767 let screen_bounds = screen.bounds();
768 window_bounds.set_origin_x(
769 window_bounds.origin_x() + screen_bounds.origin_x(),
770 );
771 window_bounds.set_origin_y(
772 window_bounds.origin_y() + screen_bounds.origin_y(),
773 );
774 bounds = WindowBounds::Fixed(window_bounds);
775 } else {
776 // Screen no longer exists. Return none here.
777 return None;
778 }
779 }
780
781 Some((bounds, display))
782 })
783 .unzip()
784 };
785
786 // Use the serialized workspace to construct the new window
787 cx.add_window(
788 (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
789 |cx| build_workspace(cx, serialized_workspace),
790 )
791 .1
792 });
793
794 let workspace = workspace.downgrade();
795 notify_if_database_failed(&workspace, &mut cx);
796
797 // Call open path for each of the project paths
798 // (this will bring them to the front if they were in the serialized workspace)
799 debug_assert!(paths_to_open.len() == project_paths.len());
800 let tasks = paths_to_open
801 .iter()
802 .cloned()
803 .zip(project_paths.into_iter())
804 .map(|(abs_path, project_path)| {
805 let workspace = workspace.clone();
806 cx.spawn(|mut cx| {
807 let fs = app_state.fs.clone();
808 async move {
809 let project_path = project_path?;
810 if fs.is_file(&abs_path).await {
811 Some(
812 workspace
813 .update(&mut cx, |workspace, cx| {
814 workspace.open_path(project_path, None, true, cx)
815 })
816 .log_err()?
817 .await,
818 )
819 } else {
820 None
821 }
822 }
823 })
824 });
825
826 let opened_items = futures::future::join_all(tasks.into_iter()).await;
827
828 (workspace, opened_items)
829 })
830 }
831
832 pub fn weak_handle(&self) -> WeakViewHandle<Self> {
833 self.weak_self.clone()
834 }
835
836 pub fn left_dock(&self) -> &ViewHandle<Dock> {
837 &self.left_dock
838 }
839
840 pub fn bottom_dock(&self) -> &ViewHandle<Dock> {
841 &self.bottom_dock
842 }
843
844 pub fn right_dock(&self) -> &ViewHandle<Dock> {
845 &self.right_dock
846 }
847
848 pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
849 let dock = match panel.position(cx) {
850 DockPosition::Left => &self.left_dock,
851 DockPosition::Bottom => &self.bottom_dock,
852 DockPosition::Right => &self.right_dock,
853 };
854
855 cx.subscribe(&panel, {
856 let mut dock = dock.clone();
857 let mut prev_position = panel.position(cx);
858 move |this, panel, event, cx| {
859 if T::should_change_position_on_event(event) {
860 let new_position = panel.read(cx).position(cx);
861 let mut was_visible = false;
862 let mut size = None;
863 dock.update(cx, |dock, cx| {
864 if new_position.axis() == prev_position.axis() {
865 size = dock.panel_size(&panel);
866 }
867 prev_position = new_position;
868
869 was_visible = dock.is_open()
870 && dock
871 .active_panel()
872 .map_or(false, |active_panel| active_panel.id() == panel.id());
873 dock.remove_panel(&panel, cx);
874 });
875 dock = match panel.read(cx).position(cx) {
876 DockPosition::Left => &this.left_dock,
877 DockPosition::Bottom => &this.bottom_dock,
878 DockPosition::Right => &this.right_dock,
879 }
880 .clone();
881 dock.update(cx, |dock, cx| {
882 dock.add_panel(panel.clone(), cx);
883 if let Some(size) = size {
884 dock.resize_panel(&panel, size);
885 }
886
887 if was_visible {
888 dock.set_open(true, cx);
889 dock.activate_panel(dock.panels_len() - 1, cx);
890 }
891 });
892 } else if T::should_zoom_in_on_event(event) {
893 this.zoom_out(cx);
894 dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx));
895 } else if T::should_zoom_out_on_event(event) {
896 this.zoom_out(cx);
897 } else if T::is_focus_event(event) {
898 cx.notify();
899 }
900 }
901 })
902 .detach();
903
904 dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
905 }
906
907 pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
908 &self.status_bar
909 }
910
911 pub fn app_state(&self) -> &Arc<AppState> {
912 &self.app_state
913 }
914
915 pub fn user_store(&self) -> &ModelHandle<UserStore> {
916 &self.app_state.user_store
917 }
918
919 pub fn project(&self) -> &ModelHandle<Project> {
920 &self.project
921 }
922
923 pub fn client(&self) -> &Client {
924 &self.app_state.client
925 }
926
927 pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
928 self.titlebar_item = Some(item);
929 cx.notify();
930 }
931
932 pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
933 self.titlebar_item.clone()
934 }
935
936 /// Call the given callback with a workspace whose project is local.
937 ///
938 /// If the given workspace has a local project, then it will be passed
939 /// to the callback. Otherwise, a new empty window will be created.
940 pub fn with_local_workspace<T, F>(
941 &mut self,
942 cx: &mut ViewContext<Self>,
943 callback: F,
944 ) -> Task<Result<T>>
945 where
946 T: 'static,
947 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
948 {
949 if self.project.read(cx).is_local() {
950 Task::Ready(Some(Ok(callback(self, cx))))
951 } else {
952 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
953 cx.spawn(|_vh, mut cx| async move {
954 let (workspace, _) = task.await;
955 workspace.update(&mut cx, callback)
956 })
957 }
958 }
959
960 pub fn worktrees<'a>(
961 &self,
962 cx: &'a AppContext,
963 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
964 self.project.read(cx).worktrees(cx)
965 }
966
967 pub fn visible_worktrees<'a>(
968 &self,
969 cx: &'a AppContext,
970 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
971 self.project.read(cx).visible_worktrees(cx)
972 }
973
974 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
975 let futures = self
976 .worktrees(cx)
977 .filter_map(|worktree| worktree.read(cx).as_local())
978 .map(|worktree| worktree.scan_complete())
979 .collect::<Vec<_>>();
980 async move {
981 for future in futures {
982 future.await;
983 }
984 }
985 }
986
987 pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
988 cx.spawn(|mut cx| async move {
989 let id = cx
990 .window_ids()
991 .into_iter()
992 .find(|&id| cx.window_is_active(id));
993 if let Some(id) = id {
994 //This can only get called when the window's project connection has been lost
995 //so we don't need to prompt the user for anything and instead just close the window
996 cx.remove_window(id);
997 }
998 })
999 .detach();
1000 }
1001
1002 pub fn close(
1003 &mut self,
1004 _: &CloseWindow,
1005 cx: &mut ViewContext<Self>,
1006 ) -> Option<Task<Result<()>>> {
1007 let window_id = cx.window_id();
1008 let prepare = self.prepare_to_close(false, cx);
1009 Some(cx.spawn(|_, mut cx| async move {
1010 if prepare.await? {
1011 cx.remove_window(window_id);
1012 }
1013 Ok(())
1014 }))
1015 }
1016
1017 pub fn prepare_to_close(
1018 &mut self,
1019 quitting: bool,
1020 cx: &mut ViewContext<Self>,
1021 ) -> Task<Result<bool>> {
1022 let active_call = self.active_call().cloned();
1023 let window_id = cx.window_id();
1024
1025 cx.spawn(|this, mut cx| async move {
1026 let workspace_count = cx
1027 .window_ids()
1028 .into_iter()
1029 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
1030 .count();
1031
1032 if let Some(active_call) = active_call {
1033 if !quitting
1034 && workspace_count == 1
1035 && active_call.read_with(&cx, |call, _| call.room().is_some())
1036 {
1037 let answer = cx.prompt(
1038 window_id,
1039 PromptLevel::Warning,
1040 "Do you want to leave the current call?",
1041 &["Close window and hang up", "Cancel"],
1042 );
1043
1044 if let Some(mut answer) = answer {
1045 if answer.next().await == Some(1) {
1046 return anyhow::Ok(false);
1047 } else {
1048 active_call
1049 .update(&mut cx, |call, cx| call.hang_up(cx))
1050 .await
1051 .log_err();
1052 }
1053 }
1054 }
1055 }
1056
1057 Ok(this
1058 .update(&mut cx, |this, cx| this.save_all_internal(true, cx))?
1059 .await?)
1060 })
1061 }
1062
1063 fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1064 let save_all = self.save_all_internal(false, cx);
1065 Some(cx.foreground().spawn(async move {
1066 save_all.await?;
1067 Ok(())
1068 }))
1069 }
1070
1071 fn save_all_internal(
1072 &mut self,
1073 should_prompt_to_save: bool,
1074 cx: &mut ViewContext<Self>,
1075 ) -> Task<Result<bool>> {
1076 if self.project.read(cx).is_read_only() {
1077 return Task::ready(Ok(true));
1078 }
1079
1080 let dirty_items = self
1081 .panes
1082 .iter()
1083 .flat_map(|pane| {
1084 pane.read(cx).items().filter_map(|item| {
1085 if item.is_dirty(cx) {
1086 Some((pane.downgrade(), item.boxed_clone()))
1087 } else {
1088 None
1089 }
1090 })
1091 })
1092 .collect::<Vec<_>>();
1093
1094 let project = self.project.clone();
1095 cx.spawn(|_, mut cx| async move {
1096 for (pane, item) in dirty_items {
1097 let (singleton, project_entry_ids) =
1098 cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
1099 if singleton || !project_entry_ids.is_empty() {
1100 if let Some(ix) =
1101 pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))?
1102 {
1103 if !Pane::save_item(
1104 project.clone(),
1105 &pane,
1106 ix,
1107 &*item,
1108 should_prompt_to_save,
1109 &mut cx,
1110 )
1111 .await?
1112 {
1113 return Ok(false);
1114 }
1115 }
1116 }
1117 }
1118 Ok(true)
1119 })
1120 }
1121
1122 pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1123 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1124 files: true,
1125 directories: true,
1126 multiple: true,
1127 });
1128
1129 Some(cx.spawn(|this, mut cx| async move {
1130 if let Some(paths) = paths.recv().await.flatten() {
1131 if let Some(task) = this
1132 .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
1133 .log_err()
1134 {
1135 task.await?
1136 }
1137 }
1138 Ok(())
1139 }))
1140 }
1141
1142 pub fn open_workspace_for_paths(
1143 &mut self,
1144 paths: Vec<PathBuf>,
1145 cx: &mut ViewContext<Self>,
1146 ) -> Task<Result<()>> {
1147 let window_id = cx.window_id();
1148 let is_remote = self.project.read(cx).is_remote();
1149 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
1150 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
1151 let close_task = if is_remote || has_worktree || has_dirty_items {
1152 None
1153 } else {
1154 Some(self.prepare_to_close(false, cx))
1155 };
1156 let app_state = self.app_state.clone();
1157
1158 cx.spawn(|_, mut cx| async move {
1159 let window_id_to_replace = if let Some(close_task) = close_task {
1160 if !close_task.await? {
1161 return Ok(());
1162 }
1163 Some(window_id)
1164 } else {
1165 None
1166 };
1167 cx.update(|cx| open_paths(&paths, &app_state, window_id_to_replace, cx))
1168 .await?;
1169 Ok(())
1170 })
1171 }
1172
1173 #[allow(clippy::type_complexity)]
1174 pub fn open_paths(
1175 &mut self,
1176 mut abs_paths: Vec<PathBuf>,
1177 visible: bool,
1178 cx: &mut ViewContext<Self>,
1179 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1180 let fs = self.app_state.fs.clone();
1181
1182 // Sort the paths to ensure we add worktrees for parents before their children.
1183 abs_paths.sort_unstable();
1184 cx.spawn(|this, mut cx| async move {
1185 let mut project_paths = Vec::new();
1186 for path in &abs_paths {
1187 if let Some(project_path) = this
1188 .update(&mut cx, |this, cx| {
1189 Workspace::project_path_for_path(this.project.clone(), path, visible, cx)
1190 })
1191 .log_err()
1192 {
1193 project_paths.push(project_path.await.log_err());
1194 } else {
1195 project_paths.push(None);
1196 }
1197 }
1198
1199 let tasks = abs_paths
1200 .iter()
1201 .cloned()
1202 .zip(project_paths.into_iter())
1203 .map(|(abs_path, project_path)| {
1204 let this = this.clone();
1205 cx.spawn(|mut cx| {
1206 let fs = fs.clone();
1207 async move {
1208 let (_worktree, project_path) = project_path?;
1209 if fs.is_file(&abs_path).await {
1210 Some(
1211 this.update(&mut cx, |this, cx| {
1212 this.open_path(project_path, None, true, cx)
1213 })
1214 .log_err()?
1215 .await,
1216 )
1217 } else {
1218 None
1219 }
1220 }
1221 })
1222 })
1223 .collect::<Vec<_>>();
1224
1225 futures::future::join_all(tasks).await
1226 })
1227 }
1228
1229 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1230 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1231 files: false,
1232 directories: true,
1233 multiple: true,
1234 });
1235 cx.spawn(|this, mut cx| async move {
1236 if let Some(paths) = paths.recv().await.flatten() {
1237 let results = this
1238 .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
1239 .await;
1240 for result in results.into_iter().flatten() {
1241 result.log_err();
1242 }
1243 }
1244 anyhow::Ok(())
1245 })
1246 .detach_and_log_err(cx);
1247 }
1248
1249 fn project_path_for_path(
1250 project: ModelHandle<Project>,
1251 abs_path: &Path,
1252 visible: bool,
1253 cx: &mut AppContext,
1254 ) -> Task<Result<(ModelHandle<Worktree>, ProjectPath)>> {
1255 let entry = project.update(cx, |project, cx| {
1256 project.find_or_create_local_worktree(abs_path, visible, cx)
1257 });
1258 cx.spawn(|cx| async move {
1259 let (worktree, path) = entry.await?;
1260 let worktree_id = worktree.read_with(&cx, |t, _| t.id());
1261 Ok((
1262 worktree,
1263 ProjectPath {
1264 worktree_id,
1265 path: path.into(),
1266 },
1267 ))
1268 })
1269 }
1270
1271 /// Returns the modal that was toggled closed if it was open.
1272 pub fn toggle_modal<V, F>(
1273 &mut self,
1274 cx: &mut ViewContext<Self>,
1275 add_view: F,
1276 ) -> Option<ViewHandle<V>>
1277 where
1278 V: 'static + Modal,
1279 F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
1280 {
1281 cx.notify();
1282 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
1283 // it. Otherwise, create a new modal and set it as active.
1284 let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
1285 if let Some(already_open_modal) = already_open_modal {
1286 cx.focus_self();
1287 Some(already_open_modal)
1288 } else {
1289 let modal = add_view(self, cx);
1290 cx.subscribe(&modal, |this, _, event, cx| {
1291 if V::dismiss_on_event(event) {
1292 this.dismiss_modal(cx);
1293 }
1294 })
1295 .detach();
1296 cx.focus(&modal);
1297 self.modal = Some(modal.into_any());
1298 None
1299 }
1300 }
1301
1302 pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
1303 self.modal
1304 .as_ref()
1305 .and_then(|modal| modal.clone().downcast::<V>())
1306 }
1307
1308 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
1309 if self.modal.take().is_some() {
1310 cx.focus(&self.active_pane);
1311 cx.notify();
1312 }
1313 }
1314
1315 fn zoomed(&self, cx: &WindowContext) -> Option<AnyViewHandle> {
1316 self.zoomed_panel_for_dock(DockPosition::Left, cx)
1317 .or_else(|| self.zoomed_panel_for_dock(DockPosition::Bottom, cx))
1318 .or_else(|| self.zoomed_panel_for_dock(DockPosition::Right, cx))
1319 .or_else(|| self.zoomed_pane(cx))
1320 }
1321
1322 fn zoomed_panel_for_dock(
1323 &self,
1324 position: DockPosition,
1325 cx: &WindowContext,
1326 ) -> Option<AnyViewHandle> {
1327 let (dock, other_docks) = match position {
1328 DockPosition::Left => (&self.left_dock, [&self.bottom_dock, &self.right_dock]),
1329 DockPosition::Bottom => (&self.bottom_dock, [&self.left_dock, &self.right_dock]),
1330 DockPosition::Right => (&self.right_dock, [&self.left_dock, &self.bottom_dock]),
1331 };
1332
1333 let zoomed_panel = dock.read(&cx).zoomed_panel()?;
1334 if other_docks.iter().all(|dock| !dock.read(cx).has_focus(cx))
1335 && !self.active_pane.read(cx).has_focus()
1336 {
1337 Some(zoomed_panel.as_any().clone())
1338 } else {
1339 None
1340 }
1341 }
1342
1343 fn zoomed_pane(&self, cx: &WindowContext) -> Option<AnyViewHandle> {
1344 let active_pane = self.active_pane.read(cx);
1345 let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
1346 if active_pane.is_zoomed() && docks.iter().all(|dock| !dock.read(cx).has_focus(cx)) {
1347 Some(self.active_pane.clone().into_any())
1348 } else {
1349 None
1350 }
1351 }
1352
1353 pub fn items<'a>(
1354 &'a self,
1355 cx: &'a AppContext,
1356 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1357 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1358 }
1359
1360 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
1361 self.items_of_type(cx).max_by_key(|item| item.id())
1362 }
1363
1364 pub fn items_of_type<'a, T: Item>(
1365 &'a self,
1366 cx: &'a AppContext,
1367 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
1368 self.panes
1369 .iter()
1370 .flat_map(|pane| pane.read(cx).items_of_type())
1371 }
1372
1373 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1374 self.active_pane().read(cx).active_item()
1375 }
1376
1377 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1378 self.active_item(cx).and_then(|item| item.project_path(cx))
1379 }
1380
1381 pub fn save_active_item(
1382 &mut self,
1383 force_name_change: bool,
1384 cx: &mut ViewContext<Self>,
1385 ) -> Task<Result<()>> {
1386 let project = self.project.clone();
1387 if let Some(item) = self.active_item(cx) {
1388 if !force_name_change && item.can_save(cx) {
1389 if item.has_conflict(cx) {
1390 const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1391
1392 let mut answer = cx.prompt(
1393 PromptLevel::Warning,
1394 CONFLICT_MESSAGE,
1395 &["Overwrite", "Cancel"],
1396 );
1397 cx.spawn(|this, mut cx| async move {
1398 let answer = answer.recv().await;
1399 if answer == Some(0) {
1400 this.update(&mut cx, |this, cx| item.save(this.project.clone(), cx))?
1401 .await?;
1402 }
1403 Ok(())
1404 })
1405 } else {
1406 item.save(self.project.clone(), cx)
1407 }
1408 } else if item.is_singleton(cx) {
1409 let worktree = self.worktrees(cx).next();
1410 let start_abs_path = worktree
1411 .and_then(|w| w.read(cx).as_local())
1412 .map_or(Path::new(""), |w| w.abs_path())
1413 .to_path_buf();
1414 let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
1415 cx.spawn(|this, mut cx| async move {
1416 if let Some(abs_path) = abs_path.recv().await.flatten() {
1417 this.update(&mut cx, |_, cx| item.save_as(project, abs_path, cx))?
1418 .await?;
1419 }
1420 Ok(())
1421 })
1422 } else {
1423 Task::ready(Ok(()))
1424 }
1425 } else {
1426 Task::ready(Ok(()))
1427 }
1428 }
1429
1430 pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
1431 let dock = match dock_side {
1432 DockPosition::Left => &mut self.left_dock,
1433 DockPosition::Bottom => &mut self.bottom_dock,
1434 DockPosition::Right => &mut self.right_dock,
1435 };
1436 dock.update(cx, |dock, cx| {
1437 let open = !dock.is_open();
1438 dock.set_open(open, cx);
1439 });
1440
1441 self.serialize_workspace(cx);
1442
1443 cx.focus_self();
1444 cx.notify();
1445 }
1446
1447 pub fn toggle_panel(&mut self, action: &TogglePanel, cx: &mut ViewContext<Self>) {
1448 let dock = match action.dock_position {
1449 DockPosition::Left => &mut self.left_dock,
1450 DockPosition::Bottom => &mut self.bottom_dock,
1451 DockPosition::Right => &mut self.right_dock,
1452 };
1453 let active_item = dock.update(cx, move |dock, cx| {
1454 if dock.is_open() && dock.active_panel_index() == action.panel_index {
1455 dock.set_open(false, cx);
1456 None
1457 } else {
1458 dock.set_open(true, cx);
1459 dock.activate_panel(action.panel_index, cx);
1460 dock.active_panel().cloned()
1461 }
1462 });
1463
1464 if let Some(active_item) = active_item {
1465 if active_item.has_focus(cx) {
1466 cx.focus_self();
1467 } else {
1468 cx.focus(active_item.as_any());
1469 }
1470 } else {
1471 cx.focus_self();
1472 }
1473
1474 self.serialize_workspace(cx);
1475
1476 cx.notify();
1477 }
1478
1479 pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
1480 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1481 if let Some(panel_index) = dock.read(cx).panel_index::<T>() {
1482 let active_item = dock.update(cx, |dock, cx| {
1483 dock.set_open(true, cx);
1484 dock.activate_panel(panel_index, cx);
1485 dock.active_panel().cloned()
1486 });
1487 if let Some(active_item) = active_item {
1488 if active_item.has_focus(cx) {
1489 cx.focus_self();
1490 } else {
1491 cx.focus(active_item.as_any());
1492 }
1493 }
1494
1495 self.serialize_workspace(cx);
1496 cx.notify();
1497 break;
1498 }
1499 }
1500 }
1501
1502 fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
1503 for pane in &self.panes {
1504 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1505 }
1506
1507 self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1508 self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1509 self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1510
1511 cx.notify();
1512 }
1513
1514 pub fn focus_center(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
1515 cx.focus_self();
1516 cx.notify();
1517 }
1518
1519 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1520 let pane =
1521 cx.add_view(|cx| Pane::new(self.weak_handle(), self.app_state.background_actions, cx));
1522 cx.subscribe(&pane, Self::handle_pane_event).detach();
1523 self.panes.push(pane.clone());
1524 cx.focus(&pane);
1525 cx.emit(Event::PaneAdded(pane.clone()));
1526 pane
1527 }
1528
1529 pub fn add_item_to_center(
1530 &mut self,
1531 item: Box<dyn ItemHandle>,
1532 cx: &mut ViewContext<Self>,
1533 ) -> bool {
1534 if let Some(center_pane) = self.last_active_center_pane.clone() {
1535 if let Some(center_pane) = center_pane.upgrade(cx) {
1536 Pane::add_item(self, ¢er_pane, item, true, true, None, cx);
1537 true
1538 } else {
1539 false
1540 }
1541 } else {
1542 false
1543 }
1544 }
1545
1546 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1547 let active_pane = self.active_pane().clone();
1548 Pane::add_item(self, &active_pane, item, true, true, None, cx);
1549 }
1550
1551 pub fn open_path(
1552 &mut self,
1553 path: impl Into<ProjectPath>,
1554 pane: Option<WeakViewHandle<Pane>>,
1555 focus_item: bool,
1556 cx: &mut ViewContext<Self>,
1557 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1558 let pane = pane.unwrap_or_else(|| {
1559 self.last_active_center_pane.clone().unwrap_or_else(|| {
1560 self.panes
1561 .first()
1562 .expect("There must be an active pane")
1563 .downgrade()
1564 })
1565 });
1566
1567 let task = self.load_path(path.into(), cx);
1568 cx.spawn(|this, mut cx| async move {
1569 let (project_entry_id, build_item) = task.await?;
1570 let pane = pane
1571 .upgrade(&cx)
1572 .ok_or_else(|| anyhow!("pane was closed"))?;
1573 this.update(&mut cx, |this, cx| {
1574 Pane::open_item(this, pane, project_entry_id, focus_item, cx, build_item)
1575 })
1576 })
1577 }
1578
1579 pub(crate) fn load_path(
1580 &mut self,
1581 path: ProjectPath,
1582 cx: &mut ViewContext<Self>,
1583 ) -> Task<
1584 Result<(
1585 ProjectEntryId,
1586 impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
1587 )>,
1588 > {
1589 let project = self.project().clone();
1590 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1591 cx.spawn(|_, mut cx| async move {
1592 let (project_entry_id, project_item) = project_item.await?;
1593 let build_item = cx.update(|cx| {
1594 cx.default_global::<ProjectItemBuilders>()
1595 .get(&project_item.model_type())
1596 .ok_or_else(|| anyhow!("no item builder for project item"))
1597 .cloned()
1598 })?;
1599 let build_item =
1600 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
1601 Ok((project_entry_id, build_item))
1602 })
1603 }
1604
1605 pub fn open_project_item<T>(
1606 &mut self,
1607 project_item: ModelHandle<T::Item>,
1608 cx: &mut ViewContext<Self>,
1609 ) -> ViewHandle<T>
1610 where
1611 T: ProjectItem,
1612 {
1613 use project::Item as _;
1614
1615 let entry_id = project_item.read(cx).entry_id(cx);
1616 if let Some(item) = entry_id
1617 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1618 .and_then(|item| item.downcast())
1619 {
1620 self.activate_item(&item, cx);
1621 return item;
1622 }
1623
1624 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1625 self.add_item(Box::new(item.clone()), cx);
1626 item
1627 }
1628
1629 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1630 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
1631 let pane = self.active_pane.clone();
1632 Pane::add_item(self, &pane, Box::new(shared_screen), false, true, None, cx);
1633 }
1634 }
1635
1636 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1637 let result = self.panes.iter().find_map(|pane| {
1638 pane.read(cx)
1639 .index_for_item(item)
1640 .map(|ix| (pane.clone(), ix))
1641 });
1642 if let Some((pane, ix)) = result {
1643 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1644 true
1645 } else {
1646 false
1647 }
1648 }
1649
1650 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
1651 let panes = self.center.panes();
1652 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1653 cx.focus(&pane);
1654 } else {
1655 self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
1656 }
1657 }
1658
1659 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1660 let panes = self.center.panes();
1661 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1662 let next_ix = (ix + 1) % panes.len();
1663 let next_pane = panes[next_ix].clone();
1664 cx.focus(&next_pane);
1665 }
1666 }
1667
1668 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1669 let panes = self.center.panes();
1670 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1671 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1672 let prev_pane = panes[prev_ix].clone();
1673 cx.focus(&prev_pane);
1674 }
1675 }
1676
1677 fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1678 if self.active_pane != pane {
1679 self.active_pane
1680 .update(cx, |pane, cx| pane.set_active(false, cx));
1681 self.active_pane = pane.clone();
1682 self.active_pane
1683 .update(cx, |pane, cx| pane.set_active(true, cx));
1684 self.status_bar.update(cx, |status_bar, cx| {
1685 status_bar.set_active_pane(&self.active_pane, cx);
1686 });
1687 self.active_item_path_changed(cx);
1688 self.last_active_center_pane = Some(pane.downgrade());
1689 }
1690
1691 self.update_followers(
1692 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
1693 id: self.active_item(cx).and_then(|item| {
1694 item.to_followable_item_handle(cx)?
1695 .remote_id(&self.app_state.client, cx)
1696 .map(|id| id.to_proto())
1697 }),
1698 leader_id: self.leader_for_pane(&pane),
1699 }),
1700 cx,
1701 );
1702
1703 cx.notify();
1704 }
1705
1706 fn handle_pane_event(
1707 &mut self,
1708 pane: ViewHandle<Pane>,
1709 event: &pane::Event,
1710 cx: &mut ViewContext<Self>,
1711 ) {
1712 match event {
1713 pane::Event::Split(direction) => {
1714 self.split_pane(pane, *direction, cx);
1715 }
1716 pane::Event::Remove => self.remove_pane(pane, cx),
1717 pane::Event::ActivateItem { local } => {
1718 if *local {
1719 self.unfollow(&pane, cx);
1720 }
1721 if &pane == self.active_pane() {
1722 self.active_item_path_changed(cx);
1723 }
1724 }
1725 pane::Event::ChangeItemTitle => {
1726 if pane == self.active_pane {
1727 self.active_item_path_changed(cx);
1728 }
1729 self.update_window_edited(cx);
1730 }
1731 pane::Event::RemoveItem { item_id } => {
1732 self.update_window_edited(cx);
1733 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
1734 if entry.get().id() == pane.id() {
1735 entry.remove();
1736 }
1737 }
1738 }
1739 pane::Event::Focus => {
1740 self.handle_pane_focused(pane.clone(), cx);
1741 }
1742 pane::Event::ZoomIn => {
1743 if pane == self.active_pane {
1744 self.zoom_out(cx);
1745 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
1746 cx.notify();
1747 }
1748 }
1749 pane::Event::ZoomOut => self.zoom_out(cx),
1750 }
1751
1752 self.serialize_workspace(cx);
1753 }
1754
1755 pub fn split_pane(
1756 &mut self,
1757 pane: ViewHandle<Pane>,
1758 direction: SplitDirection,
1759 cx: &mut ViewContext<Self>,
1760 ) -> Option<ViewHandle<Pane>> {
1761 let item = pane.read(cx).active_item()?;
1762 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
1763 let new_pane = self.add_pane(cx);
1764 Pane::add_item(self, &new_pane, clone, true, true, None, cx);
1765 self.center.split(&pane, &new_pane, direction).unwrap();
1766 Some(new_pane)
1767 } else {
1768 None
1769 };
1770 cx.notify();
1771 maybe_pane_handle
1772 }
1773
1774 pub fn split_pane_with_item(
1775 &mut self,
1776 pane_to_split: WeakViewHandle<Pane>,
1777 split_direction: SplitDirection,
1778 from: WeakViewHandle<Pane>,
1779 item_id_to_move: usize,
1780 cx: &mut ViewContext<Self>,
1781 ) {
1782 let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; };
1783 let Some(from) = from.upgrade(cx) else { return; };
1784
1785 let new_pane = self.add_pane(cx);
1786 Pane::move_item(self, from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
1787 self.center
1788 .split(&pane_to_split, &new_pane, split_direction)
1789 .unwrap();
1790 cx.notify();
1791 }
1792
1793 pub fn split_pane_with_project_entry(
1794 &mut self,
1795 pane_to_split: WeakViewHandle<Pane>,
1796 split_direction: SplitDirection,
1797 project_entry: ProjectEntryId,
1798 cx: &mut ViewContext<Self>,
1799 ) -> Option<Task<Result<()>>> {
1800 let pane_to_split = pane_to_split.upgrade(cx)?;
1801 let new_pane = self.add_pane(cx);
1802 self.center
1803 .split(&pane_to_split, &new_pane, split_direction)
1804 .unwrap();
1805
1806 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
1807 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
1808 Some(cx.foreground().spawn(async move {
1809 task.await?;
1810 Ok(())
1811 }))
1812 }
1813
1814 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1815 if self.center.remove(&pane).unwrap() {
1816 self.force_remove_pane(&pane, cx);
1817 self.unfollow(&pane, cx);
1818 self.last_leaders_by_pane.remove(&pane.downgrade());
1819 for removed_item in pane.read(cx).items() {
1820 self.panes_by_item.remove(&removed_item.id());
1821 }
1822
1823 cx.notify();
1824 } else {
1825 self.active_item_path_changed(cx);
1826 }
1827 }
1828
1829 pub fn panes(&self) -> &[ViewHandle<Pane>] {
1830 &self.panes
1831 }
1832
1833 pub fn active_pane(&self) -> &ViewHandle<Pane> {
1834 &self.active_pane
1835 }
1836
1837 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
1838 if let Some(remote_id) = remote_id {
1839 self.remote_entity_subscription = Some(
1840 self.app_state
1841 .client
1842 .add_view_for_remote_entity(remote_id, cx),
1843 );
1844 } else {
1845 self.remote_entity_subscription.take();
1846 }
1847 }
1848
1849 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1850 self.leader_state.followers.remove(&peer_id);
1851 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
1852 for state in states_by_pane.into_values() {
1853 for item in state.items_by_leader_view_id.into_values() {
1854 item.set_leader_replica_id(None, cx);
1855 }
1856 }
1857 }
1858 cx.notify();
1859 }
1860
1861 pub fn toggle_follow(
1862 &mut self,
1863 leader_id: PeerId,
1864 cx: &mut ViewContext<Self>,
1865 ) -> Option<Task<Result<()>>> {
1866 let pane = self.active_pane().clone();
1867
1868 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
1869 if leader_id == prev_leader_id {
1870 return None;
1871 }
1872 }
1873
1874 self.last_leaders_by_pane
1875 .insert(pane.downgrade(), leader_id);
1876 self.follower_states_by_leader
1877 .entry(leader_id)
1878 .or_default()
1879 .insert(pane.clone(), Default::default());
1880 cx.notify();
1881
1882 let project_id = self.project.read(cx).remote_id()?;
1883 let request = self.app_state.client.request(proto::Follow {
1884 project_id,
1885 leader_id: Some(leader_id),
1886 });
1887
1888 Some(cx.spawn(|this, mut cx| async move {
1889 let response = request.await?;
1890 this.update(&mut cx, |this, _| {
1891 let state = this
1892 .follower_states_by_leader
1893 .get_mut(&leader_id)
1894 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
1895 .ok_or_else(|| anyhow!("following interrupted"))?;
1896 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
1897 Some(ViewId::from_proto(active_view_id)?)
1898 } else {
1899 None
1900 };
1901 Ok::<_, anyhow::Error>(())
1902 })??;
1903 Self::add_views_from_leader(
1904 this.clone(),
1905 leader_id,
1906 vec![pane],
1907 response.views,
1908 &mut cx,
1909 )
1910 .await?;
1911 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
1912 Ok(())
1913 }))
1914 }
1915
1916 pub fn follow_next_collaborator(
1917 &mut self,
1918 _: &FollowNextCollaborator,
1919 cx: &mut ViewContext<Self>,
1920 ) -> Option<Task<Result<()>>> {
1921 let collaborators = self.project.read(cx).collaborators();
1922 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
1923 let mut collaborators = collaborators.keys().copied();
1924 for peer_id in collaborators.by_ref() {
1925 if peer_id == leader_id {
1926 break;
1927 }
1928 }
1929 collaborators.next()
1930 } else if let Some(last_leader_id) =
1931 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
1932 {
1933 if collaborators.contains_key(last_leader_id) {
1934 Some(*last_leader_id)
1935 } else {
1936 None
1937 }
1938 } else {
1939 None
1940 };
1941
1942 next_leader_id
1943 .or_else(|| collaborators.keys().copied().next())
1944 .and_then(|leader_id| self.toggle_follow(leader_id, cx))
1945 }
1946
1947 pub fn unfollow(
1948 &mut self,
1949 pane: &ViewHandle<Pane>,
1950 cx: &mut ViewContext<Self>,
1951 ) -> Option<PeerId> {
1952 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
1953 let leader_id = *leader_id;
1954 if let Some(state) = states_by_pane.remove(pane) {
1955 for (_, item) in state.items_by_leader_view_id {
1956 item.set_leader_replica_id(None, cx);
1957 }
1958
1959 if states_by_pane.is_empty() {
1960 self.follower_states_by_leader.remove(&leader_id);
1961 if let Some(project_id) = self.project.read(cx).remote_id() {
1962 self.app_state
1963 .client
1964 .send(proto::Unfollow {
1965 project_id,
1966 leader_id: Some(leader_id),
1967 })
1968 .log_err();
1969 }
1970 }
1971
1972 cx.notify();
1973 return Some(leader_id);
1974 }
1975 }
1976 None
1977 }
1978
1979 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
1980 self.follower_states_by_leader.contains_key(&peer_id)
1981 }
1982
1983 pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
1984 self.leader_state.followers.contains(&peer_id)
1985 }
1986
1987 fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
1988 // TODO: There should be a better system in place for this
1989 // (https://github.com/zed-industries/zed/issues/1290)
1990 let is_fullscreen = cx.window_is_fullscreen();
1991 let container_theme = if is_fullscreen {
1992 let mut container_theme = theme.workspace.titlebar.container;
1993 container_theme.padding.left = container_theme.padding.right;
1994 container_theme
1995 } else {
1996 theme.workspace.titlebar.container
1997 };
1998
1999 enum TitleBar {}
2000 MouseEventHandler::<TitleBar, _>::new(0, cx, |_, cx| {
2001 Stack::new()
2002 .with_children(
2003 self.titlebar_item
2004 .as_ref()
2005 .map(|item| ChildView::new(item, cx)),
2006 )
2007 .contained()
2008 .with_style(container_theme)
2009 })
2010 .on_click(MouseButton::Left, |event, _, cx| {
2011 if event.click_count == 2 {
2012 cx.zoom_window();
2013 }
2014 })
2015 .constrained()
2016 .with_height(theme.workspace.titlebar.height)
2017 .into_any_named("titlebar")
2018 }
2019
2020 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2021 let active_entry = self.active_project_path(cx);
2022 self.project
2023 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2024 self.update_window_title(cx);
2025 }
2026
2027 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
2028 let project = self.project().read(cx);
2029 let mut title = String::new();
2030
2031 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2032 let filename = path
2033 .path
2034 .file_name()
2035 .map(|s| s.to_string_lossy())
2036 .or_else(|| {
2037 Some(Cow::Borrowed(
2038 project
2039 .worktree_for_id(path.worktree_id, cx)?
2040 .read(cx)
2041 .root_name(),
2042 ))
2043 });
2044
2045 if let Some(filename) = filename {
2046 title.push_str(filename.as_ref());
2047 title.push_str(" β ");
2048 }
2049 }
2050
2051 for (i, name) in project.worktree_root_names(cx).enumerate() {
2052 if i > 0 {
2053 title.push_str(", ");
2054 }
2055 title.push_str(name);
2056 }
2057
2058 if title.is_empty() {
2059 title = "empty project".to_string();
2060 }
2061
2062 if project.is_remote() {
2063 title.push_str(" β");
2064 } else if project.is_shared() {
2065 title.push_str(" β");
2066 }
2067
2068 cx.set_window_title(&title);
2069 }
2070
2071 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2072 let is_edited = !self.project.read(cx).is_read_only()
2073 && self
2074 .items(cx)
2075 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2076 if is_edited != self.window_edited {
2077 self.window_edited = is_edited;
2078 cx.set_window_edited(self.window_edited)
2079 }
2080 }
2081
2082 fn render_disconnected_overlay(
2083 &self,
2084 cx: &mut ViewContext<Workspace>,
2085 ) -> Option<AnyElement<Workspace>> {
2086 if self.project.read(cx).is_read_only() {
2087 enum DisconnectedOverlay {}
2088 Some(
2089 MouseEventHandler::<DisconnectedOverlay, _>::new(0, cx, |_, cx| {
2090 let theme = &cx.global::<Settings>().theme;
2091 Label::new(
2092 "Your connection to the remote project has been lost.",
2093 theme.workspace.disconnected_overlay.text.clone(),
2094 )
2095 .aligned()
2096 .contained()
2097 .with_style(theme.workspace.disconnected_overlay.container)
2098 })
2099 .with_cursor_style(CursorStyle::Arrow)
2100 .capture_all()
2101 .into_any_named("disconnected overlay"),
2102 )
2103 } else {
2104 None
2105 }
2106 }
2107
2108 fn render_notifications(
2109 &self,
2110 theme: &theme::Workspace,
2111 cx: &AppContext,
2112 ) -> Option<AnyElement<Workspace>> {
2113 if self.notifications.is_empty() {
2114 None
2115 } else {
2116 Some(
2117 Flex::column()
2118 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2119 ChildView::new(notification.as_any(), cx)
2120 .contained()
2121 .with_style(theme.notification)
2122 }))
2123 .constrained()
2124 .with_width(theme.notifications.width)
2125 .contained()
2126 .with_style(theme.notifications.container)
2127 .aligned()
2128 .bottom()
2129 .right()
2130 .into_any(),
2131 )
2132 }
2133 }
2134
2135 // RPC handlers
2136
2137 async fn handle_follow(
2138 this: WeakViewHandle<Self>,
2139 envelope: TypedEnvelope<proto::Follow>,
2140 _: Arc<Client>,
2141 mut cx: AsyncAppContext,
2142 ) -> Result<proto::FollowResponse> {
2143 this.update(&mut cx, |this, cx| {
2144 let client = &this.app_state.client;
2145 this.leader_state
2146 .followers
2147 .insert(envelope.original_sender_id()?);
2148
2149 let active_view_id = this.active_item(cx).and_then(|i| {
2150 Some(
2151 i.to_followable_item_handle(cx)?
2152 .remote_id(client, cx)?
2153 .to_proto(),
2154 )
2155 });
2156
2157 cx.notify();
2158
2159 Ok(proto::FollowResponse {
2160 active_view_id,
2161 views: this
2162 .panes()
2163 .iter()
2164 .flat_map(|pane| {
2165 let leader_id = this.leader_for_pane(pane);
2166 pane.read(cx).items().filter_map({
2167 let cx = &cx;
2168 move |item| {
2169 let item = item.to_followable_item_handle(cx)?;
2170 let id = item.remote_id(client, cx)?.to_proto();
2171 let variant = item.to_state_proto(cx)?;
2172 Some(proto::View {
2173 id: Some(id),
2174 leader_id,
2175 variant: Some(variant),
2176 })
2177 }
2178 })
2179 })
2180 .collect(),
2181 })
2182 })?
2183 }
2184
2185 async fn handle_unfollow(
2186 this: WeakViewHandle<Self>,
2187 envelope: TypedEnvelope<proto::Unfollow>,
2188 _: Arc<Client>,
2189 mut cx: AsyncAppContext,
2190 ) -> Result<()> {
2191 this.update(&mut cx, |this, cx| {
2192 this.leader_state
2193 .followers
2194 .remove(&envelope.original_sender_id()?);
2195 cx.notify();
2196 Ok(())
2197 })?
2198 }
2199
2200 async fn handle_update_followers(
2201 this: WeakViewHandle<Self>,
2202 envelope: TypedEnvelope<proto::UpdateFollowers>,
2203 _: Arc<Client>,
2204 cx: AsyncAppContext,
2205 ) -> Result<()> {
2206 let leader_id = envelope.original_sender_id()?;
2207 this.read_with(&cx, |this, _| {
2208 this.leader_updates_tx
2209 .unbounded_send((leader_id, envelope.payload))
2210 })??;
2211 Ok(())
2212 }
2213
2214 async fn process_leader_update(
2215 this: &WeakViewHandle<Self>,
2216 leader_id: PeerId,
2217 update: proto::UpdateFollowers,
2218 cx: &mut AsyncAppContext,
2219 ) -> Result<()> {
2220 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2221 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2222 this.update(cx, |this, _| {
2223 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2224 for state in state.values_mut() {
2225 state.active_view_id =
2226 if let Some(active_view_id) = update_active_view.id.clone() {
2227 Some(ViewId::from_proto(active_view_id)?)
2228 } else {
2229 None
2230 };
2231 }
2232 }
2233 anyhow::Ok(())
2234 })??;
2235 }
2236 proto::update_followers::Variant::UpdateView(update_view) => {
2237 let variant = update_view
2238 .variant
2239 .ok_or_else(|| anyhow!("missing update view variant"))?;
2240 let id = update_view
2241 .id
2242 .ok_or_else(|| anyhow!("missing update view id"))?;
2243 let mut tasks = Vec::new();
2244 this.update(cx, |this, cx| {
2245 let project = this.project.clone();
2246 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2247 for state in state.values_mut() {
2248 let view_id = ViewId::from_proto(id.clone())?;
2249 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2250 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2251 }
2252 }
2253 }
2254 anyhow::Ok(())
2255 })??;
2256 try_join_all(tasks).await.log_err();
2257 }
2258 proto::update_followers::Variant::CreateView(view) => {
2259 let panes = this.read_with(cx, |this, _| {
2260 this.follower_states_by_leader
2261 .get(&leader_id)
2262 .into_iter()
2263 .flat_map(|states_by_pane| states_by_pane.keys())
2264 .cloned()
2265 .collect()
2266 })?;
2267 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2268 }
2269 }
2270 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2271 Ok(())
2272 }
2273
2274 async fn add_views_from_leader(
2275 this: WeakViewHandle<Self>,
2276 leader_id: PeerId,
2277 panes: Vec<ViewHandle<Pane>>,
2278 views: Vec<proto::View>,
2279 cx: &mut AsyncAppContext,
2280 ) -> Result<()> {
2281 let project = this.read_with(cx, |this, _| this.project.clone())?;
2282 let replica_id = project
2283 .read_with(cx, |project, _| {
2284 project
2285 .collaborators()
2286 .get(&leader_id)
2287 .map(|c| c.replica_id)
2288 })
2289 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2290
2291 let item_builders = cx.update(|cx| {
2292 cx.default_global::<FollowableItemBuilders>()
2293 .values()
2294 .map(|b| b.0)
2295 .collect::<Vec<_>>()
2296 });
2297
2298 let mut item_tasks_by_pane = HashMap::default();
2299 for pane in panes {
2300 let mut item_tasks = Vec::new();
2301 let mut leader_view_ids = Vec::new();
2302 for view in &views {
2303 let Some(id) = &view.id else { continue };
2304 let id = ViewId::from_proto(id.clone())?;
2305 let mut variant = view.variant.clone();
2306 if variant.is_none() {
2307 Err(anyhow!("missing variant"))?;
2308 }
2309 for build_item in &item_builders {
2310 let task = cx.update(|cx| {
2311 build_item(pane.clone(), project.clone(), id, &mut variant, cx)
2312 });
2313 if let Some(task) = task {
2314 item_tasks.push(task);
2315 leader_view_ids.push(id);
2316 break;
2317 } else {
2318 assert!(variant.is_some());
2319 }
2320 }
2321 }
2322
2323 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2324 }
2325
2326 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2327 let items = futures::future::try_join_all(item_tasks).await?;
2328 this.update(cx, |this, cx| {
2329 let state = this
2330 .follower_states_by_leader
2331 .get_mut(&leader_id)?
2332 .get_mut(&pane)?;
2333
2334 for (id, item) in leader_view_ids.into_iter().zip(items) {
2335 item.set_leader_replica_id(Some(replica_id), cx);
2336 state.items_by_leader_view_id.insert(id, item);
2337 }
2338
2339 Some(())
2340 })?;
2341 }
2342 Ok(())
2343 }
2344
2345 fn update_followers(
2346 &self,
2347 update: proto::update_followers::Variant,
2348 cx: &AppContext,
2349 ) -> Option<()> {
2350 let project_id = self.project.read(cx).remote_id()?;
2351 if !self.leader_state.followers.is_empty() {
2352 self.app_state
2353 .client
2354 .send(proto::UpdateFollowers {
2355 project_id,
2356 follower_ids: self.leader_state.followers.iter().copied().collect(),
2357 variant: Some(update),
2358 })
2359 .log_err();
2360 }
2361 None
2362 }
2363
2364 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2365 self.follower_states_by_leader
2366 .iter()
2367 .find_map(|(leader_id, state)| {
2368 if state.contains_key(pane) {
2369 Some(*leader_id)
2370 } else {
2371 None
2372 }
2373 })
2374 }
2375
2376 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2377 cx.notify();
2378
2379 let call = self.active_call()?;
2380 let room = call.read(cx).room()?.read(cx);
2381 let participant = room.remote_participant_for_peer_id(leader_id)?;
2382 let mut items_to_activate = Vec::new();
2383 match participant.location {
2384 call::ParticipantLocation::SharedProject { project_id } => {
2385 if Some(project_id) == self.project.read(cx).remote_id() {
2386 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2387 if let Some(item) = state
2388 .active_view_id
2389 .and_then(|id| state.items_by_leader_view_id.get(&id))
2390 {
2391 items_to_activate.push((pane.clone(), item.boxed_clone()));
2392 } else {
2393 if let Some(shared_screen) =
2394 self.shared_screen_for_peer(leader_id, pane, cx)
2395 {
2396 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2397 }
2398 }
2399 }
2400 }
2401 }
2402 call::ParticipantLocation::UnsharedProject => {}
2403 call::ParticipantLocation::External => {
2404 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
2405 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2406 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2407 }
2408 }
2409 }
2410 }
2411
2412 for (pane, item) in items_to_activate {
2413 let pane_was_focused = pane.read(cx).has_focus();
2414 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2415 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2416 } else {
2417 Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx);
2418 }
2419
2420 if pane_was_focused {
2421 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2422 }
2423 }
2424
2425 None
2426 }
2427
2428 fn shared_screen_for_peer(
2429 &self,
2430 peer_id: PeerId,
2431 pane: &ViewHandle<Pane>,
2432 cx: &mut ViewContext<Self>,
2433 ) -> Option<ViewHandle<SharedScreen>> {
2434 let call = self.active_call()?;
2435 let room = call.read(cx).room()?.read(cx);
2436 let participant = room.remote_participant_for_peer_id(peer_id)?;
2437 let track = participant.tracks.values().next()?.clone();
2438 let user = participant.user.clone();
2439
2440 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2441 if item.read(cx).peer_id == peer_id {
2442 return Some(item);
2443 }
2444 }
2445
2446 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2447 }
2448
2449 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2450 if active {
2451 cx.background()
2452 .spawn(persistence::DB.update_timestamp(self.database_id()))
2453 .detach();
2454 } else {
2455 for pane in &self.panes {
2456 pane.update(cx, |pane, cx| {
2457 if let Some(item) = pane.active_item() {
2458 item.workspace_deactivated(cx);
2459 }
2460 if matches!(
2461 cx.global::<Settings>().autosave,
2462 Autosave::OnWindowChange | Autosave::OnFocusChange
2463 ) {
2464 for item in pane.items() {
2465 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2466 .detach_and_log_err(cx);
2467 }
2468 }
2469 });
2470 }
2471 }
2472 }
2473
2474 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
2475 self.active_call.as_ref().map(|(call, _)| call)
2476 }
2477
2478 fn on_active_call_event(
2479 &mut self,
2480 _: ModelHandle<ActiveCall>,
2481 event: &call::room::Event,
2482 cx: &mut ViewContext<Self>,
2483 ) {
2484 match event {
2485 call::room::Event::ParticipantLocationChanged { participant_id }
2486 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2487 self.leader_updated(*participant_id, cx);
2488 }
2489 _ => {}
2490 }
2491 }
2492
2493 pub fn database_id(&self) -> WorkspaceId {
2494 self.database_id
2495 }
2496
2497 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2498 let project = self.project().read(cx);
2499
2500 if project.is_local() {
2501 Some(
2502 project
2503 .visible_worktrees(cx)
2504 .map(|worktree| worktree.read(cx).abs_path())
2505 .collect::<Vec<_>>()
2506 .into(),
2507 )
2508 } else {
2509 None
2510 }
2511 }
2512
2513 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2514 match member {
2515 Member::Axis(PaneAxis { members, .. }) => {
2516 for child in members.iter() {
2517 self.remove_panes(child.clone(), cx)
2518 }
2519 }
2520 Member::Pane(pane) => {
2521 self.force_remove_pane(&pane, cx);
2522 }
2523 }
2524 }
2525
2526 fn force_remove_pane(&mut self, pane: &ViewHandle<Pane>, cx: &mut ViewContext<Workspace>) {
2527 self.panes.retain(|p| p != pane);
2528 cx.focus(self.panes.last().unwrap());
2529 if self.last_active_center_pane == Some(pane.downgrade()) {
2530 self.last_active_center_pane = None;
2531 }
2532 cx.notify();
2533 }
2534
2535 fn serialize_workspace(&self, cx: &AppContext) {
2536 fn serialize_pane_handle(
2537 pane_handle: &ViewHandle<Pane>,
2538 cx: &AppContext,
2539 ) -> SerializedPane {
2540 let (items, active) = {
2541 let pane = pane_handle.read(cx);
2542 let active_item_id = pane.active_item().map(|item| item.id());
2543 (
2544 pane.items()
2545 .filter_map(|item_handle| {
2546 Some(SerializedItem {
2547 kind: Arc::from(item_handle.serialized_item_kind()?),
2548 item_id: item_handle.id(),
2549 active: Some(item_handle.id()) == active_item_id,
2550 })
2551 })
2552 .collect::<Vec<_>>(),
2553 pane.is_active(),
2554 )
2555 };
2556
2557 SerializedPane::new(items, active)
2558 }
2559
2560 fn build_serialized_pane_group(
2561 pane_group: &Member,
2562 cx: &AppContext,
2563 ) -> SerializedPaneGroup {
2564 match pane_group {
2565 Member::Axis(PaneAxis { axis, members }) => SerializedPaneGroup::Group {
2566 axis: *axis,
2567 children: members
2568 .iter()
2569 .map(|member| build_serialized_pane_group(member, cx))
2570 .collect::<Vec<_>>(),
2571 },
2572 Member::Pane(pane_handle) => {
2573 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
2574 }
2575 }
2576 }
2577
2578 if let Some(location) = self.location(cx) {
2579 // Load bearing special case:
2580 // - with_local_workspace() relies on this to not have other stuff open
2581 // when you open your log
2582 if !location.paths().is_empty() {
2583 let center_group = build_serialized_pane_group(&self.center.root, cx);
2584
2585 let serialized_workspace = SerializedWorkspace {
2586 id: self.database_id,
2587 location,
2588 center_group,
2589 left_sidebar_open: self.left_dock.read(cx).is_open(),
2590 bounds: Default::default(),
2591 display: Default::default(),
2592 };
2593
2594 cx.background()
2595 .spawn(persistence::DB.save_workspace(serialized_workspace))
2596 .detach();
2597 }
2598 }
2599 }
2600
2601 fn load_from_serialized_workspace(
2602 workspace: WeakViewHandle<Workspace>,
2603 serialized_workspace: SerializedWorkspace,
2604 cx: &mut AppContext,
2605 ) {
2606 cx.spawn(|mut cx| async move {
2607 let (project, old_center_pane) = workspace.read_with(&cx, |workspace, _| {
2608 (
2609 workspace.project().clone(),
2610 workspace.last_active_center_pane.clone(),
2611 )
2612 })?;
2613
2614 // Traverse the splits tree and add to things
2615 let center_group = serialized_workspace
2616 .center_group
2617 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
2618 .await;
2619
2620 // Remove old panes from workspace panes list
2621 workspace.update(&mut cx, |workspace, cx| {
2622 if let Some((center_group, active_pane)) = center_group {
2623 workspace.remove_panes(workspace.center.root.clone(), cx);
2624
2625 // Swap workspace center group
2626 workspace.center = PaneGroup::with_root(center_group);
2627
2628 // Change the focus to the workspace first so that we retrigger focus in on the pane.
2629 cx.focus_self();
2630
2631 if let Some(active_pane) = active_pane {
2632 cx.focus(&active_pane);
2633 } else {
2634 cx.focus(workspace.panes.last().unwrap());
2635 }
2636 } else {
2637 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
2638 if let Some(old_center_handle) = old_center_handle {
2639 cx.focus(&old_center_handle)
2640 } else {
2641 cx.focus_self()
2642 }
2643 }
2644
2645 if workspace.left_dock().read(cx).is_open()
2646 != serialized_workspace.left_sidebar_open
2647 {
2648 workspace.toggle_dock(DockPosition::Left, cx);
2649 }
2650
2651 cx.notify();
2652 })?;
2653
2654 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
2655 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
2656 anyhow::Ok(())
2657 })
2658 .detach_and_log_err(cx);
2659 }
2660
2661 #[cfg(any(test, feature = "test-support"))]
2662 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
2663 let app_state = Arc::new(AppState {
2664 languages: project.read(cx).languages().clone(),
2665 themes: ThemeRegistry::new((), cx.font_cache().clone()),
2666 client: project.read(cx).client(),
2667 user_store: project.read(cx).user_store(),
2668 fs: project.read(cx).fs().clone(),
2669 build_window_options: |_, _, _| Default::default(),
2670 initialize_workspace: |_, _, _| {},
2671 background_actions: || &[],
2672 });
2673 Self::new(None, 0, project, app_state, cx)
2674 }
2675
2676 fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
2677 let dock = match position {
2678 DockPosition::Left => &self.left_dock,
2679 DockPosition::Right => &self.right_dock,
2680 DockPosition::Bottom => &self.bottom_dock,
2681 };
2682 let active_panel = dock.read(cx).active_panel()?;
2683 let element = if Some(active_panel.as_any()) == self.zoomed(cx).as_ref() {
2684 dock.read(cx).render_placeholder(cx)
2685 } else {
2686 ChildView::new(dock, cx).into_any()
2687 };
2688
2689 Some(
2690 element
2691 .constrained()
2692 .dynamically(move |constraint, _, cx| match position {
2693 DockPosition::Left | DockPosition::Right => SizeConstraint::new(
2694 Vector2F::new(20., constraint.min.y()),
2695 Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
2696 ),
2697 _ => constraint,
2698 })
2699 .into_any(),
2700 )
2701 }
2702}
2703
2704fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
2705 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
2706
2707 workspace
2708 .update(cx, |workspace, cx| {
2709 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
2710 workspace.show_notification_once(0, cx, |cx| {
2711 cx.add_view(|_| {
2712 MessageNotification::new("Failed to load any database file.")
2713 .with_click_message("Click to let us know about this error")
2714 .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
2715 })
2716 });
2717 } else {
2718 let backup_path = (*db::BACKUP_DB_PATH).read();
2719 if let Some(backup_path) = backup_path.clone() {
2720 workspace.show_notification_once(0, cx, move |cx| {
2721 cx.add_view(move |_| {
2722 MessageNotification::new(format!(
2723 "Database file was corrupted. Old database backed up to {}",
2724 backup_path.display()
2725 ))
2726 .with_click_message("Click to show old database in finder")
2727 .on_click(move |cx| {
2728 cx.platform().open_url(&backup_path.to_string_lossy())
2729 })
2730 })
2731 });
2732 }
2733 }
2734 })
2735 .log_err();
2736}
2737
2738impl Entity for Workspace {
2739 type Event = Event;
2740}
2741
2742impl View for Workspace {
2743 fn ui_name() -> &'static str {
2744 "Workspace"
2745 }
2746
2747 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2748 let theme = cx.global::<Settings>().theme.clone();
2749 Stack::new()
2750 .with_child(
2751 Flex::column()
2752 .with_child(self.render_titlebar(&theme, cx))
2753 .with_child(
2754 Stack::new()
2755 .with_child({
2756 let project = self.project.clone();
2757 Flex::row()
2758 .with_children(self.render_dock(DockPosition::Left, cx))
2759 .with_child(
2760 Flex::column()
2761 .with_child(
2762 FlexItem::new(self.center.render(
2763 &project,
2764 &theme,
2765 &self.follower_states_by_leader,
2766 self.active_call(),
2767 self.active_pane(),
2768 self.zoomed(cx).as_ref(),
2769 &self.app_state,
2770 cx,
2771 ))
2772 .flex(1., true),
2773 )
2774 .with_children(
2775 self.render_dock(DockPosition::Bottom, cx),
2776 )
2777 .flex(1., true),
2778 )
2779 .with_children(self.render_dock(DockPosition::Right, cx))
2780 })
2781 .with_child(Overlay::new(
2782 Stack::new()
2783 .with_children(self.zoomed(cx).map(|zoomed| {
2784 enum ZoomBackground {}
2785
2786 ChildView::new(&zoomed, cx)
2787 .contained()
2788 .with_style(theme.workspace.zoomed_foreground)
2789 .aligned()
2790 .contained()
2791 .with_style(theme.workspace.zoomed_background)
2792 .mouse::<ZoomBackground>(0)
2793 .capture_all()
2794 .on_down(MouseButton::Left, |_, this: &mut Self, cx| {
2795 this.zoom_out(cx);
2796 })
2797 }))
2798 .with_children(self.modal.as_ref().map(|modal| {
2799 ChildView::new(modal, cx)
2800 .contained()
2801 .with_style(theme.workspace.modal)
2802 .aligned()
2803 .top()
2804 }))
2805 .with_children(self.render_notifications(&theme.workspace, cx)),
2806 ))
2807 .flex(1.0, true),
2808 )
2809 .with_child(ChildView::new(&self.status_bar, cx))
2810 .contained()
2811 .with_background_color(theme.workspace.background),
2812 )
2813 .with_children(DragAndDrop::render(cx))
2814 .with_children(self.render_disconnected_overlay(cx))
2815 .into_any_named("workspace")
2816 }
2817
2818 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
2819 if cx.is_self_focused() {
2820 cx.focus(&self.active_pane);
2821 }
2822 cx.notify();
2823 }
2824}
2825
2826impl ViewId {
2827 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
2828 Ok(Self {
2829 creator: message
2830 .creator
2831 .ok_or_else(|| anyhow!("creator is missing"))?,
2832 id: message.id,
2833 })
2834 }
2835
2836 pub(crate) fn to_proto(&self) -> proto::ViewId {
2837 proto::ViewId {
2838 creator: Some(self.creator),
2839 id: self.id,
2840 }
2841 }
2842}
2843
2844pub trait WorkspaceHandle {
2845 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
2846}
2847
2848impl WorkspaceHandle for ViewHandle<Workspace> {
2849 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
2850 self.read(cx)
2851 .worktrees(cx)
2852 .flat_map(|worktree| {
2853 let worktree_id = worktree.read(cx).id();
2854 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
2855 worktree_id,
2856 path: f.path.clone(),
2857 })
2858 })
2859 .collect::<Vec<_>>()
2860 }
2861}
2862
2863impl std::fmt::Debug for OpenPaths {
2864 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2865 f.debug_struct("OpenPaths")
2866 .field("paths", &self.paths)
2867 .finish()
2868 }
2869}
2870
2871pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
2872
2873pub fn activate_workspace_for_project(
2874 cx: &mut AsyncAppContext,
2875 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
2876) -> Option<WeakViewHandle<Workspace>> {
2877 for window_id in cx.window_ids() {
2878 let handle = cx
2879 .update_window(window_id, |cx| {
2880 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
2881 let project = workspace_handle.read(cx).project.clone();
2882 if project.update(cx, &predicate) {
2883 cx.activate_window();
2884 return Some(workspace_handle.clone());
2885 }
2886 }
2887 None
2888 })
2889 .flatten();
2890
2891 if let Some(handle) = handle {
2892 return Some(handle.downgrade());
2893 }
2894 }
2895 None
2896}
2897
2898pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
2899 DB.last_workspace().await.log_err().flatten()
2900}
2901
2902#[allow(clippy::type_complexity)]
2903pub fn open_paths(
2904 abs_paths: &[PathBuf],
2905 app_state: &Arc<AppState>,
2906 requesting_window_id: Option<usize>,
2907 cx: &mut AppContext,
2908) -> Task<
2909 Result<(
2910 WeakViewHandle<Workspace>,
2911 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
2912 )>,
2913> {
2914 log::info!("open paths {:?}", abs_paths);
2915
2916 let app_state = app_state.clone();
2917 let abs_paths = abs_paths.to_vec();
2918 cx.spawn(|mut cx| async move {
2919 // Open paths in existing workspace if possible
2920 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
2921 project.contains_paths(&abs_paths, cx)
2922 });
2923
2924 if let Some(existing) = existing {
2925 Ok((
2926 existing.clone(),
2927 existing
2928 .update(&mut cx, |workspace, cx| {
2929 workspace.open_paths(abs_paths, true, cx)
2930 })?
2931 .await,
2932 ))
2933 } else {
2934 let contains_directory =
2935 futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path)))
2936 .await
2937 .contains(&false);
2938
2939 cx.update(|cx| {
2940 let task =
2941 Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx);
2942
2943 cx.spawn(|mut cx| async move {
2944 let (workspace, items) = task.await;
2945
2946 workspace.update(&mut cx, |workspace, cx| {
2947 if contains_directory {
2948 workspace.toggle_dock(DockPosition::Left, cx);
2949 }
2950 })?;
2951
2952 anyhow::Ok((workspace, items))
2953 })
2954 })
2955 .await
2956 }
2957 })
2958}
2959
2960pub fn open_new(
2961 app_state: &Arc<AppState>,
2962 cx: &mut AppContext,
2963 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
2964) -> Task<()> {
2965 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
2966 cx.spawn(|mut cx| async move {
2967 let (workspace, opened_paths) = task.await;
2968
2969 workspace
2970 .update(&mut cx, |workspace, cx| {
2971 if opened_paths.is_empty() {
2972 init(workspace, cx)
2973 }
2974 })
2975 .log_err();
2976 })
2977}
2978
2979pub fn create_and_open_local_file(
2980 path: &'static Path,
2981 cx: &mut ViewContext<Workspace>,
2982 default_content: impl 'static + Send + FnOnce() -> Rope,
2983) -> Task<Result<Box<dyn ItemHandle>>> {
2984 cx.spawn(|workspace, mut cx| async move {
2985 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
2986 if !fs.is_file(path).await {
2987 fs.create_file(path, Default::default()).await?;
2988 fs.save(path, &default_content(), Default::default())
2989 .await?;
2990 }
2991
2992 let mut items = workspace
2993 .update(&mut cx, |workspace, cx| {
2994 workspace.with_local_workspace(cx, |workspace, cx| {
2995 workspace.open_paths(vec![path.to_path_buf()], false, cx)
2996 })
2997 })?
2998 .await?
2999 .await;
3000
3001 let item = items.pop().flatten();
3002 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
3003 })
3004}
3005
3006pub fn join_remote_project(
3007 project_id: u64,
3008 follow_user_id: u64,
3009 app_state: Arc<AppState>,
3010 cx: &mut AppContext,
3011) -> Task<Result<()>> {
3012 cx.spawn(|mut cx| async move {
3013 let existing_workspace = cx
3014 .window_ids()
3015 .into_iter()
3016 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
3017 .find(|workspace| {
3018 cx.read_window(workspace.window_id(), |cx| {
3019 workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
3020 })
3021 .unwrap_or(false)
3022 });
3023
3024 let workspace = if let Some(existing_workspace) = existing_workspace {
3025 existing_workspace.downgrade()
3026 } else {
3027 let active_call = cx.read(ActiveCall::global);
3028 let room = active_call
3029 .read_with(&cx, |call, _| call.room().cloned())
3030 .ok_or_else(|| anyhow!("not in a call"))?;
3031 let project = room
3032 .update(&mut cx, |room, cx| {
3033 room.join_project(
3034 project_id,
3035 app_state.languages.clone(),
3036 app_state.fs.clone(),
3037 cx,
3038 )
3039 })
3040 .await?;
3041
3042 let (_, workspace) = cx.add_window(
3043 (app_state.build_window_options)(None, None, cx.platform().as_ref()),
3044 |cx| {
3045 let mut workspace =
3046 Workspace::new(Default::default(), 0, project, app_state.clone(), cx);
3047 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
3048 workspace
3049 },
3050 );
3051 workspace.downgrade()
3052 };
3053
3054 cx.activate_window(workspace.window_id());
3055 cx.platform().activate(true);
3056
3057 workspace.update(&mut cx, |workspace, cx| {
3058 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
3059 let follow_peer_id = room
3060 .read(cx)
3061 .remote_participants()
3062 .iter()
3063 .find(|(_, participant)| participant.user.id == follow_user_id)
3064 .map(|(_, p)| p.peer_id)
3065 .or_else(|| {
3066 // If we couldn't follow the given user, follow the host instead.
3067 let collaborator = workspace
3068 .project()
3069 .read(cx)
3070 .collaborators()
3071 .values()
3072 .find(|collaborator| collaborator.replica_id == 0)?;
3073 Some(collaborator.peer_id)
3074 });
3075
3076 if let Some(follow_peer_id) = follow_peer_id {
3077 if !workspace.is_being_followed(follow_peer_id) {
3078 workspace
3079 .toggle_follow(follow_peer_id, cx)
3080 .map(|follow| follow.detach_and_log_err(cx));
3081 }
3082 }
3083 }
3084 })?;
3085
3086 anyhow::Ok(())
3087 })
3088}
3089
3090pub fn restart(_: &Restart, cx: &mut AppContext) {
3091 let should_confirm = cx.global::<Settings>().confirm_quit;
3092 cx.spawn(|mut cx| async move {
3093 let mut workspaces = cx
3094 .window_ids()
3095 .into_iter()
3096 .filter_map(|window_id| {
3097 Some(
3098 cx.root_view(window_id)?
3099 .clone()
3100 .downcast::<Workspace>()?
3101 .downgrade(),
3102 )
3103 })
3104 .collect::<Vec<_>>();
3105
3106 // If multiple windows have unsaved changes, and need a save prompt,
3107 // prompt in the active window before switching to a different window.
3108 workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
3109
3110 if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
3111 let answer = cx.prompt(
3112 workspace.window_id(),
3113 PromptLevel::Info,
3114 "Are you sure you want to restart?",
3115 &["Restart", "Cancel"],
3116 );
3117
3118 if let Some(mut answer) = answer {
3119 let answer = answer.next().await;
3120 if answer != Some(0) {
3121 return Ok(());
3122 }
3123 }
3124 }
3125
3126 // If the user cancels any save prompt, then keep the app open.
3127 for workspace in workspaces {
3128 if !workspace
3129 .update(&mut cx, |workspace, cx| {
3130 workspace.prepare_to_close(true, cx)
3131 })?
3132 .await?
3133 {
3134 return Ok(());
3135 }
3136 }
3137 cx.platform().restart();
3138 anyhow::Ok(())
3139 })
3140 .detach_and_log_err(cx);
3141}
3142
3143fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
3144 let mut parts = value.split(',');
3145 let width: usize = parts.next()?.parse().ok()?;
3146 let height: usize = parts.next()?.parse().ok()?;
3147 Some(vec2f(width as f32, height as f32))
3148}
3149
3150#[cfg(test)]
3151mod tests {
3152 use std::{cell::RefCell, rc::Rc};
3153
3154 use crate::{
3155 dock::test::{TestPanel, TestPanelEvent},
3156 item::test::{TestItem, TestItemEvent, TestProjectItem},
3157 };
3158
3159 use super::*;
3160 use fs::FakeFs;
3161 use gpui::{executor::Deterministic, TestAppContext};
3162 use project::{Project, ProjectEntryId};
3163 use serde_json::json;
3164
3165 #[gpui::test]
3166 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
3167 cx.foreground().forbid_parking();
3168 Settings::test_async(cx);
3169
3170 let fs = FakeFs::new(cx.background());
3171 let project = Project::test(fs, [], cx).await;
3172 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3173
3174 // Adding an item with no ambiguity renders the tab without detail.
3175 let item1 = cx.add_view(window_id, |_| {
3176 let mut item = TestItem::new();
3177 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
3178 item
3179 });
3180 workspace.update(cx, |workspace, cx| {
3181 workspace.add_item(Box::new(item1.clone()), cx);
3182 });
3183 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
3184
3185 // Adding an item that creates ambiguity increases the level of detail on
3186 // both tabs.
3187 let item2 = cx.add_view(window_id, |_| {
3188 let mut item = TestItem::new();
3189 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3190 item
3191 });
3192 workspace.update(cx, |workspace, cx| {
3193 workspace.add_item(Box::new(item2.clone()), cx);
3194 });
3195 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3196 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3197
3198 // Adding an item that creates ambiguity increases the level of detail only
3199 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
3200 // we stop at the highest detail available.
3201 let item3 = cx.add_view(window_id, |_| {
3202 let mut item = TestItem::new();
3203 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3204 item
3205 });
3206 workspace.update(cx, |workspace, cx| {
3207 workspace.add_item(Box::new(item3.clone()), cx);
3208 });
3209 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3210 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3211 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3212 }
3213
3214 #[gpui::test]
3215 async fn test_tracking_active_path(cx: &mut TestAppContext) {
3216 cx.foreground().forbid_parking();
3217 Settings::test_async(cx);
3218 let fs = FakeFs::new(cx.background());
3219 fs.insert_tree(
3220 "/root1",
3221 json!({
3222 "one.txt": "",
3223 "two.txt": "",
3224 }),
3225 )
3226 .await;
3227 fs.insert_tree(
3228 "/root2",
3229 json!({
3230 "three.txt": "",
3231 }),
3232 )
3233 .await;
3234
3235 let project = Project::test(fs, ["root1".as_ref()], cx).await;
3236 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3237 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3238 let worktree_id = project.read_with(cx, |project, cx| {
3239 project.worktrees(cx).next().unwrap().read(cx).id()
3240 });
3241
3242 let item1 = cx.add_view(window_id, |cx| {
3243 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3244 });
3245 let item2 = cx.add_view(window_id, |cx| {
3246 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
3247 });
3248
3249 // Add an item to an empty pane
3250 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
3251 project.read_with(cx, |project, cx| {
3252 assert_eq!(
3253 project.active_entry(),
3254 project
3255 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3256 .map(|e| e.id)
3257 );
3258 });
3259 assert_eq!(
3260 cx.current_window_title(window_id).as_deref(),
3261 Some("one.txt β root1")
3262 );
3263
3264 // Add a second item to a non-empty pane
3265 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
3266 assert_eq!(
3267 cx.current_window_title(window_id).as_deref(),
3268 Some("two.txt β root1")
3269 );
3270 project.read_with(cx, |project, cx| {
3271 assert_eq!(
3272 project.active_entry(),
3273 project
3274 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
3275 .map(|e| e.id)
3276 );
3277 });
3278
3279 // Close the active item
3280 pane.update(cx, |pane, cx| {
3281 pane.close_active_item(&Default::default(), cx).unwrap()
3282 })
3283 .await
3284 .unwrap();
3285 assert_eq!(
3286 cx.current_window_title(window_id).as_deref(),
3287 Some("one.txt β root1")
3288 );
3289 project.read_with(cx, |project, cx| {
3290 assert_eq!(
3291 project.active_entry(),
3292 project
3293 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3294 .map(|e| e.id)
3295 );
3296 });
3297
3298 // Add a project folder
3299 project
3300 .update(cx, |project, cx| {
3301 project.find_or_create_local_worktree("/root2", true, cx)
3302 })
3303 .await
3304 .unwrap();
3305 assert_eq!(
3306 cx.current_window_title(window_id).as_deref(),
3307 Some("one.txt β root1, root2")
3308 );
3309
3310 // Remove a project folder
3311 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
3312 assert_eq!(
3313 cx.current_window_title(window_id).as_deref(),
3314 Some("one.txt β root2")
3315 );
3316 }
3317
3318 #[gpui::test]
3319 async fn test_close_window(cx: &mut TestAppContext) {
3320 cx.foreground().forbid_parking();
3321 Settings::test_async(cx);
3322 let fs = FakeFs::new(cx.background());
3323 fs.insert_tree("/root", json!({ "one": "" })).await;
3324
3325 let project = Project::test(fs, ["root".as_ref()], cx).await;
3326 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3327
3328 // When there are no dirty items, there's nothing to do.
3329 let item1 = cx.add_view(window_id, |_| TestItem::new());
3330 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
3331 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3332 assert!(task.await.unwrap());
3333
3334 // When there are dirty untitled items, prompt to save each one. If the user
3335 // cancels any prompt, then abort.
3336 let item2 = cx.add_view(window_id, |_| TestItem::new().with_dirty(true));
3337 let item3 = cx.add_view(window_id, |cx| {
3338 TestItem::new()
3339 .with_dirty(true)
3340 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3341 });
3342 workspace.update(cx, |w, cx| {
3343 w.add_item(Box::new(item2.clone()), cx);
3344 w.add_item(Box::new(item3.clone()), cx);
3345 });
3346 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3347 cx.foreground().run_until_parked();
3348 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
3349 cx.foreground().run_until_parked();
3350 assert!(!cx.has_pending_prompt(window_id));
3351 assert!(!task.await.unwrap());
3352 }
3353
3354 #[gpui::test]
3355 async fn test_close_pane_items(cx: &mut TestAppContext) {
3356 cx.foreground().forbid_parking();
3357 Settings::test_async(cx);
3358 let fs = FakeFs::new(cx.background());
3359
3360 let project = Project::test(fs, None, cx).await;
3361 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3362
3363 let item1 = cx.add_view(window_id, |cx| {
3364 TestItem::new()
3365 .with_dirty(true)
3366 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3367 });
3368 let item2 = cx.add_view(window_id, |cx| {
3369 TestItem::new()
3370 .with_dirty(true)
3371 .with_conflict(true)
3372 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3373 });
3374 let item3 = cx.add_view(window_id, |cx| {
3375 TestItem::new()
3376 .with_dirty(true)
3377 .with_conflict(true)
3378 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
3379 });
3380 let item4 = cx.add_view(window_id, |cx| {
3381 TestItem::new()
3382 .with_dirty(true)
3383 .with_project_items(&[TestProjectItem::new_untitled(cx)])
3384 });
3385 let pane = workspace.update(cx, |workspace, cx| {
3386 workspace.add_item(Box::new(item1.clone()), cx);
3387 workspace.add_item(Box::new(item2.clone()), cx);
3388 workspace.add_item(Box::new(item3.clone()), cx);
3389 workspace.add_item(Box::new(item4.clone()), cx);
3390 workspace.active_pane().clone()
3391 });
3392
3393 let close_items = pane.update(cx, |pane, cx| {
3394 pane.activate_item(1, true, true, cx);
3395 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3396 let item1_id = item1.id();
3397 let item3_id = item3.id();
3398 let item4_id = item4.id();
3399 pane.close_items(cx, move |id| [item1_id, item3_id, item4_id].contains(&id))
3400 });
3401 cx.foreground().run_until_parked();
3402
3403 // There's a prompt to save item 1.
3404 pane.read_with(cx, |pane, _| {
3405 assert_eq!(pane.items_len(), 4);
3406 assert_eq!(pane.active_item().unwrap().id(), item1.id());
3407 });
3408 assert!(cx.has_pending_prompt(window_id));
3409
3410 // Confirm saving item 1.
3411 cx.simulate_prompt_answer(window_id, 0);
3412 cx.foreground().run_until_parked();
3413
3414 // Item 1 is saved. There's a prompt to save item 3.
3415 pane.read_with(cx, |pane, cx| {
3416 assert_eq!(item1.read(cx).save_count, 1);
3417 assert_eq!(item1.read(cx).save_as_count, 0);
3418 assert_eq!(item1.read(cx).reload_count, 0);
3419 assert_eq!(pane.items_len(), 3);
3420 assert_eq!(pane.active_item().unwrap().id(), item3.id());
3421 });
3422 assert!(cx.has_pending_prompt(window_id));
3423
3424 // Cancel saving item 3.
3425 cx.simulate_prompt_answer(window_id, 1);
3426 cx.foreground().run_until_parked();
3427
3428 // Item 3 is reloaded. There's a prompt to save item 4.
3429 pane.read_with(cx, |pane, cx| {
3430 assert_eq!(item3.read(cx).save_count, 0);
3431 assert_eq!(item3.read(cx).save_as_count, 0);
3432 assert_eq!(item3.read(cx).reload_count, 1);
3433 assert_eq!(pane.items_len(), 2);
3434 assert_eq!(pane.active_item().unwrap().id(), item4.id());
3435 });
3436 assert!(cx.has_pending_prompt(window_id));
3437
3438 // Confirm saving item 4.
3439 cx.simulate_prompt_answer(window_id, 0);
3440 cx.foreground().run_until_parked();
3441
3442 // There's a prompt for a path for item 4.
3443 cx.simulate_new_path_selection(|_| Some(Default::default()));
3444 close_items.await.unwrap();
3445
3446 // The requested items are closed.
3447 pane.read_with(cx, |pane, cx| {
3448 assert_eq!(item4.read(cx).save_count, 0);
3449 assert_eq!(item4.read(cx).save_as_count, 1);
3450 assert_eq!(item4.read(cx).reload_count, 0);
3451 assert_eq!(pane.items_len(), 1);
3452 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3453 });
3454 }
3455
3456 #[gpui::test]
3457 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
3458 cx.foreground().forbid_parking();
3459 Settings::test_async(cx);
3460 let fs = FakeFs::new(cx.background());
3461
3462 let project = Project::test(fs, [], cx).await;
3463 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3464
3465 // Create several workspace items with single project entries, and two
3466 // workspace items with multiple project entries.
3467 let single_entry_items = (0..=4)
3468 .map(|project_entry_id| {
3469 cx.add_view(window_id, |cx| {
3470 TestItem::new()
3471 .with_dirty(true)
3472 .with_project_items(&[TestProjectItem::new(
3473 project_entry_id,
3474 &format!("{project_entry_id}.txt"),
3475 cx,
3476 )])
3477 })
3478 })
3479 .collect::<Vec<_>>();
3480 let item_2_3 = cx.add_view(window_id, |cx| {
3481 TestItem::new()
3482 .with_dirty(true)
3483 .with_singleton(false)
3484 .with_project_items(&[
3485 single_entry_items[2].read(cx).project_items[0].clone(),
3486 single_entry_items[3].read(cx).project_items[0].clone(),
3487 ])
3488 });
3489 let item_3_4 = cx.add_view(window_id, |cx| {
3490 TestItem::new()
3491 .with_dirty(true)
3492 .with_singleton(false)
3493 .with_project_items(&[
3494 single_entry_items[3].read(cx).project_items[0].clone(),
3495 single_entry_items[4].read(cx).project_items[0].clone(),
3496 ])
3497 });
3498
3499 // Create two panes that contain the following project entries:
3500 // left pane:
3501 // multi-entry items: (2, 3)
3502 // single-entry items: 0, 1, 2, 3, 4
3503 // right pane:
3504 // single-entry items: 1
3505 // multi-entry items: (3, 4)
3506 let left_pane = workspace.update(cx, |workspace, cx| {
3507 let left_pane = workspace.active_pane().clone();
3508 workspace.add_item(Box::new(item_2_3.clone()), cx);
3509 for item in single_entry_items {
3510 workspace.add_item(Box::new(item), cx);
3511 }
3512 left_pane.update(cx, |pane, cx| {
3513 pane.activate_item(2, true, true, cx);
3514 });
3515
3516 workspace
3517 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
3518 .unwrap();
3519
3520 left_pane
3521 });
3522
3523 //Need to cause an effect flush in order to respect new focus
3524 workspace.update(cx, |workspace, cx| {
3525 workspace.add_item(Box::new(item_3_4.clone()), cx);
3526 cx.focus(&left_pane);
3527 });
3528
3529 // When closing all of the items in the left pane, we should be prompted twice:
3530 // once for project entry 0, and once for project entry 2. After those two
3531 // prompts, the task should complete.
3532
3533 let close = left_pane.update(cx, |pane, cx| pane.close_items(cx, |_| true));
3534 cx.foreground().run_until_parked();
3535 left_pane.read_with(cx, |pane, cx| {
3536 assert_eq!(
3537 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3538 &[ProjectEntryId::from_proto(0)]
3539 );
3540 });
3541 cx.simulate_prompt_answer(window_id, 0);
3542
3543 cx.foreground().run_until_parked();
3544 left_pane.read_with(cx, |pane, cx| {
3545 assert_eq!(
3546 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3547 &[ProjectEntryId::from_proto(2)]
3548 );
3549 });
3550 cx.simulate_prompt_answer(window_id, 0);
3551
3552 cx.foreground().run_until_parked();
3553 close.await.unwrap();
3554 left_pane.read_with(cx, |pane, _| {
3555 assert_eq!(pane.items_len(), 0);
3556 });
3557 }
3558
3559 #[gpui::test]
3560 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3561 deterministic.forbid_parking();
3562
3563 Settings::test_async(cx);
3564 let fs = FakeFs::new(cx.background());
3565
3566 let project = Project::test(fs, [], cx).await;
3567 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3568 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3569
3570 let item = cx.add_view(window_id, |cx| {
3571 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3572 });
3573 let item_id = item.id();
3574 workspace.update(cx, |workspace, cx| {
3575 workspace.add_item(Box::new(item.clone()), cx);
3576 });
3577
3578 // Autosave on window change.
3579 item.update(cx, |item, cx| {
3580 cx.update_global(|settings: &mut Settings, _| {
3581 settings.autosave = Autosave::OnWindowChange;
3582 });
3583 item.is_dirty = true;
3584 });
3585
3586 // Deactivating the window saves the file.
3587 cx.simulate_window_activation(None);
3588 deterministic.run_until_parked();
3589 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
3590
3591 // Autosave on focus change.
3592 item.update(cx, |item, cx| {
3593 cx.focus_self();
3594 cx.update_global(|settings: &mut Settings, _| {
3595 settings.autosave = Autosave::OnFocusChange;
3596 });
3597 item.is_dirty = true;
3598 });
3599
3600 // Blurring the item saves the file.
3601 item.update(cx, |_, cx| cx.blur());
3602 deterministic.run_until_parked();
3603 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
3604
3605 // Deactivating the window still saves the file.
3606 cx.simulate_window_activation(Some(window_id));
3607 item.update(cx, |item, cx| {
3608 cx.focus_self();
3609 item.is_dirty = true;
3610 });
3611 cx.simulate_window_activation(None);
3612
3613 deterministic.run_until_parked();
3614 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3615
3616 // Autosave after delay.
3617 item.update(cx, |item, cx| {
3618 cx.update_global(|settings: &mut Settings, _| {
3619 settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
3620 });
3621 item.is_dirty = true;
3622 cx.emit(TestItemEvent::Edit);
3623 });
3624
3625 // Delay hasn't fully expired, so the file is still dirty and unsaved.
3626 deterministic.advance_clock(Duration::from_millis(250));
3627 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3628
3629 // After delay expires, the file is saved.
3630 deterministic.advance_clock(Duration::from_millis(250));
3631 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
3632
3633 // Autosave on focus change, ensuring closing the tab counts as such.
3634 item.update(cx, |item, cx| {
3635 cx.update_global(|settings: &mut Settings, _| {
3636 settings.autosave = Autosave::OnFocusChange;
3637 });
3638 item.is_dirty = true;
3639 });
3640
3641 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id))
3642 .await
3643 .unwrap();
3644 assert!(!cx.has_pending_prompt(window_id));
3645 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3646
3647 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
3648 workspace.update(cx, |workspace, cx| {
3649 workspace.add_item(Box::new(item.clone()), cx);
3650 });
3651 item.update(cx, |item, cx| {
3652 item.project_items[0].update(cx, |item, _| {
3653 item.entry_id = None;
3654 });
3655 item.is_dirty = true;
3656 cx.blur();
3657 });
3658 deterministic.run_until_parked();
3659 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3660
3661 // Ensure autosave is prevented for deleted files also when closing the buffer.
3662 let _close_items =
3663 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id));
3664 deterministic.run_until_parked();
3665 assert!(cx.has_pending_prompt(window_id));
3666 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3667 }
3668
3669 #[gpui::test]
3670 async fn test_pane_navigation(
3671 deterministic: Arc<Deterministic>,
3672 cx: &mut gpui::TestAppContext,
3673 ) {
3674 deterministic.forbid_parking();
3675 Settings::test_async(cx);
3676 let fs = FakeFs::new(cx.background());
3677
3678 let project = Project::test(fs, [], cx).await;
3679 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3680
3681 let item = cx.add_view(window_id, |cx| {
3682 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3683 });
3684 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3685 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
3686 let toolbar_notify_count = Rc::new(RefCell::new(0));
3687
3688 workspace.update(cx, |workspace, cx| {
3689 workspace.add_item(Box::new(item.clone()), cx);
3690 let toolbar_notification_count = toolbar_notify_count.clone();
3691 cx.observe(&toolbar, move |_, _, _| {
3692 *toolbar_notification_count.borrow_mut() += 1
3693 })
3694 .detach();
3695 });
3696
3697 pane.read_with(cx, |pane, _| {
3698 assert!(!pane.can_navigate_backward());
3699 assert!(!pane.can_navigate_forward());
3700 });
3701
3702 item.update(cx, |item, cx| {
3703 item.set_state("one".to_string(), cx);
3704 });
3705
3706 // Toolbar must be notified to re-render the navigation buttons
3707 assert_eq!(*toolbar_notify_count.borrow(), 1);
3708
3709 pane.read_with(cx, |pane, _| {
3710 assert!(pane.can_navigate_backward());
3711 assert!(!pane.can_navigate_forward());
3712 });
3713
3714 workspace
3715 .update(cx, |workspace, cx| {
3716 Pane::go_back(workspace, Some(pane.downgrade()), cx)
3717 })
3718 .await
3719 .unwrap();
3720
3721 assert_eq!(*toolbar_notify_count.borrow(), 3);
3722 pane.read_with(cx, |pane, _| {
3723 assert!(!pane.can_navigate_backward());
3724 assert!(pane.can_navigate_forward());
3725 });
3726 }
3727
3728 #[gpui::test]
3729 async fn test_panels(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3730 deterministic.forbid_parking();
3731 Settings::test_async(cx);
3732 let fs = FakeFs::new(cx.background());
3733
3734 let project = Project::test(fs, [], cx).await;
3735 let (_window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3736
3737 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
3738 // Add panel_1 on the left, panel_2 on the right.
3739 let panel_1 = cx.add_view(|_| TestPanel {
3740 position: DockPosition::Left,
3741 });
3742 workspace.add_panel(panel_1.clone(), cx);
3743 workspace
3744 .left_dock()
3745 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
3746 let panel_2 = cx.add_view(|_| TestPanel {
3747 position: DockPosition::Right,
3748 });
3749 workspace.add_panel(panel_2.clone(), cx);
3750 workspace
3751 .right_dock()
3752 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
3753
3754 let left_dock = workspace.left_dock();
3755 assert_eq!(
3756 left_dock.read(cx).active_panel().unwrap().id(),
3757 panel_1.id()
3758 );
3759 assert_eq!(
3760 left_dock.read(cx).active_panel_size().unwrap(),
3761 panel_1.default_size(cx)
3762 );
3763
3764 left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx));
3765 assert_eq!(
3766 workspace.right_dock().read(cx).active_panel().unwrap().id(),
3767 panel_2.id()
3768 );
3769
3770 (panel_1, panel_2)
3771 });
3772
3773 // Move panel_1 to the right
3774 panel_1.update(cx, |panel_1, cx| {
3775 panel_1.set_position(DockPosition::Right, cx)
3776 });
3777
3778 workspace.update(cx, |workspace, cx| {
3779 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
3780 // Since it was the only panel on the left, the left dock should now be closed.
3781 assert!(!workspace.left_dock().read(cx).is_open());
3782 assert!(workspace.left_dock().read(cx).active_panel().is_none());
3783 let right_dock = workspace.right_dock();
3784 assert_eq!(
3785 right_dock.read(cx).active_panel().unwrap().id(),
3786 panel_1.id()
3787 );
3788 assert_eq!(right_dock.read(cx).active_panel_size().unwrap(), 1337.);
3789
3790 // Now we move panel_2Β to the left
3791 panel_2.set_position(DockPosition::Left, cx);
3792 });
3793
3794 workspace.update(cx, |workspace, cx| {
3795 // Since panel_2 was not visible on the right, we don't open the left dock.
3796 assert!(!workspace.left_dock().read(cx).is_open());
3797 // And the right dock is unaffected in it's displaying of panel_1
3798 assert!(workspace.right_dock().read(cx).is_open());
3799 assert_eq!(
3800 workspace.right_dock().read(cx).active_panel().unwrap().id(),
3801 panel_1.id()
3802 );
3803 });
3804
3805 // Move panel_1 back to the left
3806 panel_1.update(cx, |panel_1, cx| {
3807 panel_1.set_position(DockPosition::Left, cx)
3808 });
3809
3810 workspace.update(cx, |workspace, cx| {
3811 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
3812 let left_dock = workspace.left_dock();
3813 assert!(left_dock.read(cx).is_open());
3814 assert_eq!(
3815 left_dock.read(cx).active_panel().unwrap().id(),
3816 panel_1.id()
3817 );
3818 assert_eq!(left_dock.read(cx).active_panel_size().unwrap(), 1337.);
3819 // And right the dock should be closed as it no longer has any panels.
3820 assert!(!workspace.right_dock().read(cx).is_open());
3821
3822 // Now we move panel_1 to the bottom
3823 panel_1.set_position(DockPosition::Bottom, cx);
3824 });
3825
3826 workspace.update(cx, |workspace, cx| {
3827 // Since panel_1 was visible on the left, we close the left dock.
3828 assert!(!workspace.left_dock().read(cx).is_open());
3829 // The bottom dock is sized based on the panel's default size,
3830 // since the panel orientation changed from vertical to horizontal.
3831 let bottom_dock = workspace.bottom_dock();
3832 assert_eq!(
3833 bottom_dock.read(cx).active_panel_size().unwrap(),
3834 panel_1.default_size(cx),
3835 );
3836 // Close bottom dock and move panel_1 back to the left.
3837 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
3838 panel_1.set_position(DockPosition::Left, cx);
3839 });
3840
3841 // Emit activated event on panel 1
3842 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
3843
3844 // Now the left dock is open and panel_1 is active and focused.
3845 workspace.read_with(cx, |workspace, cx| {
3846 let left_dock = workspace.left_dock();
3847 assert!(left_dock.read(cx).is_open());
3848 assert_eq!(
3849 left_dock.read(cx).active_panel().unwrap().id(),
3850 panel_1.id()
3851 );
3852 assert!(panel_1.is_focused(cx));
3853 });
3854
3855 // Emit closed event on panel 2, which is not active
3856 panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
3857
3858 // Wo don't close the left dock, because panel_2 wasn't the active panel
3859 workspace.read_with(cx, |workspace, cx| {
3860 let left_dock = workspace.left_dock();
3861 assert!(left_dock.read(cx).is_open());
3862 assert_eq!(
3863 left_dock.read(cx).active_panel().unwrap().id(),
3864 panel_1.id()
3865 );
3866 });
3867
3868 // Emit closed event on panel 1, which is active
3869 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
3870
3871 // Now the left dock is closed, because panel_1 was the active panel
3872 workspace.read_with(cx, |workspace, cx| {
3873 let left_dock = workspace.left_dock();
3874 assert!(!left_dock.read(cx).is_open());
3875 });
3876 }
3877}