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