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