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