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, AppContext, AsyncAppContext, AsyncWindowContext, BorrowWindow, Bounds, Context,
30 Div, DragMoveEvent, Element, Entity, EntityId, EventEmitter, FocusHandle, FocusableView,
31 GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId, ManagedView, Model,
32 ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel, Render, Size,
33 Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds,
34 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 // find an existing workspace to focus and show call controls
4038 let mut active_window =
4039 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
4040 if active_window.is_none() {
4041 // no open workspaces, make one to show the error in (blergh)
4042 let (window_handle, _) = cx
4043 .update(|cx| {
4044 Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)
4045 })?
4046 .await?;
4047
4048 active_window = Some(window_handle);
4049 }
4050
4051 if let Err(err) = result {
4052 log::error!("failed to join channel: {}", err);
4053 if let Some(active_window) = active_window {
4054 active_window
4055 .update(&mut cx, |_, cx| {
4056 cx.prompt(
4057 PromptLevel::Critical,
4058 &format!("Failed to join channel: {}", err),
4059 &["Ok"],
4060 )
4061 })?
4062 .await
4063 .ok();
4064 }
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).context("could not open zed")
4083}
4084
4085fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
4086 cx.update(|cx| {
4087 for window in cx.windows() {
4088 if let Some(workspace_window) = window.downcast::<Workspace>() {
4089 workspace_window
4090 .update(cx, |_, cx| cx.activate_window())
4091 .ok();
4092 return Some(workspace_window);
4093 }
4094 }
4095 None
4096 })
4097 .ok()
4098 .flatten()
4099}
4100
4101#[allow(clippy::type_complexity)]
4102pub fn open_paths(
4103 abs_paths: &[PathBuf],
4104 app_state: &Arc<AppState>,
4105 requesting_window: Option<WindowHandle<Workspace>>,
4106 cx: &mut AppContext,
4107) -> Task<
4108 anyhow::Result<(
4109 WindowHandle<Workspace>,
4110 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4111 )>,
4112> {
4113 let app_state = app_state.clone();
4114 let abs_paths = abs_paths.to_vec();
4115 // Open paths in existing workspace if possible
4116 let existing = activate_workspace_for_project(cx, {
4117 let abs_paths = abs_paths.clone();
4118 move |project, cx| project.contains_paths(&abs_paths, cx)
4119 });
4120 cx.spawn(move |mut cx| async move {
4121 if let Some(existing) = existing {
4122 Ok((
4123 existing.clone(),
4124 existing
4125 .update(&mut cx, |workspace, cx| {
4126 workspace.open_paths(abs_paths, OpenVisible::All, None, cx)
4127 })?
4128 .await,
4129 ))
4130 } else {
4131 cx.update(move |cx| {
4132 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
4133 })?
4134 .await
4135 }
4136 })
4137}
4138
4139pub fn open_new(
4140 app_state: &Arc<AppState>,
4141 cx: &mut AppContext,
4142 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
4143) -> Task<()> {
4144 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
4145 cx.spawn(|mut cx| async move {
4146 if let Some((workspace, opened_paths)) = task.await.log_err() {
4147 workspace
4148 .update(&mut cx, |workspace, cx| {
4149 if opened_paths.is_empty() {
4150 init(workspace, cx)
4151 }
4152 })
4153 .log_err();
4154 }
4155 })
4156}
4157
4158pub fn create_and_open_local_file(
4159 path: &'static Path,
4160 cx: &mut ViewContext<Workspace>,
4161 default_content: impl 'static + Send + FnOnce() -> Rope,
4162) -> Task<Result<Box<dyn ItemHandle>>> {
4163 cx.spawn(|workspace, mut cx| async move {
4164 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
4165 if !fs.is_file(path).await {
4166 fs.create_file(path, Default::default()).await?;
4167 fs.save(path, &default_content(), Default::default())
4168 .await?;
4169 }
4170
4171 let mut items = workspace
4172 .update(&mut cx, |workspace, cx| {
4173 workspace.with_local_workspace(cx, |workspace, cx| {
4174 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
4175 })
4176 })?
4177 .await?
4178 .await;
4179
4180 let item = items.pop().flatten();
4181 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4182 })
4183}
4184
4185pub fn join_remote_project(
4186 project_id: u64,
4187 follow_user_id: u64,
4188 app_state: Arc<AppState>,
4189 cx: &mut AppContext,
4190) -> Task<Result<()>> {
4191 let windows = cx.windows();
4192 cx.spawn(|mut cx| async move {
4193 let existing_workspace = windows.into_iter().find_map(|window| {
4194 window.downcast::<Workspace>().and_then(|window| {
4195 window
4196 .update(&mut cx, |workspace, cx| {
4197 if workspace.project().read(cx).remote_id() == Some(project_id) {
4198 Some(window)
4199 } else {
4200 None
4201 }
4202 })
4203 .unwrap_or(None)
4204 })
4205 });
4206
4207 let workspace = if let Some(existing_workspace) = existing_workspace {
4208 existing_workspace
4209 } else {
4210 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
4211 let room = active_call
4212 .read_with(&cx, |call, _| call.room().cloned())?
4213 .ok_or_else(|| anyhow!("not in a call"))?;
4214 let project = room
4215 .update(&mut cx, |room, cx| {
4216 room.join_project(
4217 project_id,
4218 app_state.languages.clone(),
4219 app_state.fs.clone(),
4220 cx,
4221 )
4222 })?
4223 .await?;
4224
4225 let window_bounds_override = window_bounds_env_override(&cx);
4226 cx.update(|cx| {
4227 let options = (app_state.build_window_options)(window_bounds_override, None, cx);
4228 cx.open_window(options, |cx| {
4229 cx.new_view(|cx| Workspace::new(0, project, app_state.clone(), cx))
4230 })
4231 })?
4232 };
4233
4234 workspace.update(&mut cx, |workspace, cx| {
4235 cx.activate(true);
4236 cx.activate_window();
4237
4238 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4239 let follow_peer_id = room
4240 .read(cx)
4241 .remote_participants()
4242 .iter()
4243 .find(|(_, participant)| participant.user.id == follow_user_id)
4244 .map(|(_, p)| p.peer_id)
4245 .or_else(|| {
4246 // If we couldn't follow the given user, follow the host instead.
4247 let collaborator = workspace
4248 .project()
4249 .read(cx)
4250 .collaborators()
4251 .values()
4252 .find(|collaborator| collaborator.replica_id == 0)?;
4253 Some(collaborator.peer_id)
4254 });
4255
4256 if let Some(follow_peer_id) = follow_peer_id {
4257 workspace.follow(follow_peer_id, cx);
4258 }
4259 }
4260 })?;
4261
4262 anyhow::Ok(())
4263 })
4264}
4265
4266pub fn restart(_: &Restart, cx: &mut AppContext) {
4267 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
4268 let mut workspace_windows = cx
4269 .windows()
4270 .into_iter()
4271 .filter_map(|window| window.downcast::<Workspace>())
4272 .collect::<Vec<_>>();
4273
4274 // If multiple windows have unsaved changes, and need a save prompt,
4275 // prompt in the active window before switching to a different window.
4276 workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
4277
4278 let mut prompt = None;
4279 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4280 prompt = window
4281 .update(cx, |_, cx| {
4282 cx.prompt(
4283 PromptLevel::Info,
4284 "Are you sure you want to restart?",
4285 &["Restart", "Cancel"],
4286 )
4287 })
4288 .ok();
4289 }
4290
4291 cx.spawn(|mut cx| async move {
4292 if let Some(prompt) = prompt {
4293 let answer = prompt.await?;
4294 if answer != 0 {
4295 return Ok(());
4296 }
4297 }
4298
4299 // If the user cancels any save prompt, then keep the app open.
4300 for window in workspace_windows {
4301 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
4302 workspace.prepare_to_close(true, cx)
4303 }) {
4304 if !should_close.await? {
4305 return Ok(());
4306 }
4307 }
4308 }
4309
4310 cx.update(|cx| cx.restart())
4311 })
4312 .detach_and_log_err(cx);
4313}
4314
4315fn parse_pixel_position_env_var(value: &str) -> Option<Point<GlobalPixels>> {
4316 let mut parts = value.split(',');
4317 let x: usize = parts.next()?.parse().ok()?;
4318 let y: usize = parts.next()?.parse().ok()?;
4319 Some(point((x as f64).into(), (y as f64).into()))
4320}
4321
4322fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
4323 let mut parts = value.split(',');
4324 let width: usize = parts.next()?.parse().ok()?;
4325 let height: usize = parts.next()?.parse().ok()?;
4326 Some(size((width as f64).into(), (height as f64).into()))
4327}
4328
4329struct DisconnectedOverlay;
4330
4331impl Element for DisconnectedOverlay {
4332 type State = AnyElement;
4333
4334 fn request_layout(
4335 &mut self,
4336 _: Option<Self::State>,
4337 cx: &mut WindowContext,
4338 ) -> (LayoutId, Self::State) {
4339 let mut background = cx.theme().colors().elevated_surface_background;
4340 background.fade_out(0.2);
4341 let mut overlay = div()
4342 .bg(background)
4343 .absolute()
4344 .left_0()
4345 .top_0()
4346 .size_full()
4347 .flex()
4348 .items_center()
4349 .justify_center()
4350 .capture_any_mouse_down(|_, cx| cx.stop_propagation())
4351 .capture_any_mouse_up(|_, cx| cx.stop_propagation())
4352 .child(Label::new(
4353 "Your connection to the remote project has been lost.",
4354 ))
4355 .into_any();
4356 (overlay.request_layout(cx), overlay)
4357 }
4358
4359 fn paint(&mut self, bounds: Bounds<Pixels>, overlay: &mut Self::State, cx: &mut WindowContext) {
4360 cx.with_z_index(u8::MAX, |cx| {
4361 cx.add_opaque_layer(bounds);
4362 overlay.paint(cx);
4363 })
4364 }
4365}
4366
4367impl IntoElement for DisconnectedOverlay {
4368 type Element = Self;
4369
4370 fn element_id(&self) -> Option<ui::prelude::ElementId> {
4371 None
4372 }
4373
4374 fn into_element(self) -> Self::Element {
4375 self
4376 }
4377}
4378
4379#[cfg(test)]
4380mod tests {
4381 use std::{cell::RefCell, rc::Rc};
4382
4383 use super::*;
4384 use crate::item::{
4385 test::{TestItem, TestProjectItem},
4386 ItemEvent,
4387 };
4388 use fs::FakeFs;
4389 use gpui::TestAppContext;
4390 use project::{Project, ProjectEntryId};
4391 use serde_json::json;
4392 use settings::SettingsStore;
4393
4394 #[gpui::test]
4395 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4396 init_test(cx);
4397
4398 let fs = FakeFs::new(cx.executor());
4399 let project = Project::test(fs, [], cx).await;
4400 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4401
4402 // Adding an item with no ambiguity renders the tab without detail.
4403 let item1 = cx.new_view(|cx| {
4404 let mut item = TestItem::new(cx);
4405 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4406 item
4407 });
4408 workspace.update(cx, |workspace, cx| {
4409 workspace.add_item(Box::new(item1.clone()), cx);
4410 });
4411 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
4412
4413 // Adding an item that creates ambiguity increases the level of detail on
4414 // both tabs.
4415 let item2 = cx.new_view(|cx| {
4416 let mut item = TestItem::new(cx);
4417 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4418 item
4419 });
4420 workspace.update(cx, |workspace, cx| {
4421 workspace.add_item(Box::new(item2.clone()), cx);
4422 });
4423 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4424 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4425
4426 // Adding an item that creates ambiguity increases the level of detail only
4427 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4428 // we stop at the highest detail available.
4429 let item3 = cx.new_view(|cx| {
4430 let mut item = TestItem::new(cx);
4431 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4432 item
4433 });
4434 workspace.update(cx, |workspace, cx| {
4435 workspace.add_item(Box::new(item3.clone()), cx);
4436 });
4437 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4438 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4439 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4440 }
4441
4442 #[gpui::test]
4443 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4444 init_test(cx);
4445
4446 let fs = FakeFs::new(cx.executor());
4447 fs.insert_tree(
4448 "/root1",
4449 json!({
4450 "one.txt": "",
4451 "two.txt": "",
4452 }),
4453 )
4454 .await;
4455 fs.insert_tree(
4456 "/root2",
4457 json!({
4458 "three.txt": "",
4459 }),
4460 )
4461 .await;
4462
4463 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4464 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4465 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4466 let worktree_id = project.update(cx, |project, cx| {
4467 project.worktrees().next().unwrap().read(cx).id()
4468 });
4469
4470 let item1 = cx.new_view(|cx| {
4471 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4472 });
4473 let item2 = cx.new_view(|cx| {
4474 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4475 });
4476
4477 // Add an item to an empty pane
4478 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4479 project.update(cx, |project, cx| {
4480 assert_eq!(
4481 project.active_entry(),
4482 project
4483 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4484 .map(|e| e.id)
4485 );
4486 });
4487 assert_eq!(cx.window_title().as_deref(), Some("one.txt β root1"));
4488
4489 // Add a second item to a non-empty pane
4490 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4491 assert_eq!(cx.window_title().as_deref(), Some("two.txt β root1"));
4492 project.update(cx, |project, cx| {
4493 assert_eq!(
4494 project.active_entry(),
4495 project
4496 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4497 .map(|e| e.id)
4498 );
4499 });
4500
4501 // Close the active item
4502 pane.update(cx, |pane, cx| {
4503 pane.close_active_item(&Default::default(), cx).unwrap()
4504 })
4505 .await
4506 .unwrap();
4507 assert_eq!(cx.window_title().as_deref(), Some("one.txt β root1"));
4508 project.update(cx, |project, cx| {
4509 assert_eq!(
4510 project.active_entry(),
4511 project
4512 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4513 .map(|e| e.id)
4514 );
4515 });
4516
4517 // Add a project folder
4518 project
4519 .update(cx, |project, cx| {
4520 project.find_or_create_local_worktree("/root2", true, cx)
4521 })
4522 .await
4523 .unwrap();
4524 assert_eq!(cx.window_title().as_deref(), Some("one.txt β root1, root2"));
4525
4526 // Remove a project folder
4527 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4528 assert_eq!(cx.window_title().as_deref(), Some("one.txt β root2"));
4529 }
4530
4531 #[gpui::test]
4532 async fn test_close_window(cx: &mut TestAppContext) {
4533 init_test(cx);
4534
4535 let fs = FakeFs::new(cx.executor());
4536 fs.insert_tree("/root", json!({ "one": "" })).await;
4537
4538 let project = Project::test(fs, ["root".as_ref()], cx).await;
4539 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4540
4541 // When there are no dirty items, there's nothing to do.
4542 let item1 = cx.new_view(|cx| TestItem::new(cx));
4543 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4544 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4545 assert!(task.await.unwrap());
4546
4547 // When there are dirty untitled items, prompt to save each one. If the user
4548 // cancels any prompt, then abort.
4549 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
4550 let item3 = cx.new_view(|cx| {
4551 TestItem::new(cx)
4552 .with_dirty(true)
4553 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4554 });
4555 workspace.update(cx, |w, cx| {
4556 w.add_item(Box::new(item2.clone()), cx);
4557 w.add_item(Box::new(item3.clone()), cx);
4558 });
4559 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4560 cx.executor().run_until_parked();
4561 cx.simulate_prompt_answer(2); // cancel save all
4562 cx.executor().run_until_parked();
4563 cx.simulate_prompt_answer(2); // cancel save all
4564 cx.executor().run_until_parked();
4565 assert!(!cx.has_pending_prompt());
4566 assert!(!task.await.unwrap());
4567 }
4568
4569 #[gpui::test]
4570 async fn test_close_pane_items(cx: &mut TestAppContext) {
4571 init_test(cx);
4572
4573 let fs = FakeFs::new(cx.executor());
4574
4575 let project = Project::test(fs, None, cx).await;
4576 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4577
4578 let item1 = cx.new_view(|cx| {
4579 TestItem::new(cx)
4580 .with_dirty(true)
4581 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4582 });
4583 let item2 = cx.new_view(|cx| {
4584 TestItem::new(cx)
4585 .with_dirty(true)
4586 .with_conflict(true)
4587 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4588 });
4589 let item3 = cx.new_view(|cx| {
4590 TestItem::new(cx)
4591 .with_dirty(true)
4592 .with_conflict(true)
4593 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4594 });
4595 let item4 = cx.new_view(|cx| {
4596 TestItem::new(cx)
4597 .with_dirty(true)
4598 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4599 });
4600 let pane = workspace.update(cx, |workspace, cx| {
4601 workspace.add_item(Box::new(item1.clone()), cx);
4602 workspace.add_item(Box::new(item2.clone()), cx);
4603 workspace.add_item(Box::new(item3.clone()), cx);
4604 workspace.add_item(Box::new(item4.clone()), cx);
4605 workspace.active_pane().clone()
4606 });
4607
4608 let close_items = pane.update(cx, |pane, cx| {
4609 pane.activate_item(1, true, true, cx);
4610 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4611 let item1_id = item1.item_id();
4612 let item3_id = item3.item_id();
4613 let item4_id = item4.item_id();
4614 pane.close_items(cx, SaveIntent::Close, move |id| {
4615 [item1_id, item3_id, item4_id].contains(&id)
4616 })
4617 });
4618 cx.executor().run_until_parked();
4619
4620 assert!(cx.has_pending_prompt());
4621 // Ignore "Save all" prompt
4622 cx.simulate_prompt_answer(2);
4623 cx.executor().run_until_parked();
4624 // There's a prompt to save item 1.
4625 pane.update(cx, |pane, _| {
4626 assert_eq!(pane.items_len(), 4);
4627 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
4628 });
4629 // Confirm saving item 1.
4630 cx.simulate_prompt_answer(0);
4631 cx.executor().run_until_parked();
4632
4633 // Item 1 is saved. There's a prompt to save item 3.
4634 pane.update(cx, |pane, cx| {
4635 assert_eq!(item1.read(cx).save_count, 1);
4636 assert_eq!(item1.read(cx).save_as_count, 0);
4637 assert_eq!(item1.read(cx).reload_count, 0);
4638 assert_eq!(pane.items_len(), 3);
4639 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
4640 });
4641 assert!(cx.has_pending_prompt());
4642
4643 // Cancel saving item 3.
4644 cx.simulate_prompt_answer(1);
4645 cx.executor().run_until_parked();
4646
4647 // Item 3 is reloaded. There's a prompt to save item 4.
4648 pane.update(cx, |pane, cx| {
4649 assert_eq!(item3.read(cx).save_count, 0);
4650 assert_eq!(item3.read(cx).save_as_count, 0);
4651 assert_eq!(item3.read(cx).reload_count, 1);
4652 assert_eq!(pane.items_len(), 2);
4653 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
4654 });
4655 assert!(cx.has_pending_prompt());
4656
4657 // Confirm saving item 4.
4658 cx.simulate_prompt_answer(0);
4659 cx.executor().run_until_parked();
4660
4661 // There's a prompt for a path for item 4.
4662 cx.simulate_new_path_selection(|_| Some(Default::default()));
4663 close_items.await.unwrap();
4664
4665 // The requested items are closed.
4666 pane.update(cx, |pane, cx| {
4667 assert_eq!(item4.read(cx).save_count, 0);
4668 assert_eq!(item4.read(cx).save_as_count, 1);
4669 assert_eq!(item4.read(cx).reload_count, 0);
4670 assert_eq!(pane.items_len(), 1);
4671 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4672 });
4673 }
4674
4675 #[gpui::test]
4676 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4677 init_test(cx);
4678
4679 let fs = FakeFs::new(cx.executor());
4680 let project = Project::test(fs, [], cx).await;
4681 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4682
4683 // Create several workspace items with single project entries, and two
4684 // workspace items with multiple project entries.
4685 let single_entry_items = (0..=4)
4686 .map(|project_entry_id| {
4687 cx.new_view(|cx| {
4688 TestItem::new(cx)
4689 .with_dirty(true)
4690 .with_project_items(&[TestProjectItem::new(
4691 project_entry_id,
4692 &format!("{project_entry_id}.txt"),
4693 cx,
4694 )])
4695 })
4696 })
4697 .collect::<Vec<_>>();
4698 let item_2_3 = cx.new_view(|cx| {
4699 TestItem::new(cx)
4700 .with_dirty(true)
4701 .with_singleton(false)
4702 .with_project_items(&[
4703 single_entry_items[2].read(cx).project_items[0].clone(),
4704 single_entry_items[3].read(cx).project_items[0].clone(),
4705 ])
4706 });
4707 let item_3_4 = cx.new_view(|cx| {
4708 TestItem::new(cx)
4709 .with_dirty(true)
4710 .with_singleton(false)
4711 .with_project_items(&[
4712 single_entry_items[3].read(cx).project_items[0].clone(),
4713 single_entry_items[4].read(cx).project_items[0].clone(),
4714 ])
4715 });
4716
4717 // Create two panes that contain the following project entries:
4718 // left pane:
4719 // multi-entry items: (2, 3)
4720 // single-entry items: 0, 1, 2, 3, 4
4721 // right pane:
4722 // single-entry items: 1
4723 // multi-entry items: (3, 4)
4724 let left_pane = workspace.update(cx, |workspace, cx| {
4725 let left_pane = workspace.active_pane().clone();
4726 workspace.add_item(Box::new(item_2_3.clone()), cx);
4727 for item in single_entry_items {
4728 workspace.add_item(Box::new(item), cx);
4729 }
4730 left_pane.update(cx, |pane, cx| {
4731 pane.activate_item(2, true, true, cx);
4732 });
4733
4734 let right_pane = workspace
4735 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
4736 .unwrap();
4737
4738 right_pane.update(cx, |pane, cx| {
4739 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
4740 });
4741
4742 left_pane
4743 });
4744
4745 cx.focus_view(&left_pane);
4746
4747 // When closing all of the items in the left pane, we should be prompted twice:
4748 // once for project entry 0, and once for project entry 2. Project entries 1,
4749 // 3, and 4 are all still open in the other paten. After those two
4750 // prompts, the task should complete.
4751
4752 let close = left_pane.update(cx, |pane, cx| {
4753 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
4754 });
4755 cx.executor().run_until_parked();
4756
4757 // Discard "Save all" prompt
4758 cx.simulate_prompt_answer(2);
4759
4760 cx.executor().run_until_parked();
4761 left_pane.update(cx, |pane, cx| {
4762 assert_eq!(
4763 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4764 &[ProjectEntryId::from_proto(0)]
4765 );
4766 });
4767 cx.simulate_prompt_answer(0);
4768
4769 cx.executor().run_until_parked();
4770 left_pane.update(cx, |pane, cx| {
4771 assert_eq!(
4772 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4773 &[ProjectEntryId::from_proto(2)]
4774 );
4775 });
4776 cx.simulate_prompt_answer(0);
4777
4778 cx.executor().run_until_parked();
4779 close.await.unwrap();
4780 left_pane.update(cx, |pane, _| {
4781 assert_eq!(pane.items_len(), 0);
4782 });
4783 }
4784
4785 #[gpui::test]
4786 async fn test_autosave(cx: &mut gpui::TestAppContext) {
4787 init_test(cx);
4788
4789 let fs = FakeFs::new(cx.executor());
4790 let project = Project::test(fs, [], cx).await;
4791 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4792 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4793
4794 let item = cx.new_view(|cx| {
4795 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4796 });
4797 let item_id = item.entity_id();
4798 workspace.update(cx, |workspace, cx| {
4799 workspace.add_item(Box::new(item.clone()), cx);
4800 });
4801
4802 // Autosave on window change.
4803 item.update(cx, |item, cx| {
4804 cx.update_global(|settings: &mut SettingsStore, cx| {
4805 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4806 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4807 })
4808 });
4809 item.is_dirty = true;
4810 });
4811
4812 // Deactivating the window saves the file.
4813 cx.deactivate_window();
4814 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
4815
4816 // Autosave on focus change.
4817 item.update(cx, |item, cx| {
4818 cx.focus_self();
4819 cx.update_global(|settings: &mut SettingsStore, cx| {
4820 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4821 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4822 })
4823 });
4824 item.is_dirty = true;
4825 });
4826
4827 // Blurring the item saves the file.
4828 item.update(cx, |_, cx| cx.blur());
4829 cx.executor().run_until_parked();
4830 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
4831
4832 // Deactivating the window still saves the file.
4833 cx.update(|cx| cx.activate_window());
4834 item.update(cx, |item, cx| {
4835 cx.focus_self();
4836 item.is_dirty = true;
4837 });
4838 cx.deactivate_window();
4839
4840 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
4841
4842 // Autosave after delay.
4843 item.update(cx, |item, cx| {
4844 cx.update_global(|settings: &mut SettingsStore, cx| {
4845 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4846 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4847 })
4848 });
4849 item.is_dirty = true;
4850 cx.emit(ItemEvent::Edit);
4851 });
4852
4853 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4854 cx.executor().advance_clock(Duration::from_millis(250));
4855 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
4856
4857 // After delay expires, the file is saved.
4858 cx.executor().advance_clock(Duration::from_millis(250));
4859 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
4860
4861 // Autosave on focus change, ensuring closing the tab counts as such.
4862 item.update(cx, |item, cx| {
4863 cx.update_global(|settings: &mut SettingsStore, cx| {
4864 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4865 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4866 })
4867 });
4868 item.is_dirty = true;
4869 });
4870
4871 pane.update(cx, |pane, cx| {
4872 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4873 })
4874 .await
4875 .unwrap();
4876 assert!(!cx.has_pending_prompt());
4877 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4878
4879 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
4880 workspace.update(cx, |workspace, cx| {
4881 workspace.add_item(Box::new(item.clone()), cx);
4882 });
4883 item.update(cx, |item, cx| {
4884 item.project_items[0].update(cx, |item, _| {
4885 item.entry_id = None;
4886 });
4887 item.is_dirty = true;
4888 cx.blur();
4889 });
4890 cx.run_until_parked();
4891 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4892
4893 // Ensure autosave is prevented for deleted files also when closing the buffer.
4894 let _close_items = pane.update(cx, |pane, cx| {
4895 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4896 });
4897 cx.run_until_parked();
4898 assert!(cx.has_pending_prompt());
4899 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4900 }
4901
4902 #[gpui::test]
4903 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
4904 init_test(cx);
4905
4906 let fs = FakeFs::new(cx.executor());
4907
4908 let project = Project::test(fs, [], cx).await;
4909 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4910
4911 let item = cx.new_view(|cx| {
4912 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4913 });
4914 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4915 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
4916 let toolbar_notify_count = Rc::new(RefCell::new(0));
4917
4918 workspace.update(cx, |workspace, cx| {
4919 workspace.add_item(Box::new(item.clone()), cx);
4920 let toolbar_notification_count = toolbar_notify_count.clone();
4921 cx.observe(&toolbar, move |_, _, _| {
4922 *toolbar_notification_count.borrow_mut() += 1
4923 })
4924 .detach();
4925 });
4926
4927 pane.update(cx, |pane, _| {
4928 assert!(!pane.can_navigate_backward());
4929 assert!(!pane.can_navigate_forward());
4930 });
4931
4932 item.update(cx, |item, cx| {
4933 item.set_state("one".to_string(), cx);
4934 });
4935
4936 // Toolbar must be notified to re-render the navigation buttons
4937 assert_eq!(*toolbar_notify_count.borrow(), 1);
4938
4939 pane.update(cx, |pane, _| {
4940 assert!(pane.can_navigate_backward());
4941 assert!(!pane.can_navigate_forward());
4942 });
4943
4944 workspace
4945 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
4946 .await
4947 .unwrap();
4948
4949 assert_eq!(*toolbar_notify_count.borrow(), 2);
4950 pane.update(cx, |pane, _| {
4951 assert!(!pane.can_navigate_backward());
4952 assert!(pane.can_navigate_forward());
4953 });
4954 }
4955
4956 // #[gpui::test]
4957 // async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
4958 // init_test(cx);
4959 // let fs = FakeFs::new(cx.executor());
4960
4961 // let project = Project::test(fs, [], cx).await;
4962 // let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4963
4964 // let panel = workspace.update(cx, |workspace, cx| {
4965 // let panel = cx.build_view(|cx| TestPanel::new(DockPosition::Right, cx));
4966 // workspace.add_panel(panel.clone(), cx);
4967
4968 // workspace
4969 // .right_dock()
4970 // .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
4971
4972 // panel
4973 // });
4974
4975 // let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4976 // pane.update(cx, |pane, cx| {
4977 // let item = cx.build_view(|cx| TestItem::new(cx));
4978 // pane.add_item(Box::new(item), true, true, None, cx);
4979 // });
4980
4981 // // Transfer focus from center to panel
4982 // workspace.update(cx, |workspace, cx| {
4983 // workspace.toggle_panel_focus::<TestPanel>(cx);
4984 // });
4985
4986 // workspace.update(cx, |workspace, cx| {
4987 // assert!(workspace.right_dock().read(cx).is_open());
4988 // assert!(!panel.is_zoomed(cx));
4989 // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
4990 // });
4991
4992 // // Transfer focus from panel to center
4993 // workspace.update(cx, |workspace, cx| {
4994 // workspace.toggle_panel_focus::<TestPanel>(cx);
4995 // });
4996
4997 // workspace.update(cx, |workspace, cx| {
4998 // assert!(workspace.right_dock().read(cx).is_open());
4999 // assert!(!panel.is_zoomed(cx));
5000 // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5001 // });
5002
5003 // // Close the dock
5004 // workspace.update(cx, |workspace, cx| {
5005 // workspace.toggle_dock(DockPosition::Right, cx);
5006 // });
5007
5008 // workspace.update(cx, |workspace, cx| {
5009 // assert!(!workspace.right_dock().read(cx).is_open());
5010 // assert!(!panel.is_zoomed(cx));
5011 // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5012 // });
5013
5014 // // Open the dock
5015 // workspace.update(cx, |workspace, cx| {
5016 // workspace.toggle_dock(DockPosition::Right, cx);
5017 // });
5018
5019 // workspace.update(cx, |workspace, cx| {
5020 // assert!(workspace.right_dock().read(cx).is_open());
5021 // assert!(!panel.is_zoomed(cx));
5022 // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5023 // });
5024
5025 // // Focus and zoom panel
5026 // panel.update(cx, |panel, cx| {
5027 // cx.focus_self();
5028 // panel.set_zoomed(true, cx)
5029 // });
5030
5031 // workspace.update(cx, |workspace, cx| {
5032 // assert!(workspace.right_dock().read(cx).is_open());
5033 // assert!(panel.is_zoomed(cx));
5034 // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5035 // });
5036
5037 // // Transfer focus to the center closes the dock
5038 // workspace.update(cx, |workspace, cx| {
5039 // workspace.toggle_panel_focus::<TestPanel>(cx);
5040 // });
5041
5042 // workspace.update(cx, |workspace, cx| {
5043 // assert!(!workspace.right_dock().read(cx).is_open());
5044 // assert!(panel.is_zoomed(cx));
5045 // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5046 // });
5047
5048 // // Transferring focus back to the panel keeps it zoomed
5049 // workspace.update(cx, |workspace, cx| {
5050 // workspace.toggle_panel_focus::<TestPanel>(cx);
5051 // });
5052
5053 // workspace.update(cx, |workspace, cx| {
5054 // assert!(workspace.right_dock().read(cx).is_open());
5055 // assert!(panel.is_zoomed(cx));
5056 // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5057 // });
5058
5059 // // Close the dock while it is zoomed
5060 // workspace.update(cx, |workspace, cx| {
5061 // workspace.toggle_dock(DockPosition::Right, cx)
5062 // });
5063
5064 // workspace.update(cx, |workspace, cx| {
5065 // assert!(!workspace.right_dock().read(cx).is_open());
5066 // assert!(panel.is_zoomed(cx));
5067 // assert!(workspace.zoomed.is_none());
5068 // assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5069 // });
5070
5071 // // Opening the dock, when it's zoomed, retains focus
5072 // workspace.update(cx, |workspace, cx| {
5073 // workspace.toggle_dock(DockPosition::Right, cx)
5074 // });
5075
5076 // workspace.update(cx, |workspace, cx| {
5077 // assert!(workspace.right_dock().read(cx).is_open());
5078 // assert!(panel.is_zoomed(cx));
5079 // assert!(workspace.zoomed.is_some());
5080 // assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5081 // });
5082
5083 // // Unzoom and close the panel, zoom the active pane.
5084 // panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5085 // workspace.update(cx, |workspace, cx| {
5086 // workspace.toggle_dock(DockPosition::Right, cx)
5087 // });
5088 // pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5089
5090 // // Opening a dock unzooms the pane.
5091 // workspace.update(cx, |workspace, cx| {
5092 // workspace.toggle_dock(DockPosition::Right, cx)
5093 // });
5094 // workspace.update(cx, |workspace, cx| {
5095 // let pane = pane.read(cx);
5096 // assert!(!pane.is_zoomed());
5097 // assert!(!pane.focus_handle(cx).is_focused(cx));
5098 // assert!(workspace.right_dock().read(cx).is_open());
5099 // assert!(workspace.zoomed.is_none());
5100 // });
5101 // }
5102
5103 // #[gpui::test]
5104 // async fn test_panels(cx: &mut gpui::TestAppContext) {
5105 // init_test(cx);
5106 // let fs = FakeFs::new(cx.executor());
5107
5108 // let project = Project::test(fs, [], cx).await;
5109 // let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5110
5111 // let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5112 // // Add panel_1 on the left, panel_2 on the right.
5113 // let panel_1 = cx.build_view(|cx| TestPanel::new(DockPosition::Left, cx));
5114 // workspace.add_panel(panel_1.clone(), cx);
5115 // workspace
5116 // .left_dock()
5117 // .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5118 // let panel_2 = cx.build_view(|cx| TestPanel::new(DockPosition::Right, cx));
5119 // workspace.add_panel(panel_2.clone(), cx);
5120 // workspace
5121 // .right_dock()
5122 // .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5123
5124 // let left_dock = workspace.left_dock();
5125 // assert_eq!(
5126 // left_dock.read(cx).visible_panel().unwrap().panel_id(),
5127 // panel_1.panel_id()
5128 // );
5129 // assert_eq!(
5130 // left_dock.read(cx).active_panel_size(cx).unwrap(),
5131 // panel_1.size(cx)
5132 // );
5133
5134 // left_dock.update(cx, |left_dock, cx| {
5135 // left_dock.resize_active_panel(Some(1337.), cx)
5136 // });
5137 // assert_eq!(
5138 // workspace
5139 // .right_dock()
5140 // .read(cx)
5141 // .visible_panel()
5142 // .unwrap()
5143 // .panel_id(),
5144 // panel_2.panel_id(),
5145 // );
5146
5147 // (panel_1, panel_2)
5148 // });
5149
5150 // // Move panel_1 to the right
5151 // panel_1.update(cx, |panel_1, cx| {
5152 // panel_1.set_position(DockPosition::Right, cx)
5153 // });
5154
5155 // workspace.update(cx, |workspace, cx| {
5156 // // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5157 // // Since it was the only panel on the left, the left dock should now be closed.
5158 // assert!(!workspace.left_dock().read(cx).is_open());
5159 // assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5160 // let right_dock = workspace.right_dock();
5161 // assert_eq!(
5162 // right_dock.read(cx).visible_panel().unwrap().panel_id(),
5163 // panel_1.panel_id()
5164 // );
5165 // assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5166
5167 // // Now we move panel_2Β to the left
5168 // panel_2.set_position(DockPosition::Left, cx);
5169 // });
5170
5171 // workspace.update(cx, |workspace, cx| {
5172 // // Since panel_2 was not visible on the right, we don't open the left dock.
5173 // assert!(!workspace.left_dock().read(cx).is_open());
5174 // // And the right dock is unaffected in it's displaying of panel_1
5175 // assert!(workspace.right_dock().read(cx).is_open());
5176 // assert_eq!(
5177 // workspace
5178 // .right_dock()
5179 // .read(cx)
5180 // .visible_panel()
5181 // .unwrap()
5182 // .panel_id(),
5183 // panel_1.panel_id(),
5184 // );
5185 // });
5186
5187 // // Move panel_1 back to the left
5188 // panel_1.update(cx, |panel_1, cx| {
5189 // panel_1.set_position(DockPosition::Left, cx)
5190 // });
5191
5192 // workspace.update(cx, |workspace, cx| {
5193 // // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5194 // let left_dock = workspace.left_dock();
5195 // assert!(left_dock.read(cx).is_open());
5196 // assert_eq!(
5197 // left_dock.read(cx).visible_panel().unwrap().panel_id(),
5198 // panel_1.panel_id()
5199 // );
5200 // assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5201 // // And right the dock should be closed as it no longer has any panels.
5202 // assert!(!workspace.right_dock().read(cx).is_open());
5203
5204 // // Now we move panel_1 to the bottom
5205 // panel_1.set_position(DockPosition::Bottom, cx);
5206 // });
5207
5208 // workspace.update(cx, |workspace, cx| {
5209 // // Since panel_1 was visible on the left, we close the left dock.
5210 // assert!(!workspace.left_dock().read(cx).is_open());
5211 // // The bottom dock is sized based on the panel's default size,
5212 // // since the panel orientation changed from vertical to horizontal.
5213 // let bottom_dock = workspace.bottom_dock();
5214 // assert_eq!(
5215 // bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5216 // panel_1.size(cx),
5217 // );
5218 // // Close bottom dock and move panel_1 back to the left.
5219 // bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5220 // panel_1.set_position(DockPosition::Left, cx);
5221 // });
5222
5223 // // Emit activated event on panel 1
5224 // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
5225
5226 // // Now the left dock is open and panel_1 is active and focused.
5227 // workspace.update(cx, |workspace, cx| {
5228 // let left_dock = workspace.left_dock();
5229 // assert!(left_dock.read(cx).is_open());
5230 // assert_eq!(
5231 // left_dock.read(cx).visible_panel().unwrap().panel_id(),
5232 // panel_1.panel_id(),
5233 // );
5234 // assert!(panel_1.focus_handle(cx).is_focused(cx));
5235 // });
5236
5237 // // Emit closed event on panel 2, which is not active
5238 // panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5239
5240 // // Wo don't close the left dock, because panel_2 wasn't the active panel
5241 // workspace.update(cx, |workspace, cx| {
5242 // let left_dock = workspace.left_dock();
5243 // assert!(left_dock.read(cx).is_open());
5244 // assert_eq!(
5245 // left_dock.read(cx).visible_panel().unwrap().panel_id(),
5246 // panel_1.panel_id(),
5247 // );
5248 // });
5249
5250 // // Emitting a ZoomIn event shows the panel as zoomed.
5251 // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
5252 // workspace.update(cx, |workspace, _| {
5253 // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5254 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5255 // });
5256
5257 // // Move panel to another dock while it is zoomed
5258 // panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5259 // workspace.update(cx, |workspace, _| {
5260 // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5261
5262 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5263 // });
5264
5265 // // If focus is transferred to another view that's not a panel or another pane, we still show
5266 // // the panel as zoomed.
5267 // let other_focus_handle = cx.update(|cx| cx.focus_handle());
5268 // cx.update(|cx| cx.focus(&other_focus_handle));
5269 // workspace.update(cx, |workspace, _| {
5270 // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5271 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5272 // });
5273
5274 // // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5275 // workspace.update(cx, |_, cx| cx.focus_self());
5276 // workspace.update(cx, |workspace, _| {
5277 // assert_eq!(workspace.zoomed, None);
5278 // assert_eq!(workspace.zoomed_position, None);
5279 // });
5280
5281 // // If focus is transferred again to another view that's not a panel or a pane, we won't
5282 // // show the panel as zoomed because it wasn't zoomed before.
5283 // cx.update(|cx| cx.focus(&other_focus_handle));
5284 // workspace.update(cx, |workspace, _| {
5285 // assert_eq!(workspace.zoomed, None);
5286 // assert_eq!(workspace.zoomed_position, None);
5287 // });
5288
5289 // // When focus is transferred back to the panel, it is zoomed again.
5290 // panel_1.update(cx, |_, cx| cx.focus_self());
5291 // workspace.update(cx, |workspace, _| {
5292 // assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5293 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5294 // });
5295
5296 // // Emitting a ZoomOut event unzooms the panel.
5297 // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
5298 // workspace.update(cx, |workspace, _| {
5299 // assert_eq!(workspace.zoomed, None);
5300 // assert_eq!(workspace.zoomed_position, None);
5301 // });
5302
5303 // // Emit closed event on panel 1, which is active
5304 // panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5305
5306 // // Now the left dock is closed, because panel_1 was the active panel
5307 // workspace.update(cx, |workspace, cx| {
5308 // let right_dock = workspace.right_dock();
5309 // assert!(!right_dock.read(cx).is_open());
5310 // });
5311 // }
5312
5313 pub fn init_test(cx: &mut TestAppContext) {
5314 cx.update(|cx| {
5315 let settings_store = SettingsStore::test(cx);
5316 cx.set_global(settings_store);
5317 theme::init(theme::LoadThemes::JustBase, cx);
5318 language::init(cx);
5319 crate::init_settings(cx);
5320 Project::init_settings(cx);
5321 });
5322 }
5323}