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