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