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