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