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 {
2788 assert!(variant.is_some());
2789 }
2790 }
2791 }
2792
2793 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2794 }
2795
2796 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2797 let items = futures::future::try_join_all(item_tasks).await?;
2798 this.update(cx, |this, cx| {
2799 let state = this.follower_states.get_mut(&pane)?;
2800 for (id, item) in leader_view_ids.into_iter().zip(items) {
2801 item.set_leader_peer_id(Some(leader_id), cx);
2802 state.items_by_leader_view_id.insert(id, item);
2803 }
2804
2805 Some(())
2806 })?;
2807 }
2808 Ok(())
2809 }
2810
2811 fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
2812 let mut is_project_item = true;
2813 let mut update = proto::UpdateActiveView::default();
2814
2815 if let Some(item) = self.active_item(cx) {
2816 if item.focus_handle(cx).contains_focused(cx) {
2817 if let Some(item) = item.to_followable_item_handle(cx) {
2818 is_project_item = item.is_project_item(cx);
2819 update = proto::UpdateActiveView {
2820 id: item
2821 .remote_id(&self.app_state.client, cx)
2822 .map(|id| id.to_proto()),
2823 leader_id: self.leader_for_pane(&self.active_pane),
2824 };
2825 }
2826 }
2827 }
2828
2829 if update.id != self.last_active_view_id {
2830 self.last_active_view_id = update.id.clone();
2831 self.update_followers(
2832 is_project_item,
2833 proto::update_followers::Variant::UpdateActiveView(update),
2834 cx,
2835 );
2836 }
2837 }
2838
2839 fn update_followers(
2840 &self,
2841 project_only: bool,
2842 update: proto::update_followers::Variant,
2843 cx: &mut WindowContext,
2844 ) -> Option<()> {
2845 // If this update only applies to for followers in the current project,
2846 // then skip it unless this project is shared. If it applies to all
2847 // followers, regardless of project, then set `project_id` to none,
2848 // indicating that it goes to all followers.
2849 let project_id = if project_only {
2850 Some(self.project.read(cx).remote_id()?)
2851 } else {
2852 None
2853 };
2854 self.app_state().workspace_store.update(cx, |store, cx| {
2855 store.update_followers(project_id, update, cx)
2856 })
2857 }
2858
2859 pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
2860 self.follower_states.get(pane).map(|state| state.leader_id)
2861 }
2862
2863 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2864 cx.notify();
2865
2866 let call = self.active_call()?;
2867 let room = call.read(cx).room()?.read(cx);
2868 let participant = room.remote_participant_for_peer_id(leader_id)?;
2869 let mut items_to_activate = Vec::new();
2870
2871 let leader_in_this_app;
2872 let leader_in_this_project;
2873 match participant.location {
2874 call::ParticipantLocation::SharedProject { project_id } => {
2875 leader_in_this_app = true;
2876 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
2877 }
2878 call::ParticipantLocation::UnsharedProject => {
2879 leader_in_this_app = true;
2880 leader_in_this_project = false;
2881 }
2882 call::ParticipantLocation::External => {
2883 leader_in_this_app = false;
2884 leader_in_this_project = false;
2885 }
2886 };
2887
2888 for (pane, state) in &self.follower_states {
2889 if state.leader_id != leader_id {
2890 continue;
2891 }
2892 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
2893 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
2894 if leader_in_this_project || !item.is_project_item(cx) {
2895 items_to_activate.push((pane.clone(), item.boxed_clone()));
2896 }
2897 }
2898 continue;
2899 }
2900
2901 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2902 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2903 }
2904 }
2905
2906 for (pane, item) in items_to_activate {
2907 let pane_was_focused = pane.read(cx).has_focus(cx);
2908 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2909 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2910 } else {
2911 pane.update(cx, |pane, cx| {
2912 pane.add_item(item.boxed_clone(), false, false, None, cx)
2913 });
2914 }
2915
2916 if pane_was_focused {
2917 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2918 }
2919 }
2920
2921 None
2922 }
2923
2924 fn shared_screen_for_peer(
2925 &self,
2926 peer_id: PeerId,
2927 pane: &View<Pane>,
2928 cx: &mut WindowContext,
2929 ) -> Option<View<SharedScreen>> {
2930 let call = self.active_call()?;
2931 let room = call.read(cx).room()?.read(cx);
2932 let participant = room.remote_participant_for_peer_id(peer_id)?;
2933 let track = participant.video_tracks.values().next()?.clone();
2934 let user = participant.user.clone();
2935
2936 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2937 if item.read(cx).peer_id == peer_id {
2938 return Some(item);
2939 }
2940 }
2941
2942 Some(cx.new_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2943 }
2944
2945 pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
2946 if cx.is_window_active() {
2947 self.update_active_view_for_followers(cx);
2948 cx.background_executor()
2949 .spawn(persistence::DB.update_timestamp(self.database_id()))
2950 .detach();
2951 } else {
2952 for pane in &self.panes {
2953 pane.update(cx, |pane, cx| {
2954 if let Some(item) = pane.active_item() {
2955 item.workspace_deactivated(cx);
2956 }
2957 if matches!(
2958 WorkspaceSettings::get_global(cx).autosave,
2959 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
2960 ) {
2961 for item in pane.items() {
2962 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2963 .detach_and_log_err(cx);
2964 }
2965 }
2966 });
2967 }
2968 }
2969 }
2970
2971 fn active_call(&self) -> Option<&Model<ActiveCall>> {
2972 self.active_call.as_ref().map(|(call, _)| call)
2973 }
2974
2975 fn on_active_call_event(
2976 &mut self,
2977 _: Model<ActiveCall>,
2978 event: &call::room::Event,
2979 cx: &mut ViewContext<Self>,
2980 ) {
2981 match event {
2982 call::room::Event::ParticipantLocationChanged { participant_id }
2983 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2984 self.leader_updated(*participant_id, cx);
2985 }
2986 _ => {}
2987 }
2988 }
2989
2990 pub fn database_id(&self) -> WorkspaceId {
2991 self.database_id
2992 }
2993
2994 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2995 let project = self.project().read(cx);
2996
2997 if project.is_local() {
2998 Some(
2999 project
3000 .visible_worktrees(cx)
3001 .map(|worktree| worktree.read(cx).abs_path())
3002 .collect::<Vec<_>>()
3003 .into(),
3004 )
3005 } else {
3006 None
3007 }
3008 }
3009
3010 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
3011 match member {
3012 Member::Axis(PaneAxis { members, .. }) => {
3013 for child in members.iter() {
3014 self.remove_panes(child.clone(), cx)
3015 }
3016 }
3017 Member::Pane(pane) => {
3018 self.force_remove_pane(&pane, cx);
3019 }
3020 }
3021 }
3022
3023 fn force_remove_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
3024 self.panes.retain(|p| p != pane);
3025 self.panes
3026 .last()
3027 .unwrap()
3028 .update(cx, |pane, cx| pane.focus(cx));
3029 if self.last_active_center_pane == Some(pane.downgrade()) {
3030 self.last_active_center_pane = None;
3031 }
3032 cx.notify();
3033 }
3034
3035 fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
3036 self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
3037 cx.background_executor()
3038 .timer(Duration::from_millis(100))
3039 .await;
3040 this.update(&mut cx, |this, cx| this.serialize_workspace(cx))
3041 .log_err();
3042 }));
3043 }
3044
3045 fn serialize_workspace(&self, cx: &mut WindowContext) {
3046 fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
3047 let (items, active) = {
3048 let pane = pane_handle.read(cx);
3049 let active_item_id = pane.active_item().map(|item| item.item_id());
3050 (
3051 pane.items()
3052 .filter_map(|item_handle| {
3053 Some(SerializedItem {
3054 kind: Arc::from(item_handle.serialized_item_kind()?),
3055 item_id: item_handle.item_id().as_u64(),
3056 active: Some(item_handle.item_id()) == active_item_id,
3057 })
3058 })
3059 .collect::<Vec<_>>(),
3060 pane.has_focus(cx),
3061 )
3062 };
3063
3064 SerializedPane::new(items, active)
3065 }
3066
3067 fn build_serialized_pane_group(
3068 pane_group: &Member,
3069 cx: &WindowContext,
3070 ) -> SerializedPaneGroup {
3071 match pane_group {
3072 Member::Axis(PaneAxis {
3073 axis,
3074 members,
3075 flexes,
3076 bounding_boxes: _,
3077 }) => SerializedPaneGroup::Group {
3078 axis: SerializedAxis(*axis),
3079 children: members
3080 .iter()
3081 .map(|member| build_serialized_pane_group(member, cx))
3082 .collect::<Vec<_>>(),
3083 flexes: Some(flexes.lock().clone()),
3084 },
3085 Member::Pane(pane_handle) => {
3086 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
3087 }
3088 }
3089 }
3090
3091 fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
3092 let left_dock = this.left_dock.read(cx);
3093 let left_visible = left_dock.is_open();
3094 let left_active_panel = left_dock
3095 .visible_panel()
3096 .and_then(|panel| Some(panel.persistent_name().to_string()));
3097 let left_dock_zoom = left_dock
3098 .visible_panel()
3099 .map(|panel| panel.is_zoomed(cx))
3100 .unwrap_or(false);
3101
3102 let right_dock = this.right_dock.read(cx);
3103 let right_visible = right_dock.is_open();
3104 let right_active_panel = right_dock
3105 .visible_panel()
3106 .and_then(|panel| Some(panel.persistent_name().to_string()));
3107 let right_dock_zoom = right_dock
3108 .visible_panel()
3109 .map(|panel| panel.is_zoomed(cx))
3110 .unwrap_or(false);
3111
3112 let bottom_dock = this.bottom_dock.read(cx);
3113 let bottom_visible = bottom_dock.is_open();
3114 let bottom_active_panel = bottom_dock
3115 .visible_panel()
3116 .and_then(|panel| Some(panel.persistent_name().to_string()));
3117 let bottom_dock_zoom = bottom_dock
3118 .visible_panel()
3119 .map(|panel| panel.is_zoomed(cx))
3120 .unwrap_or(false);
3121
3122 DockStructure {
3123 left: DockData {
3124 visible: left_visible,
3125 active_panel: left_active_panel,
3126 zoom: left_dock_zoom,
3127 },
3128 right: DockData {
3129 visible: right_visible,
3130 active_panel: right_active_panel,
3131 zoom: right_dock_zoom,
3132 },
3133 bottom: DockData {
3134 visible: bottom_visible,
3135 active_panel: bottom_active_panel,
3136 zoom: bottom_dock_zoom,
3137 },
3138 }
3139 }
3140
3141 if let Some(location) = self.location(cx) {
3142 // Load bearing special case:
3143 // - with_local_workspace() relies on this to not have other stuff open
3144 // when you open your log
3145 if !location.paths().is_empty() {
3146 let center_group = build_serialized_pane_group(&self.center.root, cx);
3147 let docks = build_serialized_docks(self, cx);
3148
3149 let serialized_workspace = SerializedWorkspace {
3150 id: self.database_id,
3151 location,
3152 center_group,
3153 bounds: Default::default(),
3154 display: Default::default(),
3155 docks,
3156 };
3157
3158 cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace))
3159 .detach();
3160 }
3161 }
3162 }
3163
3164 pub(crate) fn load_workspace(
3165 serialized_workspace: SerializedWorkspace,
3166 paths_to_open: Vec<Option<ProjectPath>>,
3167 cx: &mut ViewContext<Workspace>,
3168 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
3169 cx.spawn(|workspace, mut cx| async move {
3170 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
3171
3172 let mut center_group = None;
3173 let mut center_items = None;
3174
3175 // Traverse the splits tree and add to things
3176 if let Some((group, active_pane, items)) = serialized_workspace
3177 .center_group
3178 .deserialize(
3179 &project,
3180 serialized_workspace.id,
3181 workspace.clone(),
3182 &mut cx,
3183 )
3184 .await
3185 {
3186 center_items = Some(items);
3187 center_group = Some((group, active_pane))
3188 }
3189
3190 let mut items_by_project_path = cx.update(|cx| {
3191 center_items
3192 .unwrap_or_default()
3193 .into_iter()
3194 .filter_map(|item| {
3195 let item = item?;
3196 let project_path = item.project_path(cx)?;
3197 Some((project_path, item))
3198 })
3199 .collect::<HashMap<_, _>>()
3200 })?;
3201
3202 let opened_items = paths_to_open
3203 .into_iter()
3204 .map(|path_to_open| {
3205 path_to_open
3206 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
3207 })
3208 .collect::<Vec<_>>();
3209
3210 // Remove old panes from workspace panes list
3211 workspace.update(&mut cx, |workspace, cx| {
3212 if let Some((center_group, active_pane)) = center_group {
3213 workspace.remove_panes(workspace.center.root.clone(), cx);
3214
3215 // Swap workspace center group
3216 workspace.center = PaneGroup::with_root(center_group);
3217 workspace.last_active_center_pane = active_pane.as_ref().map(|p| p.downgrade());
3218 if let Some(active_pane) = active_pane {
3219 workspace.active_pane = active_pane;
3220 cx.focus_self();
3221 } else {
3222 workspace.active_pane = workspace.center.first_pane().clone();
3223 }
3224 }
3225
3226 let docks = serialized_workspace.docks;
3227
3228 let right = docks.right.clone();
3229 workspace
3230 .right_dock
3231 .update(cx, |dock, _| dock.serialized_dock = Some(right));
3232 let left = docks.left.clone();
3233 workspace
3234 .left_dock
3235 .update(cx, |dock, _| dock.serialized_dock = Some(left));
3236 let bottom = docks.bottom.clone();
3237 workspace
3238 .bottom_dock
3239 .update(cx, |dock, _| dock.serialized_dock = Some(bottom));
3240
3241 cx.notify();
3242 })?;
3243
3244 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3245 workspace.update(&mut cx, |workspace, cx| workspace.serialize_workspace(cx))?;
3246
3247 Ok(opened_items)
3248 })
3249 }
3250
3251 fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3252 self.add_workspace_actions_listeners(div, cx)
3253 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3254 .on_action(cx.listener(Self::close_all_items_and_panes))
3255 .on_action(cx.listener(Self::save_all))
3256 .on_action(cx.listener(Self::add_folder_to_project))
3257 .on_action(cx.listener(Self::follow_next_collaborator))
3258 .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
3259 let pane = workspace.active_pane().clone();
3260 workspace.unfollow(&pane, cx);
3261 }))
3262 .on_action(cx.listener(|workspace, action: &Save, cx| {
3263 workspace
3264 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
3265 .detach_and_log_err(cx);
3266 }))
3267 .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
3268 workspace
3269 .save_active_item(SaveIntent::SaveAs, cx)
3270 .detach_and_log_err(cx);
3271 }))
3272 .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
3273 workspace.activate_previous_pane(cx)
3274 }))
3275 .on_action(
3276 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
3277 )
3278 .on_action(
3279 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
3280 workspace.activate_pane_in_direction(action.0, cx)
3281 }),
3282 )
3283 .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
3284 workspace.swap_pane_in_direction(action.0, cx)
3285 }))
3286 .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
3287 this.toggle_dock(DockPosition::Left, cx);
3288 }))
3289 .on_action(
3290 cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
3291 workspace.toggle_dock(DockPosition::Right, cx);
3292 }),
3293 )
3294 .on_action(
3295 cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
3296 workspace.toggle_dock(DockPosition::Bottom, cx);
3297 }),
3298 )
3299 .on_action(
3300 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
3301 workspace.close_all_docks(cx);
3302 }),
3303 )
3304 .on_action(cx.listener(Workspace::open))
3305 .on_action(cx.listener(Workspace::close_window))
3306 .on_action(cx.listener(Workspace::activate_pane_at_index))
3307 .on_action(
3308 cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
3309 workspace.reopen_closed_item(cx).detach();
3310 }),
3311 )
3312 }
3313
3314 #[cfg(any(test, feature = "test-support"))]
3315 pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
3316 use node_runtime::FakeNodeRuntime;
3317
3318 let client = project.read(cx).client();
3319 let user_store = project.read(cx).user_store();
3320
3321 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
3322 cx.activate_window();
3323 let app_state = Arc::new(AppState {
3324 languages: project.read(cx).languages().clone(),
3325 workspace_store,
3326 client,
3327 user_store,
3328 fs: project.read(cx).fs().clone(),
3329 build_window_options: |_, _, _| Default::default(),
3330 node_runtime: FakeNodeRuntime::new(),
3331 });
3332 let workspace = Self::new(0, project, app_state, cx);
3333 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
3334 workspace
3335 }
3336
3337 pub fn register_action<A: Action>(
3338 &mut self,
3339 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
3340 ) -> &mut Self {
3341 let callback = Arc::new(callback);
3342
3343 self.workspace_actions.push(Box::new(move |div, cx| {
3344 let callback = callback.clone();
3345 div.on_action(
3346 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
3347 )
3348 }));
3349 self
3350 }
3351
3352 fn add_workspace_actions_listeners(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3353 let mut div = div
3354 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3355 .on_action(cx.listener(Self::close_all_items_and_panes))
3356 .on_action(cx.listener(Self::add_folder_to_project))
3357 .on_action(cx.listener(Self::save_all))
3358 .on_action(cx.listener(Self::open));
3359 for action in self.workspace_actions.iter() {
3360 div = (action)(div, cx)
3361 }
3362 div
3363 }
3364
3365 pub fn has_active_modal(&self, cx: &WindowContext<'_>) -> bool {
3366 self.modal_layer.read(cx).has_active_modal()
3367 }
3368
3369 pub fn active_modal<V: ManagedView + 'static>(&mut self, cx: &AppContext) -> Option<View<V>> {
3370 self.modal_layer.read(cx).active_modal()
3371 }
3372
3373 pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut WindowContext, build: B)
3374 where
3375 B: FnOnce(&mut ViewContext<V>) -> V,
3376 {
3377 self.modal_layer
3378 .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
3379 }
3380}
3381
3382fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
3383 let display_origin = cx
3384 .update(|cx| Some(cx.displays().first()?.bounds().origin))
3385 .ok()??;
3386 ZED_WINDOW_POSITION
3387 .zip(*ZED_WINDOW_SIZE)
3388 .map(|(position, size)| {
3389 WindowBounds::Fixed(Bounds {
3390 origin: display_origin + position,
3391 size,
3392 })
3393 })
3394}
3395
3396fn open_items(
3397 serialized_workspace: Option<SerializedWorkspace>,
3398 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3399 app_state: Arc<AppState>,
3400 cx: &mut ViewContext<Workspace>,
3401) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
3402 let restored_items = serialized_workspace.map(|serialized_workspace| {
3403 Workspace::load_workspace(
3404 serialized_workspace,
3405 project_paths_to_open
3406 .iter()
3407 .map(|(_, project_path)| project_path)
3408 .cloned()
3409 .collect(),
3410 cx,
3411 )
3412 });
3413
3414 cx.spawn(|workspace, mut cx| async move {
3415 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3416
3417 if let Some(restored_items) = restored_items {
3418 let restored_items = restored_items.await?;
3419
3420 let restored_project_paths = restored_items
3421 .iter()
3422 .filter_map(|item| {
3423 cx.update(|cx| item.as_ref()?.project_path(cx))
3424 .ok()
3425 .flatten()
3426 })
3427 .collect::<HashSet<_>>();
3428
3429 for restored_item in restored_items {
3430 opened_items.push(restored_item.map(Ok));
3431 }
3432
3433 project_paths_to_open
3434 .iter_mut()
3435 .for_each(|(_, project_path)| {
3436 if let Some(project_path_to_open) = project_path {
3437 if restored_project_paths.contains(project_path_to_open) {
3438 *project_path = None;
3439 }
3440 }
3441 });
3442 } else {
3443 for _ in 0..project_paths_to_open.len() {
3444 opened_items.push(None);
3445 }
3446 }
3447 assert!(opened_items.len() == project_paths_to_open.len());
3448
3449 let tasks =
3450 project_paths_to_open
3451 .into_iter()
3452 .enumerate()
3453 .map(|(i, (abs_path, project_path))| {
3454 let workspace = workspace.clone();
3455 cx.spawn(|mut cx| {
3456 let fs = app_state.fs.clone();
3457 async move {
3458 let file_project_path = project_path?;
3459 if fs.is_file(&abs_path).await {
3460 Some((
3461 i,
3462 workspace
3463 .update(&mut cx, |workspace, cx| {
3464 workspace.open_path(file_project_path, None, true, cx)
3465 })
3466 .log_err()?
3467 .await,
3468 ))
3469 } else {
3470 None
3471 }
3472 }
3473 })
3474 });
3475
3476 let tasks = tasks.collect::<Vec<_>>();
3477
3478 let tasks = futures::future::join_all(tasks.into_iter());
3479 for maybe_opened_path in tasks.await.into_iter() {
3480 if let Some((i, path_open_result)) = maybe_opened_path {
3481 opened_items[i] = Some(path_open_result);
3482 }
3483 }
3484
3485 Ok(opened_items)
3486 })
3487}
3488
3489fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
3490 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3491
3492 workspace
3493 .update(cx, |workspace, cx| {
3494 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3495 workspace.show_notification_once(0, cx, |cx| {
3496 cx.new_view(|_| {
3497 MessageNotification::new("Failed to load the database file.")
3498 .with_click_message("Click to let us know about this error")
3499 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
3500 })
3501 });
3502 }
3503 })
3504 .log_err();
3505}
3506
3507impl FocusableView for Workspace {
3508 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3509 self.active_pane.focus_handle(cx)
3510 }
3511}
3512
3513#[derive(Clone, Render)]
3514struct DraggedDock(DockPosition);
3515
3516impl Render for Workspace {
3517 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3518 let mut context = KeyContext::default();
3519 context.add("Workspace");
3520
3521 let (ui_font, ui_font_size) = {
3522 let theme_settings = ThemeSettings::get_global(cx);
3523 (
3524 theme_settings.ui_font.family.clone(),
3525 theme_settings.ui_font_size.clone(),
3526 )
3527 };
3528
3529 let theme = cx.theme().clone();
3530 let colors = theme.colors();
3531 cx.set_rem_size(ui_font_size);
3532
3533 self.actions(div(), cx)
3534 .key_context(context)
3535 .relative()
3536 .size_full()
3537 .flex()
3538 .flex_col()
3539 .font(ui_font)
3540 .gap_0()
3541 .justify_start()
3542 .items_start()
3543 .text_color(colors.text)
3544 .bg(colors.background)
3545 .border()
3546 .border_color(colors.border)
3547 .children(self.titlebar_item.clone())
3548 .child(
3549 div()
3550 .id("workspace")
3551 .relative()
3552 .flex_1()
3553 .w_full()
3554 .flex()
3555 .flex_col()
3556 .overflow_hidden()
3557 .border_t()
3558 .border_b()
3559 .border_color(colors.border)
3560 .child(
3561 canvas({
3562 let this = cx.view().clone();
3563 move |bounds, cx| {
3564 this.update(cx, |this, _cx| {
3565 this.bounds = *bounds;
3566 })
3567 }
3568 })
3569 .absolute()
3570 .size_full(),
3571 )
3572 .on_drag_move(
3573 cx.listener(|workspace, e: &DragMoveEvent<DraggedDock>, cx| {
3574 match e.drag(cx).0 {
3575 DockPosition::Left => {
3576 let size = workspace.bounds.left() + e.event.position.x;
3577 workspace.left_dock.update(cx, |left_dock, cx| {
3578 left_dock.resize_active_panel(Some(size), cx);
3579 });
3580 }
3581 DockPosition::Right => {
3582 let size = workspace.bounds.right() - e.event.position.x;
3583 workspace.right_dock.update(cx, |right_dock, cx| {
3584 right_dock.resize_active_panel(Some(size), cx);
3585 });
3586 }
3587 DockPosition::Bottom => {
3588 let size = workspace.bounds.bottom() - e.event.position.y;
3589 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
3590 bottom_dock.resize_active_panel(Some(size), cx);
3591 });
3592 }
3593 }
3594 }),
3595 )
3596 .child(self.modal_layer.clone())
3597 .child(
3598 div()
3599 .flex()
3600 .flex_row()
3601 .h_full()
3602 // Left Dock
3603 .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
3604 || {
3605 div()
3606 .flex()
3607 .flex_none()
3608 .overflow_hidden()
3609 .child(self.left_dock.clone())
3610 },
3611 ))
3612 // Panes
3613 .child(
3614 div()
3615 .flex()
3616 .flex_col()
3617 .flex_1()
3618 .overflow_hidden()
3619 .child(self.center.render(
3620 &self.project,
3621 &self.follower_states,
3622 self.active_call(),
3623 &self.active_pane,
3624 self.zoomed.as_ref(),
3625 &self.app_state,
3626 cx,
3627 ))
3628 .children(
3629 self.zoomed_position
3630 .ne(&Some(DockPosition::Bottom))
3631 .then(|| self.bottom_dock.clone()),
3632 ),
3633 )
3634 // Right Dock
3635 .children(self.zoomed_position.ne(&Some(DockPosition::Right)).then(
3636 || {
3637 div()
3638 .flex()
3639 .flex_none()
3640 .overflow_hidden()
3641 .child(self.right_dock.clone())
3642 },
3643 )),
3644 )
3645 .children(self.render_notifications(cx))
3646 .children(self.zoomed.as_ref().and_then(|view| {
3647 let zoomed_view = view.upgrade()?;
3648 let div = div()
3649 .z_index(1)
3650 .absolute()
3651 .overflow_hidden()
3652 .border_color(colors.border)
3653 .bg(colors.background)
3654 .child(zoomed_view)
3655 .inset_0()
3656 .shadow_lg();
3657
3658 Some(match self.zoomed_position {
3659 Some(DockPosition::Left) => div.right_2().border_r(),
3660 Some(DockPosition::Right) => div.left_2().border_l(),
3661 Some(DockPosition::Bottom) => div.top_2().border_t(),
3662 None => div.top_2().bottom_2().left_2().right_2().border(),
3663 })
3664 })),
3665 )
3666 .child(self.status_bar.clone())
3667 .children(if self.project.read(cx).is_disconnected() {
3668 Some(DisconnectedOverlay)
3669 } else {
3670 None
3671 })
3672 }
3673}
3674
3675impl WorkspaceStore {
3676 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
3677 Self {
3678 workspaces: Default::default(),
3679 followers: Default::default(),
3680 _subscriptions: vec![
3681 client.add_request_handler(cx.weak_model(), Self::handle_follow),
3682 client.add_message_handler(cx.weak_model(), Self::handle_unfollow),
3683 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
3684 ],
3685 client,
3686 }
3687 }
3688
3689 pub fn update_followers(
3690 &self,
3691 project_id: Option<u64>,
3692 update: proto::update_followers::Variant,
3693 cx: &AppContext,
3694 ) -> Option<()> {
3695 let active_call = ActiveCall::try_global(cx)?;
3696 let room_id = active_call.read(cx).room()?.read(cx).id();
3697 let follower_ids: Vec<_> = self
3698 .followers
3699 .iter()
3700 .filter_map(|follower| {
3701 if follower.project_id == project_id || project_id.is_none() {
3702 Some(follower.peer_id.into())
3703 } else {
3704 None
3705 }
3706 })
3707 .collect();
3708 if follower_ids.is_empty() {
3709 return None;
3710 }
3711 self.client
3712 .send(proto::UpdateFollowers {
3713 room_id,
3714 project_id,
3715 follower_ids,
3716 variant: Some(update),
3717 })
3718 .log_err()
3719 }
3720
3721 pub async fn handle_follow(
3722 this: Model<Self>,
3723 envelope: TypedEnvelope<proto::Follow>,
3724 _: Arc<Client>,
3725 mut cx: AsyncAppContext,
3726 ) -> Result<proto::FollowResponse> {
3727 this.update(&mut cx, |this, cx| {
3728 let follower = Follower {
3729 project_id: envelope.payload.project_id,
3730 peer_id: envelope.original_sender_id()?,
3731 };
3732 let active_project = ActiveCall::global(cx).read(cx).location().cloned();
3733
3734 let mut response = proto::FollowResponse::default();
3735 this.workspaces.retain(|workspace| {
3736 workspace
3737 .update(cx, |workspace, cx| {
3738 let handler_response = workspace.handle_follow(follower.project_id, cx);
3739 if response.views.is_empty() {
3740 response.views = handler_response.views;
3741 } else {
3742 response.views.extend_from_slice(&handler_response.views);
3743 }
3744
3745 if let Some(active_view_id) = handler_response.active_view_id.clone() {
3746 if response.active_view_id.is_none()
3747 || Some(workspace.project.downgrade()) == active_project
3748 {
3749 response.active_view_id = Some(active_view_id);
3750 }
3751 }
3752 })
3753 .is_ok()
3754 });
3755
3756 if let Err(ix) = this.followers.binary_search(&follower) {
3757 this.followers.insert(ix, follower);
3758 }
3759
3760 Ok(response)
3761 })?
3762 }
3763
3764 async fn handle_unfollow(
3765 model: Model<Self>,
3766 envelope: TypedEnvelope<proto::Unfollow>,
3767 _: Arc<Client>,
3768 mut cx: AsyncAppContext,
3769 ) -> Result<()> {
3770 model.update(&mut cx, |this, _| {
3771 let follower = Follower {
3772 project_id: envelope.payload.project_id,
3773 peer_id: envelope.original_sender_id()?,
3774 };
3775 if let Ok(ix) = this.followers.binary_search(&follower) {
3776 this.followers.remove(ix);
3777 }
3778 Ok(())
3779 })?
3780 }
3781
3782 async fn handle_update_followers(
3783 this: Model<Self>,
3784 envelope: TypedEnvelope<proto::UpdateFollowers>,
3785 _: Arc<Client>,
3786 mut cx: AsyncAppContext,
3787 ) -> Result<()> {
3788 let leader_id = envelope.original_sender_id()?;
3789 let update = envelope.payload;
3790
3791 this.update(&mut cx, |this, cx| {
3792 this.workspaces.retain(|workspace| {
3793 workspace
3794 .update(cx, |workspace, cx| {
3795 let project_id = workspace.project.read(cx).remote_id();
3796 if update.project_id != project_id && update.project_id.is_some() {
3797 return;
3798 }
3799 workspace.handle_update_followers(leader_id, update.clone(), cx);
3800 })
3801 .is_ok()
3802 });
3803 Ok(())
3804 })?
3805 }
3806}
3807
3808impl ViewId {
3809 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
3810 Ok(Self {
3811 creator: message
3812 .creator
3813 .ok_or_else(|| anyhow!("creator is missing"))?,
3814 id: message.id,
3815 })
3816 }
3817
3818 pub(crate) fn to_proto(&self) -> proto::ViewId {
3819 proto::ViewId {
3820 creator: Some(self.creator),
3821 id: self.id,
3822 }
3823 }
3824}
3825
3826pub trait WorkspaceHandle {
3827 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
3828}
3829
3830impl WorkspaceHandle for View<Workspace> {
3831 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
3832 self.read(cx)
3833 .worktrees(cx)
3834 .flat_map(|worktree| {
3835 let worktree_id = worktree.read(cx).id();
3836 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
3837 worktree_id,
3838 path: f.path.clone(),
3839 })
3840 })
3841 .collect::<Vec<_>>()
3842 }
3843}
3844
3845impl std::fmt::Debug for OpenPaths {
3846 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3847 f.debug_struct("OpenPaths")
3848 .field("paths", &self.paths)
3849 .finish()
3850 }
3851}
3852
3853pub fn activate_workspace_for_project(
3854 cx: &mut AppContext,
3855 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
3856) -> Option<WindowHandle<Workspace>> {
3857 for window in cx.windows() {
3858 let Some(workspace) = window.downcast::<Workspace>() else {
3859 continue;
3860 };
3861
3862 let predicate = workspace
3863 .update(cx, |workspace, cx| {
3864 let project = workspace.project.read(cx);
3865 if predicate(project, cx) {
3866 cx.activate_window();
3867 true
3868 } else {
3869 false
3870 }
3871 })
3872 .log_err()
3873 .unwrap_or(false);
3874
3875 if predicate {
3876 return Some(workspace);
3877 }
3878 }
3879
3880 None
3881}
3882
3883pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
3884 DB.last_workspace().await.log_err().flatten()
3885}
3886
3887async fn join_channel_internal(
3888 channel_id: u64,
3889 app_state: &Arc<AppState>,
3890 requesting_window: Option<WindowHandle<Workspace>>,
3891 active_call: &Model<ActiveCall>,
3892 cx: &mut AsyncAppContext,
3893) -> Result<bool> {
3894 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
3895 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
3896 return (false, None);
3897 };
3898
3899 let already_in_channel = room.channel_id() == Some(channel_id);
3900 let should_prompt = room.is_sharing_project()
3901 && room.remote_participants().len() > 0
3902 && !already_in_channel;
3903 let open_room = if already_in_channel {
3904 active_call.room().cloned()
3905 } else {
3906 None
3907 };
3908 (should_prompt, open_room)
3909 })?;
3910
3911 if let Some(room) = open_room {
3912 let task = room.update(cx, |room, cx| {
3913 if let Some((project, host)) = room.most_active_project(cx) {
3914 return Some(join_remote_project(project, host, app_state.clone(), cx));
3915 }
3916
3917 None
3918 })?;
3919 if let Some(task) = task {
3920 task.await?;
3921 }
3922 return anyhow::Ok(true);
3923 }
3924
3925 if should_prompt {
3926 if let Some(workspace) = requesting_window {
3927 let answer = workspace
3928 .update(cx, |_, cx| {
3929 cx.prompt(
3930 PromptLevel::Warning,
3931 "Do you want to switch channels?",
3932 Some("Leaving this call will unshare your current project."),
3933 &["Yes, Join Channel", "Cancel"],
3934 )
3935 })?
3936 .await;
3937
3938 if answer == Ok(1) {
3939 return Ok(false);
3940 }
3941 } else {
3942 return Ok(false); // unreachable!() hopefully
3943 }
3944 }
3945
3946 let client = cx.update(|cx| active_call.read(cx).client())?;
3947
3948 let mut client_status = client.status();
3949
3950 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
3951 'outer: loop {
3952 let Some(status) = client_status.recv().await else {
3953 return Err(anyhow!("error connecting"));
3954 };
3955
3956 match status {
3957 Status::Connecting
3958 | Status::Authenticating
3959 | Status::Reconnecting
3960 | Status::Reauthenticating => continue,
3961 Status::Connected { .. } => break 'outer,
3962 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
3963 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
3964 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
3965 return Err(ErrorCode::Disconnected.into())
3966 }
3967 }
3968 }
3969
3970 let room = active_call
3971 .update(cx, |active_call, cx| {
3972 active_call.join_channel(channel_id, cx)
3973 })?
3974 .await?;
3975
3976 let Some(room) = room else {
3977 return anyhow::Ok(true);
3978 };
3979
3980 room.update(cx, |room, _| room.room_update_completed())?
3981 .await;
3982
3983 let task = room.update(cx, |room, cx| {
3984 if let Some((project, host)) = room.most_active_project(cx) {
3985 return Some(join_remote_project(project, host, app_state.clone(), cx));
3986 }
3987
3988 None
3989 })?;
3990 if let Some(task) = task {
3991 task.await?;
3992 return anyhow::Ok(true);
3993 }
3994 anyhow::Ok(false)
3995}
3996
3997pub fn join_channel(
3998 channel_id: u64,
3999 app_state: Arc<AppState>,
4000 requesting_window: Option<WindowHandle<Workspace>>,
4001 cx: &mut AppContext,
4002) -> Task<Result<()>> {
4003 let active_call = ActiveCall::global(cx);
4004 cx.spawn(|mut cx| async move {
4005 let result = join_channel_internal(
4006 channel_id,
4007 &app_state,
4008 requesting_window,
4009 &active_call,
4010 &mut cx,
4011 )
4012 .await;
4013
4014 // join channel succeeded, and opened a window
4015 if matches!(result, Ok(true)) {
4016 return anyhow::Ok(());
4017 }
4018
4019 // find an existing workspace to focus and show call controls
4020 let mut active_window =
4021 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
4022 if active_window.is_none() {
4023 // no open workspaces, make one to show the error in (blergh)
4024 let (window_handle, _) = cx
4025 .update(|cx| {
4026 Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)
4027 })?
4028 .await?;
4029
4030 active_window = Some(window_handle);
4031 }
4032
4033 if let Err(err) = result {
4034 log::error!("failed to join channel: {}", err);
4035 if let Some(active_window) = active_window {
4036 active_window
4037 .update(&mut cx, |_, cx| {
4038 let detail: SharedString = match err.error_code() {
4039 ErrorCode::SignedOut => {
4040 "Please sign in to continue.".into()
4041 },
4042 ErrorCode::UpgradeRequired => {
4043 "Your are running an unsupported version of Zed. Please update to continue.".into()
4044 },
4045 ErrorCode::NoSuchChannel => {
4046 "No matching channel was found. Please check the link and try again.".into()
4047 },
4048 ErrorCode::Forbidden => {
4049 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
4050 },
4051 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
4052 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(),
4053 _ => format!("{}\n\nPlease try again.", err).into(),
4054 };
4055 cx.prompt(
4056 PromptLevel::Critical,
4057 "Failed to join channel",
4058 Some(&detail),
4059 &["Ok"],
4060 )
4061 })?
4062 .await
4063 .ok();
4064 }
4065 }
4066
4067 // return ok, we showed the error to the user.
4068 return anyhow::Ok(());
4069 })
4070}
4071
4072pub async fn get_any_active_workspace(
4073 app_state: Arc<AppState>,
4074 mut cx: AsyncAppContext,
4075) -> anyhow::Result<WindowHandle<Workspace>> {
4076 // find an existing workspace to focus and show call controls
4077 let active_window = activate_any_workspace_window(&mut cx);
4078 if active_window.is_none() {
4079 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
4080 .await?;
4081 }
4082 activate_any_workspace_window(&mut cx).context("could not open zed")
4083}
4084
4085fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
4086 cx.update(|cx| {
4087 for window in cx.windows() {
4088 if let Some(workspace_window) = window.downcast::<Workspace>() {
4089 workspace_window
4090 .update(cx, |_, cx| cx.activate_window())
4091 .ok();
4092 return Some(workspace_window);
4093 }
4094 }
4095 None
4096 })
4097 .ok()
4098 .flatten()
4099}
4100
4101#[allow(clippy::type_complexity)]
4102pub fn open_paths(
4103 abs_paths: &[PathBuf],
4104 app_state: &Arc<AppState>,
4105 requesting_window: Option<WindowHandle<Workspace>>,
4106 cx: &mut AppContext,
4107) -> Task<
4108 anyhow::Result<(
4109 WindowHandle<Workspace>,
4110 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4111 )>,
4112> {
4113 let app_state = app_state.clone();
4114 let abs_paths = abs_paths.to_vec();
4115 // Open paths in existing workspace if possible
4116 let existing = activate_workspace_for_project(cx, {
4117 let abs_paths = abs_paths.clone();
4118 move |project, cx| project.contains_paths(&abs_paths, cx)
4119 });
4120 cx.spawn(move |mut cx| async move {
4121 if let Some(existing) = existing {
4122 Ok((
4123 existing.clone(),
4124 existing
4125 .update(&mut cx, |workspace, cx| {
4126 workspace.open_paths(abs_paths, OpenVisible::All, None, cx)
4127 })?
4128 .await,
4129 ))
4130 } else {
4131 cx.update(move |cx| {
4132 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
4133 })?
4134 .await
4135 }
4136 })
4137}
4138
4139pub fn open_new(
4140 app_state: &Arc<AppState>,
4141 cx: &mut AppContext,
4142 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
4143) -> Task<()> {
4144 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
4145 cx.spawn(|mut cx| async move {
4146 if let Some((workspace, opened_paths)) = task.await.log_err() {
4147 workspace
4148 .update(&mut cx, |workspace, cx| {
4149 if opened_paths.is_empty() {
4150 init(workspace, cx)
4151 }
4152 })
4153 .log_err();
4154 }
4155 })
4156}
4157
4158pub fn create_and_open_local_file(
4159 path: &'static Path,
4160 cx: &mut ViewContext<Workspace>,
4161 default_content: impl 'static + Send + FnOnce() -> Rope,
4162) -> Task<Result<Box<dyn ItemHandle>>> {
4163 cx.spawn(|workspace, mut cx| async move {
4164 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
4165 if !fs.is_file(path).await {
4166 fs.create_file(path, Default::default()).await?;
4167 fs.save(path, &default_content(), Default::default())
4168 .await?;
4169 }
4170
4171 let mut items = workspace
4172 .update(&mut cx, |workspace, cx| {
4173 workspace.with_local_workspace(cx, |workspace, cx| {
4174 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
4175 })
4176 })?
4177 .await?
4178 .await;
4179
4180 let item = items.pop().flatten();
4181 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4182 })
4183}
4184
4185pub fn join_remote_project(
4186 project_id: u64,
4187 follow_user_id: u64,
4188 app_state: Arc<AppState>,
4189 cx: &mut AppContext,
4190) -> Task<Result<()>> {
4191 let windows = cx.windows();
4192 cx.spawn(|mut cx| async move {
4193 let existing_workspace = windows.into_iter().find_map(|window| {
4194 window.downcast::<Workspace>().and_then(|window| {
4195 window
4196 .update(&mut cx, |workspace, cx| {
4197 if workspace.project().read(cx).remote_id() == Some(project_id) {
4198 Some(window)
4199 } else {
4200 None
4201 }
4202 })
4203 .unwrap_or(None)
4204 })
4205 });
4206
4207 let workspace = if let Some(existing_workspace) = existing_workspace {
4208 existing_workspace
4209 } else {
4210 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
4211 let room = active_call
4212 .read_with(&cx, |call, _| call.room().cloned())?
4213 .ok_or_else(|| anyhow!("not in a call"))?;
4214 let project = room
4215 .update(&mut cx, |room, cx| {
4216 room.join_project(
4217 project_id,
4218 app_state.languages.clone(),
4219 app_state.fs.clone(),
4220 cx,
4221 )
4222 })?
4223 .await?;
4224
4225 let window_bounds_override = window_bounds_env_override(&cx);
4226 cx.update(|cx| {
4227 let options = (app_state.build_window_options)(window_bounds_override, None, cx);
4228 cx.open_window(options, |cx| {
4229 cx.new_view(|cx| Workspace::new(0, project, app_state.clone(), cx))
4230 })
4231 })?
4232 };
4233
4234 workspace.update(&mut cx, |workspace, cx| {
4235 cx.activate(true);
4236 cx.activate_window();
4237
4238 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4239 let follow_peer_id = room
4240 .read(cx)
4241 .remote_participants()
4242 .iter()
4243 .find(|(_, participant)| participant.user.id == follow_user_id)
4244 .map(|(_, p)| p.peer_id)
4245 .or_else(|| {
4246 // If we couldn't follow the given user, follow the host instead.
4247 let collaborator = workspace
4248 .project()
4249 .read(cx)
4250 .collaborators()
4251 .values()
4252 .find(|collaborator| collaborator.replica_id == 0)?;
4253 Some(collaborator.peer_id)
4254 });
4255
4256 if let Some(follow_peer_id) = follow_peer_id {
4257 workspace.follow(follow_peer_id, cx);
4258 }
4259 }
4260 })?;
4261
4262 anyhow::Ok(())
4263 })
4264}
4265
4266pub fn restart(_: &Restart, cx: &mut AppContext) {
4267 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
4268 let mut workspace_windows = cx
4269 .windows()
4270 .into_iter()
4271 .filter_map(|window| window.downcast::<Workspace>())
4272 .collect::<Vec<_>>();
4273
4274 // If multiple windows have unsaved changes, and need a save prompt,
4275 // prompt in the active window before switching to a different window.
4276 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
4277
4278 let mut prompt = None;
4279 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4280 prompt = window
4281 .update(cx, |_, cx| {
4282 cx.prompt(
4283 PromptLevel::Info,
4284 "Are you sure you want to restart?",
4285 None,
4286 &["Restart", "Cancel"],
4287 )
4288 })
4289 .ok();
4290 }
4291
4292 cx.spawn(|mut cx| async move {
4293 if let Some(prompt) = prompt {
4294 let answer = prompt.await?;
4295 if answer != 0 {
4296 return Ok(());
4297 }
4298 }
4299
4300 // If the user cancels any save prompt, then keep the app open.
4301 for window in workspace_windows {
4302 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
4303 workspace.prepare_to_close(true, cx)
4304 }) {
4305 if !should_close.await? {
4306 return Ok(());
4307 }
4308 }
4309 }
4310
4311 cx.update(|cx| cx.restart())
4312 })
4313 .detach_and_log_err(cx);
4314}
4315
4316fn parse_pixel_position_env_var(value: &str) -> Option<Point<GlobalPixels>> {
4317 let mut parts = value.split(',');
4318 let x: usize = parts.next()?.parse().ok()?;
4319 let y: usize = parts.next()?.parse().ok()?;
4320 Some(point((x as f64).into(), (y as f64).into()))
4321}
4322
4323fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
4324 let mut parts = value.split(',');
4325 let width: usize = parts.next()?.parse().ok()?;
4326 let height: usize = parts.next()?.parse().ok()?;
4327 Some(size((width as f64).into(), (height as f64).into()))
4328}
4329
4330pub fn titlebar_height(cx: &mut WindowContext) -> Pixels {
4331 (1.75 * cx.rem_size()).max(px(32.))
4332}
4333
4334struct DisconnectedOverlay;
4335
4336impl Element for DisconnectedOverlay {
4337 type State = AnyElement;
4338
4339 fn request_layout(
4340 &mut self,
4341 _: Option<Self::State>,
4342 cx: &mut ElementContext,
4343 ) -> (LayoutId, Self::State) {
4344 let mut background = cx.theme().colors().elevated_surface_background;
4345 background.fade_out(0.2);
4346 let mut overlay = div()
4347 .bg(background)
4348 .absolute()
4349 .left_0()
4350 .top(titlebar_height(cx))
4351 .size_full()
4352 .flex()
4353 .items_center()
4354 .justify_center()
4355 .capture_any_mouse_down(|_, cx| cx.stop_propagation())
4356 .capture_any_mouse_up(|_, cx| cx.stop_propagation())
4357 .child(Label::new(
4358 "Your connection to the remote project has been lost.",
4359 ))
4360 .into_any();
4361 (overlay.request_layout(cx), overlay)
4362 }
4363
4364 fn paint(
4365 &mut self,
4366 bounds: Bounds<Pixels>,
4367 overlay: &mut Self::State,
4368 cx: &mut ElementContext,
4369 ) {
4370 cx.with_z_index(u16::MAX, |cx| {
4371 cx.add_opaque_layer(bounds);
4372 overlay.paint(cx);
4373 })
4374 }
4375}
4376
4377impl IntoElement for DisconnectedOverlay {
4378 type Element = Self;
4379
4380 fn element_id(&self) -> Option<ui::prelude::ElementId> {
4381 None
4382 }
4383
4384 fn into_element(self) -> Self::Element {
4385 self
4386 }
4387}
4388
4389#[cfg(test)]
4390mod tests {
4391 use std::{cell::RefCell, rc::Rc};
4392
4393 use super::*;
4394 use crate::{
4395 dock::{test::TestPanel, PanelEvent},
4396 item::{
4397 test::{TestItem, TestProjectItem},
4398 ItemEvent,
4399 },
4400 };
4401 use fs::FakeFs;
4402 use gpui::{px, DismissEvent, TestAppContext, VisualTestContext};
4403 use project::{Project, ProjectEntryId};
4404 use serde_json::json;
4405 use settings::SettingsStore;
4406
4407 #[gpui::test]
4408 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4409 init_test(cx);
4410
4411 let fs = FakeFs::new(cx.executor());
4412 let project = Project::test(fs, [], cx).await;
4413 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4414
4415 // Adding an item with no ambiguity renders the tab without detail.
4416 let item1 = cx.new_view(|cx| {
4417 let mut item = TestItem::new(cx);
4418 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4419 item
4420 });
4421 workspace.update(cx, |workspace, cx| {
4422 workspace.add_item(Box::new(item1.clone()), cx);
4423 });
4424 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
4425
4426 // Adding an item that creates ambiguity increases the level of detail on
4427 // both tabs.
4428 let item2 = cx.new_view(|cx| {
4429 let mut item = TestItem::new(cx);
4430 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4431 item
4432 });
4433 workspace.update(cx, |workspace, cx| {
4434 workspace.add_item(Box::new(item2.clone()), cx);
4435 });
4436 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4437 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4438
4439 // Adding an item that creates ambiguity increases the level of detail only
4440 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4441 // we stop at the highest detail available.
4442 let item3 = cx.new_view(|cx| {
4443 let mut item = TestItem::new(cx);
4444 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4445 item
4446 });
4447 workspace.update(cx, |workspace, cx| {
4448 workspace.add_item(Box::new(item3.clone()), cx);
4449 });
4450 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4451 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4452 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4453 }
4454
4455 #[gpui::test]
4456 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4457 init_test(cx);
4458
4459 let fs = FakeFs::new(cx.executor());
4460 fs.insert_tree(
4461 "/root1",
4462 json!({
4463 "one.txt": "",
4464 "two.txt": "",
4465 }),
4466 )
4467 .await;
4468 fs.insert_tree(
4469 "/root2",
4470 json!({
4471 "three.txt": "",
4472 }),
4473 )
4474 .await;
4475
4476 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4477 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4478 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4479 let worktree_id = project.update(cx, |project, cx| {
4480 project.worktrees().next().unwrap().read(cx).id()
4481 });
4482
4483 let item1 = cx.new_view(|cx| {
4484 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4485 });
4486 let item2 = cx.new_view(|cx| {
4487 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4488 });
4489
4490 // Add an item to an empty pane
4491 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4492 project.update(cx, |project, cx| {
4493 assert_eq!(
4494 project.active_entry(),
4495 project
4496 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4497 .map(|e| e.id)
4498 );
4499 });
4500 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
4501
4502 // Add a second item to a non-empty pane
4503 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4504 assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
4505 project.update(cx, |project, cx| {
4506 assert_eq!(
4507 project.active_entry(),
4508 project
4509 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4510 .map(|e| e.id)
4511 );
4512 });
4513
4514 // Close the active item
4515 pane.update(cx, |pane, cx| {
4516 pane.close_active_item(&Default::default(), cx).unwrap()
4517 })
4518 .await
4519 .unwrap();
4520 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
4521 project.update(cx, |project, cx| {
4522 assert_eq!(
4523 project.active_entry(),
4524 project
4525 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4526 .map(|e| e.id)
4527 );
4528 });
4529
4530 // Add a project folder
4531 project
4532 .update(cx, |project, cx| {
4533 project.find_or_create_local_worktree("/root2", true, cx)
4534 })
4535 .await
4536 .unwrap();
4537 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
4538
4539 // Remove a project folder
4540 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4541 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
4542 }
4543
4544 #[gpui::test]
4545 async fn test_close_window(cx: &mut TestAppContext) {
4546 init_test(cx);
4547
4548 let fs = FakeFs::new(cx.executor());
4549 fs.insert_tree("/root", json!({ "one": "" })).await;
4550
4551 let project = Project::test(fs, ["root".as_ref()], cx).await;
4552 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4553
4554 // When there are no dirty items, there's nothing to do.
4555 let item1 = cx.new_view(|cx| TestItem::new(cx));
4556 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4557 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4558 assert!(task.await.unwrap());
4559
4560 // When there are dirty untitled items, prompt to save each one. If the user
4561 // cancels any prompt, then abort.
4562 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
4563 let item3 = cx.new_view(|cx| {
4564 TestItem::new(cx)
4565 .with_dirty(true)
4566 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4567 });
4568 workspace.update(cx, |w, cx| {
4569 w.add_item(Box::new(item2.clone()), cx);
4570 w.add_item(Box::new(item3.clone()), cx);
4571 });
4572 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4573 cx.executor().run_until_parked();
4574 cx.simulate_prompt_answer(2); // cancel save all
4575 cx.executor().run_until_parked();
4576 cx.simulate_prompt_answer(2); // cancel save all
4577 cx.executor().run_until_parked();
4578 assert!(!cx.has_pending_prompt());
4579 assert!(!task.await.unwrap());
4580 }
4581
4582 #[gpui::test]
4583 async fn test_close_pane_items(cx: &mut TestAppContext) {
4584 init_test(cx);
4585
4586 let fs = FakeFs::new(cx.executor());
4587
4588 let project = Project::test(fs, None, cx).await;
4589 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4590
4591 let item1 = cx.new_view(|cx| {
4592 TestItem::new(cx)
4593 .with_dirty(true)
4594 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4595 });
4596 let item2 = cx.new_view(|cx| {
4597 TestItem::new(cx)
4598 .with_dirty(true)
4599 .with_conflict(true)
4600 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4601 });
4602 let item3 = cx.new_view(|cx| {
4603 TestItem::new(cx)
4604 .with_dirty(true)
4605 .with_conflict(true)
4606 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4607 });
4608 let item4 = cx.new_view(|cx| {
4609 TestItem::new(cx)
4610 .with_dirty(true)
4611 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4612 });
4613 let pane = workspace.update(cx, |workspace, cx| {
4614 workspace.add_item(Box::new(item1.clone()), cx);
4615 workspace.add_item(Box::new(item2.clone()), cx);
4616 workspace.add_item(Box::new(item3.clone()), cx);
4617 workspace.add_item(Box::new(item4.clone()), cx);
4618 workspace.active_pane().clone()
4619 });
4620
4621 let close_items = pane.update(cx, |pane, cx| {
4622 pane.activate_item(1, true, true, cx);
4623 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4624 let item1_id = item1.item_id();
4625 let item3_id = item3.item_id();
4626 let item4_id = item4.item_id();
4627 pane.close_items(cx, SaveIntent::Close, move |id| {
4628 [item1_id, item3_id, item4_id].contains(&id)
4629 })
4630 });
4631 cx.executor().run_until_parked();
4632
4633 assert!(cx.has_pending_prompt());
4634 // Ignore "Save all" prompt
4635 cx.simulate_prompt_answer(2);
4636 cx.executor().run_until_parked();
4637 // There's a prompt to save item 1.
4638 pane.update(cx, |pane, _| {
4639 assert_eq!(pane.items_len(), 4);
4640 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
4641 });
4642 // Confirm saving item 1.
4643 cx.simulate_prompt_answer(0);
4644 cx.executor().run_until_parked();
4645
4646 // Item 1 is saved. There's a prompt to save item 3.
4647 pane.update(cx, |pane, cx| {
4648 assert_eq!(item1.read(cx).save_count, 1);
4649 assert_eq!(item1.read(cx).save_as_count, 0);
4650 assert_eq!(item1.read(cx).reload_count, 0);
4651 assert_eq!(pane.items_len(), 3);
4652 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
4653 });
4654 assert!(cx.has_pending_prompt());
4655
4656 // Cancel saving item 3.
4657 cx.simulate_prompt_answer(1);
4658 cx.executor().run_until_parked();
4659
4660 // Item 3 is reloaded. There's a prompt to save item 4.
4661 pane.update(cx, |pane, cx| {
4662 assert_eq!(item3.read(cx).save_count, 0);
4663 assert_eq!(item3.read(cx).save_as_count, 0);
4664 assert_eq!(item3.read(cx).reload_count, 1);
4665 assert_eq!(pane.items_len(), 2);
4666 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
4667 });
4668 assert!(cx.has_pending_prompt());
4669
4670 // Confirm saving item 4.
4671 cx.simulate_prompt_answer(0);
4672 cx.executor().run_until_parked();
4673
4674 // There's a prompt for a path for item 4.
4675 cx.simulate_new_path_selection(|_| Some(Default::default()));
4676 close_items.await.unwrap();
4677
4678 // The requested items are closed.
4679 pane.update(cx, |pane, cx| {
4680 assert_eq!(item4.read(cx).save_count, 0);
4681 assert_eq!(item4.read(cx).save_as_count, 1);
4682 assert_eq!(item4.read(cx).reload_count, 0);
4683 assert_eq!(pane.items_len(), 1);
4684 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4685 });
4686 }
4687
4688 #[gpui::test]
4689 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4690 init_test(cx);
4691
4692 let fs = FakeFs::new(cx.executor());
4693 let project = Project::test(fs, [], cx).await;
4694 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4695
4696 // Create several workspace items with single project entries, and two
4697 // workspace items with multiple project entries.
4698 let single_entry_items = (0..=4)
4699 .map(|project_entry_id| {
4700 cx.new_view(|cx| {
4701 TestItem::new(cx)
4702 .with_dirty(true)
4703 .with_project_items(&[TestProjectItem::new(
4704 project_entry_id,
4705 &format!("{project_entry_id}.txt"),
4706 cx,
4707 )])
4708 })
4709 })
4710 .collect::<Vec<_>>();
4711 let item_2_3 = cx.new_view(|cx| {
4712 TestItem::new(cx)
4713 .with_dirty(true)
4714 .with_singleton(false)
4715 .with_project_items(&[
4716 single_entry_items[2].read(cx).project_items[0].clone(),
4717 single_entry_items[3].read(cx).project_items[0].clone(),
4718 ])
4719 });
4720 let item_3_4 = cx.new_view(|cx| {
4721 TestItem::new(cx)
4722 .with_dirty(true)
4723 .with_singleton(false)
4724 .with_project_items(&[
4725 single_entry_items[3].read(cx).project_items[0].clone(),
4726 single_entry_items[4].read(cx).project_items[0].clone(),
4727 ])
4728 });
4729
4730 // Create two panes that contain the following project entries:
4731 // left pane:
4732 // multi-entry items: (2, 3)
4733 // single-entry items: 0, 1, 2, 3, 4
4734 // right pane:
4735 // single-entry items: 1
4736 // multi-entry items: (3, 4)
4737 let left_pane = workspace.update(cx, |workspace, cx| {
4738 let left_pane = workspace.active_pane().clone();
4739 workspace.add_item(Box::new(item_2_3.clone()), cx);
4740 for item in single_entry_items {
4741 workspace.add_item(Box::new(item), cx);
4742 }
4743 left_pane.update(cx, |pane, cx| {
4744 pane.activate_item(2, true, true, cx);
4745 });
4746
4747 let right_pane = workspace
4748 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
4749 .unwrap();
4750
4751 right_pane.update(cx, |pane, cx| {
4752 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
4753 });
4754
4755 left_pane
4756 });
4757
4758 cx.focus_view(&left_pane);
4759
4760 // When closing all of the items in the left pane, we should be prompted twice:
4761 // once for project entry 0, and once for project entry 2. Project entries 1,
4762 // 3, and 4 are all still open in the other paten. After those two
4763 // prompts, the task should complete.
4764
4765 let close = left_pane.update(cx, |pane, cx| {
4766 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
4767 });
4768 cx.executor().run_until_parked();
4769
4770 // Discard "Save all" prompt
4771 cx.simulate_prompt_answer(2);
4772
4773 cx.executor().run_until_parked();
4774 left_pane.update(cx, |pane, cx| {
4775 assert_eq!(
4776 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4777 &[ProjectEntryId::from_proto(0)]
4778 );
4779 });
4780 cx.simulate_prompt_answer(0);
4781
4782 cx.executor().run_until_parked();
4783 left_pane.update(cx, |pane, cx| {
4784 assert_eq!(
4785 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4786 &[ProjectEntryId::from_proto(2)]
4787 );
4788 });
4789 cx.simulate_prompt_answer(0);
4790
4791 cx.executor().run_until_parked();
4792 close.await.unwrap();
4793 left_pane.update(cx, |pane, _| {
4794 assert_eq!(pane.items_len(), 0);
4795 });
4796 }
4797
4798 #[gpui::test]
4799 async fn test_autosave(cx: &mut gpui::TestAppContext) {
4800 init_test(cx);
4801
4802 let fs = FakeFs::new(cx.executor());
4803 let project = Project::test(fs, [], cx).await;
4804 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4805 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4806
4807 let item = cx.new_view(|cx| {
4808 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4809 });
4810 let item_id = item.entity_id();
4811 workspace.update(cx, |workspace, cx| {
4812 workspace.add_item(Box::new(item.clone()), cx);
4813 });
4814
4815 // Autosave on window change.
4816 item.update(cx, |item, cx| {
4817 cx.update_global(|settings: &mut SettingsStore, cx| {
4818 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4819 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4820 })
4821 });
4822 item.is_dirty = true;
4823 });
4824
4825 // Deactivating the window saves the file.
4826 cx.deactivate_window();
4827 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
4828
4829 // Autosave on focus change.
4830 item.update(cx, |item, cx| {
4831 cx.focus_self();
4832 cx.update_global(|settings: &mut SettingsStore, cx| {
4833 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4834 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4835 })
4836 });
4837 item.is_dirty = true;
4838 });
4839
4840 // Blurring the item saves the file.
4841 item.update(cx, |_, cx| cx.blur());
4842 cx.executor().run_until_parked();
4843 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
4844
4845 // Deactivating the window still saves the file.
4846 cx.update(|cx| cx.activate_window());
4847 item.update(cx, |item, cx| {
4848 cx.focus_self();
4849 item.is_dirty = true;
4850 });
4851 cx.deactivate_window();
4852
4853 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
4854
4855 // Autosave after delay.
4856 item.update(cx, |item, cx| {
4857 cx.update_global(|settings: &mut SettingsStore, cx| {
4858 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4859 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4860 })
4861 });
4862 item.is_dirty = true;
4863 cx.emit(ItemEvent::Edit);
4864 });
4865
4866 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4867 cx.executor().advance_clock(Duration::from_millis(250));
4868 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
4869
4870 // After delay expires, the file is saved.
4871 cx.executor().advance_clock(Duration::from_millis(250));
4872 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
4873
4874 // Autosave on focus change, ensuring closing the tab counts as such.
4875 item.update(cx, |item, cx| {
4876 cx.update_global(|settings: &mut SettingsStore, cx| {
4877 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4878 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4879 })
4880 });
4881 item.is_dirty = true;
4882 });
4883
4884 pane.update(cx, |pane, cx| {
4885 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4886 })
4887 .await
4888 .unwrap();
4889 assert!(!cx.has_pending_prompt());
4890 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4891
4892 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
4893 workspace.update(cx, |workspace, cx| {
4894 workspace.add_item(Box::new(item.clone()), cx);
4895 });
4896 item.update(cx, |item, cx| {
4897 item.project_items[0].update(cx, |item, _| {
4898 item.entry_id = None;
4899 });
4900 item.is_dirty = true;
4901 cx.blur();
4902 });
4903 cx.run_until_parked();
4904 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4905
4906 // Ensure autosave is prevented for deleted files also when closing the buffer.
4907 let _close_items = pane.update(cx, |pane, cx| {
4908 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4909 });
4910 cx.run_until_parked();
4911 assert!(cx.has_pending_prompt());
4912 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4913 }
4914
4915 #[gpui::test]
4916 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
4917 init_test(cx);
4918
4919 let fs = FakeFs::new(cx.executor());
4920
4921 let project = Project::test(fs, [], cx).await;
4922 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4923
4924 let item = cx.new_view(|cx| {
4925 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4926 });
4927 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4928 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
4929 let toolbar_notify_count = Rc::new(RefCell::new(0));
4930
4931 workspace.update(cx, |workspace, cx| {
4932 workspace.add_item(Box::new(item.clone()), cx);
4933 let toolbar_notification_count = toolbar_notify_count.clone();
4934 cx.observe(&toolbar, move |_, _, _| {
4935 *toolbar_notification_count.borrow_mut() += 1
4936 })
4937 .detach();
4938 });
4939
4940 pane.update(cx, |pane, _| {
4941 assert!(!pane.can_navigate_backward());
4942 assert!(!pane.can_navigate_forward());
4943 });
4944
4945 item.update(cx, |item, cx| {
4946 item.set_state("one".to_string(), cx);
4947 });
4948
4949 // Toolbar must be notified to re-render the navigation buttons
4950 assert_eq!(*toolbar_notify_count.borrow(), 1);
4951
4952 pane.update(cx, |pane, _| {
4953 assert!(pane.can_navigate_backward());
4954 assert!(!pane.can_navigate_forward());
4955 });
4956
4957 workspace
4958 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
4959 .await
4960 .unwrap();
4961
4962 assert_eq!(*toolbar_notify_count.borrow(), 2);
4963 pane.update(cx, |pane, _| {
4964 assert!(!pane.can_navigate_backward());
4965 assert!(pane.can_navigate_forward());
4966 });
4967 }
4968
4969 #[gpui::test]
4970 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
4971 init_test(cx);
4972 let fs = FakeFs::new(cx.executor());
4973
4974 let project = Project::test(fs, [], cx).await;
4975 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4976
4977 let panel = workspace.update(cx, |workspace, cx| {
4978 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
4979 workspace.add_panel(panel.clone(), cx);
4980
4981 workspace
4982 .right_dock()
4983 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4984
4985 panel
4986 });
4987
4988 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4989 pane.update(cx, |pane, cx| {
4990 let item = cx.new_view(|cx| TestItem::new(cx));
4991 pane.add_item(Box::new(item), true, true, None, cx);
4992 });
4993
4994 // Transfer focus from center to panel
4995 workspace.update(cx, |workspace, cx| {
4996 workspace.toggle_panel_focus::<TestPanel>(cx);
4997 });
4998
4999 workspace.update(cx, |workspace, cx| {
5000 assert!(workspace.right_dock().read(cx).is_open());
5001 assert!(!panel.is_zoomed(cx));
5002 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5003 });
5004
5005 // Transfer focus from panel to center
5006 workspace.update(cx, |workspace, cx| {
5007 workspace.toggle_panel_focus::<TestPanel>(cx);
5008 });
5009
5010 workspace.update(cx, |workspace, cx| {
5011 assert!(workspace.right_dock().read(cx).is_open());
5012 assert!(!panel.is_zoomed(cx));
5013 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5014 });
5015
5016 // Close the dock
5017 workspace.update(cx, |workspace, cx| {
5018 workspace.toggle_dock(DockPosition::Right, cx);
5019 });
5020
5021 workspace.update(cx, |workspace, cx| {
5022 assert!(!workspace.right_dock().read(cx).is_open());
5023 assert!(!panel.is_zoomed(cx));
5024 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5025 });
5026
5027 // Open the dock
5028 workspace.update(cx, |workspace, cx| {
5029 workspace.toggle_dock(DockPosition::Right, cx);
5030 });
5031
5032 workspace.update(cx, |workspace, cx| {
5033 assert!(workspace.right_dock().read(cx).is_open());
5034 assert!(!panel.is_zoomed(cx));
5035 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5036 });
5037
5038 // Focus and zoom panel
5039 panel.update(cx, |panel, cx| {
5040 cx.focus_self();
5041 panel.set_zoomed(true, cx)
5042 });
5043
5044 workspace.update(cx, |workspace, cx| {
5045 assert!(workspace.right_dock().read(cx).is_open());
5046 assert!(panel.is_zoomed(cx));
5047 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5048 });
5049
5050 // Transfer focus to the center closes the dock
5051 workspace.update(cx, |workspace, cx| {
5052 workspace.toggle_panel_focus::<TestPanel>(cx);
5053 });
5054
5055 workspace.update(cx, |workspace, cx| {
5056 assert!(!workspace.right_dock().read(cx).is_open());
5057 assert!(panel.is_zoomed(cx));
5058 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5059 });
5060
5061 // Transferring focus back to the panel keeps it zoomed
5062 workspace.update(cx, |workspace, cx| {
5063 workspace.toggle_panel_focus::<TestPanel>(cx);
5064 });
5065
5066 workspace.update(cx, |workspace, cx| {
5067 assert!(workspace.right_dock().read(cx).is_open());
5068 assert!(panel.is_zoomed(cx));
5069 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5070 });
5071
5072 // Close the dock while it is zoomed
5073 workspace.update(cx, |workspace, cx| {
5074 workspace.toggle_dock(DockPosition::Right, cx)
5075 });
5076
5077 workspace.update(cx, |workspace, cx| {
5078 assert!(!workspace.right_dock().read(cx).is_open());
5079 assert!(panel.is_zoomed(cx));
5080 assert!(workspace.zoomed.is_none());
5081 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5082 });
5083
5084 // Opening the dock, when it's zoomed, retains focus
5085 workspace.update(cx, |workspace, cx| {
5086 workspace.toggle_dock(DockPosition::Right, cx)
5087 });
5088
5089 workspace.update(cx, |workspace, cx| {
5090 assert!(workspace.right_dock().read(cx).is_open());
5091 assert!(panel.is_zoomed(cx));
5092 assert!(workspace.zoomed.is_some());
5093 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5094 });
5095
5096 // Unzoom and close the panel, zoom the active pane.
5097 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5098 workspace.update(cx, |workspace, cx| {
5099 workspace.toggle_dock(DockPosition::Right, cx)
5100 });
5101 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5102
5103 // Opening a dock unzooms the pane.
5104 workspace.update(cx, |workspace, cx| {
5105 workspace.toggle_dock(DockPosition::Right, cx)
5106 });
5107 workspace.update(cx, |workspace, cx| {
5108 let pane = pane.read(cx);
5109 assert!(!pane.is_zoomed());
5110 assert!(!pane.focus_handle(cx).is_focused(cx));
5111 assert!(workspace.right_dock().read(cx).is_open());
5112 assert!(workspace.zoomed.is_none());
5113 });
5114 }
5115
5116 struct TestModal(FocusHandle);
5117
5118 impl TestModal {
5119 fn new(cx: &mut ViewContext<Self>) -> Self {
5120 Self(cx.focus_handle())
5121 }
5122 }
5123
5124 impl EventEmitter<DismissEvent> for TestModal {}
5125
5126 impl FocusableView for TestModal {
5127 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
5128 self.0.clone()
5129 }
5130 }
5131
5132 impl ModalView for TestModal {}
5133
5134 impl Render for TestModal {
5135 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
5136 div().track_focus(&self.0)
5137 }
5138 }
5139
5140 #[gpui::test]
5141 async fn test_panels(cx: &mut gpui::TestAppContext) {
5142 init_test(cx);
5143 let fs = FakeFs::new(cx.executor());
5144
5145 let project = Project::test(fs, [], cx).await;
5146 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5147
5148 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5149 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
5150 workspace.add_panel(panel_1.clone(), cx);
5151 workspace
5152 .left_dock()
5153 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5154 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5155 workspace.add_panel(panel_2.clone(), cx);
5156 workspace
5157 .right_dock()
5158 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5159
5160 let left_dock = workspace.left_dock();
5161 assert_eq!(
5162 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5163 panel_1.panel_id()
5164 );
5165 assert_eq!(
5166 left_dock.read(cx).active_panel_size(cx).unwrap(),
5167 panel_1.size(cx)
5168 );
5169
5170 left_dock.update(cx, |left_dock, cx| {
5171 left_dock.resize_active_panel(Some(px(1337.)), cx)
5172 });
5173 assert_eq!(
5174 workspace
5175 .right_dock()
5176 .read(cx)
5177 .visible_panel()
5178 .unwrap()
5179 .panel_id(),
5180 panel_2.panel_id(),
5181 );
5182
5183 (panel_1, panel_2)
5184 });
5185
5186 // Move panel_1 to the right
5187 panel_1.update(cx, |panel_1, cx| {
5188 panel_1.set_position(DockPosition::Right, cx)
5189 });
5190
5191 workspace.update(cx, |workspace, cx| {
5192 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5193 // Since it was the only panel on the left, the left dock should now be closed.
5194 assert!(!workspace.left_dock().read(cx).is_open());
5195 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5196 let right_dock = workspace.right_dock();
5197 assert_eq!(
5198 right_dock.read(cx).visible_panel().unwrap().panel_id(),
5199 panel_1.panel_id()
5200 );
5201 assert_eq!(
5202 right_dock.read(cx).active_panel_size(cx).unwrap(),
5203 px(1337.)
5204 );
5205
5206 // Now we move panel_2 to the left
5207 panel_2.set_position(DockPosition::Left, cx);
5208 });
5209
5210 workspace.update(cx, |workspace, cx| {
5211 // Since panel_2 was not visible on the right, we don't open the left dock.
5212 assert!(!workspace.left_dock().read(cx).is_open());
5213 // And the right dock is unaffected in it's displaying of panel_1
5214 assert!(workspace.right_dock().read(cx).is_open());
5215 assert_eq!(
5216 workspace
5217 .right_dock()
5218 .read(cx)
5219 .visible_panel()
5220 .unwrap()
5221 .panel_id(),
5222 panel_1.panel_id(),
5223 );
5224 });
5225
5226 // Move panel_1 back to the left
5227 panel_1.update(cx, |panel_1, cx| {
5228 panel_1.set_position(DockPosition::Left, cx)
5229 });
5230
5231 workspace.update(cx, |workspace, cx| {
5232 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5233 let left_dock = workspace.left_dock();
5234 assert!(left_dock.read(cx).is_open());
5235 assert_eq!(
5236 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5237 panel_1.panel_id()
5238 );
5239 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
5240 // And the right dock should be closed as it no longer has any panels.
5241 assert!(!workspace.right_dock().read(cx).is_open());
5242
5243 // Now we move panel_1 to the bottom
5244 panel_1.set_position(DockPosition::Bottom, cx);
5245 });
5246
5247 workspace.update(cx, |workspace, cx| {
5248 // Since panel_1 was visible on the left, we close the left dock.
5249 assert!(!workspace.left_dock().read(cx).is_open());
5250 // The bottom dock is sized based on the panel's default size,
5251 // since the panel orientation changed from vertical to horizontal.
5252 let bottom_dock = workspace.bottom_dock();
5253 assert_eq!(
5254 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5255 panel_1.size(cx),
5256 );
5257 // Close bottom dock and move panel_1 back to the left.
5258 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5259 panel_1.set_position(DockPosition::Left, cx);
5260 });
5261
5262 // Emit activated event on panel 1
5263 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
5264
5265 // Now the left dock is open and panel_1 is active and focused.
5266 workspace.update(cx, |workspace, cx| {
5267 let left_dock = workspace.left_dock();
5268 assert!(left_dock.read(cx).is_open());
5269 assert_eq!(
5270 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5271 panel_1.panel_id(),
5272 );
5273 assert!(panel_1.focus_handle(cx).is_focused(cx));
5274 });
5275
5276 // Emit closed event on panel 2, which is not active
5277 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5278
5279 // Wo don't close the left dock, because panel_2 wasn't the active panel
5280 workspace.update(cx, |workspace, cx| {
5281 let left_dock = workspace.left_dock();
5282 assert!(left_dock.read(cx).is_open());
5283 assert_eq!(
5284 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5285 panel_1.panel_id(),
5286 );
5287 });
5288
5289 // Emitting a ZoomIn event shows the panel as zoomed.
5290 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
5291 workspace.update(cx, |workspace, _| {
5292 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5293 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5294 });
5295
5296 // Move panel to another dock while it is zoomed
5297 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5298 workspace.update(cx, |workspace, _| {
5299 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5300
5301 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5302 });
5303
5304 // This is a helper for getting a:
5305 // - valid focus on an element,
5306 // - that isn't a part of the panes and panels system of the Workspace,
5307 // - and doesn't trigger the 'on_focus_lost' API.
5308 let focus_other_view = {
5309 let workspace = workspace.clone();
5310 move |cx: &mut VisualTestContext| {
5311 workspace.update(cx, |workspace, cx| {
5312 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
5313 workspace.toggle_modal(cx, TestModal::new);
5314 workspace.toggle_modal(cx, TestModal::new);
5315 } else {
5316 workspace.toggle_modal(cx, TestModal::new);
5317 }
5318 })
5319 }
5320 };
5321
5322 // If focus is transferred to another view that's not a panel or another pane, we still show
5323 // the panel as zoomed.
5324 focus_other_view(cx);
5325 workspace.update(cx, |workspace, _| {
5326 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5327 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5328 });
5329
5330 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5331 workspace.update(cx, |_, cx| cx.focus_self());
5332 workspace.update(cx, |workspace, _| {
5333 assert_eq!(workspace.zoomed, None);
5334 assert_eq!(workspace.zoomed_position, None);
5335 });
5336
5337 // If focus is transferred again to another view that's not a panel or a pane, we won't
5338 // show the panel as zoomed because it wasn't zoomed before.
5339 focus_other_view(cx);
5340 workspace.update(cx, |workspace, _| {
5341 assert_eq!(workspace.zoomed, None);
5342 assert_eq!(workspace.zoomed_position, None);
5343 });
5344
5345 // When the panel is activated, it is zoomed again.
5346 cx.dispatch_action(ToggleRightDock);
5347 workspace.update(cx, |workspace, _| {
5348 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5349 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5350 });
5351
5352 // Emitting a ZoomOut event unzooms the panel.
5353 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
5354 workspace.update(cx, |workspace, _| {
5355 assert_eq!(workspace.zoomed, None);
5356 assert_eq!(workspace.zoomed_position, None);
5357 });
5358
5359 // Emit closed event on panel 1, which is active
5360 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5361
5362 // Now the left dock is closed, because panel_1 was the active panel
5363 workspace.update(cx, |workspace, cx| {
5364 let right_dock = workspace.right_dock();
5365 assert!(!right_dock.read(cx).is_open());
5366 });
5367 }
5368
5369 pub fn init_test(cx: &mut TestAppContext) {
5370 cx.update(|cx| {
5371 let settings_store = SettingsStore::test(cx);
5372 cx.set_global(settings_store);
5373 theme::init(theme::LoadThemes::JustBase, cx);
5374 language::init(cx);
5375 crate::init_settings(cx);
5376 Project::init_settings(cx);
5377 });
5378 }
5379}