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