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 .flex_col()
3631 .overflow_hidden()
3632 .border_t()
3633 .border_b()
3634 .border_color(cx.theme().colors().border)
3635 .on_mouse_up(gpui::MouseButton::Left, |_, cx| {
3636 cx.update_global(|drag: &mut DockDragState, cx| {
3637 drag.0 = None;
3638 })
3639 })
3640 .on_mouse_move(cx.listener(|workspace, e: &MouseMoveEvent, cx| {
3641 if let Some(types) = &cx.global::<DockDragState>().0 {
3642 let workspace_bounds = cx.global::<WorkspaceBounds>().0;
3643 match types {
3644 DockPosition::Left => {
3645 let size = e.position.x;
3646 workspace.left_dock.update(cx, |left_dock, cx| {
3647 left_dock.resize_active_panel(Some(size.0), cx);
3648 });
3649 }
3650 DockPosition::Right => {
3651 let size = workspace_bounds.size.width - e.position.x;
3652 workspace.right_dock.update(cx, |right_dock, cx| {
3653 right_dock.resize_active_panel(Some(size.0), cx);
3654 });
3655 }
3656 DockPosition::Bottom => {
3657 let size = workspace_bounds.size.height - e.position.y;
3658 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
3659 bottom_dock.resize_active_panel(Some(size.0), cx);
3660 });
3661 }
3662 }
3663 }
3664 }))
3665 .child(canvas(|bounds, cx| cx.set_global(WorkspaceBounds(bounds))))
3666 .child(self.modal_layer.clone())
3667 .child(
3668 div()
3669 .flex()
3670 .flex_row()
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 .overflow_hidden()
3687 .child(self.center.render(
3688 &self.project,
3689 &self.follower_states,
3690 self.active_call(),
3691 &self.active_pane,
3692 self.zoomed.as_ref(),
3693 &self.app_state,
3694 cx,
3695 ))
3696 .child(self.bottom_dock.clone()),
3697 )
3698 // Right Dock
3699 .child(
3700 div()
3701 .flex()
3702 .flex_none()
3703 .overflow_hidden()
3704 .child(self.right_dock.clone()),
3705 ),
3706 )
3707 .children(self.render_notifications(cx)),
3708 )
3709 .child(self.status_bar.clone())
3710 }
3711}
3712
3713// impl View for Workspace {
3714
3715// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
3716// let theme = theme::current(cx).clone();
3717// Stack::new()
3718// .with_child(
3719// Flex::column()
3720// .with_child(self.render_titlebar(&theme, cx))
3721// .with_child(
3722// Stack::new()
3723// .with_child({
3724// let project = self.project.clone();
3725// Flex::row()
3726// .with_children(self.render_dock(DockPosition::Left, cx))
3727// .with_child(
3728// Flex::column()
3729// .with_child(
3730// FlexItem::new(
3731// self.center.render(
3732// &project,
3733// &theme,
3734// &self.follower_states,
3735// self.active_call(),
3736// self.active_pane(),
3737// self.zoomed
3738// .as_ref()
3739// .and_then(|zoomed| zoomed.upgrade(cx))
3740// .as_ref(),
3741// &self.app_state,
3742// cx,
3743// ),
3744// )
3745// .flex(1., true),
3746// )
3747// .with_children(
3748// self.render_dock(DockPosition::Bottom, cx),
3749// )
3750// .flex(1., true),
3751// )
3752// .with_children(self.render_dock(DockPosition::Right, cx))
3753// })
3754// .with_child(Overlay::new(
3755// Stack::new()
3756// .with_children(self.zoomed.as_ref().and_then(|zoomed| {
3757// enum ZoomBackground {}
3758// let zoomed = zoomed.upgrade(cx)?;
3759
3760// let mut foreground_style =
3761// theme.workspace.zoomed_pane_foreground;
3762// if let Some(zoomed_dock_position) = self.zoomed_position {
3763// foreground_style =
3764// theme.workspace.zoomed_panel_foreground;
3765// let margin = foreground_style.margin.top;
3766// let border = foreground_style.border.top;
3767
3768// // Only include a margin and border on the opposite side.
3769// foreground_style.margin.top = 0.;
3770// foreground_style.margin.left = 0.;
3771// foreground_style.margin.bottom = 0.;
3772// foreground_style.margin.right = 0.;
3773// foreground_style.border.top = false;
3774// foreground_style.border.left = false;
3775// foreground_style.border.bottom = false;
3776// foreground_style.border.right = false;
3777// match zoomed_dock_position {
3778// DockPosition::Left => {
3779// foreground_style.margin.right = margin;
3780// foreground_style.border.right = border;
3781// }
3782// DockPosition::Right => {
3783// foreground_style.margin.left = margin;
3784// foreground_style.border.left = border;
3785// }
3786// DockPosition::Bottom => {
3787// foreground_style.margin.top = margin;
3788// foreground_style.border.top = border;
3789// }
3790// }
3791// }
3792
3793// Some(
3794// ChildView::new(&zoomed, cx)
3795// .contained()
3796// .with_style(foreground_style)
3797// .aligned()
3798// .contained()
3799// .with_style(theme.workspace.zoomed_background)
3800// .mouse::<ZoomBackground>(0)
3801// .capture_all()
3802// .on_down(
3803// MouseButton::Left,
3804// |_, this: &mut Self, cx| {
3805// this.zoom_out(cx);
3806// },
3807// ),
3808// )
3809// }))
3810// .with_children(self.modal.as_ref().map(|modal| {
3811// // Prevent clicks within the modal from falling
3812// // through to the rest of the workspace.
3813// enum ModalBackground {}
3814// MouseEventHandler::new::<ModalBackground, _>(
3815// 0,
3816// cx,
3817// |_, cx| ChildView::new(modal.view.as_any(), cx),
3818// )
3819// .on_click(MouseButton::Left, |_, _, _| {})
3820// .contained()
3821// .with_style(theme.workspace.modal)
3822// .aligned()
3823// .top()
3824// }))
3825// .with_children(self.render_notifications(&theme.workspace, cx)),
3826// ))
3827// .provide_resize_bounds::<WorkspaceBounds>()
3828// .flex(1.0, true),
3829// )
3830// .with_child(ChildView::new(&self.status_bar, cx))
3831// .contained()
3832// .with_background_color(theme.workspace.background),
3833// )
3834// .with_children(DragAndDrop::render(cx))
3835// .with_children(self.render_disconnected_overlay(cx))
3836// .into_any_named("workspace")
3837// }
3838
3839// fn modifiers_changed(&mut self, e: &ModifiersChangedEvent, cx: &mut ViewContext<Self>) -> bool {
3840// DragAndDrop::<Workspace>::update_modifiers(e.modifiers, cx)
3841// }
3842// }
3843
3844impl WorkspaceStore {
3845 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
3846 Self {
3847 workspaces: Default::default(),
3848 followers: Default::default(),
3849 _subscriptions: vec![
3850 client.add_request_handler(cx.weak_model(), Self::handle_follow),
3851 client.add_message_handler(cx.weak_model(), Self::handle_unfollow),
3852 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
3853 ],
3854 client,
3855 }
3856 }
3857
3858 pub fn update_followers(
3859 &self,
3860 project_id: Option<u64>,
3861 update: proto::update_followers::Variant,
3862 cx: &AppContext,
3863 ) -> Option<()> {
3864 if !cx.has_global::<Model<ActiveCall>>() {
3865 return None;
3866 }
3867
3868 let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id();
3869 let follower_ids: Vec<_> = self
3870 .followers
3871 .iter()
3872 .filter_map(|follower| {
3873 if follower.project_id == project_id || project_id.is_none() {
3874 Some(follower.peer_id.into())
3875 } else {
3876 None
3877 }
3878 })
3879 .collect();
3880 if follower_ids.is_empty() {
3881 return None;
3882 }
3883 self.client
3884 .send(proto::UpdateFollowers {
3885 room_id,
3886 project_id,
3887 follower_ids,
3888 variant: Some(update),
3889 })
3890 .log_err()
3891 }
3892
3893 pub async fn handle_follow(
3894 this: Model<Self>,
3895 envelope: TypedEnvelope<proto::Follow>,
3896 _: Arc<Client>,
3897 mut cx: AsyncAppContext,
3898 ) -> Result<proto::FollowResponse> {
3899 this.update(&mut cx, |this, cx| {
3900 let follower = Follower {
3901 project_id: envelope.payload.project_id,
3902 peer_id: envelope.original_sender_id()?,
3903 };
3904 let active_project = ActiveCall::global(cx).read(cx).location().cloned();
3905
3906 let mut response = proto::FollowResponse::default();
3907 for workspace in &this.workspaces {
3908 workspace
3909 .update(cx, |workspace, cx| {
3910 let handler_response = workspace.handle_follow(follower.project_id, cx);
3911 if response.views.is_empty() {
3912 response.views = handler_response.views;
3913 } else {
3914 response.views.extend_from_slice(&handler_response.views);
3915 }
3916
3917 if let Some(active_view_id) = handler_response.active_view_id.clone() {
3918 if response.active_view_id.is_none()
3919 || Some(workspace.project.downgrade()) == active_project
3920 {
3921 response.active_view_id = Some(active_view_id);
3922 }
3923 }
3924 })
3925 .ok();
3926 }
3927
3928 if let Err(ix) = this.followers.binary_search(&follower) {
3929 this.followers.insert(ix, follower);
3930 }
3931
3932 Ok(response)
3933 })?
3934 }
3935
3936 async fn handle_unfollow(
3937 model: Model<Self>,
3938 envelope: TypedEnvelope<proto::Unfollow>,
3939 _: Arc<Client>,
3940 mut cx: AsyncAppContext,
3941 ) -> Result<()> {
3942 model.update(&mut cx, |this, _| {
3943 let follower = Follower {
3944 project_id: envelope.payload.project_id,
3945 peer_id: envelope.original_sender_id()?,
3946 };
3947 if let Ok(ix) = this.followers.binary_search(&follower) {
3948 this.followers.remove(ix);
3949 }
3950 Ok(())
3951 })?
3952 }
3953
3954 async fn handle_update_followers(
3955 this: Model<Self>,
3956 envelope: TypedEnvelope<proto::UpdateFollowers>,
3957 _: Arc<Client>,
3958 mut cx: AsyncAppContext,
3959 ) -> Result<()> {
3960 let leader_id = envelope.original_sender_id()?;
3961 let update = envelope.payload;
3962
3963 this.update(&mut cx, |this, cx| {
3964 for workspace in &this.workspaces {
3965 workspace.update(cx, |workspace, cx| {
3966 let project_id = workspace.project.read(cx).remote_id();
3967 if update.project_id != project_id && update.project_id.is_some() {
3968 return;
3969 }
3970 workspace.handle_update_followers(leader_id, update.clone(), cx);
3971 })?;
3972 }
3973 Ok(())
3974 })?
3975 }
3976}
3977
3978impl ViewId {
3979 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
3980 Ok(Self {
3981 creator: message
3982 .creator
3983 .ok_or_else(|| anyhow!("creator is missing"))?,
3984 id: message.id,
3985 })
3986 }
3987
3988 pub(crate) fn to_proto(&self) -> proto::ViewId {
3989 proto::ViewId {
3990 creator: Some(self.creator),
3991 id: self.id,
3992 }
3993 }
3994}
3995
3996pub trait WorkspaceHandle {
3997 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
3998}
3999
4000impl WorkspaceHandle for View<Workspace> {
4001 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
4002 self.read(cx)
4003 .worktrees(cx)
4004 .flat_map(|worktree| {
4005 let worktree_id = worktree.read(cx).id();
4006 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
4007 worktree_id,
4008 path: f.path.clone(),
4009 })
4010 })
4011 .collect::<Vec<_>>()
4012 }
4013}
4014
4015impl std::fmt::Debug for OpenPaths {
4016 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4017 f.debug_struct("OpenPaths")
4018 .field("paths", &self.paths)
4019 .finish()
4020 }
4021}
4022
4023pub fn activate_workspace_for_project(
4024 cx: &mut AppContext,
4025 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
4026) -> Option<WindowHandle<Workspace>> {
4027 for window in cx.windows() {
4028 let Some(workspace) = window.downcast::<Workspace>() else {
4029 continue;
4030 };
4031
4032 let predicate = workspace
4033 .update(cx, |workspace, cx| {
4034 let project = workspace.project.read(cx);
4035 if predicate(project, cx) {
4036 cx.activate_window();
4037 true
4038 } else {
4039 false
4040 }
4041 })
4042 .log_err()
4043 .unwrap_or(false);
4044
4045 if predicate {
4046 return Some(workspace);
4047 }
4048 }
4049
4050 None
4051}
4052
4053pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
4054 DB.last_workspace().await.log_err().flatten()
4055}
4056
4057async fn join_channel_internal(
4058 channel_id: u64,
4059 app_state: &Arc<AppState>,
4060 requesting_window: Option<WindowHandle<Workspace>>,
4061 active_call: &Model<ActiveCall>,
4062 cx: &mut AsyncAppContext,
4063) -> Result<bool> {
4064 let (should_prompt, open_room) = active_call.read_with(cx, |active_call, cx| {
4065 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
4066 return (false, None);
4067 };
4068
4069 let already_in_channel = room.channel_id() == Some(channel_id);
4070 let should_prompt = room.is_sharing_project()
4071 && room.remote_participants().len() > 0
4072 && !already_in_channel;
4073 let open_room = if already_in_channel {
4074 active_call.room().cloned()
4075 } else {
4076 None
4077 };
4078 (should_prompt, open_room)
4079 })?;
4080
4081 if let Some(room) = open_room {
4082 let task = room.update(cx, |room, cx| {
4083 if let Some((project, host)) = room.most_active_project(cx) {
4084 return Some(join_remote_project(project, host, app_state.clone(), cx));
4085 }
4086
4087 None
4088 })?;
4089 if let Some(task) = task {
4090 task.await?;
4091 }
4092 return anyhow::Ok(true);
4093 }
4094
4095 if should_prompt {
4096 if let Some(workspace) = requesting_window {
4097 let answer = workspace.update(cx, |_, cx| {
4098 cx.prompt(
4099 PromptLevel::Warning,
4100 "Leaving this call will unshare your current project.\nDo you want to switch channels?",
4101 &["Yes, Join Channel", "Cancel"],
4102 )
4103 })?.await;
4104
4105 if answer == Ok(1) {
4106 return Ok(false);
4107 }
4108 } else {
4109 return Ok(false); // unreachable!() hopefully
4110 }
4111 }
4112
4113 let client = cx.update(|cx| active_call.read(cx).client())?;
4114
4115 let mut client_status = client.status();
4116
4117 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
4118 'outer: loop {
4119 let Some(status) = client_status.recv().await else {
4120 return Err(anyhow!("error connecting"));
4121 };
4122
4123 match status {
4124 Status::Connecting
4125 | Status::Authenticating
4126 | Status::Reconnecting
4127 | Status::Reauthenticating => continue,
4128 Status::Connected { .. } => break 'outer,
4129 Status::SignedOut => return Err(anyhow!("not signed in")),
4130 Status::UpgradeRequired => return Err(anyhow!("zed is out of date")),
4131 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
4132 return Err(anyhow!("zed is offline"))
4133 }
4134 }
4135 }
4136
4137 let room = active_call
4138 .update(cx, |active_call, cx| {
4139 active_call.join_channel(channel_id, cx)
4140 })?
4141 .await?;
4142
4143 let Some(room) = room else {
4144 return anyhow::Ok(true);
4145 };
4146
4147 room.update(cx, |room, _| room.room_update_completed())?
4148 .await;
4149
4150 let task = room.update(cx, |room, cx| {
4151 if let Some((project, host)) = room.most_active_project(cx) {
4152 return Some(join_remote_project(project, host, app_state.clone(), cx));
4153 }
4154
4155 None
4156 })?;
4157 if let Some(task) = task {
4158 task.await?;
4159 return anyhow::Ok(true);
4160 }
4161 anyhow::Ok(false)
4162}
4163
4164pub fn join_channel(
4165 channel_id: u64,
4166 app_state: Arc<AppState>,
4167 requesting_window: Option<WindowHandle<Workspace>>,
4168 cx: &mut AppContext,
4169) -> Task<Result<()>> {
4170 let active_call = ActiveCall::global(cx);
4171 cx.spawn(|mut cx| async move {
4172 let result = join_channel_internal(
4173 channel_id,
4174 &app_state,
4175 requesting_window,
4176 &active_call,
4177 &mut cx,
4178 )
4179 .await;
4180
4181 // join channel succeeded, and opened a window
4182 if matches!(result, Ok(true)) {
4183 return anyhow::Ok(());
4184 }
4185
4186 if requesting_window.is_some() {
4187 return anyhow::Ok(());
4188 }
4189
4190 // find an existing workspace to focus and show call controls
4191 let mut active_window = activate_any_workspace_window(&mut cx);
4192 if active_window.is_none() {
4193 // no open workspaces, make one to show the error in (blergh)
4194 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx))?
4195 .await?;
4196 }
4197
4198 active_window = activate_any_workspace_window(&mut cx);
4199 let Some(active_window) = active_window else {
4200 return anyhow::Ok(());
4201 };
4202
4203 if let Err(err) = result {
4204 active_window
4205 .update(&mut cx, |_, cx| {
4206 cx.prompt(
4207 PromptLevel::Critical,
4208 &format!("Failed to join channel: {}", err),
4209 &["Ok"],
4210 )
4211 })?
4212 .await
4213 .ok();
4214 }
4215
4216 // return ok, we showed the error to the user.
4217 return anyhow::Ok(());
4218 })
4219}
4220
4221pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<AnyWindowHandle> {
4222 cx.update(|cx| {
4223 for window in cx.windows() {
4224 let is_workspace = window.downcast::<Workspace>().is_some();
4225 if is_workspace {
4226 window.update(cx, |_, cx| cx.activate_window()).ok();
4227 return Some(window);
4228 }
4229 }
4230 None
4231 })
4232 .ok()
4233 .flatten()
4234}
4235
4236#[allow(clippy::type_complexity)]
4237pub fn open_paths(
4238 abs_paths: &[PathBuf],
4239 app_state: &Arc<AppState>,
4240 requesting_window: Option<WindowHandle<Workspace>>,
4241 cx: &mut AppContext,
4242) -> Task<
4243 anyhow::Result<(
4244 WindowHandle<Workspace>,
4245 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4246 )>,
4247> {
4248 let app_state = app_state.clone();
4249 let abs_paths = abs_paths.to_vec();
4250 // Open paths in existing workspace if possible
4251 let existing = activate_workspace_for_project(cx, {
4252 let abs_paths = abs_paths.clone();
4253 move |project, cx| project.contains_paths(&abs_paths, cx)
4254 });
4255 cx.spawn(move |mut cx| async move {
4256 if let Some(existing) = existing {
4257 Ok((
4258 existing.clone(),
4259 existing
4260 .update(&mut cx, |workspace, cx| {
4261 workspace.open_paths(abs_paths, true, cx)
4262 })?
4263 .await,
4264 ))
4265 } else {
4266 cx.update(move |cx| {
4267 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
4268 })?
4269 .await
4270 }
4271 })
4272}
4273
4274pub fn open_new(
4275 app_state: &Arc<AppState>,
4276 cx: &mut AppContext,
4277 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
4278) -> Task<()> {
4279 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
4280 cx.spawn(|mut cx| async move {
4281 if let Some((workspace, opened_paths)) = task.await.log_err() {
4282 workspace
4283 .update(&mut cx, |workspace, cx| {
4284 if opened_paths.is_empty() {
4285 init(workspace, cx)
4286 }
4287 })
4288 .log_err();
4289 }
4290 })
4291}
4292
4293pub fn create_and_open_local_file(
4294 path: &'static Path,
4295 cx: &mut ViewContext<Workspace>,
4296 default_content: impl 'static + Send + FnOnce() -> Rope,
4297) -> Task<Result<Box<dyn ItemHandle>>> {
4298 cx.spawn(|workspace, mut cx| async move {
4299 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
4300 if !fs.is_file(path).await {
4301 fs.create_file(path, Default::default()).await?;
4302 fs.save(path, &default_content(), Default::default())
4303 .await?;
4304 }
4305
4306 let mut items = workspace
4307 .update(&mut cx, |workspace, cx| {
4308 workspace.with_local_workspace(cx, |workspace, cx| {
4309 workspace.open_paths(vec![path.to_path_buf()], false, cx)
4310 })
4311 })?
4312 .await?
4313 .await;
4314
4315 let item = items.pop().flatten();
4316 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4317 })
4318}
4319
4320pub fn join_remote_project(
4321 project_id: u64,
4322 follow_user_id: u64,
4323 app_state: Arc<AppState>,
4324 cx: &mut AppContext,
4325) -> Task<Result<()>> {
4326 let windows = cx.windows();
4327 cx.spawn(|mut cx| async move {
4328 let existing_workspace = windows.into_iter().find_map(|window| {
4329 window.downcast::<Workspace>().and_then(|window| {
4330 window
4331 .update(&mut cx, |workspace, cx| {
4332 if workspace.project().read(cx).remote_id() == Some(project_id) {
4333 Some(window)
4334 } else {
4335 None
4336 }
4337 })
4338 .unwrap_or(None)
4339 })
4340 });
4341
4342 let workspace = if let Some(existing_workspace) = existing_workspace {
4343 existing_workspace
4344 } else {
4345 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
4346 let room = active_call
4347 .read_with(&cx, |call, _| call.room().cloned())?
4348 .ok_or_else(|| anyhow!("not in a call"))?;
4349 let project = room
4350 .update(&mut cx, |room, cx| {
4351 room.join_project(
4352 project_id,
4353 app_state.languages.clone(),
4354 app_state.fs.clone(),
4355 cx,
4356 )
4357 })?
4358 .await?;
4359
4360 let window_bounds_override = window_bounds_env_override(&cx);
4361 cx.update(|cx| {
4362 let options = (app_state.build_window_options)(window_bounds_override, None, cx);
4363 cx.open_window(options, |cx| {
4364 cx.build_view(|cx| Workspace::new(0, project, app_state.clone(), cx))
4365 })
4366 })?
4367 };
4368
4369 workspace.update(&mut cx, |workspace, cx| {
4370 cx.activate(true);
4371 cx.activate_window();
4372
4373 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4374 let follow_peer_id = room
4375 .read(cx)
4376 .remote_participants()
4377 .iter()
4378 .find(|(_, participant)| participant.user.id == follow_user_id)
4379 .map(|(_, p)| p.peer_id)
4380 .or_else(|| {
4381 // If we couldn't follow the given user, follow the host instead.
4382 let collaborator = workspace
4383 .project()
4384 .read(cx)
4385 .collaborators()
4386 .values()
4387 .find(|collaborator| collaborator.replica_id == 0)?;
4388 Some(collaborator.peer_id)
4389 });
4390
4391 if let Some(follow_peer_id) = follow_peer_id {
4392 workspace
4393 .follow(follow_peer_id, cx)
4394 .map(|follow| follow.detach_and_log_err(cx));
4395 }
4396 }
4397 })?;
4398
4399 anyhow::Ok(())
4400 })
4401}
4402
4403pub fn restart(_: &Restart, cx: &mut AppContext) {
4404 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
4405 let mut workspace_windows = cx
4406 .windows()
4407 .into_iter()
4408 .filter_map(|window| window.downcast::<Workspace>())
4409 .collect::<Vec<_>>();
4410
4411 // If multiple windows have unsaved changes, and need a save prompt,
4412 // prompt in the active window before switching to a different window.
4413 workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
4414
4415 let mut prompt = None;
4416 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4417 prompt = window
4418 .update(cx, |_, cx| {
4419 cx.prompt(
4420 PromptLevel::Info,
4421 "Are you sure you want to restart?",
4422 &["Restart", "Cancel"],
4423 )
4424 })
4425 .ok();
4426 }
4427
4428 cx.spawn(|mut cx| async move {
4429 if let Some(mut prompt) = prompt {
4430 let answer = prompt.await?;
4431 if answer != 0 {
4432 return Ok(());
4433 }
4434 }
4435
4436 // If the user cancels any save prompt, then keep the app open.
4437 for window in workspace_windows {
4438 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
4439 workspace.prepare_to_close(true, cx)
4440 }) {
4441 if !should_close.await? {
4442 return Ok(());
4443 }
4444 }
4445 }
4446
4447 cx.update(|cx| cx.restart())
4448 })
4449 .detach_and_log_err(cx);
4450}
4451
4452fn parse_pixel_position_env_var(value: &str) -> Option<Point<GlobalPixels>> {
4453 let mut parts = value.split(',');
4454 let x: usize = parts.next()?.parse().ok()?;
4455 let y: usize = parts.next()?.parse().ok()?;
4456 Some(point((x as f64).into(), (y as f64).into()))
4457}
4458
4459fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
4460 let mut parts = value.split(',');
4461 let width: usize = parts.next()?.parse().ok()?;
4462 let height: usize = parts.next()?.parse().ok()?;
4463 Some(size((width as f64).into(), (height as f64).into()))
4464}
4465
4466#[cfg(test)]
4467mod tests {
4468 use super::*;
4469 use crate::item::{
4470 test::{TestItem, TestProjectItem},
4471 ItemEvent,
4472 };
4473 use fs::FakeFs;
4474 use gpui::TestAppContext;
4475 use project::{Project, ProjectEntryId};
4476 use serde_json::json;
4477 use settings::SettingsStore;
4478 use std::{cell::RefCell, rc::Rc};
4479
4480 #[gpui::test]
4481 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4482 init_test(cx);
4483
4484 let fs = FakeFs::new(cx.executor());
4485 let project = Project::test(fs, [], cx).await;
4486 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4487
4488 // Adding an item with no ambiguity renders the tab without detail.
4489 let item1 = cx.build_view(|cx| {
4490 let mut item = TestItem::new(cx);
4491 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4492 item
4493 });
4494 workspace.update(cx, |workspace, cx| {
4495 workspace.add_item(Box::new(item1.clone()), cx);
4496 });
4497 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
4498
4499 // Adding an item that creates ambiguity increases the level of detail on
4500 // both tabs.
4501 let item2 = cx.build_view(|cx| {
4502 let mut item = TestItem::new(cx);
4503 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4504 item
4505 });
4506 workspace.update(cx, |workspace, cx| {
4507 workspace.add_item(Box::new(item2.clone()), cx);
4508 });
4509 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4510 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4511
4512 // Adding an item that creates ambiguity increases the level of detail only
4513 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4514 // we stop at the highest detail available.
4515 let item3 = cx.build_view(|cx| {
4516 let mut item = TestItem::new(cx);
4517 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4518 item
4519 });
4520 workspace.update(cx, |workspace, cx| {
4521 workspace.add_item(Box::new(item3.clone()), cx);
4522 });
4523 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4524 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4525 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4526 }
4527
4528 #[gpui::test]
4529 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4530 init_test(cx);
4531
4532 let fs = FakeFs::new(cx.executor());
4533 fs.insert_tree(
4534 "/root1",
4535 json!({
4536 "one.txt": "",
4537 "two.txt": "",
4538 }),
4539 )
4540 .await;
4541 fs.insert_tree(
4542 "/root2",
4543 json!({
4544 "three.txt": "",
4545 }),
4546 )
4547 .await;
4548
4549 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4550 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4551 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4552 let worktree_id = project.read_with(cx, |project, cx| {
4553 project.worktrees().next().unwrap().read(cx).id()
4554 });
4555
4556 let item1 = cx.build_view(|cx| {
4557 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4558 });
4559 let item2 = cx.build_view(|cx| {
4560 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4561 });
4562
4563 // Add an item to an empty pane
4564 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4565 project.read_with(cx, |project, cx| {
4566 assert_eq!(
4567 project.active_entry(),
4568 project
4569 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4570 .map(|e| e.id)
4571 );
4572 });
4573 assert_eq!(cx.window_title().as_deref(), Some("one.txt β root1"));
4574
4575 // Add a second item to a non-empty pane
4576 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4577 assert_eq!(cx.window_title().as_deref(), Some("two.txt β root1"));
4578 project.read_with(cx, |project, cx| {
4579 assert_eq!(
4580 project.active_entry(),
4581 project
4582 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4583 .map(|e| e.id)
4584 );
4585 });
4586
4587 // Close the active item
4588 pane.update(cx, |pane, cx| {
4589 pane.close_active_item(&Default::default(), cx).unwrap()
4590 })
4591 .await
4592 .unwrap();
4593 assert_eq!(cx.window_title().as_deref(), Some("one.txt β root1"));
4594 project.read_with(cx, |project, cx| {
4595 assert_eq!(
4596 project.active_entry(),
4597 project
4598 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4599 .map(|e| e.id)
4600 );
4601 });
4602
4603 // Add a project folder
4604 project
4605 .update(cx, |project, cx| {
4606 project.find_or_create_local_worktree("/root2", true, cx)
4607 })
4608 .await
4609 .unwrap();
4610 assert_eq!(cx.window_title().as_deref(), Some("one.txt β root1, root2"));
4611
4612 // Remove a project folder
4613 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4614 assert_eq!(cx.window_title().as_deref(), Some("one.txt β root2"));
4615 }
4616
4617 #[gpui::test]
4618 async fn test_close_window(cx: &mut TestAppContext) {
4619 init_test(cx);
4620
4621 let fs = FakeFs::new(cx.executor());
4622 fs.insert_tree("/root", json!({ "one": "" })).await;
4623
4624 let project = Project::test(fs, ["root".as_ref()], cx).await;
4625 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4626
4627 // When there are no dirty items, there's nothing to do.
4628 let item1 = cx.build_view(|cx| TestItem::new(cx));
4629 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4630 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4631 assert!(task.await.unwrap());
4632
4633 // When there are dirty untitled items, prompt to save each one. If the user
4634 // cancels any prompt, then abort.
4635 let item2 = cx.build_view(|cx| TestItem::new(cx).with_dirty(true));
4636 let item3 = cx.build_view(|cx| {
4637 TestItem::new(cx)
4638 .with_dirty(true)
4639 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4640 });
4641 workspace.update(cx, |w, cx| {
4642 w.add_item(Box::new(item2.clone()), cx);
4643 w.add_item(Box::new(item3.clone()), cx);
4644 });
4645 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4646 cx.executor().run_until_parked();
4647 cx.simulate_prompt_answer(2); // cancel save all
4648 cx.executor().run_until_parked();
4649 cx.simulate_prompt_answer(2); // cancel save all
4650 cx.executor().run_until_parked();
4651 assert!(!cx.has_pending_prompt());
4652 assert!(!task.await.unwrap());
4653 }
4654
4655 #[gpui::test]
4656 async fn test_close_pane_items(cx: &mut TestAppContext) {
4657 init_test(cx);
4658
4659 let fs = FakeFs::new(cx.executor());
4660
4661 let project = Project::test(fs, None, cx).await;
4662 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4663
4664 let item1 = cx.build_view(|cx| {
4665 TestItem::new(cx)
4666 .with_dirty(true)
4667 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4668 });
4669 let item2 = cx.build_view(|cx| {
4670 TestItem::new(cx)
4671 .with_dirty(true)
4672 .with_conflict(true)
4673 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4674 });
4675 let item3 = cx.build_view(|cx| {
4676 TestItem::new(cx)
4677 .with_dirty(true)
4678 .with_conflict(true)
4679 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4680 });
4681 let item4 = cx.build_view(|cx| {
4682 TestItem::new(cx)
4683 .with_dirty(true)
4684 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4685 });
4686 let pane = workspace.update(cx, |workspace, cx| {
4687 workspace.add_item(Box::new(item1.clone()), cx);
4688 workspace.add_item(Box::new(item2.clone()), cx);
4689 workspace.add_item(Box::new(item3.clone()), cx);
4690 workspace.add_item(Box::new(item4.clone()), cx);
4691 workspace.active_pane().clone()
4692 });
4693
4694 let close_items = pane.update(cx, |pane, cx| {
4695 pane.activate_item(1, true, true, cx);
4696 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4697 let item1_id = item1.item_id();
4698 let item3_id = item3.item_id();
4699 let item4_id = item4.item_id();
4700 pane.close_items(cx, SaveIntent::Close, move |id| {
4701 [item1_id, item3_id, item4_id].contains(&id)
4702 })
4703 });
4704 cx.executor().run_until_parked();
4705
4706 assert!(cx.has_pending_prompt());
4707 // Ignore "Save all" prompt
4708 cx.simulate_prompt_answer(2);
4709 cx.executor().run_until_parked();
4710 // There's a prompt to save item 1.
4711 pane.update(cx, |pane, _| {
4712 assert_eq!(pane.items_len(), 4);
4713 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
4714 });
4715 // Confirm saving item 1.
4716 cx.simulate_prompt_answer(0);
4717 cx.executor().run_until_parked();
4718
4719 // Item 1 is saved. There's a prompt to save item 3.
4720 pane.update(cx, |pane, cx| {
4721 assert_eq!(item1.read(cx).save_count, 1);
4722 assert_eq!(item1.read(cx).save_as_count, 0);
4723 assert_eq!(item1.read(cx).reload_count, 0);
4724 assert_eq!(pane.items_len(), 3);
4725 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
4726 });
4727 assert!(cx.has_pending_prompt());
4728
4729 // Cancel saving item 3.
4730 cx.simulate_prompt_answer(1);
4731 cx.executor().run_until_parked();
4732
4733 // Item 3 is reloaded. There's a prompt to save item 4.
4734 pane.update(cx, |pane, cx| {
4735 assert_eq!(item3.read(cx).save_count, 0);
4736 assert_eq!(item3.read(cx).save_as_count, 0);
4737 assert_eq!(item3.read(cx).reload_count, 1);
4738 assert_eq!(pane.items_len(), 2);
4739 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
4740 });
4741 assert!(cx.has_pending_prompt());
4742
4743 // Confirm saving item 4.
4744 cx.simulate_prompt_answer(0);
4745 cx.executor().run_until_parked();
4746
4747 // There's a prompt for a path for item 4.
4748 cx.simulate_new_path_selection(|_| Some(Default::default()));
4749 close_items.await.unwrap();
4750
4751 // The requested items are closed.
4752 pane.update(cx, |pane, cx| {
4753 assert_eq!(item4.read(cx).save_count, 0);
4754 assert_eq!(item4.read(cx).save_as_count, 1);
4755 assert_eq!(item4.read(cx).reload_count, 0);
4756 assert_eq!(pane.items_len(), 1);
4757 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4758 });
4759 }
4760
4761 #[gpui::test]
4762 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4763 init_test(cx);
4764
4765 let fs = FakeFs::new(cx.executor());
4766 let project = Project::test(fs, [], cx).await;
4767 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4768
4769 // Create several workspace items with single project entries, and two
4770 // workspace items with multiple project entries.
4771 let single_entry_items = (0..=4)
4772 .map(|project_entry_id| {
4773 cx.build_view(|cx| {
4774 TestItem::new(cx)
4775 .with_dirty(true)
4776 .with_project_items(&[TestProjectItem::new(
4777 project_entry_id,
4778 &format!("{project_entry_id}.txt"),
4779 cx,
4780 )])
4781 })
4782 })
4783 .collect::<Vec<_>>();
4784 let item_2_3 = cx.build_view(|cx| {
4785 TestItem::new(cx)
4786 .with_dirty(true)
4787 .with_singleton(false)
4788 .with_project_items(&[
4789 single_entry_items[2].read(cx).project_items[0].clone(),
4790 single_entry_items[3].read(cx).project_items[0].clone(),
4791 ])
4792 });
4793 let item_3_4 = cx.build_view(|cx| {
4794 TestItem::new(cx)
4795 .with_dirty(true)
4796 .with_singleton(false)
4797 .with_project_items(&[
4798 single_entry_items[3].read(cx).project_items[0].clone(),
4799 single_entry_items[4].read(cx).project_items[0].clone(),
4800 ])
4801 });
4802
4803 // Create two panes that contain the following project entries:
4804 // left pane:
4805 // multi-entry items: (2, 3)
4806 // single-entry items: 0, 1, 2, 3, 4
4807 // right pane:
4808 // single-entry items: 1
4809 // multi-entry items: (3, 4)
4810 let left_pane = workspace.update(cx, |workspace, cx| {
4811 let left_pane = workspace.active_pane().clone();
4812 workspace.add_item(Box::new(item_2_3.clone()), cx);
4813 for item in single_entry_items {
4814 workspace.add_item(Box::new(item), cx);
4815 }
4816 left_pane.update(cx, |pane, cx| {
4817 pane.activate_item(2, true, true, cx);
4818 });
4819
4820 let right_pane = workspace
4821 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
4822 .unwrap();
4823
4824 right_pane.update(cx, |pane, cx| {
4825 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
4826 });
4827
4828 left_pane
4829 });
4830
4831 cx.focus_view(&left_pane);
4832
4833 // When closing all of the items in the left pane, we should be prompted twice:
4834 // once for project entry 0, and once for project entry 2. Project entries 1,
4835 // 3, and 4 are all still open in the other paten. After those two
4836 // prompts, the task should complete.
4837
4838 let close = left_pane.update(cx, |pane, cx| {
4839 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
4840 });
4841 cx.executor().run_until_parked();
4842
4843 // Discard "Save all" prompt
4844 cx.simulate_prompt_answer(2);
4845
4846 cx.executor().run_until_parked();
4847 left_pane.update(cx, |pane, cx| {
4848 assert_eq!(
4849 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4850 &[ProjectEntryId::from_proto(0)]
4851 );
4852 });
4853 cx.simulate_prompt_answer(0);
4854
4855 cx.executor().run_until_parked();
4856 left_pane.update(cx, |pane, cx| {
4857 assert_eq!(
4858 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4859 &[ProjectEntryId::from_proto(2)]
4860 );
4861 });
4862 cx.simulate_prompt_answer(0);
4863
4864 cx.executor().run_until_parked();
4865 close.await.unwrap();
4866 left_pane.update(cx, |pane, _| {
4867 assert_eq!(pane.items_len(), 0);
4868 });
4869 }
4870
4871 #[gpui::test]
4872 async fn test_autosave(cx: &mut gpui::TestAppContext) {
4873 init_test(cx);
4874
4875 let fs = FakeFs::new(cx.executor());
4876 let project = Project::test(fs, [], cx).await;
4877 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4878 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4879
4880 let item = cx.build_view(|cx| {
4881 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4882 });
4883 let item_id = item.entity_id();
4884 workspace.update(cx, |workspace, cx| {
4885 workspace.add_item(Box::new(item.clone()), cx);
4886 });
4887
4888 // Autosave on window change.
4889 item.update(cx, |item, cx| {
4890 cx.update_global(|settings: &mut SettingsStore, cx| {
4891 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4892 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4893 })
4894 });
4895 item.is_dirty = true;
4896 });
4897
4898 // Deactivating the window saves the file.
4899 cx.simulate_deactivation();
4900 cx.executor().run_until_parked();
4901 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
4902
4903 // Autosave on focus change.
4904 item.update(cx, |item, cx| {
4905 cx.focus_self();
4906 cx.update_global(|settings: &mut SettingsStore, cx| {
4907 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4908 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4909 })
4910 });
4911 item.is_dirty = true;
4912 });
4913
4914 // Blurring the item saves the file.
4915 item.update(cx, |_, cx| cx.blur());
4916 cx.executor().run_until_parked();
4917 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
4918
4919 // Deactivating the window still saves the file.
4920 cx.simulate_activation();
4921 item.update(cx, |item, cx| {
4922 cx.focus_self();
4923 item.is_dirty = true;
4924 });
4925 cx.simulate_deactivation();
4926
4927 cx.executor().run_until_parked();
4928 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
4929
4930 // Autosave after delay.
4931 item.update(cx, |item, cx| {
4932 cx.update_global(|settings: &mut SettingsStore, cx| {
4933 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4934 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4935 })
4936 });
4937 item.is_dirty = true;
4938 cx.emit(ItemEvent::Edit);
4939 });
4940
4941 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4942 cx.executor().advance_clock(Duration::from_millis(250));
4943 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
4944
4945 // After delay expires, the file is saved.
4946 cx.executor().advance_clock(Duration::from_millis(250));
4947 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
4948
4949 // Autosave on focus change, ensuring closing the tab counts as such.
4950 item.update(cx, |item, cx| {
4951 cx.update_global(|settings: &mut SettingsStore, cx| {
4952 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4953 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4954 })
4955 });
4956 item.is_dirty = true;
4957 });
4958
4959 pane.update(cx, |pane, cx| {
4960 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4961 })
4962 .await
4963 .unwrap();
4964 assert!(!cx.has_pending_prompt());
4965 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4966
4967 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
4968 workspace.update(cx, |workspace, cx| {
4969 workspace.add_item(Box::new(item.clone()), cx);
4970 });
4971 item.update(cx, |item, cx| {
4972 item.project_items[0].update(cx, |item, _| {
4973 item.entry_id = None;
4974 });
4975 item.is_dirty = true;
4976 cx.blur();
4977 });
4978 cx.executor().run_until_parked();
4979 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4980
4981 // Ensure autosave is prevented for deleted files also when closing the buffer.
4982 let _close_items = pane.update(cx, |pane, cx| {
4983 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4984 });
4985 cx.executor().run_until_parked();
4986 assert!(cx.has_pending_prompt());
4987 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4988 }
4989
4990 #[gpui::test]
4991 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
4992 init_test(cx);
4993
4994 let fs = FakeFs::new(cx.executor());
4995
4996 let project = Project::test(fs, [], cx).await;
4997 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4998
4999 let item = cx.build_view(|cx| {
5000 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5001 });
5002 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5003 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
5004 let toolbar_notify_count = Rc::new(RefCell::new(0));
5005
5006 workspace.update(cx, |workspace, cx| {
5007 workspace.add_item(Box::new(item.clone()), cx);
5008 let toolbar_notification_count = toolbar_notify_count.clone();
5009 cx.observe(&toolbar, move |_, _, _| {
5010 *toolbar_notification_count.borrow_mut() += 1
5011 })
5012 .detach();
5013 });
5014
5015 pane.update(cx, |pane, _| {
5016 assert!(!pane.can_navigate_backward());
5017 assert!(!pane.can_navigate_forward());
5018 });
5019
5020 item.update(cx, |item, cx| {
5021 item.set_state("one".to_string(), cx);
5022 });
5023
5024 // Toolbar must be notified to re-render the navigation buttons
5025 assert_eq!(*toolbar_notify_count.borrow(), 1);
5026
5027 pane.update(cx, |pane, _| {
5028 assert!(pane.can_navigate_backward());
5029 assert!(!pane.can_navigate_forward());
5030 });
5031
5032 workspace
5033 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
5034 .await
5035 .unwrap();
5036
5037 assert_eq!(*toolbar_notify_count.borrow(), 2);
5038 pane.update(cx, |pane, _| {
5039 assert!(!pane.can_navigate_backward());
5040 assert!(pane.can_navigate_forward());
5041 });
5042 }
5043
5044 // #[gpui::test]
5045 // async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
5046 // init_test(cx);
5047 // let fs = FakeFs::new(cx.executor());
5048
5049 // let project = Project::test(fs, [], cx).await;
5050 // let window = cx.add_window(|cx| Workspace::test_new(project, cx));
5051 // let workspace = window.root(cx);
5052
5053 // let panel = workspace.update(cx, |workspace, cx| {
5054 // let panel = cx.build_view(|_| TestPanel::new(DockPosition::Right));
5055 // workspace.add_panel(panel.clone(), cx);
5056
5057 // workspace
5058 // .right_dock()
5059 // .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5060
5061 // panel
5062 // });
5063
5064 // let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5065 // pane.update(cx, |pane, cx| {
5066 // let item = cx.build_view(|_| TestItem::new(cx));
5067 // pane.add_item(Box::new(item), true, true, None, cx);
5068 // });
5069
5070 // // Transfer focus from center to panel
5071 // workspace.update(cx, |workspace, cx| {
5072 // workspace.toggle_panel_focus::<TestPanel>(cx);
5073 // });
5074
5075 // workspace.update(cx, |workspace, cx| {
5076 // assert!(workspace.right_dock().read(cx).is_open());
5077 // assert!(!panel.is_zoomed(cx));
5078 // assert!(panel.has_focus(cx));
5079 // });
5080
5081 // // Transfer focus from panel to center
5082 // workspace.update(cx, |workspace, cx| {
5083 // workspace.toggle_panel_focus::<TestPanel>(cx);
5084 // });
5085
5086 // workspace.update(cx, |workspace, cx| {
5087 // assert!(workspace.right_dock().read(cx).is_open());
5088 // assert!(!panel.is_zoomed(cx));
5089 // assert!(!panel.has_focus(cx));
5090 // });
5091
5092 // // Close the dock
5093 // workspace.update(cx, |workspace, cx| {
5094 // workspace.toggle_dock(DockPosition::Right, cx);
5095 // });
5096
5097 // workspace.update(cx, |workspace, cx| {
5098 // assert!(!workspace.right_dock().read(cx).is_open());
5099 // assert!(!panel.is_zoomed(cx));
5100 // assert!(!panel.has_focus(cx));
5101 // });
5102
5103 // // Open the dock
5104 // workspace.update(cx, |workspace, cx| {
5105 // workspace.toggle_dock(DockPosition::Right, cx);
5106 // });
5107
5108 // workspace.update(cx, |workspace, cx| {
5109 // assert!(workspace.right_dock().read(cx).is_open());
5110 // assert!(!panel.is_zoomed(cx));
5111 // assert!(panel.has_focus(cx));
5112 // });
5113
5114 // // Focus and zoom panel
5115 // panel.update(cx, |panel, cx| {
5116 // cx.focus_self();
5117 // panel.set_zoomed(true, cx)
5118 // });
5119
5120 // workspace.update(cx, |workspace, cx| {
5121 // assert!(workspace.right_dock().read(cx).is_open());
5122 // assert!(panel.is_zoomed(cx));
5123 // assert!(panel.has_focus(cx));
5124 // });
5125
5126 // // Transfer focus to the center closes the dock
5127 // workspace.update(cx, |workspace, cx| {
5128 // workspace.toggle_panel_focus::<TestPanel>(cx);
5129 // });
5130
5131 // workspace.update(cx, |workspace, cx| {
5132 // assert!(!workspace.right_dock().read(cx).is_open());
5133 // assert!(panel.is_zoomed(cx));
5134 // assert!(!panel.has_focus(cx));
5135 // });
5136
5137 // // Transferring focus back to the panel keeps it zoomed
5138 // workspace.update(cx, |workspace, cx| {
5139 // workspace.toggle_panel_focus::<TestPanel>(cx);
5140 // });
5141
5142 // workspace.update(cx, |workspace, cx| {
5143 // assert!(workspace.right_dock().read(cx).is_open());
5144 // assert!(panel.is_zoomed(cx));
5145 // assert!(panel.has_focus(cx));
5146 // });
5147
5148 // // Close the dock while it is zoomed
5149 // workspace.update(cx, |workspace, cx| {
5150 // workspace.toggle_dock(DockPosition::Right, cx)
5151 // });
5152
5153 // workspace.update(cx, |workspace, cx| {
5154 // assert!(!workspace.right_dock().read(cx).is_open());
5155 // assert!(panel.is_zoomed(cx));
5156 // assert!(workspace.zoomed.is_none());
5157 // assert!(!panel.has_focus(cx));
5158 // });
5159
5160 // // Opening the dock, when it's zoomed, retains focus
5161 // workspace.update(cx, |workspace, cx| {
5162 // workspace.toggle_dock(DockPosition::Right, cx)
5163 // });
5164
5165 // workspace.update(cx, |workspace, cx| {
5166 // assert!(workspace.right_dock().read(cx).is_open());
5167 // assert!(panel.is_zoomed(cx));
5168 // assert!(workspace.zoomed.is_some());
5169 // assert!(panel.has_focus(cx));
5170 // });
5171
5172 // // Unzoom and close the panel, zoom the active pane.
5173 // panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5174 // workspace.update(cx, |workspace, cx| {
5175 // workspace.toggle_dock(DockPosition::Right, cx)
5176 // });
5177 // pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5178
5179 // // Opening a dock unzooms the pane.
5180 // workspace.update(cx, |workspace, cx| {
5181 // workspace.toggle_dock(DockPosition::Right, cx)
5182 // });
5183 // workspace.update(cx, |workspace, cx| {
5184 // let pane = pane.read(cx);
5185 // assert!(!pane.is_zoomed());
5186 // assert!(!pane.has_focus());
5187 // assert!(workspace.right_dock().read(cx).is_open());
5188 // assert!(workspace.zoomed.is_none());
5189 // });
5190 // }
5191
5192 // #[gpui::test]
5193 // async fn test_panels(cx: &mut gpui::TestAppContext) {
5194 // init_test(cx);
5195 // let fs = FakeFs::new(cx.executor());
5196
5197 // let project = Project::test(fs, [], cx).await;
5198 // let window = cx.add_window(|cx| Workspace::test_new(project, cx));
5199 // let workspace = window.root(cx);
5200
5201 // let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5202 // // Add panel_1 on the left, panel_2 on the right.
5203 // let panel_1 = cx.build_view(|_| TestPanel::new(DockPosition::Left));
5204 // workspace.add_panel(panel_1.clone(), cx);
5205 // workspace
5206 // .left_dock()
5207 // .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5208 // let panel_2 = cx.build_view(|_| TestPanel::new(DockPosition::Right));
5209 // workspace.add_panel(panel_2.clone(), cx);
5210 // workspace
5211 // .right_dock()
5212 // .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5213
5214 // let left_dock = workspace.left_dock();
5215 // assert_eq!(
5216 // left_dock.read(cx).visible_panel().unwrap().id(),
5217 // panel_1.id()
5218 // );
5219 // assert_eq!(
5220 // left_dock.read(cx).active_panel_size(cx).unwrap(),
5221 // panel_1.size(cx)
5222 // );
5223
5224 // left_dock.update(cx, |left_dock, cx| {
5225 // left_dock.resize_active_panel(Some(1337.), cx)
5226 // });
5227 // assert_eq!(
5228 // workspace
5229 // .right_dock()
5230 // .read(cx)
5231 // .visible_panel()
5232 // .unwrap()
5233 // .id(),
5234 // panel_2.id()
5235 // );
5236
5237 // (panel_1, panel_2)
5238 // });
5239
5240 // // Move panel_1 to the right
5241 // panel_1.update(cx, |panel_1, cx| {
5242 // panel_1.set_position(DockPosition::Right, cx)
5243 // });
5244
5245 // workspace.update(cx, |workspace, cx| {
5246 // // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5247 // // Since it was the only panel on the left, the left dock should now be closed.
5248 // assert!(!workspace.left_dock().read(cx).is_open());
5249 // assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5250 // let right_dock = workspace.right_dock();
5251 // assert_eq!(
5252 // right_dock.read(cx).visible_panel().unwrap().id(),
5253 // panel_1.id()
5254 // );
5255 // assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5256
5257 // // Now we move panel_2Β to the left
5258 // panel_2.set_position(DockPosition::Left, cx);
5259 // });
5260
5261 // workspace.update(cx, |workspace, cx| {
5262 // // Since panel_2 was not visible on the right, we don't open the left dock.
5263 // assert!(!workspace.left_dock().read(cx).is_open());
5264 // // And the right dock is unaffected in it's displaying of panel_1
5265 // assert!(workspace.right_dock().read(cx).is_open());
5266 // assert_eq!(
5267 // workspace
5268 // .right_dock()
5269 // .read(cx)
5270 // .visible_panel()
5271 // .unwrap()
5272 // .id(),
5273 // panel_1.id()
5274 // );
5275 // });
5276
5277 // // Move panel_1 back to the left
5278 // panel_1.update(cx, |panel_1, cx| {
5279 // panel_1.set_position(DockPosition::Left, cx)
5280 // });
5281
5282 // workspace.update(cx, |workspace, cx| {
5283 // // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5284 // let left_dock = workspace.left_dock();
5285 // assert!(left_dock.read(cx).is_open());
5286 // assert_eq!(
5287 // left_dock.read(cx).visible_panel().unwrap().id(),
5288 // panel_1.id()
5289 // );
5290 // assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5291 // // And right the dock should be closed as it no longer has any panels.
5292 // assert!(!workspace.right_dock().read(cx).is_open());
5293
5294 // // Now we move panel_1 to the bottom
5295 // panel_1.set_position(DockPosition::Bottom, cx);
5296 // });
5297
5298 // workspace.update(cx, |workspace, cx| {
5299 // // Since panel_1 was visible on the left, we close the left dock.
5300 // assert!(!workspace.left_dock().read(cx).is_open());
5301 // // The bottom dock is sized based on the panel's default size,
5302 // // since the panel orientation changed from vertical to horizontal.
5303 // let bottom_dock = workspace.bottom_dock();
5304 // assert_eq!(
5305 // bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5306 // panel_1.size(cx),
5307 // );
5308 // // Close bottom dock and move panel_1 back to the left.
5309 // bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5310 // panel_1.set_position(DockPosition::Left, cx);
5311 // });
5312
5313 // // Emit activated event on panel 1
5314 // panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
5315
5316 // // Now the left dock is open and panel_1 is active and focused.
5317 // workspace.update(cx, |workspace, cx| {
5318 // let left_dock = workspace.left_dock();
5319 // assert!(left_dock.read(cx).is_open());
5320 // assert_eq!(
5321 // left_dock.read(cx).visible_panel().unwrap().id(),
5322 // panel_1.id()
5323 // );
5324 // assert!(panel_1.is_focused(cx));
5325 // });
5326
5327 // // Emit closed event on panel 2, which is not active
5328 // panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
5329
5330 // // Wo don't close the left dock, because panel_2 wasn't the active panel
5331 // workspace.update(cx, |workspace, cx| {
5332 // let left_dock = workspace.left_dock();
5333 // assert!(left_dock.read(cx).is_open());
5334 // assert_eq!(
5335 // left_dock.read(cx).visible_panel().unwrap().id(),
5336 // panel_1.id()
5337 // );
5338 // });
5339
5340 // // Emitting a ZoomIn event shows the panel as zoomed.
5341 // panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
5342 // workspace.update(cx, |workspace, _| {
5343 // assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5344 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5345 // });
5346
5347 // // Move panel to another dock while it is zoomed
5348 // panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5349 // workspace.update(cx, |workspace, _| {
5350 // assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5351 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5352 // });
5353
5354 // // If focus is transferred to another view that's not a panel or another pane, we still show
5355 // // the panel as zoomed.
5356 // let focus_receiver = cx.build_view(|_| EmptyView);
5357 // focus_receiver.update(cx, |_, cx| cx.focus_self());
5358 // workspace.update(cx, |workspace, _| {
5359 // assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5360 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5361 // });
5362
5363 // // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5364 // workspace.update(cx, |_, cx| cx.focus_self());
5365 // workspace.update(cx, |workspace, _| {
5366 // assert_eq!(workspace.zoomed, None);
5367 // assert_eq!(workspace.zoomed_position, None);
5368 // });
5369
5370 // // If focus is transferred again to another view that's not a panel or a pane, we won't
5371 // // show the panel as zoomed because it wasn't zoomed before.
5372 // focus_receiver.update(cx, |_, cx| cx.focus_self());
5373 // workspace.update(cx, |workspace, _| {
5374 // assert_eq!(workspace.zoomed, None);
5375 // assert_eq!(workspace.zoomed_position, None);
5376 // });
5377
5378 // // When focus is transferred back to the panel, it is zoomed again.
5379 // panel_1.update(cx, |_, cx| cx.focus_self());
5380 // workspace.update(cx, |workspace, _| {
5381 // assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5382 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5383 // });
5384
5385 // // Emitting a ZoomOut event unzooms the panel.
5386 // panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
5387 // workspace.update(cx, |workspace, _| {
5388 // assert_eq!(workspace.zoomed, None);
5389 // assert_eq!(workspace.zoomed_position, None);
5390 // });
5391
5392 // // Emit closed event on panel 1, which is active
5393 // panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
5394
5395 // // Now the left dock is closed, because panel_1 was the active panel
5396 // workspace.update(cx, |workspace, cx| {
5397 // let right_dock = workspace.right_dock();
5398 // assert!(!right_dock.read(cx).is_open());
5399 // });
5400 // }
5401
5402 pub fn init_test(cx: &mut TestAppContext) {
5403 cx.update(|cx| {
5404 let settings_store = SettingsStore::test(cx);
5405 cx.set_global(settings_store);
5406 theme::init(theme::LoadThemes::JustBase, cx);
5407 language::init(cx);
5408 crate::init_settings(cx);
5409 Project::init_settings(cx);
5410 });
5411 }
5412}