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