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