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