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