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