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