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