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