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