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