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 proto::FollowResponse {
2747 active_view_id,
2748 views: self
2749 .panes()
2750 .iter()
2751 .flat_map(|pane| {
2752 let leader_id = self.leader_for_pane(pane);
2753 pane.read(cx).items().filter_map({
2754 let cx = &cx;
2755 move |item| {
2756 let item = item.to_followable_item_handle(cx)?;
2757
2758 // If the item belongs to a particular project, then it should
2759 // only be included if this project is shared, and the follower
2760 // is in the project.
2761 //
2762 // Some items, like channel notes, do not belong to a particular
2763 // project, so they should be included regardless of whether the
2764 // current project is shared, or what project the follower is in.
2765 if item.is_project_item(cx)
2766 && (project_id.is_none() || project_id != follower_project_id)
2767 {
2768 return None;
2769 }
2770
2771 let id = item.remote_id(client, cx)?.to_proto();
2772 let variant = item.to_state_proto(cx)?;
2773 Some(proto::View {
2774 id: Some(id),
2775 leader_id,
2776 variant: Some(variant),
2777 })
2778 }
2779 })
2780 })
2781 .collect(),
2782 }
2783 }
2784
2785 fn handle_update_followers(
2786 &mut self,
2787 leader_id: PeerId,
2788 message: proto::UpdateFollowers,
2789 _cx: &mut ViewContext<Self>,
2790 ) {
2791 self.leader_updates_tx
2792 .unbounded_send((leader_id, message))
2793 .ok();
2794 }
2795
2796 async fn process_leader_update(
2797 this: &WeakView<Self>,
2798 leader_id: PeerId,
2799 update: proto::UpdateFollowers,
2800 cx: &mut AsyncWindowContext,
2801 ) -> Result<()> {
2802 match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
2803 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2804 this.update(cx, |this, _| {
2805 for (_, state) in &mut this.follower_states {
2806 if state.leader_id == leader_id {
2807 state.active_view_id =
2808 if let Some(active_view_id) = update_active_view.id.clone() {
2809 Some(ViewId::from_proto(active_view_id)?)
2810 } else {
2811 None
2812 };
2813 }
2814 }
2815 anyhow::Ok(())
2816 })??;
2817 }
2818 proto::update_followers::Variant::UpdateView(update_view) => {
2819 let variant = update_view
2820 .variant
2821 .ok_or_else(|| anyhow!("missing update view variant"))?;
2822 let id = update_view
2823 .id
2824 .ok_or_else(|| anyhow!("missing update view id"))?;
2825 let mut tasks = Vec::new();
2826 this.update(cx, |this, cx| {
2827 let project = this.project.clone();
2828 for (_, state) in &mut this.follower_states {
2829 if state.leader_id == leader_id {
2830 let view_id = ViewId::from_proto(id.clone())?;
2831 if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
2832 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
2833 }
2834 }
2835 }
2836 anyhow::Ok(())
2837 })??;
2838 try_join_all(tasks).await.log_err();
2839 }
2840 proto::update_followers::Variant::CreateView(view) => {
2841 let panes = this.update(cx, |this, _| {
2842 this.follower_states
2843 .iter()
2844 .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane))
2845 .cloned()
2846 .collect()
2847 })?;
2848 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
2849 }
2850 }
2851 this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
2852 Ok(())
2853 }
2854
2855 async fn add_views_from_leader(
2856 this: WeakView<Self>,
2857 leader_id: PeerId,
2858 panes: Vec<View<Pane>>,
2859 views: Vec<proto::View>,
2860 cx: &mut AsyncWindowContext,
2861 ) -> Result<()> {
2862 let this = this.upgrade().context("workspace dropped")?;
2863
2864 let item_builders = cx.update(|cx| {
2865 cx.default_global::<FollowableItemBuilders>()
2866 .values()
2867 .map(|b| b.0)
2868 .collect::<Vec<_>>()
2869 })?;
2870
2871 let mut item_tasks_by_pane = HashMap::default();
2872 for pane in panes {
2873 let mut item_tasks = Vec::new();
2874 let mut leader_view_ids = Vec::new();
2875 for view in &views {
2876 let Some(id) = &view.id else { continue };
2877 let id = ViewId::from_proto(id.clone())?;
2878 let mut variant = view.variant.clone();
2879 if variant.is_none() {
2880 Err(anyhow!("missing view variant"))?;
2881 }
2882 for build_item in &item_builders {
2883 let task = cx.update(|cx| {
2884 build_item(pane.clone(), this.clone(), id, &mut variant, cx)
2885 })?;
2886 if let Some(task) = task {
2887 item_tasks.push(task);
2888 leader_view_ids.push(id);
2889 break;
2890 } else if variant.is_none() {
2891 Err(anyhow!(
2892 "failed to construct view from leader (maybe from a different version of zed?)"
2893 ))?;
2894 }
2895 }
2896 }
2897
2898 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2899 }
2900
2901 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2902 let items = futures::future::try_join_all(item_tasks).await?;
2903 this.update(cx, |this, cx| {
2904 let state = this.follower_states.get_mut(&pane)?;
2905 for (id, item) in leader_view_ids.into_iter().zip(items) {
2906 item.set_leader_peer_id(Some(leader_id), cx);
2907 state.items_by_leader_view_id.insert(id, item);
2908 }
2909
2910 Some(())
2911 })?;
2912 }
2913 Ok(())
2914 }
2915
2916 pub fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
2917 let mut is_project_item = true;
2918 let mut update = proto::UpdateActiveView::default();
2919 if cx.is_window_active() {
2920 if let Some(item) = self.active_item(cx) {
2921 if item.focus_handle(cx).contains_focused(cx) {
2922 if let Some(item) = item.to_followable_item_handle(cx) {
2923 is_project_item = item.is_project_item(cx);
2924 update = proto::UpdateActiveView {
2925 id: item
2926 .remote_id(&self.app_state.client, cx)
2927 .map(|id| id.to_proto()),
2928 leader_id: self.leader_for_pane(&self.active_pane),
2929 };
2930 }
2931 }
2932 }
2933 }
2934
2935 if &update.id != &self.last_active_view_id {
2936 self.last_active_view_id = update.id.clone();
2937 self.update_followers(
2938 is_project_item,
2939 proto::update_followers::Variant::UpdateActiveView(update),
2940 cx,
2941 );
2942 }
2943 }
2944
2945 fn update_followers(
2946 &self,
2947 project_only: bool,
2948 update: proto::update_followers::Variant,
2949 cx: &mut WindowContext,
2950 ) -> Option<()> {
2951 // If this update only applies to for followers in the current project,
2952 // then skip it unless this project is shared. If it applies to all
2953 // followers, regardless of project, then set `project_id` to none,
2954 // indicating that it goes to all followers.
2955 let project_id = if project_only {
2956 Some(self.project.read(cx).remote_id()?)
2957 } else {
2958 None
2959 };
2960 self.app_state().workspace_store.update(cx, |store, cx| {
2961 store.update_followers(project_id, update, cx)
2962 })
2963 }
2964
2965 pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
2966 self.follower_states.get(pane).map(|state| state.leader_id)
2967 }
2968
2969 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2970 cx.notify();
2971
2972 let call = self.active_call()?;
2973 let room = call.read(cx).room()?.read(cx);
2974 let participant = room.remote_participant_for_peer_id(leader_id)?;
2975 let mut items_to_activate = Vec::new();
2976
2977 let leader_in_this_app;
2978 let leader_in_this_project;
2979 match participant.location {
2980 call::ParticipantLocation::SharedProject { project_id } => {
2981 leader_in_this_app = true;
2982 leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
2983 }
2984 call::ParticipantLocation::UnsharedProject => {
2985 leader_in_this_app = true;
2986 leader_in_this_project = false;
2987 }
2988 call::ParticipantLocation::External => {
2989 leader_in_this_app = false;
2990 leader_in_this_project = false;
2991 }
2992 };
2993
2994 for (pane, state) in &self.follower_states {
2995 if state.leader_id != leader_id {
2996 continue;
2997 }
2998 if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
2999 if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
3000 if leader_in_this_project || !item.is_project_item(cx) {
3001 items_to_activate.push((pane.clone(), item.boxed_clone()));
3002 }
3003 }
3004 continue;
3005 }
3006
3007 if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
3008 items_to_activate.push((pane.clone(), Box::new(shared_screen)));
3009 }
3010 }
3011
3012 for (pane, item) in items_to_activate {
3013 let pane_was_focused = pane.read(cx).has_focus(cx);
3014 if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
3015 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
3016 } else {
3017 pane.update(cx, |pane, cx| {
3018 pane.add_item(item.boxed_clone(), false, false, None, cx)
3019 });
3020 }
3021
3022 if pane_was_focused {
3023 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
3024 }
3025 }
3026
3027 None
3028 }
3029
3030 fn shared_screen_for_peer(
3031 &self,
3032 peer_id: PeerId,
3033 pane: &View<Pane>,
3034 cx: &mut WindowContext,
3035 ) -> Option<View<SharedScreen>> {
3036 let call = self.active_call()?;
3037 let room = call.read(cx).room()?.read(cx);
3038 let participant = room.remote_participant_for_peer_id(peer_id)?;
3039 let track = participant.video_tracks.values().next()?.clone();
3040 let user = participant.user.clone();
3041
3042 for item in pane.read(cx).items_of_type::<SharedScreen>() {
3043 if item.read(cx).peer_id == peer_id {
3044 return Some(item);
3045 }
3046 }
3047
3048 Some(cx.new_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
3049 }
3050
3051 pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
3052 if cx.is_window_active() {
3053 self.update_active_view_for_followers(cx);
3054 cx.background_executor()
3055 .spawn(persistence::DB.update_timestamp(self.database_id()))
3056 .detach();
3057 } else {
3058 for pane in &self.panes {
3059 pane.update(cx, |pane, cx| {
3060 if let Some(item) = pane.active_item() {
3061 item.workspace_deactivated(cx);
3062 }
3063 if matches!(
3064 WorkspaceSettings::get_global(cx).autosave,
3065 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3066 ) {
3067 for item in pane.items() {
3068 Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
3069 .detach_and_log_err(cx);
3070 }
3071 }
3072 });
3073 }
3074 }
3075 }
3076
3077 fn active_call(&self) -> Option<&Model<ActiveCall>> {
3078 self.active_call.as_ref().map(|(call, _)| call)
3079 }
3080
3081 fn on_active_call_event(
3082 &mut self,
3083 _: Model<ActiveCall>,
3084 event: &call::room::Event,
3085 cx: &mut ViewContext<Self>,
3086 ) {
3087 match event {
3088 call::room::Event::ParticipantLocationChanged { participant_id }
3089 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
3090 self.leader_updated(*participant_id, cx);
3091 }
3092 _ => {}
3093 }
3094 }
3095
3096 pub fn database_id(&self) -> WorkspaceId {
3097 self.database_id
3098 }
3099
3100 fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
3101 let project = self.project().read(cx);
3102
3103 if project.is_local() {
3104 Some(
3105 project
3106 .visible_worktrees(cx)
3107 .map(|worktree| worktree.read(cx).abs_path())
3108 .collect::<Vec<_>>()
3109 .into(),
3110 )
3111 } else {
3112 None
3113 }
3114 }
3115
3116 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
3117 match member {
3118 Member::Axis(PaneAxis { members, .. }) => {
3119 for child in members.iter() {
3120 self.remove_panes(child.clone(), cx)
3121 }
3122 }
3123 Member::Pane(pane) => {
3124 self.force_remove_pane(&pane, cx);
3125 }
3126 }
3127 }
3128
3129 fn force_remove_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
3130 self.panes.retain(|p| p != pane);
3131 self.panes
3132 .last()
3133 .unwrap()
3134 .update(cx, |pane, cx| pane.focus(cx));
3135 if self.last_active_center_pane == Some(pane.downgrade()) {
3136 self.last_active_center_pane = None;
3137 }
3138 cx.notify();
3139 }
3140
3141 fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
3142 self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
3143 cx.background_executor()
3144 .timer(Duration::from_millis(100))
3145 .await;
3146 this.update(&mut cx, |this, cx| this.serialize_workspace(cx))
3147 .log_err();
3148 }));
3149 }
3150
3151 fn serialize_workspace(&self, cx: &mut WindowContext) {
3152 fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
3153 let (items, active) = {
3154 let pane = pane_handle.read(cx);
3155 let active_item_id = pane.active_item().map(|item| item.item_id());
3156 (
3157 pane.items()
3158 .filter_map(|item_handle| {
3159 Some(SerializedItem {
3160 kind: Arc::from(item_handle.serialized_item_kind()?),
3161 item_id: item_handle.item_id().as_u64(),
3162 active: Some(item_handle.item_id()) == active_item_id,
3163 })
3164 })
3165 .collect::<Vec<_>>(),
3166 pane.has_focus(cx),
3167 )
3168 };
3169
3170 SerializedPane::new(items, active)
3171 }
3172
3173 fn build_serialized_pane_group(
3174 pane_group: &Member,
3175 cx: &WindowContext,
3176 ) -> SerializedPaneGroup {
3177 match pane_group {
3178 Member::Axis(PaneAxis {
3179 axis,
3180 members,
3181 flexes,
3182 bounding_boxes: _,
3183 }) => SerializedPaneGroup::Group {
3184 axis: SerializedAxis(*axis),
3185 children: members
3186 .iter()
3187 .map(|member| build_serialized_pane_group(member, cx))
3188 .collect::<Vec<_>>(),
3189 flexes: Some(flexes.lock().clone()),
3190 },
3191 Member::Pane(pane_handle) => {
3192 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
3193 }
3194 }
3195 }
3196
3197 fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
3198 let left_dock = this.left_dock.read(cx);
3199 let left_visible = left_dock.is_open();
3200 let left_active_panel = left_dock
3201 .visible_panel()
3202 .and_then(|panel| Some(panel.persistent_name().to_string()));
3203 let left_dock_zoom = left_dock
3204 .visible_panel()
3205 .map(|panel| panel.is_zoomed(cx))
3206 .unwrap_or(false);
3207
3208 let right_dock = this.right_dock.read(cx);
3209 let right_visible = right_dock.is_open();
3210 let right_active_panel = right_dock
3211 .visible_panel()
3212 .and_then(|panel| Some(panel.persistent_name().to_string()));
3213 let right_dock_zoom = right_dock
3214 .visible_panel()
3215 .map(|panel| panel.is_zoomed(cx))
3216 .unwrap_or(false);
3217
3218 let bottom_dock = this.bottom_dock.read(cx);
3219 let bottom_visible = bottom_dock.is_open();
3220 let bottom_active_panel = bottom_dock
3221 .visible_panel()
3222 .and_then(|panel| Some(panel.persistent_name().to_string()));
3223 let bottom_dock_zoom = bottom_dock
3224 .visible_panel()
3225 .map(|panel| panel.is_zoomed(cx))
3226 .unwrap_or(false);
3227
3228 DockStructure {
3229 left: DockData {
3230 visible: left_visible,
3231 active_panel: left_active_panel,
3232 zoom: left_dock_zoom,
3233 },
3234 right: DockData {
3235 visible: right_visible,
3236 active_panel: right_active_panel,
3237 zoom: right_dock_zoom,
3238 },
3239 bottom: DockData {
3240 visible: bottom_visible,
3241 active_panel: bottom_active_panel,
3242 zoom: bottom_dock_zoom,
3243 },
3244 }
3245 }
3246
3247 if let Some(location) = self.location(cx) {
3248 // Load bearing special case:
3249 // - with_local_workspace() relies on this to not have other stuff open
3250 // when you open your log
3251 if !location.paths().is_empty() {
3252 let center_group = build_serialized_pane_group(&self.center.root, cx);
3253 let docks = build_serialized_docks(self, cx);
3254
3255 let serialized_workspace = SerializedWorkspace {
3256 id: self.database_id,
3257 location,
3258 center_group,
3259 bounds: Default::default(),
3260 display: Default::default(),
3261 docks,
3262 };
3263
3264 cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace))
3265 .detach();
3266 }
3267 }
3268 }
3269
3270 pub(crate) fn load_workspace(
3271 serialized_workspace: SerializedWorkspace,
3272 paths_to_open: Vec<Option<ProjectPath>>,
3273 cx: &mut ViewContext<Workspace>,
3274 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
3275 cx.spawn(|workspace, mut cx| async move {
3276 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
3277
3278 let mut center_group = None;
3279 let mut center_items = None;
3280
3281 // Traverse the splits tree and add to things
3282 if let Some((group, active_pane, items)) = serialized_workspace
3283 .center_group
3284 .deserialize(
3285 &project,
3286 serialized_workspace.id,
3287 workspace.clone(),
3288 &mut cx,
3289 )
3290 .await
3291 {
3292 center_items = Some(items);
3293 center_group = Some((group, active_pane))
3294 }
3295
3296 let mut items_by_project_path = cx.update(|cx| {
3297 center_items
3298 .unwrap_or_default()
3299 .into_iter()
3300 .filter_map(|item| {
3301 let item = item?;
3302 let project_path = item.project_path(cx)?;
3303 Some((project_path, item))
3304 })
3305 .collect::<HashMap<_, _>>()
3306 })?;
3307
3308 let opened_items = paths_to_open
3309 .into_iter()
3310 .map(|path_to_open| {
3311 path_to_open
3312 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
3313 })
3314 .collect::<Vec<_>>();
3315
3316 // Remove old panes from workspace panes list
3317 workspace.update(&mut cx, |workspace, cx| {
3318 if let Some((center_group, active_pane)) = center_group {
3319 workspace.remove_panes(workspace.center.root.clone(), cx);
3320
3321 // Swap workspace center group
3322 workspace.center = PaneGroup::with_root(center_group);
3323 workspace.last_active_center_pane = active_pane.as_ref().map(|p| p.downgrade());
3324 if let Some(active_pane) = active_pane {
3325 workspace.active_pane = active_pane;
3326 cx.focus_self();
3327 } else {
3328 workspace.active_pane = workspace.center.first_pane().clone();
3329 }
3330 }
3331
3332 let docks = serialized_workspace.docks;
3333
3334 let right = docks.right.clone();
3335 workspace
3336 .right_dock
3337 .update(cx, |dock, _| dock.serialized_dock = Some(right));
3338 let left = docks.left.clone();
3339 workspace
3340 .left_dock
3341 .update(cx, |dock, _| dock.serialized_dock = Some(left));
3342 let bottom = docks.bottom.clone();
3343 workspace
3344 .bottom_dock
3345 .update(cx, |dock, _| dock.serialized_dock = Some(bottom));
3346
3347 cx.notify();
3348 })?;
3349
3350 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3351 workspace.update(&mut cx, |workspace, cx| workspace.serialize_workspace(cx))?;
3352
3353 Ok(opened_items)
3354 })
3355 }
3356
3357 fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3358 self.add_workspace_actions_listeners(div, cx)
3359 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3360 .on_action(cx.listener(Self::close_all_items_and_panes))
3361 .on_action(cx.listener(Self::save_all))
3362 .on_action(cx.listener(Self::add_folder_to_project))
3363 .on_action(cx.listener(Self::follow_next_collaborator))
3364 .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
3365 let pane = workspace.active_pane().clone();
3366 workspace.unfollow(&pane, cx);
3367 }))
3368 .on_action(cx.listener(|workspace, action: &Save, cx| {
3369 workspace
3370 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
3371 .detach_and_log_err(cx);
3372 }))
3373 .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
3374 workspace
3375 .save_active_item(SaveIntent::SaveAs, cx)
3376 .detach_and_log_err(cx);
3377 }))
3378 .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
3379 workspace.activate_previous_pane(cx)
3380 }))
3381 .on_action(
3382 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
3383 )
3384 .on_action(
3385 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
3386 workspace.activate_pane_in_direction(action.0, cx)
3387 }),
3388 )
3389 .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
3390 workspace.swap_pane_in_direction(action.0, cx)
3391 }))
3392 .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
3393 this.toggle_dock(DockPosition::Left, cx);
3394 }))
3395 .on_action(
3396 cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
3397 workspace.toggle_dock(DockPosition::Right, cx);
3398 }),
3399 )
3400 .on_action(
3401 cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
3402 workspace.toggle_dock(DockPosition::Bottom, cx);
3403 }),
3404 )
3405 .on_action(
3406 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
3407 workspace.close_all_docks(cx);
3408 }),
3409 )
3410 .on_action(cx.listener(Workspace::open))
3411 .on_action(cx.listener(Workspace::close_window))
3412 .on_action(cx.listener(Workspace::activate_pane_at_index))
3413 .on_action(
3414 cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
3415 workspace.reopen_closed_item(cx).detach();
3416 }),
3417 )
3418 .on_action(|_: &ToggleGraphicsProfiler, cx| cx.toggle_graphics_profiler())
3419 }
3420
3421 #[cfg(any(test, feature = "test-support"))]
3422 pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
3423 use node_runtime::FakeNodeRuntime;
3424
3425 let client = project.read(cx).client();
3426 let user_store = project.read(cx).user_store();
3427
3428 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
3429 cx.activate_window();
3430 let app_state = Arc::new(AppState {
3431 languages: project.read(cx).languages().clone(),
3432 workspace_store,
3433 client,
3434 user_store,
3435 fs: project.read(cx).fs().clone(),
3436 build_window_options: |_, _, _| Default::default(),
3437 node_runtime: FakeNodeRuntime::new(),
3438 });
3439 let workspace = Self::new(0, project, app_state, cx);
3440 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
3441 workspace
3442 }
3443
3444 pub fn register_action<A: Action>(
3445 &mut self,
3446 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
3447 ) -> &mut Self {
3448 let callback = Arc::new(callback);
3449
3450 self.workspace_actions.push(Box::new(move |div, cx| {
3451 let callback = callback.clone();
3452 div.on_action(
3453 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
3454 )
3455 }));
3456 self
3457 }
3458
3459 fn add_workspace_actions_listeners(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3460 let mut div = div
3461 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3462 .on_action(cx.listener(Self::close_all_items_and_panes))
3463 .on_action(cx.listener(Self::add_folder_to_project))
3464 .on_action(cx.listener(Self::save_all))
3465 .on_action(cx.listener(Self::open));
3466 for action in self.workspace_actions.iter() {
3467 div = (action)(div, cx)
3468 }
3469 div
3470 }
3471
3472 pub fn has_active_modal(&self, cx: &WindowContext<'_>) -> bool {
3473 self.modal_layer.read(cx).has_active_modal()
3474 }
3475
3476 pub fn active_modal<V: ManagedView + 'static>(&mut self, cx: &AppContext) -> Option<View<V>> {
3477 self.modal_layer.read(cx).active_modal()
3478 }
3479
3480 pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut WindowContext, build: B)
3481 where
3482 B: FnOnce(&mut ViewContext<V>) -> V,
3483 {
3484 self.modal_layer
3485 .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
3486 }
3487}
3488
3489fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
3490 let display_origin = cx
3491 .update(|cx| Some(cx.displays().first()?.bounds().origin))
3492 .ok()??;
3493 ZED_WINDOW_POSITION
3494 .zip(*ZED_WINDOW_SIZE)
3495 .map(|(position, size)| {
3496 WindowBounds::Fixed(Bounds {
3497 origin: display_origin + position,
3498 size,
3499 })
3500 })
3501}
3502
3503fn open_items(
3504 serialized_workspace: Option<SerializedWorkspace>,
3505 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3506 app_state: Arc<AppState>,
3507 cx: &mut ViewContext<Workspace>,
3508) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
3509 let restored_items = serialized_workspace.map(|serialized_workspace| {
3510 Workspace::load_workspace(
3511 serialized_workspace,
3512 project_paths_to_open
3513 .iter()
3514 .map(|(_, project_path)| project_path)
3515 .cloned()
3516 .collect(),
3517 cx,
3518 )
3519 });
3520
3521 cx.spawn(|workspace, mut cx| async move {
3522 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3523
3524 if let Some(restored_items) = restored_items {
3525 let restored_items = restored_items.await?;
3526
3527 let restored_project_paths = restored_items
3528 .iter()
3529 .filter_map(|item| {
3530 cx.update(|cx| item.as_ref()?.project_path(cx))
3531 .ok()
3532 .flatten()
3533 })
3534 .collect::<HashSet<_>>();
3535
3536 for restored_item in restored_items {
3537 opened_items.push(restored_item.map(Ok));
3538 }
3539
3540 project_paths_to_open
3541 .iter_mut()
3542 .for_each(|(_, project_path)| {
3543 if let Some(project_path_to_open) = project_path {
3544 if restored_project_paths.contains(project_path_to_open) {
3545 *project_path = None;
3546 }
3547 }
3548 });
3549 } else {
3550 for _ in 0..project_paths_to_open.len() {
3551 opened_items.push(None);
3552 }
3553 }
3554 assert!(opened_items.len() == project_paths_to_open.len());
3555
3556 let tasks =
3557 project_paths_to_open
3558 .into_iter()
3559 .enumerate()
3560 .map(|(i, (abs_path, project_path))| {
3561 let workspace = workspace.clone();
3562 cx.spawn(|mut cx| {
3563 let fs = app_state.fs.clone();
3564 async move {
3565 let file_project_path = project_path?;
3566 if fs.is_file(&abs_path).await {
3567 Some((
3568 i,
3569 workspace
3570 .update(&mut cx, |workspace, cx| {
3571 workspace.open_path(file_project_path, None, true, cx)
3572 })
3573 .log_err()?
3574 .await,
3575 ))
3576 } else {
3577 None
3578 }
3579 }
3580 })
3581 });
3582
3583 let tasks = tasks.collect::<Vec<_>>();
3584
3585 let tasks = futures::future::join_all(tasks.into_iter());
3586 for maybe_opened_path in tasks.await.into_iter() {
3587 if let Some((i, path_open_result)) = maybe_opened_path {
3588 opened_items[i] = Some(path_open_result);
3589 }
3590 }
3591
3592 Ok(opened_items)
3593 })
3594}
3595
3596enum ActivateInDirectionTarget {
3597 Pane(View<Pane>),
3598 Dock(View<Dock>),
3599}
3600
3601fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
3602 const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3603
3604 workspace
3605 .update(cx, |workspace, cx| {
3606 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3607 workspace.show_notification_once(0, cx, |cx| {
3608 cx.new_view(|_| {
3609 MessageNotification::new("Failed to load the database file.")
3610 .with_click_message("Click to let us know about this error")
3611 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
3612 })
3613 });
3614 }
3615 })
3616 .log_err();
3617}
3618
3619impl FocusableView for Workspace {
3620 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3621 self.active_pane.focus_handle(cx)
3622 }
3623}
3624
3625#[derive(Clone, Render)]
3626struct DraggedDock(DockPosition);
3627
3628impl Render for Workspace {
3629 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3630 let mut context = KeyContext::default();
3631 context.add("Workspace");
3632
3633 let (ui_font, ui_font_size) = {
3634 let theme_settings = ThemeSettings::get_global(cx);
3635 (
3636 theme_settings.ui_font.family.clone(),
3637 theme_settings.ui_font_size.clone(),
3638 )
3639 };
3640
3641 let theme = cx.theme().clone();
3642 let colors = theme.colors();
3643 cx.set_rem_size(ui_font_size);
3644
3645 self.actions(div(), cx)
3646 .key_context(context)
3647 .relative()
3648 .size_full()
3649 .flex()
3650 .flex_col()
3651 .font(ui_font)
3652 .gap_0()
3653 .justify_start()
3654 .items_start()
3655 .text_color(colors.text)
3656 .bg(colors.background)
3657 .border()
3658 .border_color(colors.border)
3659 .children(self.titlebar_item.clone())
3660 .child(
3661 div()
3662 .id("workspace")
3663 .relative()
3664 .flex_1()
3665 .w_full()
3666 .flex()
3667 .flex_col()
3668 .overflow_hidden()
3669 .border_t()
3670 .border_b()
3671 .border_color(colors.border)
3672 .child(
3673 canvas({
3674 let this = cx.view().clone();
3675 move |bounds, cx| {
3676 this.update(cx, |this, _cx| {
3677 this.bounds = *bounds;
3678 })
3679 }
3680 })
3681 .absolute()
3682 .size_full(),
3683 )
3684 .on_drag_move(
3685 cx.listener(|workspace, e: &DragMoveEvent<DraggedDock>, cx| {
3686 match e.drag(cx).0 {
3687 DockPosition::Left => {
3688 let size = workspace.bounds.left() + e.event.position.x;
3689 workspace.left_dock.update(cx, |left_dock, cx| {
3690 left_dock.resize_active_panel(Some(size), cx);
3691 });
3692 }
3693 DockPosition::Right => {
3694 let size = workspace.bounds.right() - e.event.position.x;
3695 workspace.right_dock.update(cx, |right_dock, cx| {
3696 right_dock.resize_active_panel(Some(size), cx);
3697 });
3698 }
3699 DockPosition::Bottom => {
3700 let size = workspace.bounds.bottom() - e.event.position.y;
3701 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
3702 bottom_dock.resize_active_panel(Some(size), cx);
3703 });
3704 }
3705 }
3706 }),
3707 )
3708 .child(self.modal_layer.clone())
3709 .child(
3710 div()
3711 .flex()
3712 .flex_row()
3713 .h_full()
3714 // Left Dock
3715 .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
3716 || {
3717 div()
3718 .flex()
3719 .flex_none()
3720 .overflow_hidden()
3721 .child(self.left_dock.clone())
3722 },
3723 ))
3724 // Panes
3725 .child(
3726 div()
3727 .flex()
3728 .flex_col()
3729 .flex_1()
3730 .overflow_hidden()
3731 .child(self.center.render(
3732 &self.project,
3733 &self.follower_states,
3734 self.active_call(),
3735 &self.active_pane,
3736 self.zoomed.as_ref(),
3737 &self.app_state,
3738 cx,
3739 ))
3740 .children(
3741 self.zoomed_position
3742 .ne(&Some(DockPosition::Bottom))
3743 .then(|| self.bottom_dock.clone()),
3744 ),
3745 )
3746 // Right Dock
3747 .children(self.zoomed_position.ne(&Some(DockPosition::Right)).then(
3748 || {
3749 div()
3750 .flex()
3751 .flex_none()
3752 .overflow_hidden()
3753 .child(self.right_dock.clone())
3754 },
3755 )),
3756 )
3757 .children(self.render_notifications(cx))
3758 .children(self.zoomed.as_ref().and_then(|view| {
3759 let zoomed_view = view.upgrade()?;
3760 let div = div()
3761 .z_index(1)
3762 .absolute()
3763 .overflow_hidden()
3764 .border_color(colors.border)
3765 .bg(colors.background)
3766 .child(zoomed_view)
3767 .inset_0()
3768 .shadow_lg();
3769
3770 Some(match self.zoomed_position {
3771 Some(DockPosition::Left) => div.right_2().border_r(),
3772 Some(DockPosition::Right) => div.left_2().border_l(),
3773 Some(DockPosition::Bottom) => div.top_2().border_t(),
3774 None => div.top_2().bottom_2().left_2().right_2().border(),
3775 })
3776 })),
3777 )
3778 .child(self.status_bar.clone())
3779 .children(if self.project.read(cx).is_disconnected() {
3780 Some(DisconnectedOverlay)
3781 } else {
3782 None
3783 })
3784 }
3785}
3786
3787impl WorkspaceStore {
3788 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
3789 Self {
3790 workspaces: Default::default(),
3791 followers: Default::default(),
3792 _subscriptions: vec![
3793 client.add_request_handler(cx.weak_model(), Self::handle_follow),
3794 client.add_message_handler(cx.weak_model(), Self::handle_unfollow),
3795 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
3796 ],
3797 client,
3798 }
3799 }
3800
3801 pub fn update_followers(
3802 &self,
3803 project_id: Option<u64>,
3804 update: proto::update_followers::Variant,
3805 cx: &AppContext,
3806 ) -> Option<()> {
3807 let active_call = ActiveCall::try_global(cx)?;
3808 let room_id = active_call.read(cx).room()?.read(cx).id();
3809 let follower_ids: Vec<_> = self
3810 .followers
3811 .iter()
3812 .filter_map(|follower| {
3813 if follower.project_id == project_id || project_id.is_none() {
3814 Some(follower.peer_id.into())
3815 } else {
3816 None
3817 }
3818 })
3819 .collect();
3820 if follower_ids.is_empty() {
3821 return None;
3822 }
3823 self.client
3824 .send(proto::UpdateFollowers {
3825 room_id,
3826 project_id,
3827 follower_ids,
3828 variant: Some(update),
3829 })
3830 .log_err()
3831 }
3832
3833 pub async fn handle_follow(
3834 this: Model<Self>,
3835 envelope: TypedEnvelope<proto::Follow>,
3836 _: Arc<Client>,
3837 mut cx: AsyncAppContext,
3838 ) -> Result<proto::FollowResponse> {
3839 this.update(&mut cx, |this, cx| {
3840 let follower = Follower {
3841 project_id: envelope.payload.project_id,
3842 peer_id: envelope.original_sender_id()?,
3843 };
3844 let active_project = ActiveCall::global(cx).read(cx).location().cloned();
3845
3846 let mut response = proto::FollowResponse::default();
3847 this.workspaces.retain(|workspace| {
3848 workspace
3849 .update(cx, |workspace, cx| {
3850 let handler_response = workspace.handle_follow(follower.project_id, cx);
3851 if response.views.is_empty() {
3852 response.views = handler_response.views;
3853 } else {
3854 response.views.extend_from_slice(&handler_response.views);
3855 }
3856
3857 if let Some(active_view_id) = handler_response.active_view_id.clone() {
3858 if response.active_view_id.is_none()
3859 || Some(workspace.project.downgrade()) == active_project
3860 {
3861 response.active_view_id = Some(active_view_id);
3862 }
3863 }
3864 })
3865 .is_ok()
3866 });
3867
3868 if let Err(ix) = this.followers.binary_search(&follower) {
3869 this.followers.insert(ix, follower);
3870 }
3871
3872 Ok(response)
3873 })?
3874 }
3875
3876 async fn handle_unfollow(
3877 model: Model<Self>,
3878 envelope: TypedEnvelope<proto::Unfollow>,
3879 _: Arc<Client>,
3880 mut cx: AsyncAppContext,
3881 ) -> Result<()> {
3882 model.update(&mut cx, |this, _| {
3883 let follower = Follower {
3884 project_id: envelope.payload.project_id,
3885 peer_id: envelope.original_sender_id()?,
3886 };
3887 if let Ok(ix) = this.followers.binary_search(&follower) {
3888 this.followers.remove(ix);
3889 }
3890 Ok(())
3891 })?
3892 }
3893
3894 async fn handle_update_followers(
3895 this: Model<Self>,
3896 envelope: TypedEnvelope<proto::UpdateFollowers>,
3897 _: Arc<Client>,
3898 mut cx: AsyncAppContext,
3899 ) -> Result<()> {
3900 let leader_id = envelope.original_sender_id()?;
3901 let update = envelope.payload;
3902
3903 this.update(&mut cx, |this, cx| {
3904 this.workspaces.retain(|workspace| {
3905 workspace
3906 .update(cx, |workspace, cx| {
3907 let project_id = workspace.project.read(cx).remote_id();
3908 if update.project_id != project_id && update.project_id.is_some() {
3909 return;
3910 }
3911 workspace.handle_update_followers(leader_id, update.clone(), cx);
3912 })
3913 .is_ok()
3914 });
3915 Ok(())
3916 })?
3917 }
3918}
3919
3920impl ViewId {
3921 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
3922 Ok(Self {
3923 creator: message
3924 .creator
3925 .ok_or_else(|| anyhow!("creator is missing"))?,
3926 id: message.id,
3927 })
3928 }
3929
3930 pub(crate) fn to_proto(&self) -> proto::ViewId {
3931 proto::ViewId {
3932 creator: Some(self.creator),
3933 id: self.id,
3934 }
3935 }
3936}
3937
3938pub trait WorkspaceHandle {
3939 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
3940}
3941
3942impl WorkspaceHandle for View<Workspace> {
3943 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
3944 self.read(cx)
3945 .worktrees(cx)
3946 .flat_map(|worktree| {
3947 let worktree_id = worktree.read(cx).id();
3948 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
3949 worktree_id,
3950 path: f.path.clone(),
3951 })
3952 })
3953 .collect::<Vec<_>>()
3954 }
3955}
3956
3957impl std::fmt::Debug for OpenPaths {
3958 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3959 f.debug_struct("OpenPaths")
3960 .field("paths", &self.paths)
3961 .finish()
3962 }
3963}
3964
3965pub fn activate_workspace_for_project(
3966 cx: &mut AppContext,
3967 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
3968) -> Option<WindowHandle<Workspace>> {
3969 for window in cx.windows() {
3970 let Some(workspace) = window.downcast::<Workspace>() else {
3971 continue;
3972 };
3973
3974 let predicate = workspace
3975 .update(cx, |workspace, cx| {
3976 let project = workspace.project.read(cx);
3977 if predicate(project, cx) {
3978 cx.activate_window();
3979 true
3980 } else {
3981 false
3982 }
3983 })
3984 .log_err()
3985 .unwrap_or(false);
3986
3987 if predicate {
3988 return Some(workspace);
3989 }
3990 }
3991
3992 None
3993}
3994
3995pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
3996 DB.last_workspace().await.log_err().flatten()
3997}
3998
3999actions!(collab, [OpenChannelNotes]);
4000
4001async fn join_channel_internal(
4002 channel_id: u64,
4003 app_state: &Arc<AppState>,
4004 requesting_window: Option<WindowHandle<Workspace>>,
4005 active_call: &Model<ActiveCall>,
4006 cx: &mut AsyncAppContext,
4007) -> Result<bool> {
4008 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
4009 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
4010 return (false, None);
4011 };
4012
4013 let already_in_channel = room.channel_id() == Some(channel_id);
4014 let should_prompt = room.is_sharing_project()
4015 && room.remote_participants().len() > 0
4016 && !already_in_channel;
4017 let open_room = if already_in_channel {
4018 active_call.room().cloned()
4019 } else {
4020 None
4021 };
4022 (should_prompt, open_room)
4023 })?;
4024
4025 if let Some(room) = open_room {
4026 let task = room.update(cx, |room, cx| {
4027 if let Some((project, host)) = room.most_active_project(cx) {
4028 return Some(join_remote_project(project, host, app_state.clone(), cx));
4029 }
4030
4031 None
4032 })?;
4033 if let Some(task) = task {
4034 task.await?;
4035 }
4036 return anyhow::Ok(true);
4037 }
4038
4039 if should_prompt {
4040 if let Some(workspace) = requesting_window {
4041 let answer = workspace
4042 .update(cx, |_, cx| {
4043 cx.prompt(
4044 PromptLevel::Warning,
4045 "Do you want to switch channels?",
4046 Some("Leaving this call will unshare your current project."),
4047 &["Yes, Join Channel", "Cancel"],
4048 )
4049 })?
4050 .await;
4051
4052 if answer == Ok(1) {
4053 return Ok(false);
4054 }
4055 } else {
4056 return Ok(false); // unreachable!() hopefully
4057 }
4058 }
4059
4060 let client = cx.update(|cx| active_call.read(cx).client())?;
4061
4062 let mut client_status = client.status();
4063
4064 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
4065 'outer: loop {
4066 let Some(status) = client_status.recv().await else {
4067 return Err(anyhow!("error connecting"));
4068 };
4069
4070 match status {
4071 Status::Connecting
4072 | Status::Authenticating
4073 | Status::Reconnecting
4074 | Status::Reauthenticating => continue,
4075 Status::Connected { .. } => break 'outer,
4076 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
4077 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
4078 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
4079 return Err(ErrorCode::Disconnected.into())
4080 }
4081 }
4082 }
4083
4084 let room = active_call
4085 .update(cx, |active_call, cx| {
4086 active_call.join_channel(channel_id, cx)
4087 })?
4088 .await?;
4089
4090 let Some(room) = room else {
4091 return anyhow::Ok(true);
4092 };
4093
4094 room.update(cx, |room, _| room.room_update_completed())?
4095 .await;
4096
4097 let task = room.update(cx, |room, cx| {
4098 if let Some((project, host)) = room.most_active_project(cx) {
4099 return Some(join_remote_project(project, host, app_state.clone(), cx));
4100 }
4101
4102 // if you are the first to join a channel, share your project
4103 if room.remote_participants().len() == 0 && !room.local_participant_is_guest() {
4104 if let Some(workspace) = requesting_window {
4105 let project = workspace.update(cx, |workspace, cx| {
4106 if !CallSettings::get_global(cx).share_on_join {
4107 return None;
4108 }
4109 let project = workspace.project.read(cx);
4110 if project.is_local()
4111 && project.visible_worktrees(cx).any(|tree| {
4112 tree.read(cx)
4113 .root_entry()
4114 .map_or(false, |entry| entry.is_dir())
4115 })
4116 {
4117 Some(workspace.project.clone())
4118 } else {
4119 None
4120 }
4121 });
4122 if let Ok(Some(project)) = project {
4123 return Some(cx.spawn(|room, mut cx| async move {
4124 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
4125 .await?;
4126 Ok(())
4127 }));
4128 }
4129 }
4130 }
4131
4132 None
4133 })?;
4134 if let Some(task) = task {
4135 task.await?;
4136 return anyhow::Ok(true);
4137 }
4138 anyhow::Ok(false)
4139}
4140
4141pub fn join_channel(
4142 channel_id: u64,
4143 app_state: Arc<AppState>,
4144 requesting_window: Option<WindowHandle<Workspace>>,
4145 cx: &mut AppContext,
4146) -> Task<Result<()>> {
4147 let active_call = ActiveCall::global(cx);
4148 cx.spawn(|mut cx| async move {
4149 let result = join_channel_internal(
4150 channel_id,
4151 &app_state,
4152 requesting_window,
4153 &active_call,
4154 &mut cx,
4155 )
4156 .await;
4157
4158 // join channel succeeded, and opened a window
4159 if matches!(result, Ok(true)) {
4160 return anyhow::Ok(());
4161 }
4162
4163 // find an existing workspace to focus and show call controls
4164 let mut active_window =
4165 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
4166 if active_window.is_none() {
4167 // no open workspaces, make one to show the error in (blergh)
4168 let (window_handle, _) = cx
4169 .update(|cx| {
4170 Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)
4171 })?
4172 .await?;
4173
4174 if result.is_ok() {
4175 cx.update(|cx| {
4176 cx.dispatch_action(&OpenChannelNotes);
4177 }).log_err();
4178 }
4179
4180 active_window = Some(window_handle);
4181 }
4182
4183 if let Err(err) = result {
4184 log::error!("failed to join channel: {}", err);
4185 if let Some(active_window) = active_window {
4186 active_window
4187 .update(&mut cx, |_, cx| {
4188 let detail: SharedString = match err.error_code() {
4189 ErrorCode::SignedOut => {
4190 "Please sign in to continue.".into()
4191 },
4192 ErrorCode::UpgradeRequired => {
4193 "Your are running an unsupported version of Zed. Please update to continue.".into()
4194 },
4195 ErrorCode::NoSuchChannel => {
4196 "No matching channel was found. Please check the link and try again.".into()
4197 },
4198 ErrorCode::Forbidden => {
4199 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
4200 },
4201 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
4202 _ => format!("{}\n\nPlease try again.", err).into(),
4203 };
4204 cx.prompt(
4205 PromptLevel::Critical,
4206 "Failed to join channel",
4207 Some(&detail),
4208 &["Ok"],
4209 )
4210 })?
4211 .await
4212 .ok();
4213 }
4214 }
4215
4216 // return ok, we showed the error to the user.
4217 return anyhow::Ok(());
4218 })
4219}
4220
4221pub async fn get_any_active_workspace(
4222 app_state: Arc<AppState>,
4223 mut cx: AsyncAppContext,
4224) -> anyhow::Result<WindowHandle<Workspace>> {
4225 // find an existing workspace to focus and show call controls
4226 let active_window = activate_any_workspace_window(&mut cx);
4227 if active_window.is_none() {
4228 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
4229 .await?;
4230 }
4231 activate_any_workspace_window(&mut cx).context("could not open zed")
4232}
4233
4234fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
4235 cx.update(|cx| {
4236 for window in cx.windows() {
4237 if let Some(workspace_window) = window.downcast::<Workspace>() {
4238 workspace_window
4239 .update(cx, |_, cx| cx.activate_window())
4240 .ok();
4241 return Some(workspace_window);
4242 }
4243 }
4244 None
4245 })
4246 .ok()
4247 .flatten()
4248}
4249
4250#[allow(clippy::type_complexity)]
4251pub fn open_paths(
4252 abs_paths: &[PathBuf],
4253 app_state: &Arc<AppState>,
4254 requesting_window: Option<WindowHandle<Workspace>>,
4255 cx: &mut AppContext,
4256) -> Task<
4257 anyhow::Result<(
4258 WindowHandle<Workspace>,
4259 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4260 )>,
4261> {
4262 let app_state = app_state.clone();
4263 let abs_paths = abs_paths.to_vec();
4264 // Open paths in existing workspace if possible
4265 let existing = activate_workspace_for_project(cx, {
4266 let abs_paths = abs_paths.clone();
4267 move |project, cx| project.contains_paths(&abs_paths, cx)
4268 });
4269 cx.spawn(move |mut cx| async move {
4270 if let Some(existing) = existing {
4271 Ok((
4272 existing.clone(),
4273 existing
4274 .update(&mut cx, |workspace, cx| {
4275 workspace.open_paths(abs_paths, OpenVisible::All, None, cx)
4276 })?
4277 .await,
4278 ))
4279 } else {
4280 cx.update(move |cx| {
4281 Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
4282 })?
4283 .await
4284 }
4285 })
4286}
4287
4288pub fn open_new(
4289 app_state: &Arc<AppState>,
4290 cx: &mut AppContext,
4291 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
4292) -> Task<()> {
4293 let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
4294 cx.spawn(|mut cx| async move {
4295 if let Some((workspace, opened_paths)) = task.await.log_err() {
4296 workspace
4297 .update(&mut cx, |workspace, cx| {
4298 if opened_paths.is_empty() {
4299 init(workspace, cx)
4300 }
4301 })
4302 .log_err();
4303 }
4304 })
4305}
4306
4307pub fn create_and_open_local_file(
4308 path: &'static Path,
4309 cx: &mut ViewContext<Workspace>,
4310 default_content: impl 'static + Send + FnOnce() -> Rope,
4311) -> Task<Result<Box<dyn ItemHandle>>> {
4312 cx.spawn(|workspace, mut cx| async move {
4313 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
4314 if !fs.is_file(path).await {
4315 fs.create_file(path, Default::default()).await?;
4316 fs.save(path, &default_content(), Default::default())
4317 .await?;
4318 }
4319
4320 let mut items = workspace
4321 .update(&mut cx, |workspace, cx| {
4322 workspace.with_local_workspace(cx, |workspace, cx| {
4323 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
4324 })
4325 })?
4326 .await?
4327 .await;
4328
4329 let item = items.pop().flatten();
4330 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4331 })
4332}
4333
4334pub fn join_remote_project(
4335 project_id: u64,
4336 follow_user_id: u64,
4337 app_state: Arc<AppState>,
4338 cx: &mut AppContext,
4339) -> Task<Result<()>> {
4340 let windows = cx.windows();
4341 cx.spawn(|mut cx| async move {
4342 let existing_workspace = windows.into_iter().find_map(|window| {
4343 window.downcast::<Workspace>().and_then(|window| {
4344 window
4345 .update(&mut cx, |workspace, cx| {
4346 if workspace.project().read(cx).remote_id() == Some(project_id) {
4347 Some(window)
4348 } else {
4349 None
4350 }
4351 })
4352 .unwrap_or(None)
4353 })
4354 });
4355
4356 let workspace = if let Some(existing_workspace) = existing_workspace {
4357 existing_workspace
4358 } else {
4359 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
4360 let room = active_call
4361 .read_with(&cx, |call, _| call.room().cloned())?
4362 .ok_or_else(|| anyhow!("not in a call"))?;
4363 let project = room
4364 .update(&mut cx, |room, cx| {
4365 room.join_project(
4366 project_id,
4367 app_state.languages.clone(),
4368 app_state.fs.clone(),
4369 cx,
4370 )
4371 })?
4372 .await?;
4373
4374 let window_bounds_override = window_bounds_env_override(&cx);
4375 cx.update(|cx| {
4376 let options = (app_state.build_window_options)(window_bounds_override, None, cx);
4377 cx.open_window(options, |cx| {
4378 cx.new_view(|cx| Workspace::new(0, project, app_state.clone(), cx))
4379 })
4380 })?
4381 };
4382
4383 workspace.update(&mut cx, |workspace, cx| {
4384 cx.activate(true);
4385 cx.activate_window();
4386
4387 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4388 let follow_peer_id = room
4389 .read(cx)
4390 .remote_participants()
4391 .iter()
4392 .find(|(_, participant)| participant.user.id == follow_user_id)
4393 .map(|(_, p)| p.peer_id)
4394 .or_else(|| {
4395 // If we couldn't follow the given user, follow the host instead.
4396 let collaborator = workspace
4397 .project()
4398 .read(cx)
4399 .collaborators()
4400 .values()
4401 .find(|collaborator| collaborator.replica_id == 0)?;
4402 Some(collaborator.peer_id)
4403 });
4404
4405 if let Some(follow_peer_id) = follow_peer_id {
4406 workspace.follow(follow_peer_id, cx);
4407 }
4408 }
4409 })?;
4410
4411 anyhow::Ok(())
4412 })
4413}
4414
4415pub fn restart(_: &Restart, cx: &mut AppContext) {
4416 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
4417 let mut workspace_windows = cx
4418 .windows()
4419 .into_iter()
4420 .filter_map(|window| window.downcast::<Workspace>())
4421 .collect::<Vec<_>>();
4422
4423 // If multiple windows have unsaved changes, and need a save prompt,
4424 // prompt in the active window before switching to a different window.
4425 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
4426
4427 let mut prompt = None;
4428 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4429 prompt = window
4430 .update(cx, |_, cx| {
4431 cx.prompt(
4432 PromptLevel::Info,
4433 "Are you sure you want to restart?",
4434 None,
4435 &["Restart", "Cancel"],
4436 )
4437 })
4438 .ok();
4439 }
4440
4441 cx.spawn(|mut cx| async move {
4442 if let Some(prompt) = prompt {
4443 let answer = prompt.await?;
4444 if answer != 0 {
4445 return Ok(());
4446 }
4447 }
4448
4449 // If the user cancels any save prompt, then keep the app open.
4450 for window in workspace_windows {
4451 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
4452 workspace.prepare_to_close(true, cx)
4453 }) {
4454 if !should_close.await? {
4455 return Ok(());
4456 }
4457 }
4458 }
4459
4460 cx.update(|cx| cx.restart())
4461 })
4462 .detach_and_log_err(cx);
4463}
4464
4465fn parse_pixel_position_env_var(value: &str) -> Option<Point<GlobalPixels>> {
4466 let mut parts = value.split(',');
4467 let x: usize = parts.next()?.parse().ok()?;
4468 let y: usize = parts.next()?.parse().ok()?;
4469 Some(point((x as f64).into(), (y as f64).into()))
4470}
4471
4472fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
4473 let mut parts = value.split(',');
4474 let width: usize = parts.next()?.parse().ok()?;
4475 let height: usize = parts.next()?.parse().ok()?;
4476 Some(size((width as f64).into(), (height as f64).into()))
4477}
4478
4479pub fn titlebar_height(cx: &mut WindowContext) -> Pixels {
4480 (1.75 * cx.rem_size()).max(px(32.))
4481}
4482
4483struct DisconnectedOverlay;
4484
4485impl Element for DisconnectedOverlay {
4486 type State = AnyElement;
4487
4488 fn request_layout(
4489 &mut self,
4490 _: Option<Self::State>,
4491 cx: &mut ElementContext,
4492 ) -> (LayoutId, Self::State) {
4493 let mut background = cx.theme().colors().elevated_surface_background;
4494 background.fade_out(0.2);
4495 let mut overlay = div()
4496 .bg(background)
4497 .absolute()
4498 .left_0()
4499 .top(titlebar_height(cx))
4500 .size_full()
4501 .flex()
4502 .items_center()
4503 .justify_center()
4504 .capture_any_mouse_down(|_, cx| cx.stop_propagation())
4505 .capture_any_mouse_up(|_, cx| cx.stop_propagation())
4506 .child(Label::new(
4507 "Your connection to the remote project has been lost.",
4508 ))
4509 .into_any();
4510 (overlay.request_layout(cx), overlay)
4511 }
4512
4513 fn paint(
4514 &mut self,
4515 bounds: Bounds<Pixels>,
4516 overlay: &mut Self::State,
4517 cx: &mut ElementContext,
4518 ) {
4519 cx.with_z_index(u16::MAX, |cx| {
4520 cx.add_opaque_layer(bounds);
4521 overlay.paint(cx);
4522 })
4523 }
4524}
4525
4526impl IntoElement for DisconnectedOverlay {
4527 type Element = Self;
4528
4529 fn element_id(&self) -> Option<ui::prelude::ElementId> {
4530 None
4531 }
4532
4533 fn into_element(self) -> Self::Element {
4534 self
4535 }
4536}
4537
4538#[cfg(test)]
4539mod tests {
4540 use std::{cell::RefCell, rc::Rc};
4541
4542 use super::*;
4543 use crate::{
4544 dock::{test::TestPanel, PanelEvent},
4545 item::{
4546 test::{TestItem, TestProjectItem},
4547 ItemEvent,
4548 },
4549 };
4550 use fs::FakeFs;
4551 use gpui::{px, DismissEvent, TestAppContext, VisualTestContext};
4552 use project::{Project, ProjectEntryId};
4553 use serde_json::json;
4554 use settings::SettingsStore;
4555
4556 #[gpui::test]
4557 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
4558 init_test(cx);
4559
4560 let fs = FakeFs::new(cx.executor());
4561 let project = Project::test(fs, [], cx).await;
4562 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4563
4564 // Adding an item with no ambiguity renders the tab without detail.
4565 let item1 = cx.new_view(|cx| {
4566 let mut item = TestItem::new(cx);
4567 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
4568 item
4569 });
4570 workspace.update(cx, |workspace, cx| {
4571 workspace.add_item(Box::new(item1.clone()), cx);
4572 });
4573 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
4574
4575 // Adding an item that creates ambiguity increases the level of detail on
4576 // both tabs.
4577 let item2 = cx.new_view(|cx| {
4578 let mut item = TestItem::new(cx);
4579 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4580 item
4581 });
4582 workspace.update(cx, |workspace, cx| {
4583 workspace.add_item(Box::new(item2.clone()), cx);
4584 });
4585 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4586 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4587
4588 // Adding an item that creates ambiguity increases the level of detail only
4589 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
4590 // we stop at the highest detail available.
4591 let item3 = cx.new_view(|cx| {
4592 let mut item = TestItem::new(cx);
4593 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
4594 item
4595 });
4596 workspace.update(cx, |workspace, cx| {
4597 workspace.add_item(Box::new(item3.clone()), cx);
4598 });
4599 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
4600 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4601 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
4602 }
4603
4604 #[gpui::test]
4605 async fn test_tracking_active_path(cx: &mut TestAppContext) {
4606 init_test(cx);
4607
4608 let fs = FakeFs::new(cx.executor());
4609 fs.insert_tree(
4610 "/root1",
4611 json!({
4612 "one.txt": "",
4613 "two.txt": "",
4614 }),
4615 )
4616 .await;
4617 fs.insert_tree(
4618 "/root2",
4619 json!({
4620 "three.txt": "",
4621 }),
4622 )
4623 .await;
4624
4625 let project = Project::test(fs, ["root1".as_ref()], cx).await;
4626 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4627 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4628 let worktree_id = project.update(cx, |project, cx| {
4629 project.worktrees().next().unwrap().read(cx).id()
4630 });
4631
4632 let item1 = cx.new_view(|cx| {
4633 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
4634 });
4635 let item2 = cx.new_view(|cx| {
4636 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
4637 });
4638
4639 // Add an item to an empty pane
4640 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
4641 project.update(cx, |project, cx| {
4642 assert_eq!(
4643 project.active_entry(),
4644 project
4645 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4646 .map(|e| e.id)
4647 );
4648 });
4649 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
4650
4651 // Add a second item to a non-empty pane
4652 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
4653 assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
4654 project.update(cx, |project, cx| {
4655 assert_eq!(
4656 project.active_entry(),
4657 project
4658 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
4659 .map(|e| e.id)
4660 );
4661 });
4662
4663 // Close the active item
4664 pane.update(cx, |pane, cx| {
4665 pane.close_active_item(&Default::default(), cx).unwrap()
4666 })
4667 .await
4668 .unwrap();
4669 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
4670 project.update(cx, |project, cx| {
4671 assert_eq!(
4672 project.active_entry(),
4673 project
4674 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
4675 .map(|e| e.id)
4676 );
4677 });
4678
4679 // Add a project folder
4680 project
4681 .update(cx, |project, cx| {
4682 project.find_or_create_local_worktree("/root2", true, cx)
4683 })
4684 .await
4685 .unwrap();
4686 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
4687
4688 // Remove a project folder
4689 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
4690 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
4691 }
4692
4693 #[gpui::test]
4694 async fn test_close_window(cx: &mut TestAppContext) {
4695 init_test(cx);
4696
4697 let fs = FakeFs::new(cx.executor());
4698 fs.insert_tree("/root", json!({ "one": "" })).await;
4699
4700 let project = Project::test(fs, ["root".as_ref()], cx).await;
4701 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
4702
4703 // When there are no dirty items, there's nothing to do.
4704 let item1 = cx.new_view(|cx| TestItem::new(cx));
4705 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
4706 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4707 assert!(task.await.unwrap());
4708
4709 // When there are dirty untitled items, prompt to save each one. If the user
4710 // cancels any prompt, then abort.
4711 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
4712 let item3 = cx.new_view(|cx| {
4713 TestItem::new(cx)
4714 .with_dirty(true)
4715 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4716 });
4717 workspace.update(cx, |w, cx| {
4718 w.add_item(Box::new(item2.clone()), cx);
4719 w.add_item(Box::new(item3.clone()), cx);
4720 });
4721 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
4722 cx.executor().run_until_parked();
4723 cx.simulate_prompt_answer(2); // cancel save all
4724 cx.executor().run_until_parked();
4725 cx.simulate_prompt_answer(2); // cancel save all
4726 cx.executor().run_until_parked();
4727 assert!(!cx.has_pending_prompt());
4728 assert!(!task.await.unwrap());
4729 }
4730
4731 #[gpui::test]
4732 async fn test_close_pane_items(cx: &mut TestAppContext) {
4733 init_test(cx);
4734
4735 let fs = FakeFs::new(cx.executor());
4736
4737 let project = Project::test(fs, None, cx).await;
4738 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4739
4740 let item1 = cx.new_view(|cx| {
4741 TestItem::new(cx)
4742 .with_dirty(true)
4743 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4744 });
4745 let item2 = cx.new_view(|cx| {
4746 TestItem::new(cx)
4747 .with_dirty(true)
4748 .with_conflict(true)
4749 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
4750 });
4751 let item3 = cx.new_view(|cx| {
4752 TestItem::new(cx)
4753 .with_dirty(true)
4754 .with_conflict(true)
4755 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
4756 });
4757 let item4 = cx.new_view(|cx| {
4758 TestItem::new(cx)
4759 .with_dirty(true)
4760 .with_project_items(&[TestProjectItem::new_untitled(cx)])
4761 });
4762 let pane = workspace.update(cx, |workspace, cx| {
4763 workspace.add_item(Box::new(item1.clone()), cx);
4764 workspace.add_item(Box::new(item2.clone()), cx);
4765 workspace.add_item(Box::new(item3.clone()), cx);
4766 workspace.add_item(Box::new(item4.clone()), cx);
4767 workspace.active_pane().clone()
4768 });
4769
4770 let close_items = pane.update(cx, |pane, cx| {
4771 pane.activate_item(1, true, true, cx);
4772 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4773 let item1_id = item1.item_id();
4774 let item3_id = item3.item_id();
4775 let item4_id = item4.item_id();
4776 pane.close_items(cx, SaveIntent::Close, move |id| {
4777 [item1_id, item3_id, item4_id].contains(&id)
4778 })
4779 });
4780 cx.executor().run_until_parked();
4781
4782 assert!(cx.has_pending_prompt());
4783 // Ignore "Save all" prompt
4784 cx.simulate_prompt_answer(2);
4785 cx.executor().run_until_parked();
4786 // There's a prompt to save item 1.
4787 pane.update(cx, |pane, _| {
4788 assert_eq!(pane.items_len(), 4);
4789 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
4790 });
4791 // Confirm saving item 1.
4792 cx.simulate_prompt_answer(0);
4793 cx.executor().run_until_parked();
4794
4795 // Item 1 is saved. There's a prompt to save item 3.
4796 pane.update(cx, |pane, cx| {
4797 assert_eq!(item1.read(cx).save_count, 1);
4798 assert_eq!(item1.read(cx).save_as_count, 0);
4799 assert_eq!(item1.read(cx).reload_count, 0);
4800 assert_eq!(pane.items_len(), 3);
4801 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
4802 });
4803 assert!(cx.has_pending_prompt());
4804
4805 // Cancel saving item 3.
4806 cx.simulate_prompt_answer(1);
4807 cx.executor().run_until_parked();
4808
4809 // Item 3 is reloaded. There's a prompt to save item 4.
4810 pane.update(cx, |pane, cx| {
4811 assert_eq!(item3.read(cx).save_count, 0);
4812 assert_eq!(item3.read(cx).save_as_count, 0);
4813 assert_eq!(item3.read(cx).reload_count, 1);
4814 assert_eq!(pane.items_len(), 2);
4815 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
4816 });
4817 assert!(cx.has_pending_prompt());
4818
4819 // Confirm saving item 4.
4820 cx.simulate_prompt_answer(0);
4821 cx.executor().run_until_parked();
4822
4823 // There's a prompt for a path for item 4.
4824 cx.simulate_new_path_selection(|_| Some(Default::default()));
4825 close_items.await.unwrap();
4826
4827 // The requested items are closed.
4828 pane.update(cx, |pane, cx| {
4829 assert_eq!(item4.read(cx).save_count, 0);
4830 assert_eq!(item4.read(cx).save_as_count, 1);
4831 assert_eq!(item4.read(cx).reload_count, 0);
4832 assert_eq!(pane.items_len(), 1);
4833 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
4834 });
4835 }
4836
4837 #[gpui::test]
4838 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
4839 init_test(cx);
4840
4841 let fs = FakeFs::new(cx.executor());
4842 let project = Project::test(fs, [], cx).await;
4843 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4844
4845 // Create several workspace items with single project entries, and two
4846 // workspace items with multiple project entries.
4847 let single_entry_items = (0..=4)
4848 .map(|project_entry_id| {
4849 cx.new_view(|cx| {
4850 TestItem::new(cx)
4851 .with_dirty(true)
4852 .with_project_items(&[TestProjectItem::new(
4853 project_entry_id,
4854 &format!("{project_entry_id}.txt"),
4855 cx,
4856 )])
4857 })
4858 })
4859 .collect::<Vec<_>>();
4860 let item_2_3 = cx.new_view(|cx| {
4861 TestItem::new(cx)
4862 .with_dirty(true)
4863 .with_singleton(false)
4864 .with_project_items(&[
4865 single_entry_items[2].read(cx).project_items[0].clone(),
4866 single_entry_items[3].read(cx).project_items[0].clone(),
4867 ])
4868 });
4869 let item_3_4 = cx.new_view(|cx| {
4870 TestItem::new(cx)
4871 .with_dirty(true)
4872 .with_singleton(false)
4873 .with_project_items(&[
4874 single_entry_items[3].read(cx).project_items[0].clone(),
4875 single_entry_items[4].read(cx).project_items[0].clone(),
4876 ])
4877 });
4878
4879 // Create two panes that contain the following project entries:
4880 // left pane:
4881 // multi-entry items: (2, 3)
4882 // single-entry items: 0, 1, 2, 3, 4
4883 // right pane:
4884 // single-entry items: 1
4885 // multi-entry items: (3, 4)
4886 let left_pane = workspace.update(cx, |workspace, cx| {
4887 let left_pane = workspace.active_pane().clone();
4888 workspace.add_item(Box::new(item_2_3.clone()), cx);
4889 for item in single_entry_items {
4890 workspace.add_item(Box::new(item), cx);
4891 }
4892 left_pane.update(cx, |pane, cx| {
4893 pane.activate_item(2, true, true, cx);
4894 });
4895
4896 let right_pane = workspace
4897 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
4898 .unwrap();
4899
4900 right_pane.update(cx, |pane, cx| {
4901 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
4902 });
4903
4904 left_pane
4905 });
4906
4907 cx.focus_view(&left_pane);
4908
4909 // When closing all of the items in the left pane, we should be prompted twice:
4910 // once for project entry 0, and once for project entry 2. Project entries 1,
4911 // 3, and 4 are all still open in the other paten. After those two
4912 // prompts, the task should complete.
4913
4914 let close = left_pane.update(cx, |pane, cx| {
4915 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
4916 });
4917 cx.executor().run_until_parked();
4918
4919 // Discard "Save all" prompt
4920 cx.simulate_prompt_answer(2);
4921
4922 cx.executor().run_until_parked();
4923 left_pane.update(cx, |pane, cx| {
4924 assert_eq!(
4925 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4926 &[ProjectEntryId::from_proto(0)]
4927 );
4928 });
4929 cx.simulate_prompt_answer(0);
4930
4931 cx.executor().run_until_parked();
4932 left_pane.update(cx, |pane, cx| {
4933 assert_eq!(
4934 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
4935 &[ProjectEntryId::from_proto(2)]
4936 );
4937 });
4938 cx.simulate_prompt_answer(0);
4939
4940 cx.executor().run_until_parked();
4941 close.await.unwrap();
4942 left_pane.update(cx, |pane, _| {
4943 assert_eq!(pane.items_len(), 0);
4944 });
4945 }
4946
4947 #[gpui::test]
4948 async fn test_autosave(cx: &mut gpui::TestAppContext) {
4949 init_test(cx);
4950
4951 let fs = FakeFs::new(cx.executor());
4952 let project = Project::test(fs, [], cx).await;
4953 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
4954 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
4955
4956 let item = cx.new_view(|cx| {
4957 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
4958 });
4959 let item_id = item.entity_id();
4960 workspace.update(cx, |workspace, cx| {
4961 workspace.add_item(Box::new(item.clone()), cx);
4962 });
4963
4964 // Autosave on window change.
4965 item.update(cx, |item, cx| {
4966 cx.update_global(|settings: &mut SettingsStore, cx| {
4967 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4968 settings.autosave = Some(AutosaveSetting::OnWindowChange);
4969 })
4970 });
4971 item.is_dirty = true;
4972 });
4973
4974 // Deactivating the window saves the file.
4975 cx.deactivate_window();
4976 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
4977
4978 // Autosave on focus change.
4979 item.update(cx, |item, cx| {
4980 cx.focus_self();
4981 cx.update_global(|settings: &mut SettingsStore, cx| {
4982 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
4983 settings.autosave = Some(AutosaveSetting::OnFocusChange);
4984 })
4985 });
4986 item.is_dirty = true;
4987 });
4988
4989 // Blurring the item saves the file.
4990 item.update(cx, |_, cx| cx.blur());
4991 cx.executor().run_until_parked();
4992 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
4993
4994 // Deactivating the window still saves the file.
4995 cx.update(|cx| cx.activate_window());
4996 item.update(cx, |item, cx| {
4997 cx.focus_self();
4998 item.is_dirty = true;
4999 });
5000 cx.deactivate_window();
5001
5002 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
5003
5004 // Autosave after delay.
5005 item.update(cx, |item, cx| {
5006 cx.update_global(|settings: &mut SettingsStore, cx| {
5007 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5008 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
5009 })
5010 });
5011 item.is_dirty = true;
5012 cx.emit(ItemEvent::Edit);
5013 });
5014
5015 // Delay hasn't fully expired, so the file is still dirty and unsaved.
5016 cx.executor().advance_clock(Duration::from_millis(250));
5017 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
5018
5019 // After delay expires, the file is saved.
5020 cx.executor().advance_clock(Duration::from_millis(250));
5021 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
5022
5023 // Autosave on focus change, ensuring closing the tab counts as such.
5024 item.update(cx, |item, cx| {
5025 cx.update_global(|settings: &mut SettingsStore, cx| {
5026 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5027 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5028 })
5029 });
5030 item.is_dirty = true;
5031 });
5032
5033 pane.update(cx, |pane, cx| {
5034 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5035 })
5036 .await
5037 .unwrap();
5038 assert!(!cx.has_pending_prompt());
5039 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5040
5041 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
5042 workspace.update(cx, |workspace, cx| {
5043 workspace.add_item(Box::new(item.clone()), cx);
5044 });
5045 item.update(cx, |item, cx| {
5046 item.project_items[0].update(cx, |item, _| {
5047 item.entry_id = None;
5048 });
5049 item.is_dirty = true;
5050 cx.blur();
5051 });
5052 cx.run_until_parked();
5053 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5054
5055 // Ensure autosave is prevented for deleted files also when closing the buffer.
5056 let _close_items = pane.update(cx, |pane, cx| {
5057 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5058 });
5059 cx.run_until_parked();
5060 assert!(cx.has_pending_prompt());
5061 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5062 }
5063
5064 #[gpui::test]
5065 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
5066 init_test(cx);
5067
5068 let fs = FakeFs::new(cx.executor());
5069
5070 let project = Project::test(fs, [], cx).await;
5071 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5072
5073 let item = cx.new_view(|cx| {
5074 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5075 });
5076 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5077 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
5078 let toolbar_notify_count = Rc::new(RefCell::new(0));
5079
5080 workspace.update(cx, |workspace, cx| {
5081 workspace.add_item(Box::new(item.clone()), cx);
5082 let toolbar_notification_count = toolbar_notify_count.clone();
5083 cx.observe(&toolbar, move |_, _, _| {
5084 *toolbar_notification_count.borrow_mut() += 1
5085 })
5086 .detach();
5087 });
5088
5089 pane.update(cx, |pane, _| {
5090 assert!(!pane.can_navigate_backward());
5091 assert!(!pane.can_navigate_forward());
5092 });
5093
5094 item.update(cx, |item, cx| {
5095 item.set_state("one".to_string(), cx);
5096 });
5097
5098 // Toolbar must be notified to re-render the navigation buttons
5099 assert_eq!(*toolbar_notify_count.borrow(), 1);
5100
5101 pane.update(cx, |pane, _| {
5102 assert!(pane.can_navigate_backward());
5103 assert!(!pane.can_navigate_forward());
5104 });
5105
5106 workspace
5107 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
5108 .await
5109 .unwrap();
5110
5111 assert_eq!(*toolbar_notify_count.borrow(), 2);
5112 pane.update(cx, |pane, _| {
5113 assert!(!pane.can_navigate_backward());
5114 assert!(pane.can_navigate_forward());
5115 });
5116 }
5117
5118 #[gpui::test]
5119 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
5120 init_test(cx);
5121 let fs = FakeFs::new(cx.executor());
5122
5123 let project = Project::test(fs, [], cx).await;
5124 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5125
5126 let panel = workspace.update(cx, |workspace, cx| {
5127 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5128 workspace.add_panel(panel.clone(), cx);
5129
5130 workspace
5131 .right_dock()
5132 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5133
5134 panel
5135 });
5136
5137 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5138 pane.update(cx, |pane, cx| {
5139 let item = cx.new_view(|cx| TestItem::new(cx));
5140 pane.add_item(Box::new(item), true, true, None, cx);
5141 });
5142
5143 // Transfer focus from center to panel
5144 workspace.update(cx, |workspace, cx| {
5145 workspace.toggle_panel_focus::<TestPanel>(cx);
5146 });
5147
5148 workspace.update(cx, |workspace, cx| {
5149 assert!(workspace.right_dock().read(cx).is_open());
5150 assert!(!panel.is_zoomed(cx));
5151 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5152 });
5153
5154 // Transfer focus from panel to center
5155 workspace.update(cx, |workspace, cx| {
5156 workspace.toggle_panel_focus::<TestPanel>(cx);
5157 });
5158
5159 workspace.update(cx, |workspace, cx| {
5160 assert!(workspace.right_dock().read(cx).is_open());
5161 assert!(!panel.is_zoomed(cx));
5162 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5163 });
5164
5165 // Close the dock
5166 workspace.update(cx, |workspace, cx| {
5167 workspace.toggle_dock(DockPosition::Right, cx);
5168 });
5169
5170 workspace.update(cx, |workspace, cx| {
5171 assert!(!workspace.right_dock().read(cx).is_open());
5172 assert!(!panel.is_zoomed(cx));
5173 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5174 });
5175
5176 // Open the dock
5177 workspace.update(cx, |workspace, cx| {
5178 workspace.toggle_dock(DockPosition::Right, cx);
5179 });
5180
5181 workspace.update(cx, |workspace, cx| {
5182 assert!(workspace.right_dock().read(cx).is_open());
5183 assert!(!panel.is_zoomed(cx));
5184 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5185 });
5186
5187 // Focus and zoom panel
5188 panel.update(cx, |panel, cx| {
5189 cx.focus_self();
5190 panel.set_zoomed(true, cx)
5191 });
5192
5193 workspace.update(cx, |workspace, cx| {
5194 assert!(workspace.right_dock().read(cx).is_open());
5195 assert!(panel.is_zoomed(cx));
5196 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5197 });
5198
5199 // Transfer focus to the center closes the dock
5200 workspace.update(cx, |workspace, cx| {
5201 workspace.toggle_panel_focus::<TestPanel>(cx);
5202 });
5203
5204 workspace.update(cx, |workspace, cx| {
5205 assert!(!workspace.right_dock().read(cx).is_open());
5206 assert!(panel.is_zoomed(cx));
5207 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5208 });
5209
5210 // Transferring focus back to the panel keeps it zoomed
5211 workspace.update(cx, |workspace, cx| {
5212 workspace.toggle_panel_focus::<TestPanel>(cx);
5213 });
5214
5215 workspace.update(cx, |workspace, cx| {
5216 assert!(workspace.right_dock().read(cx).is_open());
5217 assert!(panel.is_zoomed(cx));
5218 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5219 });
5220
5221 // Close the dock while it is zoomed
5222 workspace.update(cx, |workspace, cx| {
5223 workspace.toggle_dock(DockPosition::Right, cx)
5224 });
5225
5226 workspace.update(cx, |workspace, cx| {
5227 assert!(!workspace.right_dock().read(cx).is_open());
5228 assert!(panel.is_zoomed(cx));
5229 assert!(workspace.zoomed.is_none());
5230 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5231 });
5232
5233 // Opening the dock, when it's zoomed, retains focus
5234 workspace.update(cx, |workspace, cx| {
5235 workspace.toggle_dock(DockPosition::Right, cx)
5236 });
5237
5238 workspace.update(cx, |workspace, cx| {
5239 assert!(workspace.right_dock().read(cx).is_open());
5240 assert!(panel.is_zoomed(cx));
5241 assert!(workspace.zoomed.is_some());
5242 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5243 });
5244
5245 // Unzoom and close the panel, zoom the active pane.
5246 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5247 workspace.update(cx, |workspace, cx| {
5248 workspace.toggle_dock(DockPosition::Right, cx)
5249 });
5250 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5251
5252 // Opening a dock unzooms the pane.
5253 workspace.update(cx, |workspace, cx| {
5254 workspace.toggle_dock(DockPosition::Right, cx)
5255 });
5256 workspace.update(cx, |workspace, cx| {
5257 let pane = pane.read(cx);
5258 assert!(!pane.is_zoomed());
5259 assert!(!pane.focus_handle(cx).is_focused(cx));
5260 assert!(workspace.right_dock().read(cx).is_open());
5261 assert!(workspace.zoomed.is_none());
5262 });
5263 }
5264
5265 struct TestModal(FocusHandle);
5266
5267 impl TestModal {
5268 fn new(cx: &mut ViewContext<Self>) -> Self {
5269 Self(cx.focus_handle())
5270 }
5271 }
5272
5273 impl EventEmitter<DismissEvent> for TestModal {}
5274
5275 impl FocusableView for TestModal {
5276 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
5277 self.0.clone()
5278 }
5279 }
5280
5281 impl ModalView for TestModal {}
5282
5283 impl Render for TestModal {
5284 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
5285 div().track_focus(&self.0)
5286 }
5287 }
5288
5289 #[gpui::test]
5290 async fn test_panels(cx: &mut gpui::TestAppContext) {
5291 init_test(cx);
5292 let fs = FakeFs::new(cx.executor());
5293
5294 let project = Project::test(fs, [], cx).await;
5295 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5296
5297 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5298 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
5299 workspace.add_panel(panel_1.clone(), cx);
5300 workspace
5301 .left_dock()
5302 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5303 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5304 workspace.add_panel(panel_2.clone(), cx);
5305 workspace
5306 .right_dock()
5307 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5308
5309 let left_dock = workspace.left_dock();
5310 assert_eq!(
5311 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5312 panel_1.panel_id()
5313 );
5314 assert_eq!(
5315 left_dock.read(cx).active_panel_size(cx).unwrap(),
5316 panel_1.size(cx)
5317 );
5318
5319 left_dock.update(cx, |left_dock, cx| {
5320 left_dock.resize_active_panel(Some(px(1337.)), cx)
5321 });
5322 assert_eq!(
5323 workspace
5324 .right_dock()
5325 .read(cx)
5326 .visible_panel()
5327 .unwrap()
5328 .panel_id(),
5329 panel_2.panel_id(),
5330 );
5331
5332 (panel_1, panel_2)
5333 });
5334
5335 // Move panel_1 to the right
5336 panel_1.update(cx, |panel_1, cx| {
5337 panel_1.set_position(DockPosition::Right, cx)
5338 });
5339
5340 workspace.update(cx, |workspace, cx| {
5341 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5342 // Since it was the only panel on the left, the left dock should now be closed.
5343 assert!(!workspace.left_dock().read(cx).is_open());
5344 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5345 let right_dock = workspace.right_dock();
5346 assert_eq!(
5347 right_dock.read(cx).visible_panel().unwrap().panel_id(),
5348 panel_1.panel_id()
5349 );
5350 assert_eq!(
5351 right_dock.read(cx).active_panel_size(cx).unwrap(),
5352 px(1337.)
5353 );
5354
5355 // Now we move panel_2 to the left
5356 panel_2.set_position(DockPosition::Left, cx);
5357 });
5358
5359 workspace.update(cx, |workspace, cx| {
5360 // Since panel_2 was not visible on the right, we don't open the left dock.
5361 assert!(!workspace.left_dock().read(cx).is_open());
5362 // And the right dock is unaffected in it's displaying of panel_1
5363 assert!(workspace.right_dock().read(cx).is_open());
5364 assert_eq!(
5365 workspace
5366 .right_dock()
5367 .read(cx)
5368 .visible_panel()
5369 .unwrap()
5370 .panel_id(),
5371 panel_1.panel_id(),
5372 );
5373 });
5374
5375 // Move panel_1 back to the left
5376 panel_1.update(cx, |panel_1, cx| {
5377 panel_1.set_position(DockPosition::Left, cx)
5378 });
5379
5380 workspace.update(cx, |workspace, cx| {
5381 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5382 let left_dock = workspace.left_dock();
5383 assert!(left_dock.read(cx).is_open());
5384 assert_eq!(
5385 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5386 panel_1.panel_id()
5387 );
5388 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
5389 // And the right dock should be closed as it no longer has any panels.
5390 assert!(!workspace.right_dock().read(cx).is_open());
5391
5392 // Now we move panel_1 to the bottom
5393 panel_1.set_position(DockPosition::Bottom, cx);
5394 });
5395
5396 workspace.update(cx, |workspace, cx| {
5397 // Since panel_1 was visible on the left, we close the left dock.
5398 assert!(!workspace.left_dock().read(cx).is_open());
5399 // The bottom dock is sized based on the panel's default size,
5400 // since the panel orientation changed from vertical to horizontal.
5401 let bottom_dock = workspace.bottom_dock();
5402 assert_eq!(
5403 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5404 panel_1.size(cx),
5405 );
5406 // Close bottom dock and move panel_1 back to the left.
5407 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5408 panel_1.set_position(DockPosition::Left, cx);
5409 });
5410
5411 // Emit activated event on panel 1
5412 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
5413
5414 // Now the left dock is open and panel_1 is active and focused.
5415 workspace.update(cx, |workspace, cx| {
5416 let left_dock = workspace.left_dock();
5417 assert!(left_dock.read(cx).is_open());
5418 assert_eq!(
5419 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5420 panel_1.panel_id(),
5421 );
5422 assert!(panel_1.focus_handle(cx).is_focused(cx));
5423 });
5424
5425 // Emit closed event on panel 2, which is not active
5426 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5427
5428 // Wo don't close the left dock, because panel_2 wasn't the active panel
5429 workspace.update(cx, |workspace, cx| {
5430 let left_dock = workspace.left_dock();
5431 assert!(left_dock.read(cx).is_open());
5432 assert_eq!(
5433 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5434 panel_1.panel_id(),
5435 );
5436 });
5437
5438 // Emitting a ZoomIn event shows the panel as zoomed.
5439 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
5440 workspace.update(cx, |workspace, _| {
5441 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5442 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5443 });
5444
5445 // Move panel to another dock while it is zoomed
5446 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5447 workspace.update(cx, |workspace, _| {
5448 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5449
5450 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5451 });
5452
5453 // This is a helper for getting a:
5454 // - valid focus on an element,
5455 // - that isn't a part of the panes and panels system of the Workspace,
5456 // - and doesn't trigger the 'on_focus_lost' API.
5457 let focus_other_view = {
5458 let workspace = workspace.clone();
5459 move |cx: &mut VisualTestContext| {
5460 workspace.update(cx, |workspace, cx| {
5461 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
5462 workspace.toggle_modal(cx, TestModal::new);
5463 workspace.toggle_modal(cx, TestModal::new);
5464 } else {
5465 workspace.toggle_modal(cx, TestModal::new);
5466 }
5467 })
5468 }
5469 };
5470
5471 // If focus is transferred to another view that's not a panel or another pane, we still show
5472 // the panel as zoomed.
5473 focus_other_view(cx);
5474 workspace.update(cx, |workspace, _| {
5475 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5476 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5477 });
5478
5479 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5480 workspace.update(cx, |_, cx| cx.focus_self());
5481 workspace.update(cx, |workspace, _| {
5482 assert_eq!(workspace.zoomed, None);
5483 assert_eq!(workspace.zoomed_position, None);
5484 });
5485
5486 // If focus is transferred again to another view that's not a panel or a pane, we won't
5487 // show the panel as zoomed because it wasn't zoomed before.
5488 focus_other_view(cx);
5489 workspace.update(cx, |workspace, _| {
5490 assert_eq!(workspace.zoomed, None);
5491 assert_eq!(workspace.zoomed_position, None);
5492 });
5493
5494 // When the panel is activated, it is zoomed again.
5495 cx.dispatch_action(ToggleRightDock);
5496 workspace.update(cx, |workspace, _| {
5497 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5498 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5499 });
5500
5501 // Emitting a ZoomOut event unzooms the panel.
5502 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
5503 workspace.update(cx, |workspace, _| {
5504 assert_eq!(workspace.zoomed, None);
5505 assert_eq!(workspace.zoomed_position, None);
5506 });
5507
5508 // Emit closed event on panel 1, which is active
5509 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5510
5511 // Now the left dock is closed, because panel_1 was the active panel
5512 workspace.update(cx, |workspace, cx| {
5513 let right_dock = workspace.right_dock();
5514 assert!(!right_dock.read(cx).is_open());
5515 });
5516 }
5517
5518 pub fn init_test(cx: &mut TestAppContext) {
5519 cx.update(|cx| {
5520 let settings_store = SettingsStore::test(cx);
5521 cx.set_global(settings_store);
5522 theme::init(theme::LoadThemes::JustBase, cx);
5523 language::init(cx);
5524 crate::init_settings(cx);
5525 Project::init_settings(cx);
5526 });
5527 }
5528}