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