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