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