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