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