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