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