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