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, outline, point, size, Action, AnyModel, AnyView,
33 AnyWeakView, AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context,
34 Div, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels,
35 InteractiveElement, KeyContext, ManagedView, Model, ModelContext, ParentElement,
36 PathPromptOptions, Pixels, Point, PromptLevel, Render, Size, Styled, Subscription, Task, View,
37 ViewContext, 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 init_settings(cx);
231 notifications::init(cx);
232
233 cx.on_action(Workspace::close_global);
234 cx.on_action(restart);
235
236 cx.on_action({
237 let app_state = Arc::downgrade(&app_state);
238 move |_: &Open, cx: &mut AppContext| {
239 let mut paths = cx.prompt_for_paths(PathPromptOptions {
240 files: true,
241 directories: true,
242 multiple: true,
243 });
244
245 if let Some(app_state) = app_state.upgrade() {
246 cx.spawn(move |mut cx| async move {
247 if let Some(paths) = paths.await.log_err().flatten() {
248 cx.update(|cx| {
249 open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
250 })
251 .ok();
252 }
253 })
254 .detach();
255 }
256 }
257 });
258}
259
260type ProjectItemBuilders =
261 HashMap<TypeId, fn(Model<Project>, AnyModel, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>>;
262pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
263 let builders = cx.default_global::<ProjectItemBuilders>();
264 builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
265 let item = model.downcast::<I::Item>().unwrap();
266 Box::new(cx.build_view(|cx| I::for_project_item(project, item, cx)))
267 });
268}
269
270type FollowableItemBuilder = fn(
271 View<Pane>,
272 View<Workspace>,
273 ViewId,
274 &mut Option<proto::view::Variant>,
275 &mut WindowContext,
276) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
277type FollowableItemBuilders = HashMap<
278 TypeId,
279 (
280 FollowableItemBuilder,
281 fn(&AnyView) -> Box<dyn FollowableItemHandle>,
282 ),
283>;
284pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
285 let builders = cx.default_global::<FollowableItemBuilders>();
286 builders.insert(
287 TypeId::of::<I>(),
288 (
289 |pane, workspace, id, state, cx| {
290 I::from_state_proto(pane, workspace, id, state, cx).map(|task| {
291 cx.foreground_executor()
292 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
293 })
294 },
295 |this| Box::new(this.clone().downcast::<I>().unwrap()),
296 ),
297 );
298}
299
300type ItemDeserializers = HashMap<
301 Arc<str>,
302 fn(
303 Model<Project>,
304 WeakView<Workspace>,
305 WorkspaceId,
306 ItemId,
307 &mut ViewContext<Pane>,
308 ) -> Task<Result<Box<dyn ItemHandle>>>,
309>;
310pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
311 if let Some(serialized_item_kind) = I::serialized_item_kind() {
312 let deserializers = cx.default_global::<ItemDeserializers>();
313 deserializers.insert(
314 Arc::from(serialized_item_kind),
315 |project, workspace, workspace_id, item_id, cx| {
316 let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
317 cx.foreground_executor()
318 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
319 },
320 );
321 }
322}
323
324pub struct AppState {
325 pub languages: Arc<LanguageRegistry>,
326 pub client: Arc<Client>,
327 pub user_store: Model<UserStore>,
328 pub workspace_store: Model<WorkspaceStore>,
329 pub fs: Arc<dyn fs::Fs>,
330 pub build_window_options:
331 fn(Option<WindowBounds>, Option<Uuid>, &mut AppContext) -> WindowOptions,
332 pub node_runtime: Arc<dyn NodeRuntime>,
333}
334
335pub struct WorkspaceStore {
336 workspaces: HashSet<WindowHandle<Workspace>>,
337 followers: Vec<Follower>,
338 client: Arc<Client>,
339 _subscriptions: Vec<client::Subscription>,
340}
341
342#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
343struct Follower {
344 project_id: Option<u64>,
345 peer_id: PeerId,
346}
347
348impl AppState {
349 #[cfg(any(test, feature = "test-support"))]
350 pub fn test(cx: &mut AppContext) -> Arc<Self> {
351 use node_runtime::FakeNodeRuntime;
352 use settings::SettingsStore;
353
354 if !cx.has_global::<SettingsStore>() {
355 let settings_store = SettingsStore::test(cx);
356 cx.set_global(settings_store);
357 }
358
359 let fs = fs::FakeFs::new(cx.background_executor().clone());
360 let languages = Arc::new(LanguageRegistry::test());
361 let http_client = util::http::FakeHttpClient::with_404_response();
362 let client = Client::new(http_client.clone(), cx);
363 let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx));
364 let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx));
365
366 theme::init(theme::LoadThemes::JustBase, cx);
367 client::init(&client, cx);
368 crate::init_settings(cx);
369
370 Arc::new(Self {
371 client,
372 fs,
373 languages,
374 user_store,
375 workspace_store,
376 node_runtime: FakeNodeRuntime::new(),
377 build_window_options: |_, _, _| Default::default(),
378 })
379 }
380}
381
382struct DelayedDebouncedEditAction {
383 task: Option<Task<()>>,
384 cancel_channel: Option<oneshot::Sender<()>>,
385}
386
387impl DelayedDebouncedEditAction {
388 fn new() -> DelayedDebouncedEditAction {
389 DelayedDebouncedEditAction {
390 task: None,
391 cancel_channel: None,
392 }
393 }
394
395 fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, func: F)
396 where
397 F: 'static + Send + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
398 {
399 if let Some(channel) = self.cancel_channel.take() {
400 _ = channel.send(());
401 }
402
403 let (sender, mut receiver) = oneshot::channel::<()>();
404 self.cancel_channel = Some(sender);
405
406 let previous_task = self.task.take();
407 self.task = Some(cx.spawn(move |workspace, mut cx| async move {
408 let mut timer = cx.background_executor().timer(delay).fuse();
409 if let Some(previous_task) = previous_task {
410 previous_task.await;
411 }
412
413 futures::select_biased! {
414 _ = receiver => return,
415 _ = timer => {}
416 }
417
418 if let Some(result) = workspace
419 .update(&mut cx, |workspace, cx| (func)(workspace, cx))
420 .log_err()
421 {
422 result.await.log_err();
423 }
424 }));
425 }
426}
427
428pub enum Event {
429 PaneAdded(View<Pane>),
430 ContactRequestedJoin(u64),
431 WorkspaceCreated(WeakView<Workspace>),
432}
433
434pub struct Workspace {
435 window_self: WindowHandle<Self>,
436 weak_self: WeakView<Self>,
437 workspace_actions: Vec<Box<dyn Fn(Div, &mut ViewContext<Self>) -> Div>>,
438 zoomed: Option<AnyWeakView>,
439 zoomed_position: Option<DockPosition>,
440 center: PaneGroup,
441 left_dock: View<Dock>,
442 bottom_dock: View<Dock>,
443 right_dock: View<Dock>,
444 panes: Vec<View<Pane>>,
445 panes_by_item: HashMap<EntityId, WeakView<Pane>>,
446 active_pane: View<Pane>,
447 last_active_center_pane: Option<WeakView<Pane>>,
448 last_active_view_id: Option<proto::ViewId>,
449 status_bar: View<StatusBar>,
450 modal_layer: View<ModalLayer>,
451 titlebar_item: Option<AnyView>,
452 notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
453 project: Model<Project>,
454 follower_states: HashMap<View<Pane>, FollowerState>,
455 last_leaders_by_pane: HashMap<WeakView<Pane>, PeerId>,
456 window_edited: bool,
457 active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
458 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
459 database_id: WorkspaceId,
460 app_state: Arc<AppState>,
461 subscriptions: Vec<Subscription>,
462 _apply_leader_updates: Task<Result<()>>,
463 _observe_current_user: Task<Result<()>>,
464 _schedule_serialize: Option<Task<()>>,
465 pane_history_timestamp: Arc<AtomicUsize>,
466 bounds: Bounds<Pixels>,
467}
468
469impl EventEmitter<Event> for Workspace {}
470
471#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
472pub struct ViewId {
473 pub creator: PeerId,
474 pub id: u64,
475}
476
477#[derive(Default)]
478struct FollowerState {
479 leader_id: PeerId,
480 active_view_id: Option<ViewId>,
481 items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
482}
483
484impl Workspace {
485 pub fn new(
486 workspace_id: WorkspaceId,
487 project: Model<Project>,
488 app_state: Arc<AppState>,
489 cx: &mut ViewContext<Self>,
490 ) -> Self {
491 cx.observe(&project, |_, _, cx| cx.notify()).detach();
492 cx.subscribe(&project, move |this, _, event, cx| {
493 match event {
494 project::Event::RemoteIdChanged(_) => {
495 this.update_window_title(cx);
496 }
497
498 project::Event::CollaboratorLeft(peer_id) => {
499 this.collaborator_left(*peer_id, cx);
500 }
501
502 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
503 this.update_window_title(cx);
504 this.serialize_workspace(cx);
505 }
506
507 project::Event::DisconnectedFromHost => {
508 this.update_window_edited(cx);
509 cx.blur();
510 }
511
512 project::Event::Closed => {
513 cx.remove_window();
514 }
515
516 project::Event::DeletedEntry(entry_id) => {
517 for pane in this.panes.iter() {
518 pane.update(cx, |pane, cx| {
519 pane.handle_deleted_project_item(*entry_id, cx)
520 });
521 }
522 }
523
524 project::Event::Notification(message) => this.show_notification(0, cx, |cx| {
525 cx.build_view(|_| MessageNotification::new(message.clone()))
526 }),
527
528 _ => {}
529 }
530 cx.notify()
531 })
532 .detach();
533 cx.on_blur_window(|this, cx| {
534 let focus_handle = this.focus_handle(cx);
535 cx.focus(&focus_handle);
536 })
537 .detach();
538
539 let weak_handle = cx.view().downgrade();
540 let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
541
542 let center_pane = cx.build_view(|cx| {
543 Pane::new(
544 weak_handle.clone(),
545 project.clone(),
546 pane_history_timestamp.clone(),
547 cx,
548 )
549 });
550 cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
551
552 cx.focus_view(¢er_pane);
553 cx.emit(Event::PaneAdded(center_pane.clone()));
554
555 let window_handle = cx.window_handle().downcast::<Workspace>().unwrap();
556 app_state.workspace_store.update(cx, |store, _| {
557 store.workspaces.insert(window_handle);
558 });
559
560 let mut current_user = app_state.user_store.read(cx).watch_current_user();
561 let mut connection_status = app_state.client.status();
562 let _observe_current_user = cx.spawn(|this, mut cx| async move {
563 current_user.next().await;
564 connection_status.next().await;
565 let mut stream =
566 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
567
568 while stream.recv().await.is_some() {
569 this.update(&mut cx, |_, cx| cx.notify())?;
570 }
571 anyhow::Ok(())
572 });
573
574 // All leader updates are enqueued and then processed in a single task, so
575 // that each asynchronous operation can be run in order.
576 let (leader_updates_tx, mut leader_updates_rx) =
577 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
578 let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
579 while let Some((leader_id, update)) = leader_updates_rx.next().await {
580 Self::process_leader_update(&this, leader_id, update, &mut cx)
581 .await
582 .log_err();
583 }
584
585 Ok(())
586 });
587
588 cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
589
590 let left_dock = cx.build_view(|cx| Dock::new(DockPosition::Left, cx));
591 let bottom_dock = cx.build_view(|cx| Dock::new(DockPosition::Bottom, cx));
592 let right_dock = cx.build_view(|cx| Dock::new(DockPosition::Right, cx));
593 let left_dock_buttons =
594 cx.build_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx));
595 let bottom_dock_buttons =
596 cx.build_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx));
597 let right_dock_buttons =
598 cx.build_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx));
599 let status_bar = cx.build_view(|cx| {
600 let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
601 status_bar.add_left_item(left_dock_buttons, cx);
602 status_bar.add_right_item(right_dock_buttons, cx);
603 status_bar.add_right_item(bottom_dock_buttons, cx);
604 status_bar
605 });
606
607 let workspace_handle = cx.view().downgrade();
608 let modal_layer = cx.build_view(|cx| ModalLayer::new());
609
610 // todo!()
611 // cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
612 // drag_and_drop.register_container(weak_handle.clone());
613 // });
614
615 let mut active_call = None;
616 if cx.has_global::<Model<ActiveCall>>() {
617 let call = cx.global::<Model<ActiveCall>>().clone();
618 let mut subscriptions = Vec::new();
619 subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
620 active_call = Some((call, subscriptions));
621 }
622
623 let subscriptions = vec![
624 cx.observe_window_activation(Self::on_window_activation_changed),
625 cx.observe_window_bounds(move |_, cx| {
626 if let Some(display) = cx.display() {
627 // Transform fixed bounds to be stored in terms of the containing display
628 let mut bounds = cx.window_bounds();
629 if let WindowBounds::Fixed(window_bounds) = &mut bounds {
630 let display_bounds = display.bounds();
631 window_bounds.origin.x -= display_bounds.origin.x;
632 window_bounds.origin.y -= display_bounds.origin.y;
633 }
634
635 if let Some(display_uuid) = display.uuid().log_err() {
636 cx.background_executor()
637 .spawn(DB.set_window_bounds(workspace_id, bounds, display_uuid))
638 .detach_and_log_err(cx);
639 }
640 }
641 cx.notify();
642 }),
643 cx.observe(&left_dock, |this, _, cx| {
644 this.serialize_workspace(cx);
645 cx.notify();
646 }),
647 cx.observe(&bottom_dock, |this, _, cx| {
648 this.serialize_workspace(cx);
649 cx.notify();
650 }),
651 cx.observe(&right_dock, |this, _, cx| {
652 this.serialize_workspace(cx);
653 cx.notify();
654 }),
655 cx.on_release(|this, cx| {
656 this.app_state.workspace_store.update(cx, |store, _| {
657 store.workspaces.remove(&this.window_self);
658 })
659 }),
660 ];
661
662 cx.defer(|this, cx| {
663 this.update_window_title(cx);
664 // todo! @nate - these are useful for testing notifications
665 // this.show_error(
666 // &anyhow::anyhow!("what happens if this message is very very very very very long"),
667 // cx,
668 // );
669
670 // this.show_notification(1, cx, |cx| {
671 // cx.build_view(|_cx| {
672 // simple_message_notification::MessageNotification::new(format!("Error:"))
673 // .with_click_message("click here because!")
674 // })
675 // });
676 });
677 Workspace {
678 window_self: window_handle,
679 weak_self: weak_handle.clone(),
680 zoomed: None,
681 zoomed_position: None,
682 center: PaneGroup::new(center_pane.clone()),
683 panes: vec![center_pane.clone()],
684 panes_by_item: Default::default(),
685 active_pane: center_pane.clone(),
686 last_active_center_pane: Some(center_pane.downgrade()),
687 last_active_view_id: None,
688 status_bar,
689 modal_layer,
690 titlebar_item: None,
691 notifications: Default::default(),
692 left_dock,
693 bottom_dock,
694 right_dock,
695 project: project.clone(),
696 follower_states: Default::default(),
697 last_leaders_by_pane: Default::default(),
698 window_edited: false,
699 active_call,
700 database_id: workspace_id,
701 app_state,
702 _observe_current_user,
703 _apply_leader_updates,
704 _schedule_serialize: None,
705 leader_updates_tx,
706 subscriptions,
707 pane_history_timestamp,
708 workspace_actions: Default::default(),
709 // This data will be incorrect, but it will be overwritten by the time it needs to be used.
710 bounds: 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#[derive(Render)]
3584struct DraggedDock(DockPosition);
3585
3586impl Render for Workspace {
3587 type Element = Div;
3588
3589 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
3590 let mut context = KeyContext::default();
3591 context.add("Workspace");
3592
3593 let (ui_font, ui_font_size) = {
3594 let theme_settings = ThemeSettings::get_global(cx);
3595 (
3596 theme_settings.ui_font.family.clone(),
3597 theme_settings.ui_font_size.clone(),
3598 )
3599 };
3600
3601 cx.set_rem_size(ui_font_size);
3602
3603 self.actions(div(), cx)
3604 .key_context(context)
3605 .relative()
3606 .size_full()
3607 .flex()
3608 .flex_col()
3609 .font(ui_font)
3610 .gap_0()
3611 .justify_start()
3612 .items_start()
3613 .text_color(cx.theme().colors().text)
3614 .bg(cx.theme().colors().background)
3615 .border()
3616 .border_color(cx.theme().colors().border)
3617 .children(self.titlebar_item.clone())
3618 .child(
3619 div()
3620 .id("workspace")
3621 .relative()
3622 .flex_1()
3623 .w_full()
3624 .flex()
3625 .flex_col()
3626 .overflow_hidden()
3627 .border_t()
3628 .border_b()
3629 .border_color(cx.theme().colors().border)
3630 .child(
3631 canvas(cx.listener(|workspace, bounds, cx| {
3632 cx.with_z_index(100, |cx| {
3633 cx.paint_quad(outline(*bounds, gpui::green()));
3634 });
3635 workspace.bounds = *bounds;
3636 }))
3637 .absolute()
3638 .size_full(),
3639 )
3640 .on_drag_move(
3641 cx.listener(|workspace, e: &DragMoveEvent<DraggedDock>, cx| {
3642 match e.drag.read(cx).0 {
3643 DockPosition::Left => {
3644 let size = workspace.bounds.left() + e.event.position.x;
3645 workspace.left_dock.update(cx, |left_dock, cx| {
3646 left_dock.resize_active_panel(Some(size.0), cx);
3647 });
3648 }
3649 DockPosition::Right => {
3650 let size = workspace.bounds.right() - e.event.position.x;
3651 workspace.right_dock.update(cx, |right_dock, cx| {
3652 right_dock.resize_active_panel(Some(size.0), cx);
3653 });
3654 }
3655 DockPosition::Bottom => {
3656 let size = workspace.bounds.bottom() - e.event.position.y;
3657 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
3658 bottom_dock.resize_active_panel(Some(size.0), cx);
3659 });
3660 }
3661 }
3662 }),
3663 )
3664 .child(self.modal_layer.clone())
3665 .child(
3666 div()
3667 .flex()
3668 .flex_row()
3669 .h_full()
3670 // Left Dock
3671 .child(
3672 div()
3673 .flex()
3674 .flex_none()
3675 .overflow_hidden()
3676 .child(self.left_dock.clone()),
3677 )
3678 // Panes
3679 .child(
3680 div()
3681 .flex()
3682 .flex_col()
3683 .flex_1()
3684 .overflow_hidden()
3685 .child(self.center.render(
3686 &self.project,
3687 &self.follower_states,
3688 self.active_call(),
3689 &self.active_pane,
3690 self.zoomed.as_ref(),
3691 &self.app_state,
3692 cx,
3693 ))
3694 .child(self.bottom_dock.clone()),
3695 )
3696 // Right Dock
3697 .child(
3698 div()
3699 .flex()
3700 .flex_none()
3701 .overflow_hidden()
3702 .child(self.right_dock.clone()),
3703 ),
3704 )
3705 .children(self.render_notifications(cx)),
3706 )
3707 .child(self.status_bar.clone())
3708 }
3709}
3710
3711// impl View for Workspace {
3712
3713// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
3714// let theme = theme::current(cx).clone();
3715// Stack::new()
3716// .with_child(
3717// Flex::column()
3718// .with_child(self.render_titlebar(&theme, cx))
3719// .with_child(
3720// Stack::new()
3721// .with_child({
3722// let project = self.project.clone();
3723// Flex::row()
3724// .with_children(self.render_dock(DockPosition::Left, cx))
3725// .with_child(
3726// Flex::column()
3727// .with_child(
3728// FlexItem::new(
3729// self.center.render(
3730// &project,
3731// &theme,
3732// &self.follower_states,
3733// self.active_call(),
3734// self.active_pane(),
3735// self.zoomed
3736// .as_ref()
3737// .and_then(|zoomed| zoomed.upgrade(cx))
3738// .as_ref(),
3739// &self.app_state,
3740// cx,
3741// ),
3742// )
3743// .flex(1., true),
3744// )
3745// .with_children(
3746// self.render_dock(DockPosition::Bottom, cx),
3747// )
3748// .flex(1., true),
3749// )
3750// .with_children(self.render_dock(DockPosition::Right, cx))
3751// })
3752// .with_child(Overlay::new(
3753// Stack::new()
3754// .with_children(self.zoomed.as_ref().and_then(|zoomed| {
3755// enum ZoomBackground {}
3756// let zoomed = zoomed.upgrade(cx)?;
3757
3758// let mut foreground_style =
3759// theme.workspace.zoomed_pane_foreground;
3760// if let Some(zoomed_dock_position) = self.zoomed_position {
3761// foreground_style =
3762// theme.workspace.zoomed_panel_foreground;
3763// let margin = foreground_style.margin.top;
3764// let border = foreground_style.border.top;
3765
3766// // Only include a margin and border on the opposite side.
3767// foreground_style.margin.top = 0.;
3768// foreground_style.margin.left = 0.;
3769// foreground_style.margin.bottom = 0.;
3770// foreground_style.margin.right = 0.;
3771// foreground_style.border.top = false;
3772// foreground_style.border.left = false;
3773// foreground_style.border.bottom = false;
3774// foreground_style.border.right = false;
3775// match zoomed_dock_position {
3776// DockPosition::Left => {
3777// foreground_style.margin.right = margin;
3778// foreground_style.border.right = border;
3779// }
3780// DockPosition::Right => {
3781// foreground_style.margin.left = margin;
3782// foreground_style.border.left = border;
3783// }
3784// DockPosition::Bottom => {
3785// foreground_style.margin.top = margin;
3786// foreground_style.border.top = border;
3787// }
3788// }
3789// }
3790
3791// Some(
3792// ChildView::new(&zoomed, cx)
3793// .contained()
3794// .with_style(foreground_style)
3795// .aligned()
3796// .contained()
3797// .with_style(theme.workspace.zoomed_background)
3798// .mouse::<ZoomBackground>(0)
3799// .capture_all()
3800// .on_down(
3801// MouseButton::Left,
3802// |_, this: &mut Self, cx| {
3803// this.zoom_out(cx);
3804// },
3805// ),
3806// )
3807// }))
3808// .with_children(self.modal.as_ref().map(|modal| {
3809// // Prevent clicks within the modal from falling
3810// // through to the rest of the workspace.
3811// enum ModalBackground {}
3812// MouseEventHandler::new::<ModalBackground, _>(
3813// 0,
3814// cx,
3815// |_, cx| ChildView::new(modal.view.as_any(), cx),
3816// )
3817// .on_click(MouseButton::Left, |_, _, _| {})
3818// .contained()
3819// .with_style(theme.workspace.modal)
3820// .aligned()
3821// .top()
3822// }))
3823// .with_children(self.render_notifications(&theme.workspace, cx)),
3824// ))
3825// .provide_resize_bounds::<WorkspaceBounds>()
3826// .flex(1.0, true),
3827// )
3828// .with_child(ChildView::new(&self.status_bar, cx))
3829// .contained()
3830// .with_background_color(theme.workspace.background),
3831// )
3832// .with_children(DragAndDrop::render(cx))
3833// .with_children(self.render_disconnected_overlay(cx))
3834// .into_any_named("workspace")
3835// }
3836
3837// fn modifiers_changed(&mut self, e: &ModifiersChangedEvent, cx: &mut ViewContext<Self>) -> bool {
3838// DragAndDrop::<Workspace>::update_modifiers(e.modifiers, cx)
3839// }
3840// }
3841
3842impl WorkspaceStore {
3843 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
3844 Self {
3845 workspaces: Default::default(),
3846 followers: Default::default(),
3847 _subscriptions: vec![
3848 client.add_request_handler(cx.weak_model(), Self::handle_follow),
3849 client.add_message_handler(cx.weak_model(), Self::handle_unfollow),
3850 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
3851 ],
3852 client,
3853 }
3854 }
3855
3856 pub fn update_followers(
3857 &self,
3858 project_id: Option<u64>,
3859 update: proto::update_followers::Variant,
3860 cx: &AppContext,
3861 ) -> Option<()> {
3862 if !cx.has_global::<Model<ActiveCall>>() {
3863 return None;
3864 }
3865
3866 let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id();
3867 let follower_ids: Vec<_> = self
3868 .followers
3869 .iter()
3870 .filter_map(|follower| {
3871 if follower.project_id == project_id || project_id.is_none() {
3872 Some(follower.peer_id.into())
3873 } else {
3874 None
3875 }
3876 })
3877 .collect();
3878 if follower_ids.is_empty() {
3879 return None;
3880 }
3881 self.client
3882 .send(proto::UpdateFollowers {
3883 room_id,
3884 project_id,
3885 follower_ids,
3886 variant: Some(update),
3887 })
3888 .log_err()
3889 }
3890
3891 pub async fn handle_follow(
3892 this: Model<Self>,
3893 envelope: TypedEnvelope<proto::Follow>,
3894 _: Arc<Client>,
3895 mut cx: AsyncAppContext,
3896 ) -> Result<proto::FollowResponse> {
3897 this.update(&mut cx, |this, cx| {
3898 let follower = Follower {
3899 project_id: envelope.payload.project_id,
3900 peer_id: envelope.original_sender_id()?,
3901 };
3902 let active_project = ActiveCall::global(cx).read(cx).location().cloned();
3903
3904 let mut response = proto::FollowResponse::default();
3905 for workspace in &this.workspaces {
3906 workspace
3907 .update(cx, |workspace, cx| {
3908 let handler_response = workspace.handle_follow(follower.project_id, cx);
3909 if response.views.is_empty() {
3910 response.views = handler_response.views;
3911 } else {
3912 response.views.extend_from_slice(&handler_response.views);
3913 }
3914
3915 if let Some(active_view_id) = handler_response.active_view_id.clone() {
3916 if response.active_view_id.is_none()
3917 || Some(workspace.project.downgrade()) == active_project
3918 {
3919 response.active_view_id = Some(active_view_id);
3920 }
3921 }
3922 })
3923 .ok();
3924 }
3925
3926 if let Err(ix) = this.followers.binary_search(&follower) {
3927 this.followers.insert(ix, follower);
3928 }
3929
3930 Ok(response)
3931 })?
3932 }
3933
3934 async fn handle_unfollow(
3935 model: Model<Self>,
3936 envelope: TypedEnvelope<proto::Unfollow>,
3937 _: Arc<Client>,
3938 mut cx: AsyncAppContext,
3939 ) -> Result<()> {
3940 model.update(&mut cx, |this, _| {
3941 let follower = Follower {
3942 project_id: envelope.payload.project_id,
3943 peer_id: envelope.original_sender_id()?,
3944 };
3945 if let Ok(ix) = this.followers.binary_search(&follower) {
3946 this.followers.remove(ix);
3947 }
3948 Ok(())
3949 })?
3950 }
3951
3952 async fn handle_update_followers(
3953 this: Model<Self>,
3954 envelope: TypedEnvelope<proto::UpdateFollowers>,
3955 _: Arc<Client>,
3956 mut cx: AsyncAppContext,
3957 ) -> Result<()> {
3958 let leader_id = envelope.original_sender_id()?;
3959 let update = envelope.payload;
3960
3961 this.update(&mut cx, |this, cx| {
3962 for workspace in &this.workspaces {
3963 workspace.update(cx, |workspace, cx| {
3964 let project_id = workspace.project.read(cx).remote_id();
3965 if update.project_id != project_id && update.project_id.is_some() {
3966 return;
3967 }
3968 workspace.handle_update_followers(leader_id, update.clone(), cx);
3969 })?;
3970 }
3971 Ok(())
3972 })?
3973 }
3974}
3975
3976impl ViewId {
3977 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
3978 Ok(Self {
3979 creator: message
3980 .creator
3981 .ok_or_else(|| anyhow!("creator is missing"))?,
3982 id: message.id,
3983 })
3984 }
3985
3986 pub(crate) fn to_proto(&self) -> proto::ViewId {
3987 proto::ViewId {
3988 creator: Some(self.creator),
3989 id: self.id,
3990 }
3991 }
3992}
3993
3994pub trait WorkspaceHandle {
3995 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
3996}
3997
3998impl WorkspaceHandle for View<Workspace> {
3999 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
4000 self.read(cx)
4001 .worktrees(cx)
4002 .flat_map(|worktree| {
4003 let worktree_id = worktree.read(cx).id();
4004 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
4005 worktree_id,
4006 path: f.path.clone(),
4007 })
4008 })
4009 .collect::<Vec<_>>()
4010 }
4011}
4012
4013impl std::fmt::Debug for OpenPaths {
4014 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4015 f.debug_struct("OpenPaths")
4016 .field("paths", &self.paths)
4017 .finish()
4018 }
4019}
4020
4021pub fn activate_workspace_for_project(
4022 cx: &mut AppContext,
4023 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
4024) -> Option<WindowHandle<Workspace>> {
4025 for window in cx.windows() {
4026 let Some(workspace) = window.downcast::<Workspace>() else {
4027 continue;
4028 };
4029
4030 let predicate = workspace
4031 .update(cx, |workspace, cx| {
4032 let project = workspace.project.read(cx);
4033 if predicate(project, cx) {
4034 cx.activate_window();
4035 true
4036 } else {
4037 false
4038 }
4039 })
4040 .log_err()
4041 .unwrap_or(false);
4042
4043 if predicate {
4044 return Some(workspace);
4045 }
4046 }
4047
4048 None
4049}
4050
4051pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
4052 DB.last_workspace().await.log_err().flatten()
4053}
4054
4055async fn join_channel_internal(
4056 channel_id: u64,
4057 app_state: &Arc<AppState>,
4058 requesting_window: Option<WindowHandle<Workspace>>,
4059 active_call: &Model<ActiveCall>,
4060 cx: &mut AsyncAppContext,
4061) -> Result<bool> {
4062 let (should_prompt, open_room) = active_call.read_with(cx, |active_call, cx| {
4063 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
4064 return (false, None);
4065 };
4066
4067 let already_in_channel = room.channel_id() == Some(channel_id);
4068 let should_prompt = room.is_sharing_project()
4069 && room.remote_participants().len() > 0
4070 && !already_in_channel;
4071 let open_room = if already_in_channel {
4072 active_call.room().cloned()
4073 } else {
4074 None
4075 };
4076 (should_prompt, open_room)
4077 })?;
4078
4079 if let Some(room) = open_room {
4080 let task = room.update(cx, |room, cx| {
4081 if let Some((project, host)) = room.most_active_project(cx) {
4082 return Some(join_remote_project(project, host, app_state.clone(), cx));
4083 }
4084
4085 None
4086 })?;
4087 if let Some(task) = task {
4088 task.await?;
4089 }
4090 return anyhow::Ok(true);
4091 }
4092
4093 if should_prompt {
4094 if let Some(workspace) = requesting_window {
4095 let answer = workspace.update(cx, |_, cx| {
4096 cx.prompt(
4097 PromptLevel::Warning,
4098 "Leaving this call will unshare your current project.\nDo you want to switch channels?",
4099 &["Yes, Join Channel", "Cancel"],
4100 )
4101 })?.await;
4102
4103 if answer == Ok(1) {
4104 return Ok(false);
4105 }
4106 } else {
4107 return Ok(false); // unreachable!() hopefully
4108 }
4109 }
4110
4111 let client = cx.update(|cx| active_call.read(cx).client())?;
4112
4113 let mut client_status = client.status();
4114
4115 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
4116 'outer: loop {
4117 let Some(status) = client_status.recv().await else {
4118 return Err(anyhow!("error connecting"));
4119 };
4120
4121 match status {
4122 Status::Connecting
4123 | Status::Authenticating
4124 | Status::Reconnecting
4125 | Status::Reauthenticating => continue,
4126 Status::Connected { .. } => break 'outer,
4127 Status::SignedOut => return Err(anyhow!("not signed in")),
4128 Status::UpgradeRequired => return Err(anyhow!("zed is out of date")),
4129 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
4130 return Err(anyhow!("zed is offline"))
4131 }
4132 }
4133 }
4134
4135 let room = active_call
4136 .update(cx, |active_call, cx| {
4137 active_call.join_channel(channel_id, cx)
4138 })?
4139 .await?;
4140
4141 let Some(room) = room else {
4142 return anyhow::Ok(true);
4143 };
4144
4145 room.update(cx, |room, _| room.room_update_completed())?
4146 .await;
4147
4148 let task = room.update(cx, |room, cx| {
4149 if let Some((project, host)) = room.most_active_project(cx) {
4150 return Some(join_remote_project(project, host, app_state.clone(), cx));
4151 }
4152
4153 None
4154 })?;
4155 if let Some(task) = task {
4156 task.await?;
4157 return anyhow::Ok(true);
4158 }
4159 anyhow::Ok(false)
4160}
4161
4162pub fn join_channel(
4163 channel_id: u64,
4164 app_state: Arc<AppState>,
4165 requesting_window: Option<WindowHandle<Workspace>>,
4166 cx: &mut AppContext,
4167) -> Task<Result<()>> {
4168 let active_call = ActiveCall::global(cx);
4169 cx.spawn(|mut cx| async move {
4170 let result = join_channel_internal(
4171 channel_id,
4172 &app_state,
4173 requesting_window,
4174 &active_call,
4175 &mut cx,
4176 )
4177 .await;
4178
4179 // join channel succeeded, and opened a window
4180 if matches!(result, Ok(true)) {
4181 return anyhow::Ok(());
4182 }
4183
4184 if requesting_window.is_some() {
4185 return anyhow::Ok(());
4186 }
4187
4188 // find an existing workspace to focus and show call controls
4189 let mut active_window = activate_any_workspace_window(&mut cx);
4190 if active_window.is_none() {
4191 // no open workspaces, make one to show the error in (blergh)
4192 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx))?
4193 .await?;
4194 }
4195
4196 active_window = activate_any_workspace_window(&mut cx);
4197 let Some(active_window) = active_window else {
4198 return anyhow::Ok(());
4199 };
4200
4201 if let Err(err) = result {
4202 active_window
4203 .update(&mut cx, |_, cx| {
4204 cx.prompt(
4205 PromptLevel::Critical,
4206 &format!("Failed to join channel: {}", err),
4207 &["Ok"],
4208 )
4209 })?
4210 .await
4211 .ok();
4212 }
4213
4214 // return ok, we showed the error to the user.
4215 return anyhow::Ok(());
4216 })
4217}
4218
4219pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<AnyWindowHandle> {
4220 cx.update(|cx| {
4221 for window in cx.windows() {
4222 let is_workspace = window.downcast::<Workspace>().is_some();
4223 if is_workspace {
4224 window.update(cx, |_, cx| cx.activate_window()).ok();
4225 return Some(window);
4226 }
4227 }
4228 None
4229 })
4230 .ok()
4231 .flatten()
4232}
4233
4234#[allow(clippy::type_complexity)]
4235pub fn open_paths(
4236 abs_paths: &[PathBuf],
4237 app_state: &Arc<AppState>,
4238 requesting_window: Option<WindowHandle<Workspace>>,
4239 cx: &mut AppContext,
4240) -> Task<
4241 anyhow::Result<(
4242 WindowHandle<Workspace>,
4243 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4244 )>,
4245> {
4246 let app_state = app_state.clone();
4247 let abs_paths = abs_paths.to_vec();
4248 // Open paths in existing workspace if possible
4249 let existing = activate_workspace_for_project(cx, {
4250 let abs_paths = abs_paths.clone();
4251 move |project, cx| project.contains_paths(&abs_paths, cx)
4252 });
4253 cx.spawn(move |mut cx| async move {
4254 if let Some(existing) = existing {
4255 Ok((
4256 existing.clone(),
4257 existing
4258 .update(&mut cx, |workspace, cx| {
4259 workspace.open_paths(abs_paths, true, cx)
4260 })?
4261 .await,
4262 ))
4263 } else {
4264 cx.update(move |cx| {
4265 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
4266 })?
4267 .await
4268 }
4269 })
4270}
4271
4272pub fn open_new(
4273 app_state: &Arc<AppState>,
4274 cx: &mut AppContext,
4275 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
4276) -> Task<()> {
4277 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
4278 cx.spawn(|mut cx| async move {
4279 if let Some((workspace, opened_paths)) = task.await.log_err() {
4280 workspace
4281 .update(&mut cx, |workspace, cx| {
4282 if opened_paths.is_empty() {
4283 init(workspace, cx)
4284 }
4285 })
4286 .log_err();
4287 }
4288 })
4289}
4290
4291pub fn create_and_open_local_file(
4292 path: &'static Path,
4293 cx: &mut ViewContext<Workspace>,
4294 default_content: impl 'static + Send + FnOnce() -> Rope,
4295) -> Task<Result<Box<dyn ItemHandle>>> {
4296 cx.spawn(|workspace, mut cx| async move {
4297 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
4298 if !fs.is_file(path).await {
4299 fs.create_file(path, Default::default()).await?;
4300 fs.save(path, &default_content(), Default::default())
4301 .await?;
4302 }
4303
4304 let mut items = workspace
4305 .update(&mut cx, |workspace, cx| {
4306 workspace.with_local_workspace(cx, |workspace, cx| {
4307 workspace.open_paths(vec![path.to_path_buf()], false, cx)
4308 })
4309 })?
4310 .await?
4311 .await;
4312
4313 let item = items.pop().flatten();
4314 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4315 })
4316}
4317
4318pub fn join_remote_project(
4319 project_id: u64,
4320 follow_user_id: u64,
4321 app_state: Arc<AppState>,
4322 cx: &mut AppContext,
4323) -> Task<Result<()>> {
4324 let windows = cx.windows();
4325 cx.spawn(|mut cx| async move {
4326 let existing_workspace = windows.into_iter().find_map(|window| {
4327 window.downcast::<Workspace>().and_then(|window| {
4328 window
4329 .update(&mut cx, |workspace, cx| {
4330 if workspace.project().read(cx).remote_id() == Some(project_id) {
4331 Some(window)
4332 } else {
4333 None
4334 }
4335 })
4336 .unwrap_or(None)
4337 })
4338 });
4339
4340 let workspace = if let Some(existing_workspace) = existing_workspace {
4341 existing_workspace
4342 } else {
4343 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
4344 let room = active_call
4345 .read_with(&cx, |call, _| call.room().cloned())?
4346 .ok_or_else(|| anyhow!("not in a call"))?;
4347 let project = room
4348 .update(&mut cx, |room, cx| {
4349 room.join_project(
4350 project_id,
4351 app_state.languages.clone(),
4352 app_state.fs.clone(),
4353 cx,
4354 )
4355 })?
4356 .await?;
4357
4358 let window_bounds_override = window_bounds_env_override(&cx);
4359 cx.update(|cx| {
4360 let options = (app_state.build_window_options)(window_bounds_override, None, cx);
4361 cx.open_window(options, |cx| {
4362 cx.build_view(|cx| Workspace::new(0, project, app_state.clone(), cx))
4363 })
4364 })?
4365 };
4366
4367 workspace.update(&mut cx, |workspace, cx| {
4368 cx.activate(true);
4369 cx.activate_window();
4370
4371 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4372 let follow_peer_id = room
4373 .read(cx)
4374 .remote_participants()
4375 .iter()
4376 .find(|(_, participant)| participant.user.id == follow_user_id)
4377 .map(|(_, p)| p.peer_id)
4378 .or_else(|| {
4379 // If we couldn't follow the given user, follow the host instead.
4380 let collaborator = workspace
4381 .project()
4382 .read(cx)
4383 .collaborators()
4384 .values()
4385 .find(|collaborator| collaborator.replica_id == 0)?;
4386 Some(collaborator.peer_id)
4387 });
4388
4389 if let Some(follow_peer_id) = follow_peer_id {
4390 workspace
4391 .follow(follow_peer_id, cx)
4392 .map(|follow| follow.detach_and_log_err(cx));
4393 }
4394 }
4395 })?;
4396
4397 anyhow::Ok(())
4398 })
4399}
4400
4401pub fn restart(_: &Restart, cx: &mut AppContext) {
4402 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
4403 let mut workspace_windows = cx
4404 .windows()
4405 .into_iter()
4406 .filter_map(|window| window.downcast::<Workspace>())
4407 .collect::<Vec<_>>();
4408
4409 // If multiple windows have unsaved changes, and need a save prompt,
4410 // prompt in the active window before switching to a different window.
4411 workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
4412
4413 let mut prompt = None;
4414 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4415 prompt = window
4416 .update(cx, |_, cx| {
4417 cx.prompt(
4418 PromptLevel::Info,
4419 "Are you sure you want to restart?",
4420 &["Restart", "Cancel"],
4421 )
4422 })
4423 .ok();
4424 }
4425
4426 cx.spawn(|mut cx| async move {
4427 if let Some(mut prompt) = prompt {
4428 let answer = prompt.await?;
4429 if answer != 0 {
4430 return Ok(());
4431 }
4432 }
4433
4434 // If the user cancels any save prompt, then keep the app open.
4435 for window in workspace_windows {
4436 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
4437 workspace.prepare_to_close(true, cx)
4438 }) {
4439 if !should_close.await? {
4440 return Ok(());
4441 }
4442 }
4443 }
4444
4445 cx.update(|cx| cx.restart())
4446 })
4447 .detach_and_log_err(cx);
4448}
4449
4450fn parse_pixel_position_env_var(value: &str) -> Option<Point<GlobalPixels>> {
4451 let mut parts = value.split(',');
4452 let x: usize = parts.next()?.parse().ok()?;
4453 let y: usize = parts.next()?.parse().ok()?;
4454 Some(point((x as f64).into(), (y as f64).into()))
4455}
4456
4457fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
4458 let mut parts = value.split(',');
4459 let width: usize = parts.next()?.parse().ok()?;
4460 let height: usize = parts.next()?.parse().ok()?;
4461 Some(size((width as f64).into(), (height as f64).into()))
4462}
4463
4464#[cfg(test)]
4465mod tests {
4466 use super::*;
4467 use crate::item::{
4468 test::{TestItem, TestProjectItem},
4469 ItemEvent,
4470 };
4471 use fs::FakeFs;
4472 use gpui::TestAppContext;
4473 use project::{Project, ProjectEntryId};
4474 use serde_json::json;
4475 use settings::SettingsStore;
4476 use std::{cell::RefCell, rc::Rc};
4477
4478 #[gpui::test]
4479 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4480 init_test(cx);
4481
4482 let fs = FakeFs::new(cx.executor());
4483 let project = Project::test(fs, [], cx).await;
4484 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4485
4486 // Adding an item with no ambiguity renders the tab without detail.
4487 let item1 = cx.build_view(|cx| {
4488 let mut item = TestItem::new(cx);
4489 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4490 item
4491 });
4492 workspace.update(cx, |workspace, cx| {
4493 workspace.add_item(Box::new(item1.clone()), cx);
4494 });
4495 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
4496
4497 // Adding an item that creates ambiguity increases the level of detail on
4498 // both tabs.
4499 let item2 = cx.build_view(|cx| {
4500 let mut item = TestItem::new(cx);
4501 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4502 item
4503 });
4504 workspace.update(cx, |workspace, cx| {
4505 workspace.add_item(Box::new(item2.clone()), cx);
4506 });
4507 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4508 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4509
4510 // Adding an item that creates ambiguity increases the level of detail only
4511 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4512 // we stop at the highest detail available.
4513 let item3 = cx.build_view(|cx| {
4514 let mut item = TestItem::new(cx);
4515 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4516 item
4517 });
4518 workspace.update(cx, |workspace, cx| {
4519 workspace.add_item(Box::new(item3.clone()), cx);
4520 });
4521 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4522 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4523 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4524 }
4525
4526 #[gpui::test]
4527 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4528 init_test(cx);
4529
4530 let fs = FakeFs::new(cx.executor());
4531 fs.insert_tree(
4532 "/root1",
4533 json!({
4534 "one.txt": "",
4535 "two.txt": "",
4536 }),
4537 )
4538 .await;
4539 fs.insert_tree(
4540 "/root2",
4541 json!({
4542 "three.txt": "",
4543 }),
4544 )
4545 .await;
4546
4547 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4548 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4549 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4550 let worktree_id = project.read_with(cx, |project, cx| {
4551 project.worktrees().next().unwrap().read(cx).id()
4552 });
4553
4554 let item1 = cx.build_view(|cx| {
4555 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4556 });
4557 let item2 = cx.build_view(|cx| {
4558 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4559 });
4560
4561 // Add an item to an empty pane
4562 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4563 project.read_with(cx, |project, cx| {
4564 assert_eq!(
4565 project.active_entry(),
4566 project
4567 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4568 .map(|e| e.id)
4569 );
4570 });
4571 assert_eq!(cx.window_title().as_deref(), Some("one.txt β root1"));
4572
4573 // Add a second item to a non-empty pane
4574 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4575 assert_eq!(cx.window_title().as_deref(), Some("two.txt β root1"));
4576 project.read_with(cx, |project, cx| {
4577 assert_eq!(
4578 project.active_entry(),
4579 project
4580 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4581 .map(|e| e.id)
4582 );
4583 });
4584
4585 // Close the active item
4586 pane.update(cx, |pane, cx| {
4587 pane.close_active_item(&Default::default(), cx).unwrap()
4588 })
4589 .await
4590 .unwrap();
4591 assert_eq!(cx.window_title().as_deref(), Some("one.txt β root1"));
4592 project.read_with(cx, |project, cx| {
4593 assert_eq!(
4594 project.active_entry(),
4595 project
4596 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4597 .map(|e| e.id)
4598 );
4599 });
4600
4601 // Add a project folder
4602 project
4603 .update(cx, |project, cx| {
4604 project.find_or_create_local_worktree("/root2", true, cx)
4605 })
4606 .await
4607 .unwrap();
4608 assert_eq!(cx.window_title().as_deref(), Some("one.txt β root1, root2"));
4609
4610 // Remove a project folder
4611 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4612 assert_eq!(cx.window_title().as_deref(), Some("one.txt β root2"));
4613 }
4614
4615 #[gpui::test]
4616 async fn test_close_window(cx: &mut TestAppContext) {
4617 init_test(cx);
4618
4619 let fs = FakeFs::new(cx.executor());
4620 fs.insert_tree("/root", json!({ "one": "" })).await;
4621
4622 let project = Project::test(fs, ["root".as_ref()], cx).await;
4623 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4624
4625 // When there are no dirty items, there's nothing to do.
4626 let item1 = cx.build_view(|cx| TestItem::new(cx));
4627 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4628 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4629 assert!(task.await.unwrap());
4630
4631 // When there are dirty untitled items, prompt to save each one. If the user
4632 // cancels any prompt, then abort.
4633 let item2 = cx.build_view(|cx| TestItem::new(cx).with_dirty(true));
4634 let item3 = cx.build_view(|cx| {
4635 TestItem::new(cx)
4636 .with_dirty(true)
4637 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4638 });
4639 workspace.update(cx, |w, cx| {
4640 w.add_item(Box::new(item2.clone()), cx);
4641 w.add_item(Box::new(item3.clone()), cx);
4642 });
4643 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4644 cx.executor().run_until_parked();
4645 cx.simulate_prompt_answer(2); // cancel save all
4646 cx.executor().run_until_parked();
4647 cx.simulate_prompt_answer(2); // cancel save all
4648 cx.executor().run_until_parked();
4649 assert!(!cx.has_pending_prompt());
4650 assert!(!task.await.unwrap());
4651 }
4652
4653 #[gpui::test]
4654 async fn test_close_pane_items(cx: &mut TestAppContext) {
4655 init_test(cx);
4656
4657 let fs = FakeFs::new(cx.executor());
4658
4659 let project = Project::test(fs, None, cx).await;
4660 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4661
4662 let item1 = cx.build_view(|cx| {
4663 TestItem::new(cx)
4664 .with_dirty(true)
4665 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4666 });
4667 let item2 = cx.build_view(|cx| {
4668 TestItem::new(cx)
4669 .with_dirty(true)
4670 .with_conflict(true)
4671 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4672 });
4673 let item3 = cx.build_view(|cx| {
4674 TestItem::new(cx)
4675 .with_dirty(true)
4676 .with_conflict(true)
4677 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4678 });
4679 let item4 = cx.build_view(|cx| {
4680 TestItem::new(cx)
4681 .with_dirty(true)
4682 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4683 });
4684 let pane = workspace.update(cx, |workspace, cx| {
4685 workspace.add_item(Box::new(item1.clone()), cx);
4686 workspace.add_item(Box::new(item2.clone()), cx);
4687 workspace.add_item(Box::new(item3.clone()), cx);
4688 workspace.add_item(Box::new(item4.clone()), cx);
4689 workspace.active_pane().clone()
4690 });
4691
4692 let close_items = pane.update(cx, |pane, cx| {
4693 pane.activate_item(1, true, true, cx);
4694 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4695 let item1_id = item1.item_id();
4696 let item3_id = item3.item_id();
4697 let item4_id = item4.item_id();
4698 pane.close_items(cx, SaveIntent::Close, move |id| {
4699 [item1_id, item3_id, item4_id].contains(&id)
4700 })
4701 });
4702 cx.executor().run_until_parked();
4703
4704 assert!(cx.has_pending_prompt());
4705 // Ignore "Save all" prompt
4706 cx.simulate_prompt_answer(2);
4707 cx.executor().run_until_parked();
4708 // There's a prompt to save item 1.
4709 pane.update(cx, |pane, _| {
4710 assert_eq!(pane.items_len(), 4);
4711 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
4712 });
4713 // Confirm saving item 1.
4714 cx.simulate_prompt_answer(0);
4715 cx.executor().run_until_parked();
4716
4717 // Item 1 is saved. There's a prompt to save item 3.
4718 pane.update(cx, |pane, cx| {
4719 assert_eq!(item1.read(cx).save_count, 1);
4720 assert_eq!(item1.read(cx).save_as_count, 0);
4721 assert_eq!(item1.read(cx).reload_count, 0);
4722 assert_eq!(pane.items_len(), 3);
4723 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
4724 });
4725 assert!(cx.has_pending_prompt());
4726
4727 // Cancel saving item 3.
4728 cx.simulate_prompt_answer(1);
4729 cx.executor().run_until_parked();
4730
4731 // Item 3 is reloaded. There's a prompt to save item 4.
4732 pane.update(cx, |pane, cx| {
4733 assert_eq!(item3.read(cx).save_count, 0);
4734 assert_eq!(item3.read(cx).save_as_count, 0);
4735 assert_eq!(item3.read(cx).reload_count, 1);
4736 assert_eq!(pane.items_len(), 2);
4737 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
4738 });
4739 assert!(cx.has_pending_prompt());
4740
4741 // Confirm saving item 4.
4742 cx.simulate_prompt_answer(0);
4743 cx.executor().run_until_parked();
4744
4745 // There's a prompt for a path for item 4.
4746 cx.simulate_new_path_selection(|_| Some(Default::default()));
4747 close_items.await.unwrap();
4748
4749 // The requested items are closed.
4750 pane.update(cx, |pane, cx| {
4751 assert_eq!(item4.read(cx).save_count, 0);
4752 assert_eq!(item4.read(cx).save_as_count, 1);
4753 assert_eq!(item4.read(cx).reload_count, 0);
4754 assert_eq!(pane.items_len(), 1);
4755 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4756 });
4757 }
4758
4759 #[gpui::test]
4760 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4761 init_test(cx);
4762
4763 let fs = FakeFs::new(cx.executor());
4764 let project = Project::test(fs, [], cx).await;
4765 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4766
4767 // Create several workspace items with single project entries, and two
4768 // workspace items with multiple project entries.
4769 let single_entry_items = (0..=4)
4770 .map(|project_entry_id| {
4771 cx.build_view(|cx| {
4772 TestItem::new(cx)
4773 .with_dirty(true)
4774 .with_project_items(&[TestProjectItem::new(
4775 project_entry_id,
4776 &format!("{project_entry_id}.txt"),
4777 cx,
4778 )])
4779 })
4780 })
4781 .collect::<Vec<_>>();
4782 let item_2_3 = cx.build_view(|cx| {
4783 TestItem::new(cx)
4784 .with_dirty(true)
4785 .with_singleton(false)
4786 .with_project_items(&[
4787 single_entry_items[2].read(cx).project_items[0].clone(),
4788 single_entry_items[3].read(cx).project_items[0].clone(),
4789 ])
4790 });
4791 let item_3_4 = cx.build_view(|cx| {
4792 TestItem::new(cx)
4793 .with_dirty(true)
4794 .with_singleton(false)
4795 .with_project_items(&[
4796 single_entry_items[3].read(cx).project_items[0].clone(),
4797 single_entry_items[4].read(cx).project_items[0].clone(),
4798 ])
4799 });
4800
4801 // Create two panes that contain the following project entries:
4802 // left pane:
4803 // multi-entry items: (2, 3)
4804 // single-entry items: 0, 1, 2, 3, 4
4805 // right pane:
4806 // single-entry items: 1
4807 // multi-entry items: (3, 4)
4808 let left_pane = workspace.update(cx, |workspace, cx| {
4809 let left_pane = workspace.active_pane().clone();
4810 workspace.add_item(Box::new(item_2_3.clone()), cx);
4811 for item in single_entry_items {
4812 workspace.add_item(Box::new(item), cx);
4813 }
4814 left_pane.update(cx, |pane, cx| {
4815 pane.activate_item(2, true, true, cx);
4816 });
4817
4818 let right_pane = workspace
4819 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
4820 .unwrap();
4821
4822 right_pane.update(cx, |pane, cx| {
4823 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
4824 });
4825
4826 left_pane
4827 });
4828
4829 cx.focus_view(&left_pane);
4830
4831 // When closing all of the items in the left pane, we should be prompted twice:
4832 // once for project entry 0, and once for project entry 2. Project entries 1,
4833 // 3, and 4 are all still open in the other paten. After those two
4834 // prompts, the task should complete.
4835
4836 let close = left_pane.update(cx, |pane, cx| {
4837 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
4838 });
4839 cx.executor().run_until_parked();
4840
4841 // Discard "Save all" prompt
4842 cx.simulate_prompt_answer(2);
4843
4844 cx.executor().run_until_parked();
4845 left_pane.update(cx, |pane, cx| {
4846 assert_eq!(
4847 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4848 &[ProjectEntryId::from_proto(0)]
4849 );
4850 });
4851 cx.simulate_prompt_answer(0);
4852
4853 cx.executor().run_until_parked();
4854 left_pane.update(cx, |pane, cx| {
4855 assert_eq!(
4856 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4857 &[ProjectEntryId::from_proto(2)]
4858 );
4859 });
4860 cx.simulate_prompt_answer(0);
4861
4862 cx.executor().run_until_parked();
4863 close.await.unwrap();
4864 left_pane.update(cx, |pane, _| {
4865 assert_eq!(pane.items_len(), 0);
4866 });
4867 }
4868
4869 #[gpui::test]
4870 async fn test_autosave(cx: &mut gpui::TestAppContext) {
4871 init_test(cx);
4872
4873 let fs = FakeFs::new(cx.executor());
4874 let project = Project::test(fs, [], cx).await;
4875 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4876 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4877
4878 let item = cx.build_view(|cx| {
4879 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4880 });
4881 let item_id = item.entity_id();
4882 workspace.update(cx, |workspace, cx| {
4883 workspace.add_item(Box::new(item.clone()), cx);
4884 });
4885
4886 // Autosave on window change.
4887 item.update(cx, |item, cx| {
4888 cx.update_global(|settings: &mut SettingsStore, cx| {
4889 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4890 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4891 })
4892 });
4893 item.is_dirty = true;
4894 });
4895
4896 // Deactivating the window saves the file.
4897 cx.simulate_deactivation();
4898 cx.executor().run_until_parked();
4899 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
4900
4901 // Autosave on focus change.
4902 item.update(cx, |item, cx| {
4903 cx.focus_self();
4904 cx.update_global(|settings: &mut SettingsStore, cx| {
4905 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4906 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4907 })
4908 });
4909 item.is_dirty = true;
4910 });
4911
4912 // Blurring the item saves the file.
4913 item.update(cx, |_, cx| cx.blur());
4914 cx.executor().run_until_parked();
4915 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
4916
4917 // Deactivating the window still saves the file.
4918 cx.simulate_activation();
4919 item.update(cx, |item, cx| {
4920 cx.focus_self();
4921 item.is_dirty = true;
4922 });
4923 cx.simulate_deactivation();
4924
4925 cx.executor().run_until_parked();
4926 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
4927
4928 // Autosave after delay.
4929 item.update(cx, |item, cx| {
4930 cx.update_global(|settings: &mut SettingsStore, cx| {
4931 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4932 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
4933 })
4934 });
4935 item.is_dirty = true;
4936 cx.emit(ItemEvent::Edit);
4937 });
4938
4939 // Delay hasn't fully expired, so the file is still dirty and unsaved.
4940 cx.executor().advance_clock(Duration::from_millis(250));
4941 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
4942
4943 // After delay expires, the file is saved.
4944 cx.executor().advance_clock(Duration::from_millis(250));
4945 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
4946
4947 // Autosave on focus change, ensuring closing the tab counts as such.
4948 item.update(cx, |item, cx| {
4949 cx.update_global(|settings: &mut SettingsStore, cx| {
4950 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4951 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4952 })
4953 });
4954 item.is_dirty = true;
4955 });
4956
4957 pane.update(cx, |pane, cx| {
4958 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4959 })
4960 .await
4961 .unwrap();
4962 assert!(!cx.has_pending_prompt());
4963 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4964
4965 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
4966 workspace.update(cx, |workspace, cx| {
4967 workspace.add_item(Box::new(item.clone()), cx);
4968 });
4969 item.update(cx, |item, cx| {
4970 item.project_items[0].update(cx, |item, _| {
4971 item.entry_id = None;
4972 });
4973 item.is_dirty = true;
4974 cx.blur();
4975 });
4976 cx.executor().run_until_parked();
4977 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4978
4979 // Ensure autosave is prevented for deleted files also when closing the buffer.
4980 let _close_items = pane.update(cx, |pane, cx| {
4981 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
4982 });
4983 cx.executor().run_until_parked();
4984 assert!(cx.has_pending_prompt());
4985 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
4986 }
4987
4988 #[gpui::test]
4989 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
4990 init_test(cx);
4991
4992 let fs = FakeFs::new(cx.executor());
4993
4994 let project = Project::test(fs, [], cx).await;
4995 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4996
4997 let item = cx.build_view(|cx| {
4998 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4999 });
5000 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5001 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
5002 let toolbar_notify_count = Rc::new(RefCell::new(0));
5003
5004 workspace.update(cx, |workspace, cx| {
5005 workspace.add_item(Box::new(item.clone()), cx);
5006 let toolbar_notification_count = toolbar_notify_count.clone();
5007 cx.observe(&toolbar, move |_, _, _| {
5008 *toolbar_notification_count.borrow_mut() += 1
5009 })
5010 .detach();
5011 });
5012
5013 pane.update(cx, |pane, _| {
5014 assert!(!pane.can_navigate_backward());
5015 assert!(!pane.can_navigate_forward());
5016 });
5017
5018 item.update(cx, |item, cx| {
5019 item.set_state("one".to_string(), cx);
5020 });
5021
5022 // Toolbar must be notified to re-render the navigation buttons
5023 assert_eq!(*toolbar_notify_count.borrow(), 1);
5024
5025 pane.update(cx, |pane, _| {
5026 assert!(pane.can_navigate_backward());
5027 assert!(!pane.can_navigate_forward());
5028 });
5029
5030 workspace
5031 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
5032 .await
5033 .unwrap();
5034
5035 assert_eq!(*toolbar_notify_count.borrow(), 2);
5036 pane.update(cx, |pane, _| {
5037 assert!(!pane.can_navigate_backward());
5038 assert!(pane.can_navigate_forward());
5039 });
5040 }
5041
5042 // #[gpui::test]
5043 // async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
5044 // init_test(cx);
5045 // let fs = FakeFs::new(cx.executor());
5046
5047 // let project = Project::test(fs, [], cx).await;
5048 // let window = cx.add_window(|cx| Workspace::test_new(project, cx));
5049 // let workspace = window.root(cx);
5050
5051 // let panel = workspace.update(cx, |workspace, cx| {
5052 // let panel = cx.build_view(|_| TestPanel::new(DockPosition::Right));
5053 // workspace.add_panel(panel.clone(), cx);
5054
5055 // workspace
5056 // .right_dock()
5057 // .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5058
5059 // panel
5060 // });
5061
5062 // let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5063 // pane.update(cx, |pane, cx| {
5064 // let item = cx.build_view(|_| TestItem::new(cx));
5065 // pane.add_item(Box::new(item), true, true, None, cx);
5066 // });
5067
5068 // // Transfer focus from center to panel
5069 // workspace.update(cx, |workspace, cx| {
5070 // workspace.toggle_panel_focus::<TestPanel>(cx);
5071 // });
5072
5073 // workspace.update(cx, |workspace, cx| {
5074 // assert!(workspace.right_dock().read(cx).is_open());
5075 // assert!(!panel.is_zoomed(cx));
5076 // assert!(panel.has_focus(cx));
5077 // });
5078
5079 // // Transfer focus from panel to center
5080 // workspace.update(cx, |workspace, cx| {
5081 // workspace.toggle_panel_focus::<TestPanel>(cx);
5082 // });
5083
5084 // workspace.update(cx, |workspace, cx| {
5085 // assert!(workspace.right_dock().read(cx).is_open());
5086 // assert!(!panel.is_zoomed(cx));
5087 // assert!(!panel.has_focus(cx));
5088 // });
5089
5090 // // Close the dock
5091 // workspace.update(cx, |workspace, cx| {
5092 // workspace.toggle_dock(DockPosition::Right, cx);
5093 // });
5094
5095 // workspace.update(cx, |workspace, cx| {
5096 // assert!(!workspace.right_dock().read(cx).is_open());
5097 // assert!(!panel.is_zoomed(cx));
5098 // assert!(!panel.has_focus(cx));
5099 // });
5100
5101 // // Open the dock
5102 // workspace.update(cx, |workspace, cx| {
5103 // workspace.toggle_dock(DockPosition::Right, cx);
5104 // });
5105
5106 // workspace.update(cx, |workspace, cx| {
5107 // assert!(workspace.right_dock().read(cx).is_open());
5108 // assert!(!panel.is_zoomed(cx));
5109 // assert!(panel.has_focus(cx));
5110 // });
5111
5112 // // Focus and zoom panel
5113 // panel.update(cx, |panel, cx| {
5114 // cx.focus_self();
5115 // panel.set_zoomed(true, cx)
5116 // });
5117
5118 // workspace.update(cx, |workspace, cx| {
5119 // assert!(workspace.right_dock().read(cx).is_open());
5120 // assert!(panel.is_zoomed(cx));
5121 // assert!(panel.has_focus(cx));
5122 // });
5123
5124 // // Transfer focus to the center closes the dock
5125 // workspace.update(cx, |workspace, cx| {
5126 // workspace.toggle_panel_focus::<TestPanel>(cx);
5127 // });
5128
5129 // workspace.update(cx, |workspace, cx| {
5130 // assert!(!workspace.right_dock().read(cx).is_open());
5131 // assert!(panel.is_zoomed(cx));
5132 // assert!(!panel.has_focus(cx));
5133 // });
5134
5135 // // Transferring focus back to the panel keeps it zoomed
5136 // workspace.update(cx, |workspace, cx| {
5137 // workspace.toggle_panel_focus::<TestPanel>(cx);
5138 // });
5139
5140 // workspace.update(cx, |workspace, cx| {
5141 // assert!(workspace.right_dock().read(cx).is_open());
5142 // assert!(panel.is_zoomed(cx));
5143 // assert!(panel.has_focus(cx));
5144 // });
5145
5146 // // Close the dock while it is zoomed
5147 // workspace.update(cx, |workspace, cx| {
5148 // workspace.toggle_dock(DockPosition::Right, cx)
5149 // });
5150
5151 // workspace.update(cx, |workspace, cx| {
5152 // assert!(!workspace.right_dock().read(cx).is_open());
5153 // assert!(panel.is_zoomed(cx));
5154 // assert!(workspace.zoomed.is_none());
5155 // assert!(!panel.has_focus(cx));
5156 // });
5157
5158 // // Opening the dock, when it's zoomed, retains focus
5159 // workspace.update(cx, |workspace, cx| {
5160 // workspace.toggle_dock(DockPosition::Right, cx)
5161 // });
5162
5163 // workspace.update(cx, |workspace, cx| {
5164 // assert!(workspace.right_dock().read(cx).is_open());
5165 // assert!(panel.is_zoomed(cx));
5166 // assert!(workspace.zoomed.is_some());
5167 // assert!(panel.has_focus(cx));
5168 // });
5169
5170 // // Unzoom and close the panel, zoom the active pane.
5171 // panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5172 // workspace.update(cx, |workspace, cx| {
5173 // workspace.toggle_dock(DockPosition::Right, cx)
5174 // });
5175 // pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5176
5177 // // Opening a dock unzooms the pane.
5178 // workspace.update(cx, |workspace, cx| {
5179 // workspace.toggle_dock(DockPosition::Right, cx)
5180 // });
5181 // workspace.update(cx, |workspace, cx| {
5182 // let pane = pane.read(cx);
5183 // assert!(!pane.is_zoomed());
5184 // assert!(!pane.has_focus());
5185 // assert!(workspace.right_dock().read(cx).is_open());
5186 // assert!(workspace.zoomed.is_none());
5187 // });
5188 // }
5189
5190 // #[gpui::test]
5191 // async fn test_panels(cx: &mut gpui::TestAppContext) {
5192 // init_test(cx);
5193 // let fs = FakeFs::new(cx.executor());
5194
5195 // let project = Project::test(fs, [], cx).await;
5196 // let window = cx.add_window(|cx| Workspace::test_new(project, cx));
5197 // let workspace = window.root(cx);
5198
5199 // let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5200 // // Add panel_1 on the left, panel_2 on the right.
5201 // let panel_1 = cx.build_view(|_| TestPanel::new(DockPosition::Left));
5202 // workspace.add_panel(panel_1.clone(), cx);
5203 // workspace
5204 // .left_dock()
5205 // .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5206 // let panel_2 = cx.build_view(|_| TestPanel::new(DockPosition::Right));
5207 // workspace.add_panel(panel_2.clone(), cx);
5208 // workspace
5209 // .right_dock()
5210 // .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5211
5212 // let left_dock = workspace.left_dock();
5213 // assert_eq!(
5214 // left_dock.read(cx).visible_panel().unwrap().id(),
5215 // panel_1.id()
5216 // );
5217 // assert_eq!(
5218 // left_dock.read(cx).active_panel_size(cx).unwrap(),
5219 // panel_1.size(cx)
5220 // );
5221
5222 // left_dock.update(cx, |left_dock, cx| {
5223 // left_dock.resize_active_panel(Some(1337.), cx)
5224 // });
5225 // assert_eq!(
5226 // workspace
5227 // .right_dock()
5228 // .read(cx)
5229 // .visible_panel()
5230 // .unwrap()
5231 // .id(),
5232 // panel_2.id()
5233 // );
5234
5235 // (panel_1, panel_2)
5236 // });
5237
5238 // // Move panel_1 to the right
5239 // panel_1.update(cx, |panel_1, cx| {
5240 // panel_1.set_position(DockPosition::Right, cx)
5241 // });
5242
5243 // workspace.update(cx, |workspace, cx| {
5244 // // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5245 // // Since it was the only panel on the left, the left dock should now be closed.
5246 // assert!(!workspace.left_dock().read(cx).is_open());
5247 // assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5248 // let right_dock = workspace.right_dock();
5249 // assert_eq!(
5250 // right_dock.read(cx).visible_panel().unwrap().id(),
5251 // panel_1.id()
5252 // );
5253 // assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5254
5255 // // Now we move panel_2Β to the left
5256 // panel_2.set_position(DockPosition::Left, cx);
5257 // });
5258
5259 // workspace.update(cx, |workspace, cx| {
5260 // // Since panel_2 was not visible on the right, we don't open the left dock.
5261 // assert!(!workspace.left_dock().read(cx).is_open());
5262 // // And the right dock is unaffected in it's displaying of panel_1
5263 // assert!(workspace.right_dock().read(cx).is_open());
5264 // assert_eq!(
5265 // workspace
5266 // .right_dock()
5267 // .read(cx)
5268 // .visible_panel()
5269 // .unwrap()
5270 // .id(),
5271 // panel_1.id()
5272 // );
5273 // });
5274
5275 // // Move panel_1 back to the left
5276 // panel_1.update(cx, |panel_1, cx| {
5277 // panel_1.set_position(DockPosition::Left, cx)
5278 // });
5279
5280 // workspace.update(cx, |workspace, cx| {
5281 // // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5282 // let left_dock = workspace.left_dock();
5283 // assert!(left_dock.read(cx).is_open());
5284 // assert_eq!(
5285 // left_dock.read(cx).visible_panel().unwrap().id(),
5286 // panel_1.id()
5287 // );
5288 // assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
5289 // // And right the dock should be closed as it no longer has any panels.
5290 // assert!(!workspace.right_dock().read(cx).is_open());
5291
5292 // // Now we move panel_1 to the bottom
5293 // panel_1.set_position(DockPosition::Bottom, cx);
5294 // });
5295
5296 // workspace.update(cx, |workspace, cx| {
5297 // // Since panel_1 was visible on the left, we close the left dock.
5298 // assert!(!workspace.left_dock().read(cx).is_open());
5299 // // The bottom dock is sized based on the panel's default size,
5300 // // since the panel orientation changed from vertical to horizontal.
5301 // let bottom_dock = workspace.bottom_dock();
5302 // assert_eq!(
5303 // bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5304 // panel_1.size(cx),
5305 // );
5306 // // Close bottom dock and move panel_1 back to the left.
5307 // bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5308 // panel_1.set_position(DockPosition::Left, cx);
5309 // });
5310
5311 // // Emit activated event on panel 1
5312 // panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
5313
5314 // // Now the left dock is open and panel_1 is active and focused.
5315 // workspace.update(cx, |workspace, cx| {
5316 // let left_dock = workspace.left_dock();
5317 // assert!(left_dock.read(cx).is_open());
5318 // assert_eq!(
5319 // left_dock.read(cx).visible_panel().unwrap().id(),
5320 // panel_1.id()
5321 // );
5322 // assert!(panel_1.is_focused(cx));
5323 // });
5324
5325 // // Emit closed event on panel 2, which is not active
5326 // panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
5327
5328 // // Wo don't close the left dock, because panel_2 wasn't the active panel
5329 // workspace.update(cx, |workspace, cx| {
5330 // let left_dock = workspace.left_dock();
5331 // assert!(left_dock.read(cx).is_open());
5332 // assert_eq!(
5333 // left_dock.read(cx).visible_panel().unwrap().id(),
5334 // panel_1.id()
5335 // );
5336 // });
5337
5338 // // Emitting a ZoomIn event shows the panel as zoomed.
5339 // panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
5340 // workspace.update(cx, |workspace, _| {
5341 // assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5342 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5343 // });
5344
5345 // // Move panel to another dock while it is zoomed
5346 // panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5347 // workspace.update(cx, |workspace, _| {
5348 // assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5349 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5350 // });
5351
5352 // // If focus is transferred to another view that's not a panel or another pane, we still show
5353 // // the panel as zoomed.
5354 // let focus_receiver = cx.build_view(|_| EmptyView);
5355 // focus_receiver.update(cx, |_, cx| cx.focus_self());
5356 // workspace.update(cx, |workspace, _| {
5357 // assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5358 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5359 // });
5360
5361 // // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5362 // workspace.update(cx, |_, cx| cx.focus_self());
5363 // workspace.update(cx, |workspace, _| {
5364 // assert_eq!(workspace.zoomed, None);
5365 // assert_eq!(workspace.zoomed_position, None);
5366 // });
5367
5368 // // If focus is transferred again to another view that's not a panel or a pane, we won't
5369 // // show the panel as zoomed because it wasn't zoomed before.
5370 // focus_receiver.update(cx, |_, cx| cx.focus_self());
5371 // workspace.update(cx, |workspace, _| {
5372 // assert_eq!(workspace.zoomed, None);
5373 // assert_eq!(workspace.zoomed_position, None);
5374 // });
5375
5376 // // When focus is transferred back to the panel, it is zoomed again.
5377 // panel_1.update(cx, |_, cx| cx.focus_self());
5378 // workspace.update(cx, |workspace, _| {
5379 // assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
5380 // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5381 // });
5382
5383 // // Emitting a ZoomOut event unzooms the panel.
5384 // panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
5385 // workspace.update(cx, |workspace, _| {
5386 // assert_eq!(workspace.zoomed, None);
5387 // assert_eq!(workspace.zoomed_position, None);
5388 // });
5389
5390 // // Emit closed event on panel 1, which is active
5391 // panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
5392
5393 // // Now the left dock is closed, because panel_1 was the active panel
5394 // workspace.update(cx, |workspace, cx| {
5395 // let right_dock = workspace.right_dock();
5396 // assert!(!right_dock.read(cx).is_open());
5397 // });
5398 // }
5399
5400 pub fn init_test(cx: &mut TestAppContext) {
5401 cx.update(|cx| {
5402 let settings_store = SettingsStore::test(cx);
5403 cx.set_global(settings_store);
5404 theme::init(theme::LoadThemes::JustBase, cx);
5405 language::init(cx);
5406 crate::init_settings(cx);
5407 Project::init_settings(cx);
5408 });
5409 }
5410}