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