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