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