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