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