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