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