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