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