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