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