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