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