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