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