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