1pub mod dock;
2/// NOTE: Focus only 'takes' after an update has flushed_effects.
3///
4/// This may cause issues when you're trying to write tests that use workspace focus to add items at
5/// specific locations.
6pub mod item;
7pub mod notifications;
8pub mod pane;
9pub mod pane_group;
10mod persistence;
11pub mod searchable;
12pub mod shared_screen;
13mod status_bar;
14mod toolbar;
15
16use anyhow::{anyhow, Context, Result};
17use assets::Assets;
18use call::ActiveCall;
19use client::{
20 proto::{self, PeerId},
21 Client, TypedEnvelope, UserStore,
22};
23use collections::{hash_map, HashMap, HashSet};
24use drag_and_drop::DragAndDrop;
25use futures::{
26 channel::{mpsc, oneshot},
27 future::try_join_all,
28 FutureExt, StreamExt,
29};
30use gpui::{
31 actions,
32 elements::*,
33 geometry::{
34 rect::RectF,
35 vector::{vec2f, Vector2F},
36 },
37 impl_actions,
38 platform::{
39 CursorStyle, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds,
40 WindowOptions,
41 },
42 AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
43 SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
44 WindowContext,
45};
46use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
47use language::{LanguageRegistry, Rope};
48use std::{
49 any::TypeId,
50 borrow::Cow,
51 cmp, env,
52 future::Future,
53 path::{Path, PathBuf},
54 str,
55 sync::Arc,
56 time::Duration,
57};
58
59use crate::{
60 notifications::simple_message_notification::MessageNotification,
61 persistence::model::{
62 DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
63 },
64};
65use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle, TogglePanel};
66use lazy_static::lazy_static;
67use notifications::{NotificationHandle, NotifyResultExt};
68pub use pane::*;
69pub use pane_group::*;
70use persistence::{model::SerializedItem, DB};
71pub use persistence::{
72 model::{ItemId, WorkspaceLocation},
73 WorkspaceDb, DB as WORKSPACE_DB,
74};
75use postage::prelude::Stream;
76use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
77use serde::Deserialize;
78use settings::{Autosave, Settings};
79use shared_screen::SharedScreen;
80use status_bar::StatusBar;
81pub use status_bar::StatusItemView;
82use theme::{Theme, ThemeRegistry};
83pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
84use util::{paths, ResultExt};
85
86lazy_static! {
87 static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
88 .ok()
89 .as_deref()
90 .and_then(parse_pixel_position_env_var);
91 static ref ZED_WINDOW_POSITION: Option<Vector2F> = env::var("ZED_WINDOW_POSITION")
92 .ok()
93 .as_deref()
94 .and_then(parse_pixel_position_env_var);
95}
96
97pub trait Modal: View {
98 fn dismiss_on_event(event: &Self::Event) -> bool;
99}
100
101#[derive(Clone, PartialEq)]
102pub struct RemoveWorktreeFromProject(pub WorktreeId);
103
104actions!(
105 workspace,
106 [
107 Open,
108 NewFile,
109 NewWindow,
110 CloseWindow,
111 AddFolderToProject,
112 Unfollow,
113 Save,
114 SaveAs,
115 SaveAll,
116 ActivatePreviousPane,
117 ActivateNextPane,
118 FollowNextCollaborator,
119 ToggleLeftDock,
120 NewTerminal,
121 ToggleTerminalFocus,
122 NewSearch,
123 Feedback,
124 Restart,
125 Welcome,
126 ToggleZoom,
127 ]
128);
129
130actions!(zed, [OpenSettings]);
131
132#[derive(Clone, PartialEq)]
133pub struct OpenPaths {
134 pub paths: Vec<PathBuf>,
135}
136
137#[derive(Clone, Deserialize, PartialEq)]
138pub struct ActivatePane(pub usize);
139
140pub struct Toast {
141 id: usize,
142 msg: Cow<'static, str>,
143 on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
144}
145
146impl Toast {
147 pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
148 Toast {
149 id,
150 msg: msg.into(),
151 on_click: None,
152 }
153 }
154
155 pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
156 where
157 M: Into<Cow<'static, str>>,
158 F: Fn(&mut WindowContext) + 'static,
159 {
160 self.on_click = Some((message.into(), Arc::new(on_click)));
161 self
162 }
163}
164
165impl PartialEq for Toast {
166 fn eq(&self, other: &Self) -> bool {
167 self.id == other.id
168 && self.msg == other.msg
169 && self.on_click.is_some() == other.on_click.is_some()
170 }
171}
172
173impl Clone for Toast {
174 fn clone(&self) -> Self {
175 Toast {
176 id: self.id,
177 msg: self.msg.to_owned(),
178 on_click: self.on_click.clone(),
179 }
180 }
181}
182
183pub type WorkspaceId = i64;
184
185impl_actions!(workspace, [ActivatePane]);
186
187pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
188 pane::init(cx);
189 notifications::init(cx);
190
191 cx.add_global_action({
192 let app_state = Arc::downgrade(&app_state);
193 move |_: &Open, cx: &mut AppContext| {
194 let mut paths = cx.prompt_for_paths(PathPromptOptions {
195 files: true,
196 directories: true,
197 multiple: true,
198 });
199
200 if let Some(app_state) = app_state.upgrade() {
201 cx.spawn(move |mut cx| async move {
202 if let Some(paths) = paths.recv().await.flatten() {
203 cx.update(|cx| {
204 open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
205 });
206 }
207 })
208 .detach();
209 }
210 }
211 });
212 cx.add_async_action(Workspace::open);
213
214 cx.add_async_action(Workspace::follow_next_collaborator);
215 cx.add_async_action(Workspace::close);
216 cx.add_global_action(Workspace::close_global);
217 cx.add_global_action(restart);
218 cx.add_async_action(Workspace::save_all);
219 cx.add_action(Workspace::add_folder_to_project);
220 cx.add_action(
221 |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
222 let pane = workspace.active_pane().clone();
223 workspace.unfollow(&pane, cx);
224 },
225 );
226 cx.add_action(
227 |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
228 workspace.save_active_item(false, cx).detach_and_log_err(cx);
229 },
230 );
231 cx.add_action(
232 |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
233 workspace.save_active_item(true, cx).detach_and_log_err(cx);
234 },
235 );
236 cx.add_action(Workspace::toggle_panel);
237 cx.add_action(Workspace::focus_center);
238 cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
239 workspace.activate_previous_pane(cx)
240 });
241 cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
242 workspace.activate_next_pane(cx)
243 });
244 cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
245 workspace.toggle_dock(DockPosition::Left, cx);
246 });
247 cx.add_action(Workspace::activate_pane_at_index);
248
249 cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
250 cx.spawn(|workspace, mut cx| async move {
251 let err = install_cli::install_cli(&cx)
252 .await
253 .context("Failed to create CLI symlink");
254
255 workspace.update(&mut cx, |workspace, cx| {
256 if matches!(err, Err(_)) {
257 err.notify_err(workspace, cx);
258 } else {
259 workspace.show_notification(1, cx, |cx| {
260 cx.add_view(|_| {
261 MessageNotification::new("Successfully installed the `zed` binary")
262 })
263 });
264 }
265 })
266 })
267 .detach();
268 });
269
270 cx.add_action(
271 move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
272 create_and_open_local_file(&paths::SETTINGS, cx, || {
273 Settings::initial_user_settings_content(&Assets)
274 .as_ref()
275 .into()
276 })
277 .detach_and_log_err(cx);
278 },
279 );
280
281 let client = &app_state.client;
282 client.add_view_request_handler(Workspace::handle_follow);
283 client.add_view_message_handler(Workspace::handle_unfollow);
284 client.add_view_message_handler(Workspace::handle_update_followers);
285}
286
287type ProjectItemBuilders = HashMap<
288 TypeId,
289 fn(ModelHandle<Project>, AnyModelHandle, &mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
290>;
291pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
292 cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
293 builders.insert(TypeId::of::<I::Item>(), |project, model, cx| {
294 let item = model.downcast::<I::Item>().unwrap();
295 Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx)))
296 });
297 });
298}
299
300type FollowableItemBuilder = fn(
301 ViewHandle<Pane>,
302 ModelHandle<Project>,
303 ViewId,
304 &mut Option<proto::view::Variant>,
305 &mut AppContext,
306) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
307type FollowableItemBuilders = HashMap<
308 TypeId,
309 (
310 FollowableItemBuilder,
311 fn(&AnyViewHandle) -> Box<dyn FollowableItemHandle>,
312 ),
313>;
314pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
315 cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
316 builders.insert(
317 TypeId::of::<I>(),
318 (
319 |pane, project, id, state, cx| {
320 I::from_state_proto(pane, project, id, state, cx).map(|task| {
321 cx.foreground()
322 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
323 })
324 },
325 |this| Box::new(this.clone().downcast::<I>().unwrap()),
326 ),
327 );
328 });
329}
330
331type ItemDeserializers = HashMap<
332 Arc<str>,
333 fn(
334 ModelHandle<Project>,
335 WeakViewHandle<Workspace>,
336 WorkspaceId,
337 ItemId,
338 &mut ViewContext<Pane>,
339 ) -> Task<Result<Box<dyn ItemHandle>>>,
340>;
341pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
342 cx.update_default_global(|deserializers: &mut ItemDeserializers, _cx| {
343 if let Some(serialized_item_kind) = I::serialized_item_kind() {
344 deserializers.insert(
345 Arc::from(serialized_item_kind),
346 |project, workspace, workspace_id, item_id, cx| {
347 let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
348 cx.foreground()
349 .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
350 },
351 );
352 }
353 });
354}
355
356pub struct AppState {
357 pub languages: Arc<LanguageRegistry>,
358 pub themes: Arc<ThemeRegistry>,
359 pub client: Arc<client::Client>,
360 pub user_store: ModelHandle<client::UserStore>,
361 pub fs: Arc<dyn fs::Fs>,
362 pub build_window_options:
363 fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
364 pub initialize_workspace:
365 fn(WeakViewHandle<Workspace>, bool, Arc<AppState>, AsyncAppContext) -> Task<Result<()>>,
366 pub background_actions: BackgroundActions,
367}
368
369impl AppState {
370 #[cfg(any(test, feature = "test-support"))]
371 pub fn test(cx: &mut AppContext) -> Arc<Self> {
372 let settings = Settings::test(cx);
373 cx.set_global(settings);
374
375 let fs = fs::FakeFs::new(cx.background().clone());
376 let languages = Arc::new(LanguageRegistry::test());
377 let http_client = util::http::FakeHttpClient::with_404_response();
378 let client = Client::new(http_client.clone(), cx);
379 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
380 let themes = ThemeRegistry::new((), cx.font_cache().clone());
381 Arc::new(Self {
382 client,
383 themes,
384 fs,
385 languages,
386 user_store,
387 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
388 build_window_options: |_, _, _| Default::default(),
389 background_actions: || &[],
390 })
391 }
392}
393
394struct DelayedDebouncedEditAction {
395 task: Option<Task<()>>,
396 cancel_channel: Option<oneshot::Sender<()>>,
397}
398
399impl DelayedDebouncedEditAction {
400 fn new() -> DelayedDebouncedEditAction {
401 DelayedDebouncedEditAction {
402 task: None,
403 cancel_channel: None,
404 }
405 }
406
407 fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Workspace>, f: F)
408 where
409 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> Task<Result<()>>,
410 {
411 if let Some(channel) = self.cancel_channel.take() {
412 _ = channel.send(());
413 }
414
415 let (sender, mut receiver) = oneshot::channel::<()>();
416 self.cancel_channel = Some(sender);
417
418 let previous_task = self.task.take();
419 self.task = Some(cx.spawn(|workspace, mut cx| async move {
420 let mut timer = cx.background().timer(delay).fuse();
421 if let Some(previous_task) = previous_task {
422 previous_task.await;
423 }
424
425 futures::select_biased! {
426 _ = receiver => return,
427 _ = timer => {}
428 }
429
430 if let Some(result) = workspace
431 .update(&mut cx, |workspace, cx| (f)(workspace, cx))
432 .log_err()
433 {
434 result.await.log_err();
435 }
436 }));
437 }
438}
439
440pub enum Event {
441 PaneAdded(ViewHandle<Pane>),
442 ContactRequestedJoin(u64),
443}
444
445pub struct Workspace {
446 weak_self: WeakViewHandle<Self>,
447 remote_entity_subscription: Option<client::Subscription>,
448 modal: Option<AnyViewHandle>,
449 center: PaneGroup,
450 left_dock: ViewHandle<Dock>,
451 bottom_dock: ViewHandle<Dock>,
452 right_dock: ViewHandle<Dock>,
453 panes: Vec<ViewHandle<Pane>>,
454 panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
455 active_pane: ViewHandle<Pane>,
456 last_active_center_pane: Option<WeakViewHandle<Pane>>,
457 status_bar: ViewHandle<StatusBar>,
458 titlebar_item: Option<AnyViewHandle>,
459 notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
460 project: ModelHandle<Project>,
461 leader_state: LeaderState,
462 follower_states_by_leader: FollowerStatesByLeader,
463 last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
464 window_edited: bool,
465 active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
466 leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
467 database_id: WorkspaceId,
468 app_state: Arc<AppState>,
469 _subscriptions: Vec<Subscription>,
470 _apply_leader_updates: Task<Result<()>>,
471 _observe_current_user: Task<Result<()>>,
472}
473
474#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
475pub struct ViewId {
476 pub creator: PeerId,
477 pub id: u64,
478}
479
480#[derive(Default)]
481struct LeaderState {
482 followers: HashSet<PeerId>,
483}
484
485type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
486
487#[derive(Default)]
488struct FollowerState {
489 active_view_id: Option<ViewId>,
490 items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
491}
492
493impl Workspace {
494 pub fn new(
495 serialized_workspace: Option<SerializedWorkspace>,
496 workspace_id: WorkspaceId,
497 project: ModelHandle<Project>,
498 app_state: Arc<AppState>,
499 cx: &mut ViewContext<Self>,
500 ) -> Self {
501 cx.observe(&project, |_, _, cx| cx.notify()).detach();
502 cx.subscribe(&project, move |this, _, event, cx| {
503 match event {
504 project::Event::RemoteIdChanged(remote_id) => {
505 this.update_window_title(cx);
506 this.project_remote_id_changed(*remote_id, cx);
507 }
508
509 project::Event::CollaboratorLeft(peer_id) => {
510 this.collaborator_left(*peer_id, cx);
511 }
512
513 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
514 this.update_window_title(cx);
515 this.serialize_workspace(cx);
516 }
517
518 project::Event::DisconnectedFromHost => {
519 this.update_window_edited(cx);
520 cx.blur();
521 }
522
523 project::Event::Closed => {
524 cx.remove_window();
525 }
526
527 _ => {}
528 }
529 cx.notify()
530 })
531 .detach();
532
533 let weak_handle = cx.weak_handle();
534
535 let center_pane =
536 cx.add_view(|cx| Pane::new(weak_handle.clone(), app_state.background_actions, cx));
537 cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
538 cx.focus(¢er_pane);
539 cx.emit(Event::PaneAdded(center_pane.clone()));
540
541 let mut current_user = app_state.user_store.read(cx).watch_current_user();
542 let mut connection_status = app_state.client.status();
543 let _observe_current_user = cx.spawn(|this, mut cx| async move {
544 current_user.recv().await;
545 connection_status.recv().await;
546 let mut stream =
547 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
548
549 while stream.recv().await.is_some() {
550 this.update(&mut cx, |_, cx| cx.notify())?;
551 }
552 anyhow::Ok(())
553 });
554
555 // All leader updates are enqueued and then processed in a single task, so
556 // that each asynchronous operation can be run in order.
557 let (leader_updates_tx, mut leader_updates_rx) =
558 mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
559 let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
560 while let Some((leader_id, update)) = leader_updates_rx.next().await {
561 Self::process_leader_update(&this, leader_id, update, &mut cx)
562 .await
563 .log_err();
564 }
565
566 Ok(())
567 });
568
569 cx.emit_global(WorkspaceCreated(weak_handle.clone()));
570
571 let left_dock = cx.add_view(|_| Dock::new(DockPosition::Left));
572 let bottom_dock = cx.add_view(|_| Dock::new(DockPosition::Bottom));
573 let right_dock = cx.add_view(|_| Dock::new(DockPosition::Right));
574 let left_dock_buttons =
575 cx.add_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx));
576 let bottom_dock_buttons =
577 cx.add_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx));
578 let right_dock_buttons =
579 cx.add_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx));
580 let status_bar = cx.add_view(|cx| {
581 let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
582 status_bar.add_left_item(left_dock_buttons, cx);
583 status_bar.add_right_item(right_dock_buttons, cx);
584 status_bar.add_right_item(bottom_dock_buttons, cx);
585 status_bar
586 });
587
588 cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
589 drag_and_drop.register_container(weak_handle.clone());
590 });
591
592 let mut active_call = None;
593 if cx.has_global::<ModelHandle<ActiveCall>>() {
594 let call = cx.global::<ModelHandle<ActiveCall>>().clone();
595 let mut subscriptions = Vec::new();
596 subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
597 active_call = Some((call, subscriptions));
598 }
599
600 let subscriptions = vec![
601 cx.observe_fullscreen(|_, _, cx| cx.notify()),
602 cx.observe_window_activation(Self::on_window_activation_changed),
603 cx.observe_window_bounds(move |_, mut bounds, display, cx| {
604 // Transform fixed bounds to be stored in terms of the containing display
605 if let WindowBounds::Fixed(mut window_bounds) = bounds {
606 if let Some(screen) = cx.platform().screen_by_id(display) {
607 let screen_bounds = screen.bounds();
608 window_bounds
609 .set_origin_x(window_bounds.origin_x() - screen_bounds.origin_x());
610 window_bounds
611 .set_origin_y(window_bounds.origin_y() - screen_bounds.origin_y());
612 bounds = WindowBounds::Fixed(window_bounds);
613 }
614 }
615
616 cx.background()
617 .spawn(DB.set_window_bounds(workspace_id, bounds, display))
618 .detach_and_log_err(cx);
619 }),
620 cx.observe(&left_dock, |this, _, cx| {
621 this.serialize_workspace(cx);
622 cx.notify();
623 }),
624 cx.observe(&bottom_dock, |this, _, cx| {
625 this.serialize_workspace(cx);
626 cx.notify();
627 }),
628 cx.observe(&right_dock, |this, _, cx| {
629 this.serialize_workspace(cx);
630 cx.notify();
631 }),
632 ];
633
634 let mut this = Workspace {
635 weak_self: weak_handle.clone(),
636 modal: None,
637 center: PaneGroup::new(center_pane.clone()),
638 panes: vec![center_pane.clone()],
639 panes_by_item: Default::default(),
640 active_pane: center_pane.clone(),
641 last_active_center_pane: Some(center_pane.downgrade()),
642 status_bar,
643 titlebar_item: None,
644 notifications: Default::default(),
645 remote_entity_subscription: None,
646 left_dock,
647 bottom_dock,
648 right_dock,
649 project: project.clone(),
650 leader_state: Default::default(),
651 follower_states_by_leader: Default::default(),
652 last_leaders_by_pane: Default::default(),
653 window_edited: false,
654 active_call,
655 database_id: workspace_id,
656 app_state,
657 _observe_current_user,
658 _apply_leader_updates,
659 leader_updates_tx,
660 _subscriptions: subscriptions,
661 };
662 this.project_remote_id_changed(project.read(cx).remote_id(), cx);
663 cx.defer(|this, cx| this.update_window_title(cx));
664
665 if let Some(serialized_workspace) = serialized_workspace {
666 cx.defer(move |_, cx| {
667 Self::load_from_serialized_workspace(weak_handle, serialized_workspace, cx)
668 });
669 }
670
671 this
672 }
673
674 fn new_local(
675 abs_paths: Vec<PathBuf>,
676 app_state: Arc<AppState>,
677 requesting_window_id: Option<usize>,
678 cx: &mut AppContext,
679 ) -> Task<(
680 WeakViewHandle<Workspace>,
681 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
682 )> {
683 let project_handle = Project::local(
684 app_state.client.clone(),
685 app_state.user_store.clone(),
686 app_state.languages.clone(),
687 app_state.fs.clone(),
688 cx,
689 );
690
691 cx.spawn(|mut cx| async move {
692 let mut serialized_workspace =
693 persistence::DB.workspace_for_roots(&abs_paths.as_slice());
694
695 let paths_to_open = serialized_workspace
696 .as_ref()
697 .map(|workspace| workspace.location.paths())
698 .unwrap_or(Arc::new(abs_paths));
699
700 // Get project paths for all of the abs_paths
701 let mut worktree_roots: HashSet<Arc<Path>> = Default::default();
702 let mut project_paths = Vec::new();
703 for path in paths_to_open.iter() {
704 if let Some((worktree, project_entry)) = cx
705 .update(|cx| {
706 Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
707 })
708 .await
709 .log_err()
710 {
711 worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path()));
712 project_paths.push(Some(project_entry));
713 } else {
714 project_paths.push(None);
715 }
716 }
717
718 let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
719 serialized_workspace.id
720 } else {
721 DB.next_id().await.unwrap_or(0)
722 };
723
724 let window_bounds_override =
725 ZED_WINDOW_POSITION
726 .zip(*ZED_WINDOW_SIZE)
727 .map(|(position, size)| {
728 WindowBounds::Fixed(RectF::new(
729 cx.platform().screens()[0].bounds().origin() + position,
730 size,
731 ))
732 });
733
734 let was_deserialized = serialized_workspace.is_some();
735
736 let workspace = requesting_window_id
737 .and_then(|window_id| {
738 cx.update(|cx| {
739 cx.replace_root_view(window_id, |cx| {
740 Workspace::new(
741 serialized_workspace.take(),
742 workspace_id,
743 project_handle.clone(),
744 app_state.clone(),
745 cx,
746 )
747 })
748 })
749 })
750 .unwrap_or_else(|| {
751 let (bounds, display) = if let Some(bounds) = window_bounds_override {
752 (Some(bounds), None)
753 } else {
754 serialized_workspace
755 .as_ref()
756 .and_then(|serialized_workspace| {
757 let display = serialized_workspace.display?;
758 let mut bounds = serialized_workspace.bounds?;
759
760 // Stored bounds are relative to the containing display.
761 // So convert back to global coordinates if that screen still exists
762 if let WindowBounds::Fixed(mut window_bounds) = bounds {
763 if let Some(screen) = cx.platform().screen_by_id(display) {
764 let screen_bounds = screen.bounds();
765 window_bounds.set_origin_x(
766 window_bounds.origin_x() + screen_bounds.origin_x(),
767 );
768 window_bounds.set_origin_y(
769 window_bounds.origin_y() + screen_bounds.origin_y(),
770 );
771 bounds = WindowBounds::Fixed(window_bounds);
772 } else {
773 // Screen no longer exists. Return none here.
774 return None;
775 }
776 }
777
778 Some((bounds, display))
779 })
780 .unzip()
781 };
782
783 // Use the serialized workspace to construct the new window
784 cx.add_window(
785 (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
786 |cx| {
787 Workspace::new(
788 serialized_workspace,
789 workspace_id,
790 project_handle.clone(),
791 app_state.clone(),
792 cx,
793 )
794 },
795 )
796 .1
797 });
798
799 (app_state.initialize_workspace)(
800 workspace.downgrade(),
801 was_deserialized,
802 app_state.clone(),
803 cx.clone(),
804 )
805 .await
806 .log_err();
807
808 cx.update_window(workspace.window_id(), |cx| cx.activate_window());
809
810 let workspace = workspace.downgrade();
811 notify_if_database_failed(&workspace, &mut cx);
812
813 // Call open path for each of the project paths
814 // (this will bring them to the front if they were in the serialized workspace)
815 debug_assert!(paths_to_open.len() == project_paths.len());
816 let tasks = paths_to_open
817 .iter()
818 .cloned()
819 .zip(project_paths.into_iter())
820 .map(|(abs_path, project_path)| {
821 let workspace = workspace.clone();
822 cx.spawn(|mut cx| {
823 let fs = app_state.fs.clone();
824 async move {
825 let project_path = project_path?;
826 if fs.is_file(&abs_path).await {
827 Some(
828 workspace
829 .update(&mut cx, |workspace, cx| {
830 workspace.open_path(project_path, None, true, cx)
831 })
832 .log_err()?
833 .await,
834 )
835 } else {
836 None
837 }
838 }
839 })
840 });
841
842 let opened_items = futures::future::join_all(tasks.into_iter()).await;
843
844 (workspace, opened_items)
845 })
846 }
847
848 pub fn weak_handle(&self) -> WeakViewHandle<Self> {
849 self.weak_self.clone()
850 }
851
852 pub fn left_dock(&self) -> &ViewHandle<Dock> {
853 &self.left_dock
854 }
855
856 pub fn bottom_dock(&self) -> &ViewHandle<Dock> {
857 &self.bottom_dock
858 }
859
860 pub fn right_dock(&self) -> &ViewHandle<Dock> {
861 &self.right_dock
862 }
863
864 pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
865 let dock = match panel.position(cx) {
866 DockPosition::Left => &self.left_dock,
867 DockPosition::Bottom => &self.bottom_dock,
868 DockPosition::Right => &self.right_dock,
869 };
870
871 cx.subscribe(&panel, {
872 let mut dock = dock.clone();
873 let mut prev_position = panel.position(cx);
874 move |this, panel, event, cx| {
875 if T::should_change_position_on_event(event) {
876 let new_position = panel.read(cx).position(cx);
877 let mut was_visible = false;
878 let mut size = None;
879 dock.update(cx, |dock, cx| {
880 if new_position.axis() == prev_position.axis() {
881 size = dock.panel_size(&panel);
882 }
883 prev_position = new_position;
884
885 was_visible = dock.is_open()
886 && dock
887 .active_panel()
888 .map_or(false, |active_panel| active_panel.id() == panel.id());
889 dock.remove_panel(&panel, cx);
890 });
891 dock = match panel.read(cx).position(cx) {
892 DockPosition::Left => &this.left_dock,
893 DockPosition::Bottom => &this.bottom_dock,
894 DockPosition::Right => &this.right_dock,
895 }
896 .clone();
897 dock.update(cx, |dock, cx| {
898 dock.add_panel(panel.clone(), cx);
899 if let Some(size) = size {
900 dock.resize_panel(&panel, size);
901 }
902
903 if was_visible {
904 dock.set_open(true, cx);
905 dock.activate_panel(dock.panels_len() - 1, cx);
906 }
907 });
908 } else if T::should_zoom_in_on_event(event) {
909 this.zoom_out(cx);
910 dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx));
911 } else if T::should_zoom_out_on_event(event) {
912 this.zoom_out(cx);
913 } else if T::is_focus_event(event) {
914 cx.notify();
915 }
916 }
917 })
918 .detach();
919
920 dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
921 }
922
923 pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
924 &self.status_bar
925 }
926
927 pub fn app_state(&self) -> &Arc<AppState> {
928 &self.app_state
929 }
930
931 pub fn user_store(&self) -> &ModelHandle<UserStore> {
932 &self.app_state.user_store
933 }
934
935 pub fn project(&self) -> &ModelHandle<Project> {
936 &self.project
937 }
938
939 pub fn client(&self) -> &Client {
940 &self.app_state.client
941 }
942
943 pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
944 self.titlebar_item = Some(item);
945 cx.notify();
946 }
947
948 pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
949 self.titlebar_item.clone()
950 }
951
952 /// Call the given callback with a workspace whose project is local.
953 ///
954 /// If the given workspace has a local project, then it will be passed
955 /// to the callback. Otherwise, a new empty window will be created.
956 pub fn with_local_workspace<T, F>(
957 &mut self,
958 cx: &mut ViewContext<Self>,
959 callback: F,
960 ) -> Task<Result<T>>
961 where
962 T: 'static,
963 F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
964 {
965 if self.project.read(cx).is_local() {
966 Task::Ready(Some(Ok(callback(self, cx))))
967 } else {
968 let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
969 cx.spawn(|_vh, mut cx| async move {
970 let (workspace, _) = task.await;
971 workspace.update(&mut cx, callback)
972 })
973 }
974 }
975
976 pub fn worktrees<'a>(
977 &self,
978 cx: &'a AppContext,
979 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
980 self.project.read(cx).worktrees(cx)
981 }
982
983 pub fn visible_worktrees<'a>(
984 &self,
985 cx: &'a AppContext,
986 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
987 self.project.read(cx).visible_worktrees(cx)
988 }
989
990 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
991 let futures = self
992 .worktrees(cx)
993 .filter_map(|worktree| worktree.read(cx).as_local())
994 .map(|worktree| worktree.scan_complete())
995 .collect::<Vec<_>>();
996 async move {
997 for future in futures {
998 future.await;
999 }
1000 }
1001 }
1002
1003 pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
1004 cx.spawn(|mut cx| async move {
1005 let id = cx
1006 .window_ids()
1007 .into_iter()
1008 .find(|&id| cx.window_is_active(id));
1009 if let Some(id) = id {
1010 //This can only get called when the window's project connection has been lost
1011 //so we don't need to prompt the user for anything and instead just close the window
1012 cx.remove_window(id);
1013 }
1014 })
1015 .detach();
1016 }
1017
1018 pub fn close(
1019 &mut self,
1020 _: &CloseWindow,
1021 cx: &mut ViewContext<Self>,
1022 ) -> Option<Task<Result<()>>> {
1023 let window_id = cx.window_id();
1024 let prepare = self.prepare_to_close(false, cx);
1025 Some(cx.spawn(|_, mut cx| async move {
1026 if prepare.await? {
1027 cx.remove_window(window_id);
1028 }
1029 Ok(())
1030 }))
1031 }
1032
1033 pub fn prepare_to_close(
1034 &mut self,
1035 quitting: bool,
1036 cx: &mut ViewContext<Self>,
1037 ) -> Task<Result<bool>> {
1038 let active_call = self.active_call().cloned();
1039 let window_id = cx.window_id();
1040
1041 cx.spawn(|this, mut cx| async move {
1042 let workspace_count = cx
1043 .window_ids()
1044 .into_iter()
1045 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
1046 .count();
1047
1048 if let Some(active_call) = active_call {
1049 if !quitting
1050 && workspace_count == 1
1051 && active_call.read_with(&cx, |call, _| call.room().is_some())
1052 {
1053 let answer = cx.prompt(
1054 window_id,
1055 PromptLevel::Warning,
1056 "Do you want to leave the current call?",
1057 &["Close window and hang up", "Cancel"],
1058 );
1059
1060 if let Some(mut answer) = answer {
1061 if answer.next().await == Some(1) {
1062 return anyhow::Ok(false);
1063 } else {
1064 active_call
1065 .update(&mut cx, |call, cx| call.hang_up(cx))
1066 .await
1067 .log_err();
1068 }
1069 }
1070 }
1071 }
1072
1073 Ok(this
1074 .update(&mut cx, |this, cx| this.save_all_internal(true, cx))?
1075 .await?)
1076 })
1077 }
1078
1079 fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1080 let save_all = self.save_all_internal(false, cx);
1081 Some(cx.foreground().spawn(async move {
1082 save_all.await?;
1083 Ok(())
1084 }))
1085 }
1086
1087 fn save_all_internal(
1088 &mut self,
1089 should_prompt_to_save: bool,
1090 cx: &mut ViewContext<Self>,
1091 ) -> Task<Result<bool>> {
1092 if self.project.read(cx).is_read_only() {
1093 return Task::ready(Ok(true));
1094 }
1095
1096 let dirty_items = self
1097 .panes
1098 .iter()
1099 .flat_map(|pane| {
1100 pane.read(cx).items().filter_map(|item| {
1101 if item.is_dirty(cx) {
1102 Some((pane.downgrade(), item.boxed_clone()))
1103 } else {
1104 None
1105 }
1106 })
1107 })
1108 .collect::<Vec<_>>();
1109
1110 let project = self.project.clone();
1111 cx.spawn(|_, mut cx| async move {
1112 for (pane, item) in dirty_items {
1113 let (singleton, project_entry_ids) =
1114 cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
1115 if singleton || !project_entry_ids.is_empty() {
1116 if let Some(ix) =
1117 pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))?
1118 {
1119 if !Pane::save_item(
1120 project.clone(),
1121 &pane,
1122 ix,
1123 &*item,
1124 should_prompt_to_save,
1125 &mut cx,
1126 )
1127 .await?
1128 {
1129 return Ok(false);
1130 }
1131 }
1132 }
1133 }
1134 Ok(true)
1135 })
1136 }
1137
1138 pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1139 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1140 files: true,
1141 directories: true,
1142 multiple: true,
1143 });
1144
1145 Some(cx.spawn(|this, mut cx| async move {
1146 if let Some(paths) = paths.recv().await.flatten() {
1147 if let Some(task) = this
1148 .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
1149 .log_err()
1150 {
1151 task.await?
1152 }
1153 }
1154 Ok(())
1155 }))
1156 }
1157
1158 pub fn open_workspace_for_paths(
1159 &mut self,
1160 paths: Vec<PathBuf>,
1161 cx: &mut ViewContext<Self>,
1162 ) -> Task<Result<()>> {
1163 let window_id = cx.window_id();
1164 let is_remote = self.project.read(cx).is_remote();
1165 let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
1166 let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
1167 let close_task = if is_remote || has_worktree || has_dirty_items {
1168 None
1169 } else {
1170 Some(self.prepare_to_close(false, cx))
1171 };
1172 let app_state = self.app_state.clone();
1173
1174 cx.spawn(|_, mut cx| async move {
1175 let window_id_to_replace = if let Some(close_task) = close_task {
1176 if !close_task.await? {
1177 return Ok(());
1178 }
1179 Some(window_id)
1180 } else {
1181 None
1182 };
1183 cx.update(|cx| open_paths(&paths, &app_state, window_id_to_replace, cx))
1184 .await?;
1185 Ok(())
1186 })
1187 }
1188
1189 #[allow(clippy::type_complexity)]
1190 pub fn open_paths(
1191 &mut self,
1192 mut abs_paths: Vec<PathBuf>,
1193 visible: bool,
1194 cx: &mut ViewContext<Self>,
1195 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
1196 let fs = self.app_state.fs.clone();
1197
1198 // Sort the paths to ensure we add worktrees for parents before their children.
1199 abs_paths.sort_unstable();
1200 cx.spawn(|this, mut cx| async move {
1201 let mut project_paths = Vec::new();
1202 for path in &abs_paths {
1203 if let Some(project_path) = this
1204 .update(&mut cx, |this, cx| {
1205 Workspace::project_path_for_path(this.project.clone(), path, visible, cx)
1206 })
1207 .log_err()
1208 {
1209 project_paths.push(project_path.await.log_err());
1210 } else {
1211 project_paths.push(None);
1212 }
1213 }
1214
1215 let tasks = abs_paths
1216 .iter()
1217 .cloned()
1218 .zip(project_paths.into_iter())
1219 .map(|(abs_path, project_path)| {
1220 let this = this.clone();
1221 cx.spawn(|mut cx| {
1222 let fs = fs.clone();
1223 async move {
1224 let (_worktree, project_path) = project_path?;
1225 if fs.is_file(&abs_path).await {
1226 Some(
1227 this.update(&mut cx, |this, cx| {
1228 this.open_path(project_path, None, true, cx)
1229 })
1230 .log_err()?
1231 .await,
1232 )
1233 } else {
1234 None
1235 }
1236 }
1237 })
1238 })
1239 .collect::<Vec<_>>();
1240
1241 futures::future::join_all(tasks).await
1242 })
1243 }
1244
1245 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1246 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1247 files: false,
1248 directories: true,
1249 multiple: true,
1250 });
1251 cx.spawn(|this, mut cx| async move {
1252 if let Some(paths) = paths.recv().await.flatten() {
1253 let results = this
1254 .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
1255 .await;
1256 for result in results.into_iter().flatten() {
1257 result.log_err();
1258 }
1259 }
1260 anyhow::Ok(())
1261 })
1262 .detach_and_log_err(cx);
1263 }
1264
1265 fn project_path_for_path(
1266 project: ModelHandle<Project>,
1267 abs_path: &Path,
1268 visible: bool,
1269 cx: &mut AppContext,
1270 ) -> Task<Result<(ModelHandle<Worktree>, ProjectPath)>> {
1271 let entry = project.update(cx, |project, cx| {
1272 project.find_or_create_local_worktree(abs_path, visible, cx)
1273 });
1274 cx.spawn(|cx| async move {
1275 let (worktree, path) = entry.await?;
1276 let worktree_id = worktree.read_with(&cx, |t, _| t.id());
1277 Ok((
1278 worktree,
1279 ProjectPath {
1280 worktree_id,
1281 path: path.into(),
1282 },
1283 ))
1284 })
1285 }
1286
1287 /// Returns the modal that was toggled closed if it was open.
1288 pub fn toggle_modal<V, F>(
1289 &mut self,
1290 cx: &mut ViewContext<Self>,
1291 add_view: F,
1292 ) -> Option<ViewHandle<V>>
1293 where
1294 V: 'static + Modal,
1295 F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
1296 {
1297 cx.notify();
1298 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
1299 // it. Otherwise, create a new modal and set it as active.
1300 let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
1301 if let Some(already_open_modal) = already_open_modal {
1302 cx.focus_self();
1303 Some(already_open_modal)
1304 } else {
1305 let modal = add_view(self, cx);
1306 cx.subscribe(&modal, |this, _, event, cx| {
1307 if V::dismiss_on_event(event) {
1308 this.dismiss_modal(cx);
1309 }
1310 })
1311 .detach();
1312 cx.focus(&modal);
1313 self.modal = Some(modal.into_any());
1314 None
1315 }
1316 }
1317
1318 pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
1319 self.modal
1320 .as_ref()
1321 .and_then(|modal| modal.clone().downcast::<V>())
1322 }
1323
1324 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
1325 if self.modal.take().is_some() {
1326 cx.focus(&self.active_pane);
1327 cx.notify();
1328 }
1329 }
1330
1331 fn zoomed(&self, cx: &WindowContext) -> Option<AnyViewHandle> {
1332 self.zoomed_panel_for_dock(DockPosition::Left, cx)
1333 .or_else(|| self.zoomed_panel_for_dock(DockPosition::Bottom, cx))
1334 .or_else(|| self.zoomed_panel_for_dock(DockPosition::Right, cx))
1335 .or_else(|| self.zoomed_pane(cx))
1336 }
1337
1338 fn zoomed_panel_for_dock(
1339 &self,
1340 position: DockPosition,
1341 cx: &WindowContext,
1342 ) -> Option<AnyViewHandle> {
1343 let (dock, other_docks) = match position {
1344 DockPosition::Left => (&self.left_dock, [&self.bottom_dock, &self.right_dock]),
1345 DockPosition::Bottom => (&self.bottom_dock, [&self.left_dock, &self.right_dock]),
1346 DockPosition::Right => (&self.right_dock, [&self.left_dock, &self.bottom_dock]),
1347 };
1348
1349 let zoomed_panel = dock.read(&cx).zoomed_panel(cx)?;
1350 if other_docks.iter().all(|dock| !dock.read(cx).has_focus(cx))
1351 && !self.active_pane.read(cx).has_focus()
1352 {
1353 Some(zoomed_panel.as_any().clone())
1354 } else {
1355 None
1356 }
1357 }
1358
1359 fn zoomed_pane(&self, cx: &WindowContext) -> Option<AnyViewHandle> {
1360 let active_pane = self.active_pane.read(cx);
1361 let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
1362 if active_pane.is_zoomed() && docks.iter().all(|dock| !dock.read(cx).has_focus(cx)) {
1363 Some(self.active_pane.clone().into_any())
1364 } else {
1365 None
1366 }
1367 }
1368
1369 pub fn items<'a>(
1370 &'a self,
1371 cx: &'a AppContext,
1372 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1373 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1374 }
1375
1376 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
1377 self.items_of_type(cx).max_by_key(|item| item.id())
1378 }
1379
1380 pub fn items_of_type<'a, T: Item>(
1381 &'a self,
1382 cx: &'a AppContext,
1383 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
1384 self.panes
1385 .iter()
1386 .flat_map(|pane| pane.read(cx).items_of_type())
1387 }
1388
1389 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1390 self.active_pane().read(cx).active_item()
1391 }
1392
1393 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1394 self.active_item(cx).and_then(|item| item.project_path(cx))
1395 }
1396
1397 pub fn save_active_item(
1398 &mut self,
1399 force_name_change: bool,
1400 cx: &mut ViewContext<Self>,
1401 ) -> Task<Result<()>> {
1402 let project = self.project.clone();
1403 if let Some(item) = self.active_item(cx) {
1404 if !force_name_change && item.can_save(cx) {
1405 if item.has_conflict(cx) {
1406 const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1407
1408 let mut answer = cx.prompt(
1409 PromptLevel::Warning,
1410 CONFLICT_MESSAGE,
1411 &["Overwrite", "Cancel"],
1412 );
1413 cx.spawn(|this, mut cx| async move {
1414 let answer = answer.recv().await;
1415 if answer == Some(0) {
1416 this.update(&mut cx, |this, cx| item.save(this.project.clone(), cx))?
1417 .await?;
1418 }
1419 Ok(())
1420 })
1421 } else {
1422 item.save(self.project.clone(), cx)
1423 }
1424 } else if item.is_singleton(cx) {
1425 let worktree = self.worktrees(cx).next();
1426 let start_abs_path = worktree
1427 .and_then(|w| w.read(cx).as_local())
1428 .map_or(Path::new(""), |w| w.abs_path())
1429 .to_path_buf();
1430 let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
1431 cx.spawn(|this, mut cx| async move {
1432 if let Some(abs_path) = abs_path.recv().await.flatten() {
1433 this.update(&mut cx, |_, cx| item.save_as(project, abs_path, cx))?
1434 .await?;
1435 }
1436 Ok(())
1437 })
1438 } else {
1439 Task::ready(Ok(()))
1440 }
1441 } else {
1442 Task::ready(Ok(()))
1443 }
1444 }
1445
1446 pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
1447 let dock = match dock_side {
1448 DockPosition::Left => &mut self.left_dock,
1449 DockPosition::Bottom => &mut self.bottom_dock,
1450 DockPosition::Right => &mut self.right_dock,
1451 };
1452 dock.update(cx, |dock, cx| {
1453 let open = !dock.is_open();
1454 dock.set_open(open, cx);
1455 });
1456
1457 self.serialize_workspace(cx);
1458
1459 cx.focus_self();
1460 cx.notify();
1461 }
1462
1463 pub fn toggle_panel(&mut self, action: &TogglePanel, cx: &mut ViewContext<Self>) {
1464 let dock = match action.dock_position {
1465 DockPosition::Left => &mut self.left_dock,
1466 DockPosition::Bottom => &mut self.bottom_dock,
1467 DockPosition::Right => &mut self.right_dock,
1468 };
1469 let active_item = dock.update(cx, move |dock, cx| {
1470 if dock.is_open() && dock.active_panel_index() == action.panel_index {
1471 dock.set_open(false, cx);
1472 None
1473 } else {
1474 dock.set_open(true, cx);
1475 dock.activate_panel(action.panel_index, cx);
1476 dock.active_panel().cloned()
1477 }
1478 });
1479
1480 if let Some(active_item) = active_item {
1481 if active_item.has_focus(cx) {
1482 cx.focus_self();
1483 } else {
1484 cx.focus(active_item.as_any());
1485 }
1486 } else {
1487 cx.focus_self();
1488 }
1489
1490 self.serialize_workspace(cx);
1491
1492 cx.notify();
1493 }
1494
1495 pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
1496 for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
1497 if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
1498 let active_item = dock.update(cx, |dock, cx| {
1499 dock.set_open(true, cx);
1500 dock.activate_panel(panel_index, cx);
1501 dock.active_panel().cloned()
1502 });
1503 if let Some(active_item) = active_item {
1504 if active_item.has_focus(cx) {
1505 cx.focus_self();
1506 } else {
1507 cx.focus(active_item.as_any());
1508 }
1509 }
1510
1511 self.serialize_workspace(cx);
1512 cx.notify();
1513 break;
1514 }
1515 }
1516 }
1517
1518 fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
1519 for pane in &self.panes {
1520 pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
1521 }
1522
1523 self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1524 self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1525 self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx));
1526
1527 cx.notify();
1528 }
1529
1530 pub fn focus_center(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
1531 cx.focus_self();
1532 cx.notify();
1533 }
1534
1535 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1536 let pane =
1537 cx.add_view(|cx| Pane::new(self.weak_handle(), self.app_state.background_actions, cx));
1538 cx.subscribe(&pane, Self::handle_pane_event).detach();
1539 self.panes.push(pane.clone());
1540 cx.focus(&pane);
1541 cx.emit(Event::PaneAdded(pane.clone()));
1542 pane
1543 }
1544
1545 pub fn add_item_to_center(
1546 &mut self,
1547 item: Box<dyn ItemHandle>,
1548 cx: &mut ViewContext<Self>,
1549 ) -> bool {
1550 if let Some(center_pane) = self.last_active_center_pane.clone() {
1551 if let Some(center_pane) = center_pane.upgrade(cx) {
1552 Pane::add_item(self, ¢er_pane, item, true, true, None, cx);
1553 true
1554 } else {
1555 false
1556 }
1557 } else {
1558 false
1559 }
1560 }
1561
1562 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1563 let active_pane = self.active_pane().clone();
1564 Pane::add_item(self, &active_pane, item, true, true, None, cx);
1565 }
1566
1567 pub fn open_path(
1568 &mut self,
1569 path: impl Into<ProjectPath>,
1570 pane: Option<WeakViewHandle<Pane>>,
1571 focus_item: bool,
1572 cx: &mut ViewContext<Self>,
1573 ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
1574 let pane = pane.unwrap_or_else(|| {
1575 self.last_active_center_pane.clone().unwrap_or_else(|| {
1576 self.panes
1577 .first()
1578 .expect("There must be an active pane")
1579 .downgrade()
1580 })
1581 });
1582
1583 let task = self.load_path(path.into(), cx);
1584 cx.spawn(|this, mut cx| async move {
1585 let (project_entry_id, build_item) = task.await?;
1586 let pane = pane
1587 .upgrade(&cx)
1588 .ok_or_else(|| anyhow!("pane was closed"))?;
1589 this.update(&mut cx, |this, cx| {
1590 Pane::open_item(this, pane, project_entry_id, focus_item, cx, build_item)
1591 })
1592 })
1593 }
1594
1595 pub(crate) fn load_path(
1596 &mut self,
1597 path: ProjectPath,
1598 cx: &mut ViewContext<Self>,
1599 ) -> Task<
1600 Result<(
1601 ProjectEntryId,
1602 impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
1603 )>,
1604 > {
1605 let project = self.project().clone();
1606 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1607 cx.spawn(|_, mut cx| async move {
1608 let (project_entry_id, project_item) = project_item.await?;
1609 let build_item = cx.update(|cx| {
1610 cx.default_global::<ProjectItemBuilders>()
1611 .get(&project_item.model_type())
1612 .ok_or_else(|| anyhow!("no item builder for project item"))
1613 .cloned()
1614 })?;
1615 let build_item =
1616 move |cx: &mut ViewContext<Pane>| build_item(project, project_item, cx);
1617 Ok((project_entry_id, build_item))
1618 })
1619 }
1620
1621 pub fn open_project_item<T>(
1622 &mut self,
1623 project_item: ModelHandle<T::Item>,
1624 cx: &mut ViewContext<Self>,
1625 ) -> ViewHandle<T>
1626 where
1627 T: ProjectItem,
1628 {
1629 use project::Item as _;
1630
1631 let entry_id = project_item.read(cx).entry_id(cx);
1632 if let Some(item) = entry_id
1633 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1634 .and_then(|item| item.downcast())
1635 {
1636 self.activate_item(&item, cx);
1637 return item;
1638 }
1639
1640 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1641 self.add_item(Box::new(item.clone()), cx);
1642 item
1643 }
1644
1645 pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1646 if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
1647 let pane = self.active_pane.clone();
1648 Pane::add_item(self, &pane, Box::new(shared_screen), false, true, None, cx);
1649 }
1650 }
1651
1652 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1653 let result = self.panes.iter().find_map(|pane| {
1654 pane.read(cx)
1655 .index_for_item(item)
1656 .map(|ix| (pane.clone(), ix))
1657 });
1658 if let Some((pane, ix)) = result {
1659 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1660 true
1661 } else {
1662 false
1663 }
1664 }
1665
1666 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
1667 let panes = self.center.panes();
1668 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1669 cx.focus(&pane);
1670 } else {
1671 self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
1672 }
1673 }
1674
1675 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1676 let panes = self.center.panes();
1677 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1678 let next_ix = (ix + 1) % panes.len();
1679 let next_pane = panes[next_ix].clone();
1680 cx.focus(&next_pane);
1681 }
1682 }
1683
1684 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1685 let panes = self.center.panes();
1686 if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
1687 let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
1688 let prev_pane = panes[prev_ix].clone();
1689 cx.focus(&prev_pane);
1690 }
1691 }
1692
1693 fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1694 if self.active_pane != pane {
1695 self.active_pane
1696 .update(cx, |pane, cx| pane.set_active(false, cx));
1697 self.active_pane = pane.clone();
1698 self.active_pane
1699 .update(cx, |pane, cx| pane.set_active(true, cx));
1700 self.status_bar.update(cx, |status_bar, cx| {
1701 status_bar.set_active_pane(&self.active_pane, cx);
1702 });
1703 self.active_item_path_changed(cx);
1704 self.last_active_center_pane = Some(pane.downgrade());
1705 }
1706
1707 self.update_followers(
1708 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
1709 id: self.active_item(cx).and_then(|item| {
1710 item.to_followable_item_handle(cx)?
1711 .remote_id(&self.app_state.client, cx)
1712 .map(|id| id.to_proto())
1713 }),
1714 leader_id: self.leader_for_pane(&pane),
1715 }),
1716 cx,
1717 );
1718
1719 cx.notify();
1720 }
1721
1722 fn handle_pane_event(
1723 &mut self,
1724 pane: ViewHandle<Pane>,
1725 event: &pane::Event,
1726 cx: &mut ViewContext<Self>,
1727 ) {
1728 match event {
1729 pane::Event::Split(direction) => {
1730 self.split_pane(pane, *direction, cx);
1731 }
1732 pane::Event::Remove => self.remove_pane(pane, cx),
1733 pane::Event::ActivateItem { local } => {
1734 if *local {
1735 self.unfollow(&pane, cx);
1736 }
1737 if &pane == self.active_pane() {
1738 self.active_item_path_changed(cx);
1739 }
1740 }
1741 pane::Event::ChangeItemTitle => {
1742 if pane == self.active_pane {
1743 self.active_item_path_changed(cx);
1744 }
1745 self.update_window_edited(cx);
1746 }
1747 pane::Event::RemoveItem { item_id } => {
1748 self.update_window_edited(cx);
1749 if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
1750 if entry.get().id() == pane.id() {
1751 entry.remove();
1752 }
1753 }
1754 }
1755 pane::Event::Focus => {
1756 self.handle_pane_focused(pane.clone(), cx);
1757 }
1758 pane::Event::ZoomIn => {
1759 if pane == self.active_pane {
1760 self.zoom_out(cx);
1761 pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
1762 cx.notify();
1763 }
1764 }
1765 pane::Event::ZoomOut => self.zoom_out(cx),
1766 }
1767
1768 self.serialize_workspace(cx);
1769 }
1770
1771 pub fn split_pane(
1772 &mut self,
1773 pane: ViewHandle<Pane>,
1774 direction: SplitDirection,
1775 cx: &mut ViewContext<Self>,
1776 ) -> Option<ViewHandle<Pane>> {
1777 let item = pane.read(cx).active_item()?;
1778 let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
1779 let new_pane = self.add_pane(cx);
1780 Pane::add_item(self, &new_pane, clone, true, true, None, cx);
1781 self.center.split(&pane, &new_pane, direction).unwrap();
1782 Some(new_pane)
1783 } else {
1784 None
1785 };
1786 cx.notify();
1787 maybe_pane_handle
1788 }
1789
1790 pub fn split_pane_with_item(
1791 &mut self,
1792 pane_to_split: WeakViewHandle<Pane>,
1793 split_direction: SplitDirection,
1794 from: WeakViewHandle<Pane>,
1795 item_id_to_move: usize,
1796 cx: &mut ViewContext<Self>,
1797 ) {
1798 let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; };
1799 let Some(from) = from.upgrade(cx) else { return; };
1800
1801 let new_pane = self.add_pane(cx);
1802 Pane::move_item(self, from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
1803 self.center
1804 .split(&pane_to_split, &new_pane, split_direction)
1805 .unwrap();
1806 cx.notify();
1807 }
1808
1809 pub fn split_pane_with_project_entry(
1810 &mut self,
1811 pane_to_split: WeakViewHandle<Pane>,
1812 split_direction: SplitDirection,
1813 project_entry: ProjectEntryId,
1814 cx: &mut ViewContext<Self>,
1815 ) -> Option<Task<Result<()>>> {
1816 let pane_to_split = pane_to_split.upgrade(cx)?;
1817 let new_pane = self.add_pane(cx);
1818 self.center
1819 .split(&pane_to_split, &new_pane, split_direction)
1820 .unwrap();
1821
1822 let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
1823 let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
1824 Some(cx.foreground().spawn(async move {
1825 task.await?;
1826 Ok(())
1827 }))
1828 }
1829
1830 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1831 if self.center.remove(&pane).unwrap() {
1832 self.force_remove_pane(&pane, cx);
1833 self.unfollow(&pane, cx);
1834 self.last_leaders_by_pane.remove(&pane.downgrade());
1835 for removed_item in pane.read(cx).items() {
1836 self.panes_by_item.remove(&removed_item.id());
1837 }
1838
1839 cx.notify();
1840 } else {
1841 self.active_item_path_changed(cx);
1842 }
1843 }
1844
1845 pub fn panes(&self) -> &[ViewHandle<Pane>] {
1846 &self.panes
1847 }
1848
1849 pub fn active_pane(&self) -> &ViewHandle<Pane> {
1850 &self.active_pane
1851 }
1852
1853 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
1854 if let Some(remote_id) = remote_id {
1855 self.remote_entity_subscription = Some(
1856 self.app_state
1857 .client
1858 .add_view_for_remote_entity(remote_id, cx),
1859 );
1860 } else {
1861 self.remote_entity_subscription.take();
1862 }
1863 }
1864
1865 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1866 self.leader_state.followers.remove(&peer_id);
1867 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
1868 for state in states_by_pane.into_values() {
1869 for item in state.items_by_leader_view_id.into_values() {
1870 item.set_leader_replica_id(None, cx);
1871 }
1872 }
1873 }
1874 cx.notify();
1875 }
1876
1877 pub fn toggle_follow(
1878 &mut self,
1879 leader_id: PeerId,
1880 cx: &mut ViewContext<Self>,
1881 ) -> Option<Task<Result<()>>> {
1882 let pane = self.active_pane().clone();
1883
1884 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
1885 if leader_id == prev_leader_id {
1886 return None;
1887 }
1888 }
1889
1890 self.last_leaders_by_pane
1891 .insert(pane.downgrade(), leader_id);
1892 self.follower_states_by_leader
1893 .entry(leader_id)
1894 .or_default()
1895 .insert(pane.clone(), Default::default());
1896 cx.notify();
1897
1898 let project_id = self.project.read(cx).remote_id()?;
1899 let request = self.app_state.client.request(proto::Follow {
1900 project_id,
1901 leader_id: Some(leader_id),
1902 });
1903
1904 Some(cx.spawn(|this, mut cx| async move {
1905 let response = request.await?;
1906 this.update(&mut cx, |this, _| {
1907 let state = this
1908 .follower_states_by_leader
1909 .get_mut(&leader_id)
1910 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
1911 .ok_or_else(|| anyhow!("following interrupted"))?;
1912 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
1913 Some(ViewId::from_proto(active_view_id)?)
1914 } else {
1915 None
1916 };
1917 Ok::<_, anyhow::Error>(())
1918 })??;
1919 Self::add_views_from_leader(
1920 this.clone(),
1921 leader_id,
1922 vec![pane],
1923 response.views,
1924 &mut cx,
1925 )
1926 .await?;
1927 this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
1928 Ok(())
1929 }))
1930 }
1931
1932 pub fn follow_next_collaborator(
1933 &mut self,
1934 _: &FollowNextCollaborator,
1935 cx: &mut ViewContext<Self>,
1936 ) -> Option<Task<Result<()>>> {
1937 let collaborators = self.project.read(cx).collaborators();
1938 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
1939 let mut collaborators = collaborators.keys().copied();
1940 for peer_id in collaborators.by_ref() {
1941 if peer_id == leader_id {
1942 break;
1943 }
1944 }
1945 collaborators.next()
1946 } else if let Some(last_leader_id) =
1947 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
1948 {
1949 if collaborators.contains_key(last_leader_id) {
1950 Some(*last_leader_id)
1951 } else {
1952 None
1953 }
1954 } else {
1955 None
1956 };
1957
1958 next_leader_id
1959 .or_else(|| collaborators.keys().copied().next())
1960 .and_then(|leader_id| self.toggle_follow(leader_id, cx))
1961 }
1962
1963 pub fn unfollow(
1964 &mut self,
1965 pane: &ViewHandle<Pane>,
1966 cx: &mut ViewContext<Self>,
1967 ) -> Option<PeerId> {
1968 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
1969 let leader_id = *leader_id;
1970 if let Some(state) = states_by_pane.remove(pane) {
1971 for (_, item) in state.items_by_leader_view_id {
1972 item.set_leader_replica_id(None, cx);
1973 }
1974
1975 if states_by_pane.is_empty() {
1976 self.follower_states_by_leader.remove(&leader_id);
1977 if let Some(project_id) = self.project.read(cx).remote_id() {
1978 self.app_state
1979 .client
1980 .send(proto::Unfollow {
1981 project_id,
1982 leader_id: Some(leader_id),
1983 })
1984 .log_err();
1985 }
1986 }
1987
1988 cx.notify();
1989 return Some(leader_id);
1990 }
1991 }
1992 None
1993 }
1994
1995 pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
1996 self.follower_states_by_leader.contains_key(&peer_id)
1997 }
1998
1999 pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
2000 self.leader_state.followers.contains(&peer_id)
2001 }
2002
2003 fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2004 // TODO: There should be a better system in place for this
2005 // (https://github.com/zed-industries/zed/issues/1290)
2006 let is_fullscreen = cx.window_is_fullscreen();
2007 let container_theme = if is_fullscreen {
2008 let mut container_theme = theme.workspace.titlebar.container;
2009 container_theme.padding.left = container_theme.padding.right;
2010 container_theme
2011 } else {
2012 theme.workspace.titlebar.container
2013 };
2014
2015 enum TitleBar {}
2016 MouseEventHandler::<TitleBar, _>::new(0, cx, |_, cx| {
2017 Stack::new()
2018 .with_children(
2019 self.titlebar_item
2020 .as_ref()
2021 .map(|item| ChildView::new(item, cx)),
2022 )
2023 .contained()
2024 .with_style(container_theme)
2025 })
2026 .on_click(MouseButton::Left, |event, _, cx| {
2027 if event.click_count == 2 {
2028 cx.zoom_window();
2029 }
2030 })
2031 .constrained()
2032 .with_height(theme.workspace.titlebar.height)
2033 .into_any_named("titlebar")
2034 }
2035
2036 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
2037 let active_entry = self.active_project_path(cx);
2038 self.project
2039 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
2040 self.update_window_title(cx);
2041 }
2042
2043 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
2044 let project = self.project().read(cx);
2045 let mut title = String::new();
2046
2047 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
2048 let filename = path
2049 .path
2050 .file_name()
2051 .map(|s| s.to_string_lossy())
2052 .or_else(|| {
2053 Some(Cow::Borrowed(
2054 project
2055 .worktree_for_id(path.worktree_id, cx)?
2056 .read(cx)
2057 .root_name(),
2058 ))
2059 });
2060
2061 if let Some(filename) = filename {
2062 title.push_str(filename.as_ref());
2063 title.push_str(" β ");
2064 }
2065 }
2066
2067 for (i, name) in project.worktree_root_names(cx).enumerate() {
2068 if i > 0 {
2069 title.push_str(", ");
2070 }
2071 title.push_str(name);
2072 }
2073
2074 if title.is_empty() {
2075 title = "empty project".to_string();
2076 }
2077
2078 if project.is_remote() {
2079 title.push_str(" β");
2080 } else if project.is_shared() {
2081 title.push_str(" β");
2082 }
2083
2084 cx.set_window_title(&title);
2085 }
2086
2087 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
2088 let is_edited = !self.project.read(cx).is_read_only()
2089 && self
2090 .items(cx)
2091 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
2092 if is_edited != self.window_edited {
2093 self.window_edited = is_edited;
2094 cx.set_window_edited(self.window_edited)
2095 }
2096 }
2097
2098 fn render_disconnected_overlay(
2099 &self,
2100 cx: &mut ViewContext<Workspace>,
2101 ) -> Option<AnyElement<Workspace>> {
2102 if self.project.read(cx).is_read_only() {
2103 enum DisconnectedOverlay {}
2104 Some(
2105 MouseEventHandler::<DisconnectedOverlay, _>::new(0, cx, |_, cx| {
2106 let theme = &cx.global::<Settings>().theme;
2107 Label::new(
2108 "Your connection to the remote project has been lost.",
2109 theme.workspace.disconnected_overlay.text.clone(),
2110 )
2111 .aligned()
2112 .contained()
2113 .with_style(theme.workspace.disconnected_overlay.container)
2114 })
2115 .with_cursor_style(CursorStyle::Arrow)
2116 .capture_all()
2117 .into_any_named("disconnected overlay"),
2118 )
2119 } else {
2120 None
2121 }
2122 }
2123
2124 fn render_notifications(
2125 &self,
2126 theme: &theme::Workspace,
2127 cx: &AppContext,
2128 ) -> Option<AnyElement<Workspace>> {
2129 if self.notifications.is_empty() {
2130 None
2131 } else {
2132 Some(
2133 Flex::column()
2134 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2135 ChildView::new(notification.as_any(), cx)
2136 .contained()
2137 .with_style(theme.notification)
2138 }))
2139 .constrained()
2140 .with_width(theme.notifications.width)
2141 .contained()
2142 .with_style(theme.notifications.container)
2143 .aligned()
2144 .bottom()
2145 .right()
2146 .into_any(),
2147 )
2148 }
2149 }
2150
2151 // RPC handlers
2152
2153 async fn handle_follow(
2154 this: WeakViewHandle<Self>,
2155 envelope: TypedEnvelope<proto::Follow>,
2156 _: Arc<Client>,
2157 mut cx: AsyncAppContext,
2158 ) -> Result<proto::FollowResponse> {
2159 this.update(&mut cx, |this, cx| {
2160 let client = &this.app_state.client;
2161 this.leader_state
2162 .followers
2163 .insert(envelope.original_sender_id()?);
2164
2165 let active_view_id = this.active_item(cx).and_then(|i| {
2166 Some(
2167 i.to_followable_item_handle(cx)?
2168 .remote_id(client, cx)?
2169 .to_proto(),
2170 )
2171 });
2172
2173 cx.notify();
2174
2175 Ok(proto::FollowResponse {
2176 active_view_id,
2177 views: this
2178 .panes()
2179 .iter()
2180 .flat_map(|pane| {
2181 let leader_id = this.leader_for_pane(pane);
2182 pane.read(cx).items().filter_map({
2183 let cx = &cx;
2184 move |item| {
2185 let item = item.to_followable_item_handle(cx)?;
2186 let id = item.remote_id(client, cx)?.to_proto();
2187 let variant = item.to_state_proto(cx)?;
2188 Some(proto::View {
2189 id: Some(id),
2190 leader_id,
2191 variant: Some(variant),
2192 })
2193 }
2194 })
2195 })
2196 .collect(),
2197 })
2198 })?
2199 }
2200
2201 async fn handle_unfollow(
2202 this: WeakViewHandle<Self>,
2203 envelope: TypedEnvelope<proto::Unfollow>,
2204 _: Arc<Client>,
2205 mut cx: AsyncAppContext,
2206 ) -> Result<()> {
2207 this.update(&mut cx, |this, cx| {
2208 this.leader_state
2209 .followers
2210 .remove(&envelope.original_sender_id()?);
2211 cx.notify();
2212 Ok(())
2213 })?
2214 }
2215
2216 async fn handle_update_followers(
2217 this: WeakViewHandle<Self>,
2218 envelope: TypedEnvelope<proto::UpdateFollowers>,
2219 _: Arc<Client>,
2220 cx: AsyncAppContext,
2221 ) -> Result<()> {
2222 let leader_id = envelope.original_sender_id()?;
2223 this.read_with(&cx, |this, _| {
2224 this.leader_updates_tx
2225 .unbounded_send((leader_id, envelope.payload))
2226 })??;
2227 Ok(())
2228 }
2229
2230 async fn process_leader_update(
2231 this: &WeakViewHandle<Self>,
2232 leader_id: PeerId,
2233 update: proto::UpdateFollowers,
2234 cx: &mut AsyncAppContext,
2235 ) -> Result<()> {
2236 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2237 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2238 this.update(cx, |this, _| {
2239 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2240 for state in state.values_mut() {
2241 state.active_view_id =
2242 if let Some(active_view_id) = update_active_view.id.clone() {
2243 Some(ViewId::from_proto(active_view_id)?)
2244 } else {
2245 None
2246 };
2247 }
2248 }
2249 anyhow::Ok(())
2250 })??;
2251 }
2252 proto::update_followers::Variant::UpdateView(update_view) => {
2253 let variant = update_view
2254 .variant
2255 .ok_or_else(|| anyhow!("missing update view variant"))?;
2256 let id = update_view
2257 .id
2258 .ok_or_else(|| anyhow!("missing update view id"))?;
2259 let mut tasks = Vec::new();
2260 this.update(cx, |this, cx| {
2261 let project = this.project.clone();
2262 if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
2263 for state in state.values_mut() {
2264 let view_id = ViewId::from_proto(id.clone())?;
2265 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2266 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2267 }
2268 }
2269 }
2270 anyhow::Ok(())
2271 })??;
2272 try_join_all(tasks).await.log_err();
2273 }
2274 proto::update_followers::Variant::CreateView(view) => {
2275 let panes = this.read_with(cx, |this, _| {
2276 this.follower_states_by_leader
2277 .get(&leader_id)
2278 .into_iter()
2279 .flat_map(|states_by_pane| states_by_pane.keys())
2280 .cloned()
2281 .collect()
2282 })?;
2283 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2284 }
2285 }
2286 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2287 Ok(())
2288 }
2289
2290 async fn add_views_from_leader(
2291 this: WeakViewHandle<Self>,
2292 leader_id: PeerId,
2293 panes: Vec<ViewHandle<Pane>>,
2294 views: Vec<proto::View>,
2295 cx: &mut AsyncAppContext,
2296 ) -> Result<()> {
2297 let project = this.read_with(cx, |this, _| this.project.clone())?;
2298 let replica_id = project
2299 .read_with(cx, |project, _| {
2300 project
2301 .collaborators()
2302 .get(&leader_id)
2303 .map(|c| c.replica_id)
2304 })
2305 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2306
2307 let item_builders = cx.update(|cx| {
2308 cx.default_global::<FollowableItemBuilders>()
2309 .values()
2310 .map(|b| b.0)
2311 .collect::<Vec<_>>()
2312 });
2313
2314 let mut item_tasks_by_pane = HashMap::default();
2315 for pane in panes {
2316 let mut item_tasks = Vec::new();
2317 let mut leader_view_ids = Vec::new();
2318 for view in &views {
2319 let Some(id) = &view.id else { continue };
2320 let id = ViewId::from_proto(id.clone())?;
2321 let mut variant = view.variant.clone();
2322 if variant.is_none() {
2323 Err(anyhow!("missing variant"))?;
2324 }
2325 for build_item in &item_builders {
2326 let task = cx.update(|cx| {
2327 build_item(pane.clone(), project.clone(), id, &mut variant, cx)
2328 });
2329 if let Some(task) = task {
2330 item_tasks.push(task);
2331 leader_view_ids.push(id);
2332 break;
2333 } else {
2334 assert!(variant.is_some());
2335 }
2336 }
2337 }
2338
2339 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2340 }
2341
2342 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2343 let items = futures::future::try_join_all(item_tasks).await?;
2344 this.update(cx, |this, cx| {
2345 let state = this
2346 .follower_states_by_leader
2347 .get_mut(&leader_id)?
2348 .get_mut(&pane)?;
2349
2350 for (id, item) in leader_view_ids.into_iter().zip(items) {
2351 item.set_leader_replica_id(Some(replica_id), cx);
2352 state.items_by_leader_view_id.insert(id, item);
2353 }
2354
2355 Some(())
2356 })?;
2357 }
2358 Ok(())
2359 }
2360
2361 fn update_followers(
2362 &self,
2363 update: proto::update_followers::Variant,
2364 cx: &AppContext,
2365 ) -> Option<()> {
2366 let project_id = self.project.read(cx).remote_id()?;
2367 if !self.leader_state.followers.is_empty() {
2368 self.app_state
2369 .client
2370 .send(proto::UpdateFollowers {
2371 project_id,
2372 follower_ids: self.leader_state.followers.iter().copied().collect(),
2373 variant: Some(update),
2374 })
2375 .log_err();
2376 }
2377 None
2378 }
2379
2380 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2381 self.follower_states_by_leader
2382 .iter()
2383 .find_map(|(leader_id, state)| {
2384 if state.contains_key(pane) {
2385 Some(*leader_id)
2386 } else {
2387 None
2388 }
2389 })
2390 }
2391
2392 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2393 cx.notify();
2394
2395 let call = self.active_call()?;
2396 let room = call.read(cx).room()?.read(cx);
2397 let participant = room.remote_participant_for_peer_id(leader_id)?;
2398 let mut items_to_activate = Vec::new();
2399 match participant.location {
2400 call::ParticipantLocation::SharedProject { project_id } => {
2401 if Some(project_id) == self.project.read(cx).remote_id() {
2402 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2403 if let Some(item) = state
2404 .active_view_id
2405 .and_then(|id| state.items_by_leader_view_id.get(&id))
2406 {
2407 items_to_activate.push((pane.clone(), item.boxed_clone()));
2408 } else {
2409 if let Some(shared_screen) =
2410 self.shared_screen_for_peer(leader_id, pane, cx)
2411 {
2412 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2413 }
2414 }
2415 }
2416 }
2417 }
2418 call::ParticipantLocation::UnsharedProject => {}
2419 call::ParticipantLocation::External => {
2420 for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
2421 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
2422 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
2423 }
2424 }
2425 }
2426 }
2427
2428 for (pane, item) in items_to_activate {
2429 let pane_was_focused = pane.read(cx).has_focus();
2430 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
2431 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
2432 } else {
2433 Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx);
2434 }
2435
2436 if pane_was_focused {
2437 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2438 }
2439 }
2440
2441 None
2442 }
2443
2444 fn shared_screen_for_peer(
2445 &self,
2446 peer_id: PeerId,
2447 pane: &ViewHandle<Pane>,
2448 cx: &mut ViewContext<Self>,
2449 ) -> Option<ViewHandle<SharedScreen>> {
2450 let call = self.active_call()?;
2451 let room = call.read(cx).room()?.read(cx);
2452 let participant = room.remote_participant_for_peer_id(peer_id)?;
2453 let track = participant.tracks.values().next()?.clone();
2454 let user = participant.user.clone();
2455
2456 for item in pane.read(cx).items_of_type::<SharedScreen>() {
2457 if item.read(cx).peer_id == peer_id {
2458 return Some(item);
2459 }
2460 }
2461
2462 Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
2463 }
2464
2465 pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
2466 if active {
2467 cx.background()
2468 .spawn(persistence::DB.update_timestamp(self.database_id()))
2469 .detach();
2470 } else {
2471 for pane in &self.panes {
2472 pane.update(cx, |pane, cx| {
2473 if let Some(item) = pane.active_item() {
2474 item.workspace_deactivated(cx);
2475 }
2476 if matches!(
2477 cx.global::<Settings>().autosave,
2478 Autosave::OnWindowChange | Autosave::OnFocusChange
2479 ) {
2480 for item in pane.items() {
2481 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
2482 .detach_and_log_err(cx);
2483 }
2484 }
2485 });
2486 }
2487 }
2488 }
2489
2490 fn active_call(&self) -> Option<&ModelHandle<ActiveCall>> {
2491 self.active_call.as_ref().map(|(call, _)| call)
2492 }
2493
2494 fn on_active_call_event(
2495 &mut self,
2496 _: ModelHandle<ActiveCall>,
2497 event: &call::room::Event,
2498 cx: &mut ViewContext<Self>,
2499 ) {
2500 match event {
2501 call::room::Event::ParticipantLocationChanged { participant_id }
2502 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
2503 self.leader_updated(*participant_id, cx);
2504 }
2505 _ => {}
2506 }
2507 }
2508
2509 pub fn database_id(&self) -> WorkspaceId {
2510 self.database_id
2511 }
2512
2513 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
2514 let project = self.project().read(cx);
2515
2516 if project.is_local() {
2517 Some(
2518 project
2519 .visible_worktrees(cx)
2520 .map(|worktree| worktree.read(cx).abs_path())
2521 .collect::<Vec<_>>()
2522 .into(),
2523 )
2524 } else {
2525 None
2526 }
2527 }
2528
2529 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
2530 match member {
2531 Member::Axis(PaneAxis { members, .. }) => {
2532 for child in members.iter() {
2533 self.remove_panes(child.clone(), cx)
2534 }
2535 }
2536 Member::Pane(pane) => {
2537 self.force_remove_pane(&pane, cx);
2538 }
2539 }
2540 }
2541
2542 fn force_remove_pane(&mut self, pane: &ViewHandle<Pane>, cx: &mut ViewContext<Workspace>) {
2543 self.panes.retain(|p| p != pane);
2544 cx.focus(self.panes.last().unwrap());
2545 if self.last_active_center_pane == Some(pane.downgrade()) {
2546 self.last_active_center_pane = None;
2547 }
2548 cx.notify();
2549 }
2550
2551 fn serialize_workspace(&self, cx: &AppContext) {
2552 fn serialize_pane_handle(
2553 pane_handle: &ViewHandle<Pane>,
2554 cx: &AppContext,
2555 ) -> SerializedPane {
2556 let (items, active) = {
2557 let pane = pane_handle.read(cx);
2558 let active_item_id = pane.active_item().map(|item| item.id());
2559 (
2560 pane.items()
2561 .filter_map(|item_handle| {
2562 Some(SerializedItem {
2563 kind: Arc::from(item_handle.serialized_item_kind()?),
2564 item_id: item_handle.id(),
2565 active: Some(item_handle.id()) == active_item_id,
2566 })
2567 })
2568 .collect::<Vec<_>>(),
2569 pane.is_active(),
2570 )
2571 };
2572
2573 SerializedPane::new(items, active)
2574 }
2575
2576 fn build_serialized_pane_group(
2577 pane_group: &Member,
2578 cx: &AppContext,
2579 ) -> SerializedPaneGroup {
2580 match pane_group {
2581 Member::Axis(PaneAxis { axis, members }) => SerializedPaneGroup::Group {
2582 axis: *axis,
2583 children: members
2584 .iter()
2585 .map(|member| build_serialized_pane_group(member, cx))
2586 .collect::<Vec<_>>(),
2587 },
2588 Member::Pane(pane_handle) => {
2589 SerializedPaneGroup::Pane(serialize_pane_handle(&pane_handle, cx))
2590 }
2591 }
2592 }
2593
2594 fn build_serialized_docks(this: &Workspace, cx: &AppContext) -> DockStructure {
2595 let left_dock = this.left_dock.read(cx);
2596 let left_visible = left_dock.is_open();
2597 let left_active_panel = left_dock.active_panel().and_then(|panel| {
2598 Some(
2599 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2600 .to_string(),
2601 )
2602 });
2603
2604 let right_dock = this.right_dock.read(cx);
2605 let right_visible = right_dock.is_open();
2606 let right_active_panel = right_dock.active_panel().and_then(|panel| {
2607 Some(
2608 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2609 .to_string(),
2610 )
2611 });
2612
2613 let bottom_dock = this.bottom_dock.read(cx);
2614 let bottom_visible = bottom_dock.is_open();
2615 let bottom_active_panel = bottom_dock.active_panel().and_then(|panel| {
2616 Some(
2617 cx.view_ui_name(panel.as_any().window_id(), panel.id())?
2618 .to_string(),
2619 )
2620 });
2621
2622 DockStructure {
2623 left: DockData {
2624 visible: left_visible,
2625 active_panel: left_active_panel,
2626 },
2627 right: DockData {
2628 visible: right_visible,
2629 active_panel: right_active_panel,
2630 },
2631 bottom: DockData {
2632 visible: bottom_visible,
2633 active_panel: bottom_active_panel,
2634 },
2635 }
2636 }
2637
2638 if let Some(location) = self.location(cx) {
2639 // Load bearing special case:
2640 // - with_local_workspace() relies on this to not have other stuff open
2641 // when you open your log
2642 if !location.paths().is_empty() {
2643 let center_group = build_serialized_pane_group(&self.center.root, cx);
2644 let docks = build_serialized_docks(self, cx);
2645
2646 let serialized_workspace = SerializedWorkspace {
2647 id: self.database_id,
2648 location,
2649 center_group,
2650 bounds: Default::default(),
2651 display: Default::default(),
2652 docks,
2653 };
2654
2655 cx.background()
2656 .spawn(persistence::DB.save_workspace(serialized_workspace))
2657 .detach();
2658 }
2659 }
2660 }
2661
2662 fn load_from_serialized_workspace(
2663 workspace: WeakViewHandle<Workspace>,
2664 serialized_workspace: SerializedWorkspace,
2665 cx: &mut AppContext,
2666 ) {
2667 cx.spawn(|mut cx| async move {
2668 let (project, old_center_pane) = workspace.read_with(&cx, |workspace, _| {
2669 (
2670 workspace.project().clone(),
2671 workspace.last_active_center_pane.clone(),
2672 )
2673 })?;
2674
2675 // Traverse the splits tree and add to things
2676 let center_group = serialized_workspace
2677 .center_group
2678 .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
2679 .await;
2680
2681 // Remove old panes from workspace panes list
2682 workspace.update(&mut cx, |workspace, cx| {
2683 if let Some((center_group, active_pane)) = center_group {
2684 workspace.remove_panes(workspace.center.root.clone(), cx);
2685
2686 // Swap workspace center group
2687 workspace.center = PaneGroup::with_root(center_group);
2688
2689 // Change the focus to the workspace first so that we retrigger focus in on the pane.
2690 cx.focus_self();
2691
2692 if let Some(active_pane) = active_pane {
2693 cx.focus(&active_pane);
2694 } else {
2695 cx.focus(workspace.panes.last().unwrap());
2696 }
2697 } else {
2698 let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
2699 if let Some(old_center_handle) = old_center_handle {
2700 cx.focus(&old_center_handle)
2701 } else {
2702 cx.focus_self()
2703 }
2704 }
2705
2706 let docks = serialized_workspace.docks;
2707 workspace.left_dock.update(cx, |dock, cx| {
2708 dock.set_open(docks.left.visible, cx);
2709 if let Some(active_panel) = docks.left.active_panel {
2710 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
2711 dock.activate_panel(ix, cx);
2712 }
2713 }
2714 });
2715 workspace.right_dock.update(cx, |dock, cx| {
2716 dock.set_open(docks.right.visible, cx);
2717 if let Some(active_panel) = docks.right.active_panel {
2718 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
2719 dock.activate_panel(ix, cx);
2720 }
2721 }
2722 });
2723 workspace.bottom_dock.update(cx, |dock, cx| {
2724 dock.set_open(docks.bottom.visible, cx);
2725 if let Some(active_panel) = docks.bottom.active_panel {
2726 if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
2727 dock.activate_panel(ix, cx);
2728 }
2729 }
2730 });
2731
2732 cx.notify();
2733 })?;
2734
2735 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
2736 workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
2737 anyhow::Ok(())
2738 })
2739 .detach_and_log_err(cx);
2740 }
2741
2742 #[cfg(any(test, feature = "test-support"))]
2743 pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
2744 let app_state = Arc::new(AppState {
2745 languages: project.read(cx).languages().clone(),
2746 themes: ThemeRegistry::new((), cx.font_cache().clone()),
2747 client: project.read(cx).client(),
2748 user_store: project.read(cx).user_store(),
2749 fs: project.read(cx).fs().clone(),
2750 build_window_options: |_, _, _| Default::default(),
2751 initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
2752 background_actions: || &[],
2753 });
2754 Self::new(None, 0, project, app_state, cx)
2755 }
2756
2757 fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
2758 let dock = match position {
2759 DockPosition::Left => &self.left_dock,
2760 DockPosition::Right => &self.right_dock,
2761 DockPosition::Bottom => &self.bottom_dock,
2762 };
2763 let active_panel = dock.read(cx).active_panel()?;
2764 let element = if Some(active_panel.as_any()) == self.zoomed(cx).as_ref() {
2765 dock.read(cx).render_placeholder(cx)
2766 } else {
2767 ChildView::new(dock, cx).into_any()
2768 };
2769
2770 Some(
2771 element
2772 .constrained()
2773 .dynamically(move |constraint, _, cx| match position {
2774 DockPosition::Left | DockPosition::Right => SizeConstraint::new(
2775 Vector2F::new(20., constraint.min.y()),
2776 Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
2777 ),
2778 _ => constraint,
2779 })
2780 .into_any(),
2781 )
2782 }
2783}
2784
2785fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
2786 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
2787
2788 workspace
2789 .update(cx, |workspace, cx| {
2790 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
2791 workspace.show_notification_once(0, cx, |cx| {
2792 cx.add_view(|_| {
2793 MessageNotification::new("Failed to load any database file.")
2794 .with_click_message("Click to let us know about this error")
2795 .on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
2796 })
2797 });
2798 } else {
2799 let backup_path = (*db::BACKUP_DB_PATH).read();
2800 if let Some(backup_path) = backup_path.clone() {
2801 workspace.show_notification_once(0, cx, move |cx| {
2802 cx.add_view(move |_| {
2803 MessageNotification::new(format!(
2804 "Database file was corrupted. Old database backed up to {}",
2805 backup_path.display()
2806 ))
2807 .with_click_message("Click to show old database in finder")
2808 .on_click(move |cx| {
2809 cx.platform().open_url(&backup_path.to_string_lossy())
2810 })
2811 })
2812 });
2813 }
2814 }
2815 })
2816 .log_err();
2817}
2818
2819impl Entity for Workspace {
2820 type Event = Event;
2821}
2822
2823impl View for Workspace {
2824 fn ui_name() -> &'static str {
2825 "Workspace"
2826 }
2827
2828 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2829 let theme = cx.global::<Settings>().theme.clone();
2830 Stack::new()
2831 .with_child(
2832 Flex::column()
2833 .with_child(self.render_titlebar(&theme, cx))
2834 .with_child(
2835 Stack::new()
2836 .with_child({
2837 let project = self.project.clone();
2838 Flex::row()
2839 .with_children(self.render_dock(DockPosition::Left, cx))
2840 .with_child(
2841 Flex::column()
2842 .with_child(
2843 FlexItem::new(self.center.render(
2844 &project,
2845 &theme,
2846 &self.follower_states_by_leader,
2847 self.active_call(),
2848 self.active_pane(),
2849 self.zoomed(cx).as_ref(),
2850 &self.app_state,
2851 cx,
2852 ))
2853 .flex(1., true),
2854 )
2855 .with_children(
2856 self.render_dock(DockPosition::Bottom, cx),
2857 )
2858 .flex(1., true),
2859 )
2860 .with_children(self.render_dock(DockPosition::Right, cx))
2861 })
2862 .with_child(Overlay::new(
2863 Stack::new()
2864 .with_children(self.zoomed(cx).map(|zoomed| {
2865 enum ZoomBackground {}
2866
2867 ChildView::new(&zoomed, cx)
2868 .contained()
2869 .with_style(theme.workspace.zoomed_foreground)
2870 .aligned()
2871 .contained()
2872 .with_style(theme.workspace.zoomed_background)
2873 .mouse::<ZoomBackground>(0)
2874 .capture_all()
2875 .on_down(MouseButton::Left, |_, this: &mut Self, cx| {
2876 this.zoom_out(cx);
2877 })
2878 }))
2879 .with_children(self.modal.as_ref().map(|modal| {
2880 ChildView::new(modal, cx)
2881 .contained()
2882 .with_style(theme.workspace.modal)
2883 .aligned()
2884 .top()
2885 }))
2886 .with_children(self.render_notifications(&theme.workspace, cx)),
2887 ))
2888 .flex(1.0, true),
2889 )
2890 .with_child(ChildView::new(&self.status_bar, cx))
2891 .contained()
2892 .with_background_color(theme.workspace.background),
2893 )
2894 .with_children(DragAndDrop::render(cx))
2895 .with_children(self.render_disconnected_overlay(cx))
2896 .into_any_named("workspace")
2897 }
2898
2899 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
2900 if cx.is_self_focused() {
2901 cx.focus(&self.active_pane);
2902 }
2903 cx.notify();
2904 }
2905}
2906
2907impl ViewId {
2908 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
2909 Ok(Self {
2910 creator: message
2911 .creator
2912 .ok_or_else(|| anyhow!("creator is missing"))?,
2913 id: message.id,
2914 })
2915 }
2916
2917 pub(crate) fn to_proto(&self) -> proto::ViewId {
2918 proto::ViewId {
2919 creator: Some(self.creator),
2920 id: self.id,
2921 }
2922 }
2923}
2924
2925pub trait WorkspaceHandle {
2926 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
2927}
2928
2929impl WorkspaceHandle for ViewHandle<Workspace> {
2930 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
2931 self.read(cx)
2932 .worktrees(cx)
2933 .flat_map(|worktree| {
2934 let worktree_id = worktree.read(cx).id();
2935 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
2936 worktree_id,
2937 path: f.path.clone(),
2938 })
2939 })
2940 .collect::<Vec<_>>()
2941 }
2942}
2943
2944impl std::fmt::Debug for OpenPaths {
2945 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2946 f.debug_struct("OpenPaths")
2947 .field("paths", &self.paths)
2948 .finish()
2949 }
2950}
2951
2952pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
2953
2954pub fn activate_workspace_for_project(
2955 cx: &mut AsyncAppContext,
2956 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
2957) -> Option<WeakViewHandle<Workspace>> {
2958 for window_id in cx.window_ids() {
2959 let handle = cx
2960 .update_window(window_id, |cx| {
2961 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
2962 let project = workspace_handle.read(cx).project.clone();
2963 if project.update(cx, &predicate) {
2964 cx.activate_window();
2965 return Some(workspace_handle.clone());
2966 }
2967 }
2968 None
2969 })
2970 .flatten();
2971
2972 if let Some(handle) = handle {
2973 return Some(handle.downgrade());
2974 }
2975 }
2976 None
2977}
2978
2979pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
2980 DB.last_workspace().await.log_err().flatten()
2981}
2982
2983#[allow(clippy::type_complexity)]
2984pub fn open_paths(
2985 abs_paths: &[PathBuf],
2986 app_state: &Arc<AppState>,
2987 requesting_window_id: Option<usize>,
2988 cx: &mut AppContext,
2989) -> Task<
2990 Result<(
2991 WeakViewHandle<Workspace>,
2992 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
2993 )>,
2994> {
2995 log::info!("open paths {:?}", abs_paths);
2996
2997 let app_state = app_state.clone();
2998 let abs_paths = abs_paths.to_vec();
2999 cx.spawn(|mut cx| async move {
3000 // Open paths in existing workspace if possible
3001 let existing = activate_workspace_for_project(&mut cx, |project, cx| {
3002 project.contains_paths(&abs_paths, cx)
3003 });
3004
3005 if let Some(existing) = existing {
3006 Ok((
3007 existing.clone(),
3008 existing
3009 .update(&mut cx, |workspace, cx| {
3010 workspace.open_paths(abs_paths, true, cx)
3011 })?
3012 .await,
3013 ))
3014 } else {
3015 Ok(cx
3016 .update(|cx| {
3017 Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx)
3018 })
3019 .await)
3020 }
3021 })
3022}
3023
3024pub fn open_new(
3025 app_state: &Arc<AppState>,
3026 cx: &mut AppContext,
3027 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
3028) -> Task<()> {
3029 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
3030 cx.spawn(|mut cx| async move {
3031 let (workspace, opened_paths) = task.await;
3032
3033 workspace
3034 .update(&mut cx, |workspace, cx| {
3035 if opened_paths.is_empty() {
3036 init(workspace, cx)
3037 }
3038 })
3039 .log_err();
3040 })
3041}
3042
3043pub fn create_and_open_local_file(
3044 path: &'static Path,
3045 cx: &mut ViewContext<Workspace>,
3046 default_content: impl 'static + Send + FnOnce() -> Rope,
3047) -> Task<Result<Box<dyn ItemHandle>>> {
3048 cx.spawn(|workspace, mut cx| async move {
3049 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
3050 if !fs.is_file(path).await {
3051 fs.create_file(path, Default::default()).await?;
3052 fs.save(path, &default_content(), Default::default())
3053 .await?;
3054 }
3055
3056 let mut items = workspace
3057 .update(&mut cx, |workspace, cx| {
3058 workspace.with_local_workspace(cx, |workspace, cx| {
3059 workspace.open_paths(vec![path.to_path_buf()], false, cx)
3060 })
3061 })?
3062 .await?
3063 .await;
3064
3065 let item = items.pop().flatten();
3066 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
3067 })
3068}
3069
3070pub fn join_remote_project(
3071 project_id: u64,
3072 follow_user_id: u64,
3073 app_state: Arc<AppState>,
3074 cx: &mut AppContext,
3075) -> Task<Result<()>> {
3076 cx.spawn(|mut cx| async move {
3077 let existing_workspace = cx
3078 .window_ids()
3079 .into_iter()
3080 .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
3081 .find(|workspace| {
3082 cx.read_window(workspace.window_id(), |cx| {
3083 workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
3084 })
3085 .unwrap_or(false)
3086 });
3087
3088 let workspace = if let Some(existing_workspace) = existing_workspace {
3089 existing_workspace.downgrade()
3090 } else {
3091 let active_call = cx.read(ActiveCall::global);
3092 let room = active_call
3093 .read_with(&cx, |call, _| call.room().cloned())
3094 .ok_or_else(|| anyhow!("not in a call"))?;
3095 let project = room
3096 .update(&mut cx, |room, cx| {
3097 room.join_project(
3098 project_id,
3099 app_state.languages.clone(),
3100 app_state.fs.clone(),
3101 cx,
3102 )
3103 })
3104 .await?;
3105
3106 let (_, workspace) = cx.add_window(
3107 (app_state.build_window_options)(None, None, cx.platform().as_ref()),
3108 |cx| Workspace::new(None, 0, project, app_state.clone(), cx),
3109 );
3110 (app_state.initialize_workspace)(
3111 workspace.downgrade(),
3112 false,
3113 app_state.clone(),
3114 cx.clone(),
3115 )
3116 .await
3117 .log_err();
3118
3119 workspace.downgrade()
3120 };
3121
3122 cx.activate_window(workspace.window_id());
3123 cx.platform().activate(true);
3124
3125 workspace.update(&mut cx, |workspace, cx| {
3126 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
3127 let follow_peer_id = room
3128 .read(cx)
3129 .remote_participants()
3130 .iter()
3131 .find(|(_, participant)| participant.user.id == follow_user_id)
3132 .map(|(_, p)| p.peer_id)
3133 .or_else(|| {
3134 // If we couldn't follow the given user, follow the host instead.
3135 let collaborator = workspace
3136 .project()
3137 .read(cx)
3138 .collaborators()
3139 .values()
3140 .find(|collaborator| collaborator.replica_id == 0)?;
3141 Some(collaborator.peer_id)
3142 });
3143
3144 if let Some(follow_peer_id) = follow_peer_id {
3145 if !workspace.is_being_followed(follow_peer_id) {
3146 workspace
3147 .toggle_follow(follow_peer_id, cx)
3148 .map(|follow| follow.detach_and_log_err(cx));
3149 }
3150 }
3151 }
3152 })?;
3153
3154 anyhow::Ok(())
3155 })
3156}
3157
3158pub fn restart(_: &Restart, cx: &mut AppContext) {
3159 let should_confirm = cx.global::<Settings>().confirm_quit;
3160 cx.spawn(|mut cx| async move {
3161 let mut workspaces = cx
3162 .window_ids()
3163 .into_iter()
3164 .filter_map(|window_id| {
3165 Some(
3166 cx.root_view(window_id)?
3167 .clone()
3168 .downcast::<Workspace>()?
3169 .downgrade(),
3170 )
3171 })
3172 .collect::<Vec<_>>();
3173
3174 // If multiple windows have unsaved changes, and need a save prompt,
3175 // prompt in the active window before switching to a different window.
3176 workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
3177
3178 if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
3179 let answer = cx.prompt(
3180 workspace.window_id(),
3181 PromptLevel::Info,
3182 "Are you sure you want to restart?",
3183 &["Restart", "Cancel"],
3184 );
3185
3186 if let Some(mut answer) = answer {
3187 let answer = answer.next().await;
3188 if answer != Some(0) {
3189 return Ok(());
3190 }
3191 }
3192 }
3193
3194 // If the user cancels any save prompt, then keep the app open.
3195 for workspace in workspaces {
3196 if !workspace
3197 .update(&mut cx, |workspace, cx| {
3198 workspace.prepare_to_close(true, cx)
3199 })?
3200 .await?
3201 {
3202 return Ok(());
3203 }
3204 }
3205 cx.platform().restart();
3206 anyhow::Ok(())
3207 })
3208 .detach_and_log_err(cx);
3209}
3210
3211fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
3212 let mut parts = value.split(',');
3213 let width: usize = parts.next()?.parse().ok()?;
3214 let height: usize = parts.next()?.parse().ok()?;
3215 Some(vec2f(width as f32, height as f32))
3216}
3217
3218#[cfg(test)]
3219mod tests {
3220 use std::{cell::RefCell, rc::Rc};
3221
3222 use crate::{
3223 dock::test::{TestPanel, TestPanelEvent},
3224 item::test::{TestItem, TestItemEvent, TestProjectItem},
3225 };
3226
3227 use super::*;
3228 use fs::FakeFs;
3229 use gpui::{executor::Deterministic, TestAppContext};
3230 use project::{Project, ProjectEntryId};
3231 use serde_json::json;
3232
3233 #[gpui::test]
3234 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
3235 cx.foreground().forbid_parking();
3236 Settings::test_async(cx);
3237
3238 let fs = FakeFs::new(cx.background());
3239 let project = Project::test(fs, [], cx).await;
3240 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3241
3242 // Adding an item with no ambiguity renders the tab without detail.
3243 let item1 = cx.add_view(window_id, |_| {
3244 let mut item = TestItem::new();
3245 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
3246 item
3247 });
3248 workspace.update(cx, |workspace, cx| {
3249 workspace.add_item(Box::new(item1.clone()), cx);
3250 });
3251 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
3252
3253 // Adding an item that creates ambiguity increases the level of detail on
3254 // both tabs.
3255 let item2 = cx.add_view(window_id, |_| {
3256 let mut item = TestItem::new();
3257 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3258 item
3259 });
3260 workspace.update(cx, |workspace, cx| {
3261 workspace.add_item(Box::new(item2.clone()), cx);
3262 });
3263 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3264 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3265
3266 // Adding an item that creates ambiguity increases the level of detail only
3267 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
3268 // we stop at the highest detail available.
3269 let item3 = cx.add_view(window_id, |_| {
3270 let mut item = TestItem::new();
3271 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
3272 item
3273 });
3274 workspace.update(cx, |workspace, cx| {
3275 workspace.add_item(Box::new(item3.clone()), cx);
3276 });
3277 item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
3278 item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3279 item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
3280 }
3281
3282 #[gpui::test]
3283 async fn test_tracking_active_path(cx: &mut TestAppContext) {
3284 cx.foreground().forbid_parking();
3285 Settings::test_async(cx);
3286 let fs = FakeFs::new(cx.background());
3287 fs.insert_tree(
3288 "/root1",
3289 json!({
3290 "one.txt": "",
3291 "two.txt": "",
3292 }),
3293 )
3294 .await;
3295 fs.insert_tree(
3296 "/root2",
3297 json!({
3298 "three.txt": "",
3299 }),
3300 )
3301 .await;
3302
3303 let project = Project::test(fs, ["root1".as_ref()], cx).await;
3304 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3305 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3306 let worktree_id = project.read_with(cx, |project, cx| {
3307 project.worktrees(cx).next().unwrap().read(cx).id()
3308 });
3309
3310 let item1 = cx.add_view(window_id, |cx| {
3311 TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
3312 });
3313 let item2 = cx.add_view(window_id, |cx| {
3314 TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
3315 });
3316
3317 // Add an item to an empty pane
3318 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
3319 project.read_with(cx, |project, cx| {
3320 assert_eq!(
3321 project.active_entry(),
3322 project
3323 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3324 .map(|e| e.id)
3325 );
3326 });
3327 assert_eq!(
3328 cx.current_window_title(window_id).as_deref(),
3329 Some("one.txt β root1")
3330 );
3331
3332 // Add a second item to a non-empty pane
3333 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
3334 assert_eq!(
3335 cx.current_window_title(window_id).as_deref(),
3336 Some("two.txt β root1")
3337 );
3338 project.read_with(cx, |project, cx| {
3339 assert_eq!(
3340 project.active_entry(),
3341 project
3342 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
3343 .map(|e| e.id)
3344 );
3345 });
3346
3347 // Close the active item
3348 pane.update(cx, |pane, cx| {
3349 pane.close_active_item(&Default::default(), cx).unwrap()
3350 })
3351 .await
3352 .unwrap();
3353 assert_eq!(
3354 cx.current_window_title(window_id).as_deref(),
3355 Some("one.txt β root1")
3356 );
3357 project.read_with(cx, |project, cx| {
3358 assert_eq!(
3359 project.active_entry(),
3360 project
3361 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
3362 .map(|e| e.id)
3363 );
3364 });
3365
3366 // Add a project folder
3367 project
3368 .update(cx, |project, cx| {
3369 project.find_or_create_local_worktree("/root2", true, cx)
3370 })
3371 .await
3372 .unwrap();
3373 assert_eq!(
3374 cx.current_window_title(window_id).as_deref(),
3375 Some("one.txt β root1, root2")
3376 );
3377
3378 // Remove a project folder
3379 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
3380 assert_eq!(
3381 cx.current_window_title(window_id).as_deref(),
3382 Some("one.txt β root2")
3383 );
3384 }
3385
3386 #[gpui::test]
3387 async fn test_close_window(cx: &mut TestAppContext) {
3388 cx.foreground().forbid_parking();
3389 Settings::test_async(cx);
3390 let fs = FakeFs::new(cx.background());
3391 fs.insert_tree("/root", json!({ "one": "" })).await;
3392
3393 let project = Project::test(fs, ["root".as_ref()], cx).await;
3394 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
3395
3396 // When there are no dirty items, there's nothing to do.
3397 let item1 = cx.add_view(window_id, |_| TestItem::new());
3398 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
3399 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3400 assert!(task.await.unwrap());
3401
3402 // When there are dirty untitled items, prompt to save each one. If the user
3403 // cancels any prompt, then abort.
3404 let item2 = cx.add_view(window_id, |_| TestItem::new().with_dirty(true));
3405 let item3 = cx.add_view(window_id, |cx| {
3406 TestItem::new()
3407 .with_dirty(true)
3408 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3409 });
3410 workspace.update(cx, |w, cx| {
3411 w.add_item(Box::new(item2.clone()), cx);
3412 w.add_item(Box::new(item3.clone()), cx);
3413 });
3414 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
3415 cx.foreground().run_until_parked();
3416 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
3417 cx.foreground().run_until_parked();
3418 assert!(!cx.has_pending_prompt(window_id));
3419 assert!(!task.await.unwrap());
3420 }
3421
3422 #[gpui::test]
3423 async fn test_close_pane_items(cx: &mut TestAppContext) {
3424 cx.foreground().forbid_parking();
3425 Settings::test_async(cx);
3426 let fs = FakeFs::new(cx.background());
3427
3428 let project = Project::test(fs, None, cx).await;
3429 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3430
3431 let item1 = cx.add_view(window_id, |cx| {
3432 TestItem::new()
3433 .with_dirty(true)
3434 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3435 });
3436 let item2 = cx.add_view(window_id, |cx| {
3437 TestItem::new()
3438 .with_dirty(true)
3439 .with_conflict(true)
3440 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
3441 });
3442 let item3 = cx.add_view(window_id, |cx| {
3443 TestItem::new()
3444 .with_dirty(true)
3445 .with_conflict(true)
3446 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
3447 });
3448 let item4 = cx.add_view(window_id, |cx| {
3449 TestItem::new()
3450 .with_dirty(true)
3451 .with_project_items(&[TestProjectItem::new_untitled(cx)])
3452 });
3453 let pane = workspace.update(cx, |workspace, cx| {
3454 workspace.add_item(Box::new(item1.clone()), cx);
3455 workspace.add_item(Box::new(item2.clone()), cx);
3456 workspace.add_item(Box::new(item3.clone()), cx);
3457 workspace.add_item(Box::new(item4.clone()), cx);
3458 workspace.active_pane().clone()
3459 });
3460
3461 let close_items = pane.update(cx, |pane, cx| {
3462 pane.activate_item(1, true, true, cx);
3463 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3464 let item1_id = item1.id();
3465 let item3_id = item3.id();
3466 let item4_id = item4.id();
3467 pane.close_items(cx, move |id| [item1_id, item3_id, item4_id].contains(&id))
3468 });
3469 cx.foreground().run_until_parked();
3470
3471 // There's a prompt to save item 1.
3472 pane.read_with(cx, |pane, _| {
3473 assert_eq!(pane.items_len(), 4);
3474 assert_eq!(pane.active_item().unwrap().id(), item1.id());
3475 });
3476 assert!(cx.has_pending_prompt(window_id));
3477
3478 // Confirm saving item 1.
3479 cx.simulate_prompt_answer(window_id, 0);
3480 cx.foreground().run_until_parked();
3481
3482 // Item 1 is saved. There's a prompt to save item 3.
3483 pane.read_with(cx, |pane, cx| {
3484 assert_eq!(item1.read(cx).save_count, 1);
3485 assert_eq!(item1.read(cx).save_as_count, 0);
3486 assert_eq!(item1.read(cx).reload_count, 0);
3487 assert_eq!(pane.items_len(), 3);
3488 assert_eq!(pane.active_item().unwrap().id(), item3.id());
3489 });
3490 assert!(cx.has_pending_prompt(window_id));
3491
3492 // Cancel saving item 3.
3493 cx.simulate_prompt_answer(window_id, 1);
3494 cx.foreground().run_until_parked();
3495
3496 // Item 3 is reloaded. There's a prompt to save item 4.
3497 pane.read_with(cx, |pane, cx| {
3498 assert_eq!(item3.read(cx).save_count, 0);
3499 assert_eq!(item3.read(cx).save_as_count, 0);
3500 assert_eq!(item3.read(cx).reload_count, 1);
3501 assert_eq!(pane.items_len(), 2);
3502 assert_eq!(pane.active_item().unwrap().id(), item4.id());
3503 });
3504 assert!(cx.has_pending_prompt(window_id));
3505
3506 // Confirm saving item 4.
3507 cx.simulate_prompt_answer(window_id, 0);
3508 cx.foreground().run_until_parked();
3509
3510 // There's a prompt for a path for item 4.
3511 cx.simulate_new_path_selection(|_| Some(Default::default()));
3512 close_items.await.unwrap();
3513
3514 // The requested items are closed.
3515 pane.read_with(cx, |pane, cx| {
3516 assert_eq!(item4.read(cx).save_count, 0);
3517 assert_eq!(item4.read(cx).save_as_count, 1);
3518 assert_eq!(item4.read(cx).reload_count, 0);
3519 assert_eq!(pane.items_len(), 1);
3520 assert_eq!(pane.active_item().unwrap().id(), item2.id());
3521 });
3522 }
3523
3524 #[gpui::test]
3525 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
3526 cx.foreground().forbid_parking();
3527 Settings::test_async(cx);
3528 let fs = FakeFs::new(cx.background());
3529
3530 let project = Project::test(fs, [], cx).await;
3531 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3532
3533 // Create several workspace items with single project entries, and two
3534 // workspace items with multiple project entries.
3535 let single_entry_items = (0..=4)
3536 .map(|project_entry_id| {
3537 cx.add_view(window_id, |cx| {
3538 TestItem::new()
3539 .with_dirty(true)
3540 .with_project_items(&[TestProjectItem::new(
3541 project_entry_id,
3542 &format!("{project_entry_id}.txt"),
3543 cx,
3544 )])
3545 })
3546 })
3547 .collect::<Vec<_>>();
3548 let item_2_3 = cx.add_view(window_id, |cx| {
3549 TestItem::new()
3550 .with_dirty(true)
3551 .with_singleton(false)
3552 .with_project_items(&[
3553 single_entry_items[2].read(cx).project_items[0].clone(),
3554 single_entry_items[3].read(cx).project_items[0].clone(),
3555 ])
3556 });
3557 let item_3_4 = cx.add_view(window_id, |cx| {
3558 TestItem::new()
3559 .with_dirty(true)
3560 .with_singleton(false)
3561 .with_project_items(&[
3562 single_entry_items[3].read(cx).project_items[0].clone(),
3563 single_entry_items[4].read(cx).project_items[0].clone(),
3564 ])
3565 });
3566
3567 // Create two panes that contain the following project entries:
3568 // left pane:
3569 // multi-entry items: (2, 3)
3570 // single-entry items: 0, 1, 2, 3, 4
3571 // right pane:
3572 // single-entry items: 1
3573 // multi-entry items: (3, 4)
3574 let left_pane = workspace.update(cx, |workspace, cx| {
3575 let left_pane = workspace.active_pane().clone();
3576 workspace.add_item(Box::new(item_2_3.clone()), cx);
3577 for item in single_entry_items {
3578 workspace.add_item(Box::new(item), cx);
3579 }
3580 left_pane.update(cx, |pane, cx| {
3581 pane.activate_item(2, true, true, cx);
3582 });
3583
3584 workspace
3585 .split_pane(left_pane.clone(), SplitDirection::Right, cx)
3586 .unwrap();
3587
3588 left_pane
3589 });
3590
3591 //Need to cause an effect flush in order to respect new focus
3592 workspace.update(cx, |workspace, cx| {
3593 workspace.add_item(Box::new(item_3_4.clone()), cx);
3594 cx.focus(&left_pane);
3595 });
3596
3597 // When closing all of the items in the left pane, we should be prompted twice:
3598 // once for project entry 0, and once for project entry 2. After those two
3599 // prompts, the task should complete.
3600
3601 let close = left_pane.update(cx, |pane, cx| pane.close_items(cx, |_| true));
3602 cx.foreground().run_until_parked();
3603 left_pane.read_with(cx, |pane, cx| {
3604 assert_eq!(
3605 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3606 &[ProjectEntryId::from_proto(0)]
3607 );
3608 });
3609 cx.simulate_prompt_answer(window_id, 0);
3610
3611 cx.foreground().run_until_parked();
3612 left_pane.read_with(cx, |pane, cx| {
3613 assert_eq!(
3614 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
3615 &[ProjectEntryId::from_proto(2)]
3616 );
3617 });
3618 cx.simulate_prompt_answer(window_id, 0);
3619
3620 cx.foreground().run_until_parked();
3621 close.await.unwrap();
3622 left_pane.read_with(cx, |pane, _| {
3623 assert_eq!(pane.items_len(), 0);
3624 });
3625 }
3626
3627 #[gpui::test]
3628 async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3629 deterministic.forbid_parking();
3630
3631 Settings::test_async(cx);
3632 let fs = FakeFs::new(cx.background());
3633
3634 let project = Project::test(fs, [], cx).await;
3635 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3636 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3637
3638 let item = cx.add_view(window_id, |cx| {
3639 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3640 });
3641 let item_id = item.id();
3642 workspace.update(cx, |workspace, cx| {
3643 workspace.add_item(Box::new(item.clone()), cx);
3644 });
3645
3646 // Autosave on window change.
3647 item.update(cx, |item, cx| {
3648 cx.update_global(|settings: &mut Settings, _| {
3649 settings.autosave = Autosave::OnWindowChange;
3650 });
3651 item.is_dirty = true;
3652 });
3653
3654 // Deactivating the window saves the file.
3655 cx.simulate_window_activation(None);
3656 deterministic.run_until_parked();
3657 item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
3658
3659 // Autosave on focus change.
3660 item.update(cx, |item, cx| {
3661 cx.focus_self();
3662 cx.update_global(|settings: &mut Settings, _| {
3663 settings.autosave = Autosave::OnFocusChange;
3664 });
3665 item.is_dirty = true;
3666 });
3667
3668 // Blurring the item saves the file.
3669 item.update(cx, |_, cx| cx.blur());
3670 deterministic.run_until_parked();
3671 item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
3672
3673 // Deactivating the window still saves the file.
3674 cx.simulate_window_activation(Some(window_id));
3675 item.update(cx, |item, cx| {
3676 cx.focus_self();
3677 item.is_dirty = true;
3678 });
3679 cx.simulate_window_activation(None);
3680
3681 deterministic.run_until_parked();
3682 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3683
3684 // Autosave after delay.
3685 item.update(cx, |item, cx| {
3686 cx.update_global(|settings: &mut Settings, _| {
3687 settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
3688 });
3689 item.is_dirty = true;
3690 cx.emit(TestItemEvent::Edit);
3691 });
3692
3693 // Delay hasn't fully expired, so the file is still dirty and unsaved.
3694 deterministic.advance_clock(Duration::from_millis(250));
3695 item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
3696
3697 // After delay expires, the file is saved.
3698 deterministic.advance_clock(Duration::from_millis(250));
3699 item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
3700
3701 // Autosave on focus change, ensuring closing the tab counts as such.
3702 item.update(cx, |item, cx| {
3703 cx.update_global(|settings: &mut Settings, _| {
3704 settings.autosave = Autosave::OnFocusChange;
3705 });
3706 item.is_dirty = true;
3707 });
3708
3709 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id))
3710 .await
3711 .unwrap();
3712 assert!(!cx.has_pending_prompt(window_id));
3713 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3714
3715 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
3716 workspace.update(cx, |workspace, cx| {
3717 workspace.add_item(Box::new(item.clone()), cx);
3718 });
3719 item.update(cx, |item, cx| {
3720 item.project_items[0].update(cx, |item, _| {
3721 item.entry_id = None;
3722 });
3723 item.is_dirty = true;
3724 cx.blur();
3725 });
3726 deterministic.run_until_parked();
3727 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3728
3729 // Ensure autosave is prevented for deleted files also when closing the buffer.
3730 let _close_items =
3731 pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id));
3732 deterministic.run_until_parked();
3733 assert!(cx.has_pending_prompt(window_id));
3734 item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
3735 }
3736
3737 #[gpui::test]
3738 async fn test_pane_navigation(
3739 deterministic: Arc<Deterministic>,
3740 cx: &mut gpui::TestAppContext,
3741 ) {
3742 deterministic.forbid_parking();
3743 Settings::test_async(cx);
3744 let fs = FakeFs::new(cx.background());
3745
3746 let project = Project::test(fs, [], cx).await;
3747 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3748
3749 let item = cx.add_view(window_id, |cx| {
3750 TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
3751 });
3752 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
3753 let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
3754 let toolbar_notify_count = Rc::new(RefCell::new(0));
3755
3756 workspace.update(cx, |workspace, cx| {
3757 workspace.add_item(Box::new(item.clone()), cx);
3758 let toolbar_notification_count = toolbar_notify_count.clone();
3759 cx.observe(&toolbar, move |_, _, _| {
3760 *toolbar_notification_count.borrow_mut() += 1
3761 })
3762 .detach();
3763 });
3764
3765 pane.read_with(cx, |pane, _| {
3766 assert!(!pane.can_navigate_backward());
3767 assert!(!pane.can_navigate_forward());
3768 });
3769
3770 item.update(cx, |item, cx| {
3771 item.set_state("one".to_string(), cx);
3772 });
3773
3774 // Toolbar must be notified to re-render the navigation buttons
3775 assert_eq!(*toolbar_notify_count.borrow(), 1);
3776
3777 pane.read_with(cx, |pane, _| {
3778 assert!(pane.can_navigate_backward());
3779 assert!(!pane.can_navigate_forward());
3780 });
3781
3782 workspace
3783 .update(cx, |workspace, cx| {
3784 Pane::go_back(workspace, Some(pane.downgrade()), cx)
3785 })
3786 .await
3787 .unwrap();
3788
3789 assert_eq!(*toolbar_notify_count.borrow(), 3);
3790 pane.read_with(cx, |pane, _| {
3791 assert!(!pane.can_navigate_backward());
3792 assert!(pane.can_navigate_forward());
3793 });
3794 }
3795
3796 #[gpui::test]
3797 async fn test_panels(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
3798 deterministic.forbid_parking();
3799 Settings::test_async(cx);
3800 let fs = FakeFs::new(cx.background());
3801
3802 let project = Project::test(fs, [], cx).await;
3803 let (_window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
3804
3805 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
3806 // Add panel_1 on the left, panel_2 on the right.
3807 let panel_1 = cx.add_view(|_| TestPanel {
3808 position: DockPosition::Left,
3809 });
3810 workspace.add_panel(panel_1.clone(), cx);
3811 workspace
3812 .left_dock()
3813 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
3814 let panel_2 = cx.add_view(|_| TestPanel {
3815 position: DockPosition::Right,
3816 });
3817 workspace.add_panel(panel_2.clone(), cx);
3818 workspace
3819 .right_dock()
3820 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
3821
3822 let left_dock = workspace.left_dock();
3823 assert_eq!(
3824 left_dock.read(cx).active_panel().unwrap().id(),
3825 panel_1.id()
3826 );
3827 assert_eq!(
3828 left_dock.read(cx).active_panel_size().unwrap(),
3829 panel_1.default_size(cx)
3830 );
3831
3832 left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx));
3833 assert_eq!(
3834 workspace.right_dock().read(cx).active_panel().unwrap().id(),
3835 panel_2.id()
3836 );
3837
3838 (panel_1, panel_2)
3839 });
3840
3841 // Move panel_1 to the right
3842 panel_1.update(cx, |panel_1, cx| {
3843 panel_1.set_position(DockPosition::Right, cx)
3844 });
3845
3846 workspace.update(cx, |workspace, cx| {
3847 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
3848 // Since it was the only panel on the left, the left dock should now be closed.
3849 assert!(!workspace.left_dock().read(cx).is_open());
3850 assert!(workspace.left_dock().read(cx).active_panel().is_none());
3851 let right_dock = workspace.right_dock();
3852 assert_eq!(
3853 right_dock.read(cx).active_panel().unwrap().id(),
3854 panel_1.id()
3855 );
3856 assert_eq!(right_dock.read(cx).active_panel_size().unwrap(), 1337.);
3857
3858 // Now we move panel_2Β to the left
3859 panel_2.set_position(DockPosition::Left, cx);
3860 });
3861
3862 workspace.update(cx, |workspace, cx| {
3863 // Since panel_2 was not visible on the right, we don't open the left dock.
3864 assert!(!workspace.left_dock().read(cx).is_open());
3865 // And the right dock is unaffected in it's displaying of panel_1
3866 assert!(workspace.right_dock().read(cx).is_open());
3867 assert_eq!(
3868 workspace.right_dock().read(cx).active_panel().unwrap().id(),
3869 panel_1.id()
3870 );
3871 });
3872
3873 // Move panel_1 back to the left
3874 panel_1.update(cx, |panel_1, cx| {
3875 panel_1.set_position(DockPosition::Left, cx)
3876 });
3877
3878 workspace.update(cx, |workspace, cx| {
3879 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
3880 let left_dock = workspace.left_dock();
3881 assert!(left_dock.read(cx).is_open());
3882 assert_eq!(
3883 left_dock.read(cx).active_panel().unwrap().id(),
3884 panel_1.id()
3885 );
3886 assert_eq!(left_dock.read(cx).active_panel_size().unwrap(), 1337.);
3887 // And right the dock should be closed as it no longer has any panels.
3888 assert!(!workspace.right_dock().read(cx).is_open());
3889
3890 // Now we move panel_1 to the bottom
3891 panel_1.set_position(DockPosition::Bottom, cx);
3892 });
3893
3894 workspace.update(cx, |workspace, cx| {
3895 // Since panel_1 was visible on the left, we close the left dock.
3896 assert!(!workspace.left_dock().read(cx).is_open());
3897 // The bottom dock is sized based on the panel's default size,
3898 // since the panel orientation changed from vertical to horizontal.
3899 let bottom_dock = workspace.bottom_dock();
3900 assert_eq!(
3901 bottom_dock.read(cx).active_panel_size().unwrap(),
3902 panel_1.default_size(cx),
3903 );
3904 // Close bottom dock and move panel_1 back to the left.
3905 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
3906 panel_1.set_position(DockPosition::Left, cx);
3907 });
3908
3909 // Emit activated event on panel 1
3910 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
3911
3912 // Now the left dock is open and panel_1 is active and focused.
3913 workspace.read_with(cx, |workspace, cx| {
3914 let left_dock = workspace.left_dock();
3915 assert!(left_dock.read(cx).is_open());
3916 assert_eq!(
3917 left_dock.read(cx).active_panel().unwrap().id(),
3918 panel_1.id()
3919 );
3920 assert!(panel_1.is_focused(cx));
3921 });
3922
3923 // Emit closed event on panel 2, which is not active
3924 panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
3925
3926 // Wo don't close the left dock, because panel_2 wasn't the active panel
3927 workspace.read_with(cx, |workspace, cx| {
3928 let left_dock = workspace.left_dock();
3929 assert!(left_dock.read(cx).is_open());
3930 assert_eq!(
3931 left_dock.read(cx).active_panel().unwrap().id(),
3932 panel_1.id()
3933 );
3934 });
3935
3936 // Emit closed event on panel 1, which is active
3937 panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
3938
3939 // Now the left dock is closed, because panel_1 was the active panel
3940 workspace.read_with(cx, |workspace, cx| {
3941 let left_dock = workspace.left_dock();
3942 assert!(!left_dock.read(cx).is_open());
3943 });
3944 }
3945}