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