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