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, WorkspaceLocation},
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::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 location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
3396 let project = self.project().read(cx);
3397
3398 if project.is_local() {
3399 Some(
3400 project
3401 .visible_worktrees(cx)
3402 .map(|worktree| worktree.read(cx).abs_path())
3403 .collect::<Vec<_>>()
3404 .into(),
3405 )
3406 } else {
3407 None
3408 }
3409 }
3410
3411 fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
3412 match member {
3413 Member::Axis(PaneAxis { members, .. }) => {
3414 for child in members.iter() {
3415 self.remove_panes(child.clone(), cx)
3416 }
3417 }
3418 Member::Pane(pane) => {
3419 self.force_remove_pane(&pane, cx);
3420 }
3421 }
3422 }
3423
3424 fn force_remove_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
3425 self.panes.retain(|p| p != pane);
3426 self.panes
3427 .last()
3428 .unwrap()
3429 .update(cx, |pane, cx| pane.focus(cx));
3430 if self.last_active_center_pane == Some(pane.downgrade()) {
3431 self.last_active_center_pane = None;
3432 }
3433 cx.notify();
3434 }
3435
3436 fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
3437 self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
3438 cx.background_executor()
3439 .timer(Duration::from_millis(100))
3440 .await;
3441 this.update(&mut cx, |this, cx| this.serialize_workspace(cx).detach())
3442 .log_err();
3443 }));
3444 }
3445
3446 fn serialize_workspace(&self, cx: &mut WindowContext) -> Task<()> {
3447 fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
3448 let (items, active) = {
3449 let pane = pane_handle.read(cx);
3450 let active_item_id = pane.active_item().map(|item| item.item_id());
3451 (
3452 pane.items()
3453 .filter_map(|item_handle| {
3454 Some(SerializedItem {
3455 kind: Arc::from(item_handle.serialized_item_kind()?),
3456 item_id: item_handle.item_id().as_u64(),
3457 active: Some(item_handle.item_id()) == active_item_id,
3458 preview: pane.is_active_preview_item(item_handle.item_id()),
3459 })
3460 })
3461 .collect::<Vec<_>>(),
3462 pane.has_focus(cx),
3463 )
3464 };
3465
3466 SerializedPane::new(items, active)
3467 }
3468
3469 fn build_serialized_pane_group(
3470 pane_group: &Member,
3471 cx: &WindowContext,
3472 ) -> SerializedPaneGroup {
3473 match pane_group {
3474 Member::Axis(PaneAxis {
3475 axis,
3476 members,
3477 flexes,
3478 bounding_boxes: _,
3479 }) => SerializedPaneGroup::Group {
3480 axis: SerializedAxis(*axis),
3481 children: members
3482 .iter()
3483 .map(|member| build_serialized_pane_group(member, cx))
3484 .collect::<Vec<_>>(),
3485 flexes: Some(flexes.lock().clone()),
3486 },
3487 Member::Pane(pane_handle) => {
3488 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
3489 }
3490 }
3491 }
3492
3493 fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
3494 let left_dock = this.left_dock.read(cx);
3495 let left_visible = left_dock.is_open();
3496 let left_active_panel = left_dock
3497 .visible_panel()
3498 .map(|panel| panel.persistent_name().to_string());
3499 let left_dock_zoom = left_dock
3500 .visible_panel()
3501 .map(|panel| panel.is_zoomed(cx))
3502 .unwrap_or(false);
3503
3504 let right_dock = this.right_dock.read(cx);
3505 let right_visible = right_dock.is_open();
3506 let right_active_panel = right_dock
3507 .visible_panel()
3508 .map(|panel| panel.persistent_name().to_string());
3509 let right_dock_zoom = right_dock
3510 .visible_panel()
3511 .map(|panel| panel.is_zoomed(cx))
3512 .unwrap_or(false);
3513
3514 let bottom_dock = this.bottom_dock.read(cx);
3515 let bottom_visible = bottom_dock.is_open();
3516 let bottom_active_panel = bottom_dock
3517 .visible_panel()
3518 .map(|panel| panel.persistent_name().to_string());
3519 let bottom_dock_zoom = bottom_dock
3520 .visible_panel()
3521 .map(|panel| panel.is_zoomed(cx))
3522 .unwrap_or(false);
3523
3524 DockStructure {
3525 left: DockData {
3526 visible: left_visible,
3527 active_panel: left_active_panel,
3528 zoom: left_dock_zoom,
3529 },
3530 right: DockData {
3531 visible: right_visible,
3532 active_panel: right_active_panel,
3533 zoom: right_dock_zoom,
3534 },
3535 bottom: DockData {
3536 visible: bottom_visible,
3537 active_panel: bottom_active_panel,
3538 zoom: bottom_dock_zoom,
3539 },
3540 }
3541 }
3542
3543 if let Some(location) = self.location(cx) {
3544 // Load bearing special case:
3545 // - with_local_workspace() relies on this to not have other stuff open
3546 // when you open your log
3547 if !location.paths().is_empty() {
3548 let center_group = build_serialized_pane_group(&self.center.root, cx);
3549 let docks = build_serialized_docks(self, cx);
3550 let serialized_workspace = SerializedWorkspace {
3551 id: self.database_id,
3552 location,
3553 center_group,
3554 bounds: Default::default(),
3555 display: Default::default(),
3556 docks,
3557 fullscreen: cx.is_fullscreen(),
3558 centered_layout: self.centered_layout,
3559 };
3560 return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
3561 }
3562 }
3563 Task::ready(())
3564 }
3565
3566 pub(crate) fn load_workspace(
3567 serialized_workspace: SerializedWorkspace,
3568 paths_to_open: Vec<Option<ProjectPath>>,
3569 cx: &mut ViewContext<Workspace>,
3570 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
3571 cx.spawn(|workspace, mut cx| async move {
3572 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
3573
3574 let mut center_group = None;
3575 let mut center_items = None;
3576
3577 // Traverse the splits tree and add to things
3578 if let Some((group, active_pane, items)) = serialized_workspace
3579 .center_group
3580 .deserialize(
3581 &project,
3582 serialized_workspace.id,
3583 workspace.clone(),
3584 &mut cx,
3585 )
3586 .await
3587 {
3588 center_items = Some(items);
3589 center_group = Some((group, active_pane))
3590 }
3591
3592 let mut items_by_project_path = cx.update(|cx| {
3593 center_items
3594 .unwrap_or_default()
3595 .into_iter()
3596 .filter_map(|item| {
3597 let item = item?;
3598 let project_path = item.project_path(cx)?;
3599 Some((project_path, item))
3600 })
3601 .collect::<HashMap<_, _>>()
3602 })?;
3603
3604 let opened_items = paths_to_open
3605 .into_iter()
3606 .map(|path_to_open| {
3607 path_to_open
3608 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
3609 })
3610 .collect::<Vec<_>>();
3611
3612 // Remove old panes from workspace panes list
3613 workspace.update(&mut cx, |workspace, cx| {
3614 if let Some((center_group, active_pane)) = center_group {
3615 workspace.remove_panes(workspace.center.root.clone(), cx);
3616
3617 // Swap workspace center group
3618 workspace.center = PaneGroup::with_root(center_group);
3619 workspace.last_active_center_pane = active_pane.as_ref().map(|p| p.downgrade());
3620 if let Some(active_pane) = active_pane {
3621 workspace.active_pane = active_pane;
3622 cx.focus_self();
3623 } else {
3624 workspace.active_pane = workspace.center.first_pane().clone();
3625 }
3626 }
3627
3628 let docks = serialized_workspace.docks;
3629
3630 let right = docks.right.clone();
3631 workspace
3632 .right_dock
3633 .update(cx, |dock, _| dock.serialized_dock = Some(right));
3634 let left = docks.left.clone();
3635 workspace
3636 .left_dock
3637 .update(cx, |dock, _| dock.serialized_dock = Some(left));
3638 let bottom = docks.bottom.clone();
3639 workspace
3640 .bottom_dock
3641 .update(cx, |dock, _| dock.serialized_dock = Some(bottom));
3642
3643 cx.notify();
3644 })?;
3645
3646 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
3647 workspace.update(&mut cx, |workspace, cx| {
3648 workspace.serialize_workspace(cx).detach()
3649 })?;
3650
3651 Ok(opened_items)
3652 })
3653 }
3654
3655 fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3656 self.add_workspace_actions_listeners(div, cx)
3657 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3658 .on_action(cx.listener(Self::close_all_items_and_panes))
3659 .on_action(cx.listener(Self::save_all))
3660 .on_action(cx.listener(Self::send_keystrokes))
3661 .on_action(cx.listener(Self::add_folder_to_project))
3662 .on_action(cx.listener(Self::follow_next_collaborator))
3663 .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
3664 let pane = workspace.active_pane().clone();
3665 workspace.unfollow(&pane, cx);
3666 }))
3667 .on_action(cx.listener(|workspace, action: &Save, cx| {
3668 workspace
3669 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
3670 .detach_and_log_err(cx);
3671 }))
3672 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, cx| {
3673 workspace
3674 .save_active_item(SaveIntent::SaveWithoutFormat, cx)
3675 .detach_and_log_err(cx);
3676 }))
3677 .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
3678 workspace
3679 .save_active_item(SaveIntent::SaveAs, cx)
3680 .detach_and_log_err(cx);
3681 }))
3682 .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
3683 workspace.activate_previous_pane(cx)
3684 }))
3685 .on_action(
3686 cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
3687 )
3688 .on_action(
3689 cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
3690 workspace.activate_pane_in_direction(action.0, cx)
3691 }),
3692 )
3693 .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
3694 workspace.swap_pane_in_direction(action.0, cx)
3695 }))
3696 .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
3697 this.toggle_dock(DockPosition::Left, cx);
3698 }))
3699 .on_action(
3700 cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
3701 workspace.toggle_dock(DockPosition::Right, cx);
3702 }),
3703 )
3704 .on_action(
3705 cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
3706 workspace.toggle_dock(DockPosition::Bottom, cx);
3707 }),
3708 )
3709 .on_action(
3710 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
3711 workspace.close_all_docks(cx);
3712 }),
3713 )
3714 .on_action(cx.listener(Workspace::open))
3715 .on_action(cx.listener(Workspace::close_window))
3716 .on_action(cx.listener(Workspace::activate_pane_at_index))
3717 .on_action(
3718 cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
3719 workspace.reopen_closed_item(cx).detach();
3720 }),
3721 )
3722 .on_action(cx.listener(Workspace::toggle_centered_layout))
3723 }
3724
3725 #[cfg(any(test, feature = "test-support"))]
3726 pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
3727 use node_runtime::FakeNodeRuntime;
3728
3729 let client = project.read(cx).client();
3730 let user_store = project.read(cx).user_store();
3731
3732 let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
3733 cx.activate_window();
3734 let app_state = Arc::new(AppState {
3735 languages: project.read(cx).languages().clone(),
3736 workspace_store,
3737 client,
3738 user_store,
3739 fs: project.read(cx).fs().clone(),
3740 build_window_options: |_, _| Default::default(),
3741 node_runtime: FakeNodeRuntime::new(),
3742 });
3743 let workspace = Self::new(Default::default(), project, app_state, cx);
3744 workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
3745 workspace
3746 }
3747
3748 pub fn register_action<A: Action>(
3749 &mut self,
3750 callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
3751 ) -> &mut Self {
3752 let callback = Arc::new(callback);
3753
3754 self.workspace_actions.push(Box::new(move |div, cx| {
3755 let callback = callback.clone();
3756 div.on_action(
3757 cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
3758 )
3759 }));
3760 self
3761 }
3762
3763 fn add_workspace_actions_listeners(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
3764 let mut div = div
3765 .on_action(cx.listener(Self::close_inactive_items_and_panes))
3766 .on_action(cx.listener(Self::close_all_items_and_panes))
3767 .on_action(cx.listener(Self::add_folder_to_project))
3768 .on_action(cx.listener(Self::save_all))
3769 .on_action(cx.listener(Self::open));
3770 for action in self.workspace_actions.iter() {
3771 div = (action)(div, cx)
3772 }
3773 div
3774 }
3775
3776 pub fn has_active_modal(&self, cx: &WindowContext<'_>) -> bool {
3777 self.modal_layer.read(cx).has_active_modal()
3778 }
3779
3780 pub fn active_modal<V: ManagedView + 'static>(&mut self, cx: &AppContext) -> Option<View<V>> {
3781 self.modal_layer.read(cx).active_modal()
3782 }
3783
3784 pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut WindowContext, build: B)
3785 where
3786 B: FnOnce(&mut ViewContext<V>) -> V,
3787 {
3788 self.modal_layer
3789 .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
3790 }
3791
3792 pub fn toggle_centered_layout(&mut self, _: &ToggleCenteredLayout, cx: &mut ViewContext<Self>) {
3793 self.centered_layout = !self.centered_layout;
3794 cx.background_executor()
3795 .spawn(DB.set_centered_layout(self.database_id, self.centered_layout))
3796 .detach_and_log_err(cx);
3797 cx.notify();
3798 }
3799
3800 fn adjust_padding(padding: Option<f32>) -> f32 {
3801 padding
3802 .unwrap_or(Self::DEFAULT_PADDING)
3803 .min(Self::MAX_PADDING)
3804 .max(0.0)
3805 }
3806}
3807
3808fn window_bounds_env_override() -> Option<Bounds<DevicePixels>> {
3809 ZED_WINDOW_POSITION
3810 .zip(*ZED_WINDOW_SIZE)
3811 .map(|(position, size)| Bounds {
3812 origin: position,
3813 size,
3814 })
3815}
3816
3817fn open_items(
3818 serialized_workspace: Option<SerializedWorkspace>,
3819 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
3820 app_state: Arc<AppState>,
3821 cx: &mut ViewContext<Workspace>,
3822) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
3823 let restored_items = serialized_workspace.map(|serialized_workspace| {
3824 Workspace::load_workspace(
3825 serialized_workspace,
3826 project_paths_to_open
3827 .iter()
3828 .map(|(_, project_path)| project_path)
3829 .cloned()
3830 .collect(),
3831 cx,
3832 )
3833 });
3834
3835 cx.spawn(|workspace, mut cx| async move {
3836 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
3837
3838 if let Some(restored_items) = restored_items {
3839 let restored_items = restored_items.await?;
3840
3841 let restored_project_paths = restored_items
3842 .iter()
3843 .filter_map(|item| {
3844 cx.update(|cx| item.as_ref()?.project_path(cx))
3845 .ok()
3846 .flatten()
3847 })
3848 .collect::<HashSet<_>>();
3849
3850 for restored_item in restored_items {
3851 opened_items.push(restored_item.map(Ok));
3852 }
3853
3854 project_paths_to_open
3855 .iter_mut()
3856 .for_each(|(_, project_path)| {
3857 if let Some(project_path_to_open) = project_path {
3858 if restored_project_paths.contains(project_path_to_open) {
3859 *project_path = None;
3860 }
3861 }
3862 });
3863 } else {
3864 for _ in 0..project_paths_to_open.len() {
3865 opened_items.push(None);
3866 }
3867 }
3868 assert!(opened_items.len() == project_paths_to_open.len());
3869
3870 let tasks =
3871 project_paths_to_open
3872 .into_iter()
3873 .enumerate()
3874 .map(|(ix, (abs_path, project_path))| {
3875 let workspace = workspace.clone();
3876 cx.spawn(|mut cx| {
3877 let fs = app_state.fs.clone();
3878 async move {
3879 let file_project_path = project_path?;
3880 if fs.is_dir(&abs_path).await {
3881 None
3882 } else {
3883 Some((
3884 ix,
3885 workspace
3886 .update(&mut cx, |workspace, cx| {
3887 workspace.open_path(file_project_path, None, true, cx)
3888 })
3889 .log_err()?
3890 .await,
3891 ))
3892 }
3893 }
3894 })
3895 });
3896
3897 let tasks = tasks.collect::<Vec<_>>();
3898
3899 let tasks = futures::future::join_all(tasks);
3900 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
3901 opened_items[ix] = Some(path_open_result);
3902 }
3903
3904 Ok(opened_items)
3905 })
3906}
3907
3908enum ActivateInDirectionTarget {
3909 Pane(View<Pane>),
3910 Dock(View<Dock>),
3911}
3912
3913fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
3914 const REPORT_ISSUE_URL: &str = "https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
3915
3916 workspace
3917 .update(cx, |workspace, cx| {
3918 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
3919 struct DatabaseFailedNotification;
3920
3921 workspace.show_notification_once(
3922 NotificationId::unique::<DatabaseFailedNotification>(),
3923 cx,
3924 |cx| {
3925 cx.new_view(|_| {
3926 MessageNotification::new("Failed to load the database file.")
3927 .with_click_message("Click to let us know about this error")
3928 .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
3929 })
3930 },
3931 );
3932 }
3933 })
3934 .log_err();
3935}
3936
3937impl FocusableView for Workspace {
3938 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3939 self.active_pane.focus_handle(cx)
3940 }
3941}
3942
3943#[derive(Clone, Render)]
3944struct DraggedDock(DockPosition);
3945
3946impl Render for Workspace {
3947 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3948 let mut context = KeyContext::new_with_defaults();
3949 context.add("Workspace");
3950 let centered_layout = self.centered_layout
3951 && self.center.panes().len() == 1
3952 && self.active_item(cx).is_some();
3953 let render_padding = |size| {
3954 (size > 0.0).then(|| {
3955 div()
3956 .h_full()
3957 .w(relative(size))
3958 .bg(cx.theme().colors().editor_background)
3959 .border_color(cx.theme().colors().pane_group_border)
3960 })
3961 };
3962 let paddings = if centered_layout {
3963 let settings = WorkspaceSettings::get_global(cx).centered_layout;
3964 (
3965 render_padding(Self::adjust_padding(settings.left_padding)),
3966 render_padding(Self::adjust_padding(settings.right_padding)),
3967 )
3968 } else {
3969 (None, None)
3970 };
3971 let (ui_font, ui_font_size) = {
3972 let theme_settings = ThemeSettings::get_global(cx);
3973 (
3974 theme_settings.ui_font.family.clone(),
3975 theme_settings.ui_font_size,
3976 )
3977 };
3978
3979 let theme = cx.theme().clone();
3980 let colors = theme.colors();
3981 cx.set_rem_size(ui_font_size);
3982
3983 self.actions(div(), cx)
3984 .key_context(context)
3985 .relative()
3986 .size_full()
3987 .flex()
3988 .flex_col()
3989 .font(ui_font)
3990 .gap_0()
3991 .justify_start()
3992 .items_start()
3993 .text_color(colors.text)
3994 .bg(colors.background)
3995 .children(self.titlebar_item.clone())
3996 .child(
3997 div()
3998 .id("workspace")
3999 .relative()
4000 .flex_1()
4001 .w_full()
4002 .flex()
4003 .flex_col()
4004 .overflow_hidden()
4005 .border_t()
4006 .border_b()
4007 .border_color(colors.border)
4008 .child({
4009 let this = cx.view().clone();
4010 canvas(
4011 move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds),
4012 |_, _, _| {},
4013 )
4014 .absolute()
4015 .size_full()
4016 })
4017 .when(self.zoomed.is_none(), |this| {
4018 this.on_drag_move(cx.listener(
4019 |workspace, e: &DragMoveEvent<DraggedDock>, cx| match e.drag(cx).0 {
4020 DockPosition::Left => {
4021 let size = workspace.bounds.left() + e.event.position.x;
4022 workspace.left_dock.update(cx, |left_dock, cx| {
4023 left_dock.resize_active_panel(Some(size), cx);
4024 });
4025 }
4026 DockPosition::Right => {
4027 let size = workspace.bounds.right() - e.event.position.x;
4028 workspace.right_dock.update(cx, |right_dock, cx| {
4029 right_dock.resize_active_panel(Some(size), cx);
4030 });
4031 }
4032 DockPosition::Bottom => {
4033 let size = workspace.bounds.bottom() - e.event.position.y;
4034 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
4035 bottom_dock.resize_active_panel(Some(size), cx);
4036 });
4037 }
4038 },
4039 ))
4040 })
4041 .child(
4042 div()
4043 .flex()
4044 .flex_row()
4045 .h_full()
4046 // Left Dock
4047 .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
4048 || {
4049 div()
4050 .flex()
4051 .flex_none()
4052 .overflow_hidden()
4053 .child(self.left_dock.clone())
4054 },
4055 ))
4056 // Panes
4057 .child(
4058 div()
4059 .flex()
4060 .flex_col()
4061 .flex_1()
4062 .overflow_hidden()
4063 .child(
4064 h_flex()
4065 .flex_1()
4066 .when_some(paddings.0, |this, p| {
4067 this.child(p.border_r_1())
4068 })
4069 .child(self.center.render(
4070 &self.project,
4071 &self.follower_states,
4072 self.active_call(),
4073 &self.active_pane,
4074 self.zoomed.as_ref(),
4075 &self.app_state,
4076 cx,
4077 ))
4078 .when_some(paddings.1, |this, p| {
4079 this.child(p.border_l_1())
4080 }),
4081 )
4082 .children(
4083 self.zoomed_position
4084 .ne(&Some(DockPosition::Bottom))
4085 .then(|| self.bottom_dock.clone()),
4086 ),
4087 )
4088 // Right Dock
4089 .children(self.zoomed_position.ne(&Some(DockPosition::Right)).then(
4090 || {
4091 div()
4092 .flex()
4093 .flex_none()
4094 .overflow_hidden()
4095 .child(self.right_dock.clone())
4096 },
4097 )),
4098 )
4099 .children(self.zoomed.as_ref().and_then(|view| {
4100 let zoomed_view = view.upgrade()?;
4101 let div = div()
4102 .occlude()
4103 .absolute()
4104 .overflow_hidden()
4105 .border_color(colors.border)
4106 .bg(colors.background)
4107 .child(zoomed_view)
4108 .inset_0()
4109 .shadow_lg();
4110
4111 Some(match self.zoomed_position {
4112 Some(DockPosition::Left) => div.right_2().border_r(),
4113 Some(DockPosition::Right) => div.left_2().border_l(),
4114 Some(DockPosition::Bottom) => div.top_2().border_t(),
4115 None => div.top_2().bottom_2().left_2().right_2().border(),
4116 })
4117 }))
4118 .child(self.modal_layer.clone())
4119 .children(self.render_notifications(cx)),
4120 )
4121 .child(self.status_bar.clone())
4122 .children(if self.project.read(cx).is_disconnected() {
4123 Some(DisconnectedOverlay)
4124 } else {
4125 None
4126 })
4127 }
4128}
4129
4130impl WorkspaceStore {
4131 pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
4132 Self {
4133 workspaces: Default::default(),
4134 _subscriptions: vec![
4135 client.add_request_handler(cx.weak_model(), Self::handle_follow),
4136 client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
4137 ],
4138 client,
4139 }
4140 }
4141
4142 pub fn update_followers(
4143 &self,
4144 project_id: Option<u64>,
4145 update: proto::update_followers::Variant,
4146 cx: &AppContext,
4147 ) -> Option<()> {
4148 let active_call = ActiveCall::try_global(cx)?;
4149 let room_id = active_call.read(cx).room()?.read(cx).id();
4150 self.client
4151 .send(proto::UpdateFollowers {
4152 room_id,
4153 project_id,
4154 variant: Some(update),
4155 })
4156 .log_err()
4157 }
4158
4159 pub async fn handle_follow(
4160 this: Model<Self>,
4161 envelope: TypedEnvelope<proto::Follow>,
4162 _: Arc<Client>,
4163 mut cx: AsyncAppContext,
4164 ) -> Result<proto::FollowResponse> {
4165 this.update(&mut cx, |this, cx| {
4166 let follower = Follower {
4167 project_id: envelope.payload.project_id,
4168 peer_id: envelope.original_sender_id()?,
4169 };
4170
4171 let mut response = proto::FollowResponse::default();
4172 this.workspaces.retain(|workspace| {
4173 workspace
4174 .update(cx, |workspace, cx| {
4175 let handler_response = workspace.handle_follow(follower.project_id, cx);
4176 if response.views.is_empty() {
4177 response.views = handler_response.views;
4178 } else {
4179 response.views.extend_from_slice(&handler_response.views);
4180 }
4181
4182 if let Some(active_view_id) = handler_response.active_view_id.clone() {
4183 if response.active_view_id.is_none()
4184 || workspace.project.read(cx).remote_id() == follower.project_id
4185 {
4186 response.active_view_id = Some(active_view_id);
4187 }
4188 }
4189
4190 if let Some(active_view) = handler_response.active_view.clone() {
4191 if response.active_view_id.is_none()
4192 || workspace.project.read(cx).remote_id() == follower.project_id
4193 {
4194 response.active_view = Some(active_view)
4195 }
4196 }
4197 })
4198 .is_ok()
4199 });
4200
4201 Ok(response)
4202 })?
4203 }
4204
4205 async fn handle_update_followers(
4206 this: Model<Self>,
4207 envelope: TypedEnvelope<proto::UpdateFollowers>,
4208 _: Arc<Client>,
4209 mut cx: AsyncAppContext,
4210 ) -> Result<()> {
4211 let leader_id = envelope.original_sender_id()?;
4212 let update = envelope.payload;
4213
4214 this.update(&mut cx, |this, cx| {
4215 this.workspaces.retain(|workspace| {
4216 workspace
4217 .update(cx, |workspace, cx| {
4218 let project_id = workspace.project.read(cx).remote_id();
4219 if update.project_id != project_id && update.project_id.is_some() {
4220 return;
4221 }
4222 workspace.handle_update_followers(leader_id, update.clone(), cx);
4223 })
4224 .is_ok()
4225 });
4226 Ok(())
4227 })?
4228 }
4229}
4230
4231impl ViewId {
4232 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
4233 Ok(Self {
4234 creator: message
4235 .creator
4236 .ok_or_else(|| anyhow!("creator is missing"))?,
4237 id: message.id,
4238 })
4239 }
4240
4241 pub(crate) fn to_proto(&self) -> proto::ViewId {
4242 proto::ViewId {
4243 creator: Some(self.creator),
4244 id: self.id,
4245 }
4246 }
4247}
4248
4249pub trait WorkspaceHandle {
4250 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
4251}
4252
4253impl WorkspaceHandle for View<Workspace> {
4254 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
4255 self.read(cx)
4256 .worktrees(cx)
4257 .flat_map(|worktree| {
4258 let worktree_id = worktree.read(cx).id();
4259 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
4260 worktree_id,
4261 path: f.path.clone(),
4262 })
4263 })
4264 .collect::<Vec<_>>()
4265 }
4266}
4267
4268impl std::fmt::Debug for OpenPaths {
4269 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4270 f.debug_struct("OpenPaths")
4271 .field("paths", &self.paths)
4272 .finish()
4273 }
4274}
4275
4276pub fn activate_workspace_for_project(
4277 cx: &mut AppContext,
4278 predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
4279) -> Option<WindowHandle<Workspace>> {
4280 for window in cx.windows() {
4281 let Some(workspace) = window.downcast::<Workspace>() else {
4282 continue;
4283 };
4284
4285 let predicate = workspace
4286 .update(cx, |workspace, cx| {
4287 let project = workspace.project.read(cx);
4288 if predicate(project, cx) {
4289 cx.activate_window();
4290 true
4291 } else {
4292 false
4293 }
4294 })
4295 .log_err()
4296 .unwrap_or(false);
4297
4298 if predicate {
4299 return Some(workspace);
4300 }
4301 }
4302
4303 None
4304}
4305
4306pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
4307 DB.last_workspace().await.log_err().flatten()
4308}
4309
4310actions!(collab, [OpenChannelNotes]);
4311actions!(zed, [OpenLog]);
4312
4313async fn join_channel_internal(
4314 channel_id: ChannelId,
4315 app_state: &Arc<AppState>,
4316 requesting_window: Option<WindowHandle<Workspace>>,
4317 active_call: &Model<ActiveCall>,
4318 cx: &mut AsyncAppContext,
4319) -> Result<bool> {
4320 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
4321 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
4322 return (false, None);
4323 };
4324
4325 let already_in_channel = room.channel_id() == Some(channel_id);
4326 let should_prompt = room.is_sharing_project()
4327 && room.remote_participants().len() > 0
4328 && !already_in_channel;
4329 let open_room = if already_in_channel {
4330 active_call.room().cloned()
4331 } else {
4332 None
4333 };
4334 (should_prompt, open_room)
4335 })?;
4336
4337 if let Some(room) = open_room {
4338 let task = room.update(cx, |room, cx| {
4339 if let Some((project, host)) = room.most_active_project(cx) {
4340 return Some(join_in_room_project(project, host, app_state.clone(), cx));
4341 }
4342
4343 None
4344 })?;
4345 if let Some(task) = task {
4346 task.await?;
4347 }
4348 return anyhow::Ok(true);
4349 }
4350
4351 if should_prompt {
4352 if let Some(workspace) = requesting_window {
4353 let answer = workspace
4354 .update(cx, |_, cx| {
4355 cx.prompt(
4356 PromptLevel::Warning,
4357 "Do you want to switch channels?",
4358 Some("Leaving this call will unshare your current project."),
4359 &["Yes, Join Channel", "Cancel"],
4360 )
4361 })?
4362 .await;
4363
4364 if answer == Ok(1) {
4365 return Ok(false);
4366 }
4367 } else {
4368 return Ok(false); // unreachable!() hopefully
4369 }
4370 }
4371
4372 let client = cx.update(|cx| active_call.read(cx).client())?;
4373
4374 let mut client_status = client.status();
4375
4376 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
4377 'outer: loop {
4378 let Some(status) = client_status.recv().await else {
4379 return Err(anyhow!("error connecting"));
4380 };
4381
4382 match status {
4383 Status::Connecting
4384 | Status::Authenticating
4385 | Status::Reconnecting
4386 | Status::Reauthenticating => continue,
4387 Status::Connected { .. } => break 'outer,
4388 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
4389 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
4390 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
4391 return Err(ErrorCode::Disconnected.into());
4392 }
4393 }
4394 }
4395
4396 let room = active_call
4397 .update(cx, |active_call, cx| {
4398 active_call.join_channel(channel_id, cx)
4399 })?
4400 .await?;
4401
4402 let Some(room) = room else {
4403 return anyhow::Ok(true);
4404 };
4405
4406 room.update(cx, |room, _| room.room_update_completed())?
4407 .await;
4408
4409 let task = room.update(cx, |room, cx| {
4410 if let Some((project, host)) = room.most_active_project(cx) {
4411 return Some(join_in_room_project(project, host, app_state.clone(), cx));
4412 }
4413
4414 // if you are the first to join a channel, share your project
4415 if room.remote_participants().len() == 0 && !room.local_participant_is_guest() {
4416 if let Some(workspace) = requesting_window {
4417 let project = workspace.update(cx, |workspace, cx| {
4418 if !CallSettings::get_global(cx).share_on_join {
4419 return None;
4420 }
4421 let project = workspace.project.read(cx);
4422 if project.is_local()
4423 && project.visible_worktrees(cx).any(|tree| {
4424 tree.read(cx)
4425 .root_entry()
4426 .map_or(false, |entry| entry.is_dir())
4427 })
4428 {
4429 Some(workspace.project.clone())
4430 } else {
4431 None
4432 }
4433 });
4434 if let Ok(Some(project)) = project {
4435 return Some(cx.spawn(|room, mut cx| async move {
4436 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
4437 .await?;
4438 Ok(())
4439 }));
4440 }
4441 }
4442 }
4443
4444 None
4445 })?;
4446 if let Some(task) = task {
4447 task.await?;
4448 return anyhow::Ok(true);
4449 }
4450 anyhow::Ok(false)
4451}
4452
4453pub fn join_channel(
4454 channel_id: ChannelId,
4455 app_state: Arc<AppState>,
4456 requesting_window: Option<WindowHandle<Workspace>>,
4457 cx: &mut AppContext,
4458) -> Task<Result<()>> {
4459 let active_call = ActiveCall::global(cx);
4460 cx.spawn(|mut cx| async move {
4461 let result = join_channel_internal(
4462 channel_id,
4463 &app_state,
4464 requesting_window,
4465 &active_call,
4466 &mut cx,
4467 )
4468 .await;
4469
4470 // join channel succeeded, and opened a window
4471 if matches!(result, Ok(true)) {
4472 return anyhow::Ok(());
4473 }
4474
4475 // find an existing workspace to focus and show call controls
4476 let mut active_window =
4477 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
4478 if active_window.is_none() {
4479 // no open workspaces, make one to show the error in (blergh)
4480 let (window_handle, _) = cx
4481 .update(|cx| {
4482 Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)
4483 })?
4484 .await?;
4485
4486 if result.is_ok() {
4487 cx.update(|cx| {
4488 cx.dispatch_action(&OpenChannelNotes);
4489 }).log_err();
4490 }
4491
4492 active_window = Some(window_handle);
4493 }
4494
4495 if let Err(err) = result {
4496 log::error!("failed to join channel: {}", err);
4497 if let Some(active_window) = active_window {
4498 active_window
4499 .update(&mut cx, |_, cx| {
4500 let detail: SharedString = match err.error_code() {
4501 ErrorCode::SignedOut => {
4502 "Please sign in to continue.".into()
4503 }
4504 ErrorCode::UpgradeRequired => {
4505 "Your are running an unsupported version of Zed. Please update to continue.".into()
4506 }
4507 ErrorCode::NoSuchChannel => {
4508 "No matching channel was found. Please check the link and try again.".into()
4509 }
4510 ErrorCode::Forbidden => {
4511 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
4512 }
4513 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
4514 _ => format!("{}\n\nPlease try again.", err).into(),
4515 };
4516 cx.prompt(
4517 PromptLevel::Critical,
4518 "Failed to join channel",
4519 Some(&detail),
4520 &["Ok"],
4521 )
4522 })?
4523 .await
4524 .ok();
4525 }
4526 }
4527
4528 // return ok, we showed the error to the user.
4529 return anyhow::Ok(());
4530 })
4531}
4532
4533pub async fn get_any_active_workspace(
4534 app_state: Arc<AppState>,
4535 mut cx: AsyncAppContext,
4536) -> anyhow::Result<WindowHandle<Workspace>> {
4537 // find an existing workspace to focus and show call controls
4538 let active_window = activate_any_workspace_window(&mut cx);
4539 if active_window.is_none() {
4540 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))?
4541 .await?;
4542 }
4543 activate_any_workspace_window(&mut cx).context("could not open zed")
4544}
4545
4546fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
4547 cx.update(|cx| {
4548 if let Some(workspace_window) = cx
4549 .active_window()
4550 .and_then(|window| window.downcast::<Workspace>())
4551 {
4552 return Some(workspace_window);
4553 }
4554
4555 for window in cx.windows() {
4556 if let Some(workspace_window) = window.downcast::<Workspace>() {
4557 workspace_window
4558 .update(cx, |_, cx| cx.activate_window())
4559 .ok();
4560 return Some(workspace_window);
4561 }
4562 }
4563 None
4564 })
4565 .ok()
4566 .flatten()
4567}
4568
4569fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
4570 cx.windows()
4571 .into_iter()
4572 .filter_map(|window| window.downcast::<Workspace>())
4573 .filter(|workspace| {
4574 workspace
4575 .read(cx)
4576 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
4577 })
4578 .collect()
4579}
4580
4581#[derive(Default)]
4582pub struct OpenOptions {
4583 pub open_new_workspace: Option<bool>,
4584 pub replace_window: Option<WindowHandle<Workspace>>,
4585}
4586
4587#[allow(clippy::type_complexity)]
4588pub fn open_paths(
4589 abs_paths: &[PathBuf],
4590 app_state: Arc<AppState>,
4591 open_options: OpenOptions,
4592 cx: &mut AppContext,
4593) -> Task<
4594 anyhow::Result<(
4595 WindowHandle<Workspace>,
4596 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
4597 )>,
4598> {
4599 let abs_paths = abs_paths.to_vec();
4600 let mut existing = None;
4601 let mut best_match = None;
4602 let mut open_visible = OpenVisible::All;
4603
4604 if open_options.open_new_workspace != Some(true) {
4605 for window in local_workspace_windows(cx) {
4606 if let Ok(workspace) = window.read(cx) {
4607 let m = workspace
4608 .project
4609 .read(cx)
4610 .visibility_for_paths(&abs_paths, cx);
4611 if m > best_match {
4612 existing = Some(window);
4613 best_match = m;
4614 } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
4615 existing = Some(window)
4616 }
4617 }
4618 }
4619 }
4620
4621 cx.spawn(move |mut cx| async move {
4622 if open_options.open_new_workspace.is_none() && existing.is_none() {
4623 let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
4624 if futures::future::join_all(all_files)
4625 .await
4626 .into_iter()
4627 .filter_map(|result| result.ok().flatten())
4628 .all(|file| !file.is_dir)
4629 {
4630 cx.update(|cx| {
4631 for window in local_workspace_windows(cx) {
4632 if let Ok(workspace) = window.read(cx) {
4633 let project = workspace.project().read(cx);
4634 if project.is_remote() {
4635 continue;
4636 }
4637 existing = Some(window);
4638 open_visible = OpenVisible::None;
4639 break;
4640 }
4641 }
4642 })?;
4643 }
4644 }
4645
4646 if let Some(existing) = existing {
4647 Ok((
4648 existing,
4649 existing
4650 .update(&mut cx, |workspace, cx| {
4651 cx.activate_window();
4652 workspace.open_paths(abs_paths, open_visible, None, cx)
4653 })?
4654 .await,
4655 ))
4656 } else {
4657 cx.update(move |cx| {
4658 Workspace::new_local(
4659 abs_paths,
4660 app_state.clone(),
4661 open_options.replace_window,
4662 cx,
4663 )
4664 })?
4665 .await
4666 }
4667 })
4668}
4669
4670pub fn open_new(
4671 app_state: Arc<AppState>,
4672 cx: &mut AppContext,
4673 init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
4674) -> Task<()> {
4675 let task = Workspace::new_local(Vec::new(), app_state, None, cx);
4676 cx.spawn(|mut cx| async move {
4677 if let Some((workspace, opened_paths)) = task.await.log_err() {
4678 workspace
4679 .update(&mut cx, |workspace, cx| {
4680 if opened_paths.is_empty() {
4681 init(workspace, cx)
4682 }
4683 })
4684 .log_err();
4685 }
4686 })
4687}
4688
4689pub fn create_and_open_local_file(
4690 path: &'static Path,
4691 cx: &mut ViewContext<Workspace>,
4692 default_content: impl 'static + Send + FnOnce() -> Rope,
4693) -> Task<Result<Box<dyn ItemHandle>>> {
4694 cx.spawn(|workspace, mut cx| async move {
4695 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
4696 if !fs.is_file(path).await {
4697 fs.create_file(path, Default::default()).await?;
4698 fs.save(path, &default_content(), Default::default())
4699 .await?;
4700 }
4701
4702 let mut items = workspace
4703 .update(&mut cx, |workspace, cx| {
4704 workspace.with_local_workspace(cx, |workspace, cx| {
4705 workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
4706 })
4707 })?
4708 .await?
4709 .await;
4710
4711 let item = items.pop().flatten();
4712 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
4713 })
4714}
4715
4716pub fn join_hosted_project(
4717 hosted_project_id: ProjectId,
4718 app_state: Arc<AppState>,
4719 cx: &mut AppContext,
4720) -> Task<Result<()>> {
4721 cx.spawn(|mut cx| async move {
4722 let existing_window = cx.update(|cx| {
4723 cx.windows().into_iter().find_map(|window| {
4724 let workspace = window.downcast::<Workspace>()?;
4725 workspace
4726 .read(cx)
4727 .is_ok_and(|workspace| {
4728 workspace.project().read(cx).hosted_project_id() == Some(hosted_project_id)
4729 })
4730 .then(|| workspace)
4731 })
4732 })?;
4733
4734 let workspace = if let Some(existing_window) = existing_window {
4735 existing_window
4736 } else {
4737 let project = Project::hosted(
4738 hosted_project_id,
4739 app_state.user_store.clone(),
4740 app_state.client.clone(),
4741 app_state.languages.clone(),
4742 app_state.fs.clone(),
4743 cx.clone(),
4744 )
4745 .await?;
4746
4747 let window_bounds_override = window_bounds_env_override();
4748 cx.update(|cx| {
4749 let mut options = (app_state.build_window_options)(None, cx);
4750 options.bounds = window_bounds_override;
4751 cx.open_window(options, |cx| {
4752 cx.new_view(|cx| {
4753 Workspace::new(Default::default(), project, app_state.clone(), cx)
4754 })
4755 })
4756 })?
4757 };
4758
4759 workspace.update(&mut cx, |_, cx| {
4760 cx.activate(true);
4761 cx.activate_window();
4762 })?;
4763
4764 Ok(())
4765 })
4766}
4767
4768pub fn join_remote_project(
4769 project_id: ProjectId,
4770 app_state: Arc<AppState>,
4771 cx: &mut AppContext,
4772) -> Task<Result<WindowHandle<Workspace>>> {
4773 let windows = cx.windows();
4774 cx.spawn(|mut cx| async move {
4775 let existing_workspace = windows.into_iter().find_map(|window| {
4776 window.downcast::<Workspace>().and_then(|window| {
4777 window
4778 .update(&mut cx, |workspace, cx| {
4779 if workspace.project().read(cx).remote_id() == Some(project_id.0) {
4780 Some(window)
4781 } else {
4782 None
4783 }
4784 })
4785 .unwrap_or(None)
4786 })
4787 });
4788
4789 let workspace = if let Some(existing_workspace) = existing_workspace {
4790 existing_workspace
4791 } else {
4792 let project = Project::remote(
4793 project_id.0,
4794 app_state.client.clone(),
4795 app_state.user_store.clone(),
4796 app_state.languages.clone(),
4797 app_state.fs.clone(),
4798 cx.clone(),
4799 )
4800 .await?;
4801
4802 let window_bounds_override = window_bounds_env_override();
4803 cx.update(|cx| {
4804 let mut options = (app_state.build_window_options)(None, cx);
4805 options.bounds = window_bounds_override;
4806 cx.open_window(options, |cx| {
4807 cx.new_view(|cx| {
4808 Workspace::new(Default::default(), project, app_state.clone(), cx)
4809 })
4810 })
4811 })?
4812 };
4813
4814 workspace.update(&mut cx, |_, cx| {
4815 cx.activate(true);
4816 cx.activate_window();
4817 })?;
4818
4819 anyhow::Ok(workspace)
4820 })
4821}
4822
4823pub fn join_in_room_project(
4824 project_id: u64,
4825 follow_user_id: u64,
4826 app_state: Arc<AppState>,
4827 cx: &mut AppContext,
4828) -> Task<Result<()>> {
4829 let windows = cx.windows();
4830 cx.spawn(|mut cx| async move {
4831 let existing_workspace = windows.into_iter().find_map(|window| {
4832 window.downcast::<Workspace>().and_then(|window| {
4833 window
4834 .update(&mut cx, |workspace, cx| {
4835 if workspace.project().read(cx).remote_id() == Some(project_id) {
4836 Some(window)
4837 } else {
4838 None
4839 }
4840 })
4841 .unwrap_or(None)
4842 })
4843 });
4844
4845 let workspace = if let Some(existing_workspace) = existing_workspace {
4846 existing_workspace
4847 } else {
4848 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
4849 let room = active_call
4850 .read_with(&cx, |call, _| call.room().cloned())?
4851 .ok_or_else(|| anyhow!("not in a call"))?;
4852 let project = room
4853 .update(&mut cx, |room, cx| {
4854 room.join_project(
4855 project_id,
4856 app_state.languages.clone(),
4857 app_state.fs.clone(),
4858 cx,
4859 )
4860 })?
4861 .await?;
4862
4863 let window_bounds_override = window_bounds_env_override();
4864 cx.update(|cx| {
4865 let mut options = (app_state.build_window_options)(None, cx);
4866 options.bounds = window_bounds_override;
4867 cx.open_window(options, |cx| {
4868 cx.new_view(|cx| {
4869 Workspace::new(Default::default(), project, app_state.clone(), cx)
4870 })
4871 })
4872 })?
4873 };
4874
4875 workspace.update(&mut cx, |workspace, cx| {
4876 cx.activate(true);
4877 cx.activate_window();
4878
4879 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
4880 let follow_peer_id = room
4881 .read(cx)
4882 .remote_participants()
4883 .iter()
4884 .find(|(_, participant)| participant.user.id == follow_user_id)
4885 .map(|(_, p)| p.peer_id)
4886 .or_else(|| {
4887 // If we couldn't follow the given user, follow the host instead.
4888 let collaborator = workspace
4889 .project()
4890 .read(cx)
4891 .collaborators()
4892 .values()
4893 .find(|collaborator| collaborator.replica_id == 0)?;
4894 Some(collaborator.peer_id)
4895 });
4896
4897 if let Some(follow_peer_id) = follow_peer_id {
4898 workspace.follow(follow_peer_id, cx);
4899 }
4900 }
4901 })?;
4902
4903 anyhow::Ok(())
4904 })
4905}
4906
4907pub fn restart(_: &Restart, cx: &mut AppContext) {
4908 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
4909 let mut workspace_windows = cx
4910 .windows()
4911 .into_iter()
4912 .filter_map(|window| window.downcast::<Workspace>())
4913 .collect::<Vec<_>>();
4914
4915 // If multiple windows have unsaved changes, and need a save prompt,
4916 // prompt in the active window before switching to a different window.
4917 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
4918
4919 let mut prompt = None;
4920 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
4921 prompt = window
4922 .update(cx, |_, cx| {
4923 cx.prompt(
4924 PromptLevel::Info,
4925 "Are you sure you want to restart?",
4926 None,
4927 &["Restart", "Cancel"],
4928 )
4929 })
4930 .ok();
4931 }
4932
4933 cx.spawn(|mut cx| async move {
4934 if let Some(prompt) = prompt {
4935 let answer = prompt.await?;
4936 if answer != 0 {
4937 return Ok(());
4938 }
4939 }
4940
4941 // If the user cancels any save prompt, then keep the app open.
4942 for window in workspace_windows {
4943 if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
4944 workspace.prepare_to_close(true, cx)
4945 }) {
4946 if !should_close.await? {
4947 return Ok(());
4948 }
4949 }
4950 }
4951
4952 cx.update(|cx| cx.restart())
4953 })
4954 .detach_and_log_err(cx);
4955}
4956
4957fn parse_pixel_position_env_var(value: &str) -> Option<Point<DevicePixels>> {
4958 let mut parts = value.split(',');
4959 let x: usize = parts.next()?.parse().ok()?;
4960 let y: usize = parts.next()?.parse().ok()?;
4961 Some(point((x as i32).into(), (y as i32).into()))
4962}
4963
4964fn parse_pixel_size_env_var(value: &str) -> Option<Size<DevicePixels>> {
4965 let mut parts = value.split(',');
4966 let width: usize = parts.next()?.parse().ok()?;
4967 let height: usize = parts.next()?.parse().ok()?;
4968 Some(size((width as i32).into(), (height as i32).into()))
4969}
4970
4971struct DisconnectedOverlay;
4972
4973impl Element for DisconnectedOverlay {
4974 type RequestLayoutState = AnyElement;
4975 type PrepaintState = ();
4976
4977 fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) {
4978 let mut background = cx.theme().colors().elevated_surface_background;
4979 background.fade_out(0.2);
4980 let mut overlay = div()
4981 .bg(background)
4982 .absolute()
4983 .left_0()
4984 .top(ui::TitleBar::height(cx))
4985 .size_full()
4986 .flex()
4987 .items_center()
4988 .justify_center()
4989 .capture_any_mouse_down(|_, cx| cx.stop_propagation())
4990 .capture_any_mouse_up(|_, cx| cx.stop_propagation())
4991 .child(Label::new(
4992 "Your connection to the remote project has been lost.",
4993 ))
4994 .into_any();
4995 (overlay.request_layout(cx), overlay)
4996 }
4997
4998 fn prepaint(
4999 &mut self,
5000 bounds: Bounds<Pixels>,
5001 overlay: &mut Self::RequestLayoutState,
5002 cx: &mut ElementContext,
5003 ) {
5004 cx.insert_hitbox(bounds, true);
5005 overlay.prepaint(cx);
5006 }
5007
5008 fn paint(
5009 &mut self,
5010 _: Bounds<Pixels>,
5011 overlay: &mut Self::RequestLayoutState,
5012 _: &mut Self::PrepaintState,
5013 cx: &mut ElementContext,
5014 ) {
5015 overlay.paint(cx)
5016 }
5017}
5018
5019impl IntoElement for DisconnectedOverlay {
5020 type Element = Self;
5021
5022 fn into_element(self) -> Self::Element {
5023 self
5024 }
5025}
5026
5027#[cfg(test)]
5028mod tests {
5029 use std::{cell::RefCell, rc::Rc};
5030
5031 use super::*;
5032 use crate::{
5033 dock::{test::TestPanel, PanelEvent},
5034 item::{
5035 test::{TestItem, TestProjectItem},
5036 ItemEvent,
5037 },
5038 };
5039 use fs::FakeFs;
5040 use gpui::{
5041 px, BorrowAppContext, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView,
5042 Render, TestAppContext, VisualTestContext,
5043 };
5044 use project::{Project, ProjectEntryId};
5045 use serde_json::json;
5046 use settings::SettingsStore;
5047
5048 #[gpui::test]
5049 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
5050 init_test(cx);
5051
5052 let fs = FakeFs::new(cx.executor());
5053 let project = Project::test(fs, [], cx).await;
5054 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
5055
5056 // Adding an item with no ambiguity renders the tab without detail.
5057 let item1 = cx.new_view(|cx| {
5058 let mut item = TestItem::new(cx);
5059 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
5060 item
5061 });
5062 workspace.update(cx, |workspace, cx| {
5063 workspace.add_item_to_active_pane(Box::new(item1.clone()), cx);
5064 });
5065 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
5066
5067 // Adding an item that creates ambiguity increases the level of detail on
5068 // both tabs.
5069 let item2 = cx.new_view(|cx| {
5070 let mut item = TestItem::new(cx);
5071 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
5072 item
5073 });
5074 workspace.update(cx, |workspace, cx| {
5075 workspace.add_item_to_active_pane(Box::new(item2.clone()), cx);
5076 });
5077 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
5078 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
5079
5080 // Adding an item that creates ambiguity increases the level of detail only
5081 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
5082 // we stop at the highest detail available.
5083 let item3 = cx.new_view(|cx| {
5084 let mut item = TestItem::new(cx);
5085 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
5086 item
5087 });
5088 workspace.update(cx, |workspace, cx| {
5089 workspace.add_item_to_active_pane(Box::new(item3.clone()), cx);
5090 });
5091 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
5092 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
5093 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
5094 }
5095
5096 #[gpui::test]
5097 async fn test_tracking_active_path(cx: &mut TestAppContext) {
5098 init_test(cx);
5099
5100 let fs = FakeFs::new(cx.executor());
5101 fs.insert_tree(
5102 "/root1",
5103 json!({
5104 "one.txt": "",
5105 "two.txt": "",
5106 }),
5107 )
5108 .await;
5109 fs.insert_tree(
5110 "/root2",
5111 json!({
5112 "three.txt": "",
5113 }),
5114 )
5115 .await;
5116
5117 let project = Project::test(fs, ["root1".as_ref()], cx).await;
5118 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
5119 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5120 let worktree_id = project.update(cx, |project, cx| {
5121 project.worktrees().next().unwrap().read(cx).id()
5122 });
5123
5124 let item1 = cx.new_view(|cx| {
5125 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
5126 });
5127 let item2 = cx.new_view(|cx| {
5128 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
5129 });
5130
5131 // Add an item to an empty pane
5132 workspace.update(cx, |workspace, cx| {
5133 workspace.add_item_to_active_pane(Box::new(item1), cx)
5134 });
5135 project.update(cx, |project, cx| {
5136 assert_eq!(
5137 project.active_entry(),
5138 project
5139 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
5140 .map(|e| e.id)
5141 );
5142 });
5143 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
5144
5145 // Add a second item to a non-empty pane
5146 workspace.update(cx, |workspace, cx| {
5147 workspace.add_item_to_active_pane(Box::new(item2), cx)
5148 });
5149 assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
5150 project.update(cx, |project, cx| {
5151 assert_eq!(
5152 project.active_entry(),
5153 project
5154 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
5155 .map(|e| e.id)
5156 );
5157 });
5158
5159 // Close the active item
5160 pane.update(cx, |pane, cx| {
5161 pane.close_active_item(&Default::default(), cx).unwrap()
5162 })
5163 .await
5164 .unwrap();
5165 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
5166 project.update(cx, |project, cx| {
5167 assert_eq!(
5168 project.active_entry(),
5169 project
5170 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
5171 .map(|e| e.id)
5172 );
5173 });
5174
5175 // Add a project folder
5176 project
5177 .update(cx, |project, cx| {
5178 project.find_or_create_local_worktree("/root2", true, cx)
5179 })
5180 .await
5181 .unwrap();
5182 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
5183
5184 // Remove a project folder
5185 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
5186 assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
5187 }
5188
5189 #[gpui::test]
5190 async fn test_close_window(cx: &mut TestAppContext) {
5191 init_test(cx);
5192
5193 let fs = FakeFs::new(cx.executor());
5194 fs.insert_tree("/root", json!({ "one": "" })).await;
5195
5196 let project = Project::test(fs, ["root".as_ref()], cx).await;
5197 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
5198
5199 // When there are no dirty items, there's nothing to do.
5200 let item1 = cx.new_view(|cx| TestItem::new(cx));
5201 workspace.update(cx, |w, cx| {
5202 w.add_item_to_active_pane(Box::new(item1.clone()), cx)
5203 });
5204 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
5205 assert!(task.await.unwrap());
5206
5207 // When there are dirty untitled items, prompt to save each one. If the user
5208 // cancels any prompt, then abort.
5209 let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
5210 let item3 = cx.new_view(|cx| {
5211 TestItem::new(cx)
5212 .with_dirty(true)
5213 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5214 });
5215 workspace.update(cx, |w, cx| {
5216 w.add_item_to_active_pane(Box::new(item2.clone()), cx);
5217 w.add_item_to_active_pane(Box::new(item3.clone()), cx);
5218 });
5219 let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
5220 cx.executor().run_until_parked();
5221 cx.simulate_prompt_answer(2); // cancel save all
5222 cx.executor().run_until_parked();
5223 cx.simulate_prompt_answer(2); // cancel save all
5224 cx.executor().run_until_parked();
5225 assert!(!cx.has_pending_prompt());
5226 assert!(!task.await.unwrap());
5227 }
5228
5229 #[gpui::test]
5230 async fn test_close_pane_items(cx: &mut TestAppContext) {
5231 init_test(cx);
5232
5233 let fs = FakeFs::new(cx.executor());
5234
5235 let project = Project::test(fs, None, cx).await;
5236 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5237
5238 let item1 = cx.new_view(|cx| {
5239 TestItem::new(cx)
5240 .with_dirty(true)
5241 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5242 });
5243 let item2 = cx.new_view(|cx| {
5244 TestItem::new(cx)
5245 .with_dirty(true)
5246 .with_conflict(true)
5247 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
5248 });
5249 let item3 = cx.new_view(|cx| {
5250 TestItem::new(cx)
5251 .with_dirty(true)
5252 .with_conflict(true)
5253 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
5254 });
5255 let item4 = cx.new_view(|cx| {
5256 TestItem::new(cx)
5257 .with_dirty(true)
5258 .with_project_items(&[TestProjectItem::new_untitled(cx)])
5259 });
5260 let pane = workspace.update(cx, |workspace, cx| {
5261 workspace.add_item_to_active_pane(Box::new(item1.clone()), cx);
5262 workspace.add_item_to_active_pane(Box::new(item2.clone()), cx);
5263 workspace.add_item_to_active_pane(Box::new(item3.clone()), cx);
5264 workspace.add_item_to_active_pane(Box::new(item4.clone()), cx);
5265 workspace.active_pane().clone()
5266 });
5267
5268 let close_items = pane.update(cx, |pane, cx| {
5269 pane.activate_item(1, true, true, cx);
5270 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
5271 let item1_id = item1.item_id();
5272 let item3_id = item3.item_id();
5273 let item4_id = item4.item_id();
5274 pane.close_items(cx, SaveIntent::Close, move |id| {
5275 [item1_id, item3_id, item4_id].contains(&id)
5276 })
5277 });
5278 cx.executor().run_until_parked();
5279
5280 assert!(cx.has_pending_prompt());
5281 // Ignore "Save all" prompt
5282 cx.simulate_prompt_answer(2);
5283 cx.executor().run_until_parked();
5284 // There's a prompt to save item 1.
5285 pane.update(cx, |pane, _| {
5286 assert_eq!(pane.items_len(), 4);
5287 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
5288 });
5289 // Confirm saving item 1.
5290 cx.simulate_prompt_answer(0);
5291 cx.executor().run_until_parked();
5292
5293 // Item 1 is saved. There's a prompt to save item 3.
5294 pane.update(cx, |pane, cx| {
5295 assert_eq!(item1.read(cx).save_count, 1);
5296 assert_eq!(item1.read(cx).save_as_count, 0);
5297 assert_eq!(item1.read(cx).reload_count, 0);
5298 assert_eq!(pane.items_len(), 3);
5299 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
5300 });
5301 assert!(cx.has_pending_prompt());
5302
5303 // Cancel saving item 3.
5304 cx.simulate_prompt_answer(1);
5305 cx.executor().run_until_parked();
5306
5307 // Item 3 is reloaded. There's a prompt to save item 4.
5308 pane.update(cx, |pane, cx| {
5309 assert_eq!(item3.read(cx).save_count, 0);
5310 assert_eq!(item3.read(cx).save_as_count, 0);
5311 assert_eq!(item3.read(cx).reload_count, 1);
5312 assert_eq!(pane.items_len(), 2);
5313 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
5314 });
5315 assert!(cx.has_pending_prompt());
5316
5317 // Confirm saving item 4.
5318 cx.simulate_prompt_answer(0);
5319 cx.executor().run_until_parked();
5320
5321 // There's a prompt for a path for item 4.
5322 cx.simulate_new_path_selection(|_| Some(Default::default()));
5323 close_items.await.unwrap();
5324
5325 // The requested items are closed.
5326 pane.update(cx, |pane, cx| {
5327 assert_eq!(item4.read(cx).save_count, 0);
5328 assert_eq!(item4.read(cx).save_as_count, 1);
5329 assert_eq!(item4.read(cx).reload_count, 0);
5330 assert_eq!(pane.items_len(), 1);
5331 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
5332 });
5333 }
5334
5335 #[gpui::test]
5336 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
5337 init_test(cx);
5338
5339 let fs = FakeFs::new(cx.executor());
5340 let project = Project::test(fs, [], cx).await;
5341 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5342
5343 // Create several workspace items with single project entries, and two
5344 // workspace items with multiple project entries.
5345 let single_entry_items = (0..=4)
5346 .map(|project_entry_id| {
5347 cx.new_view(|cx| {
5348 TestItem::new(cx)
5349 .with_dirty(true)
5350 .with_project_items(&[TestProjectItem::new(
5351 project_entry_id,
5352 &format!("{project_entry_id}.txt"),
5353 cx,
5354 )])
5355 })
5356 })
5357 .collect::<Vec<_>>();
5358 let item_2_3 = cx.new_view(|cx| {
5359 TestItem::new(cx)
5360 .with_dirty(true)
5361 .with_singleton(false)
5362 .with_project_items(&[
5363 single_entry_items[2].read(cx).project_items[0].clone(),
5364 single_entry_items[3].read(cx).project_items[0].clone(),
5365 ])
5366 });
5367 let item_3_4 = cx.new_view(|cx| {
5368 TestItem::new(cx)
5369 .with_dirty(true)
5370 .with_singleton(false)
5371 .with_project_items(&[
5372 single_entry_items[3].read(cx).project_items[0].clone(),
5373 single_entry_items[4].read(cx).project_items[0].clone(),
5374 ])
5375 });
5376
5377 // Create two panes that contain the following project entries:
5378 // left pane:
5379 // multi-entry items: (2, 3)
5380 // single-entry items: 0, 1, 2, 3, 4
5381 // right pane:
5382 // single-entry items: 1
5383 // multi-entry items: (3, 4)
5384 let left_pane = workspace.update(cx, |workspace, cx| {
5385 let left_pane = workspace.active_pane().clone();
5386 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), cx);
5387 for item in single_entry_items {
5388 workspace.add_item_to_active_pane(Box::new(item), cx);
5389 }
5390 left_pane.update(cx, |pane, cx| {
5391 pane.activate_item(2, true, true, cx);
5392 });
5393
5394 let right_pane = workspace
5395 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
5396 .unwrap();
5397
5398 right_pane.update(cx, |pane, cx| {
5399 pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
5400 });
5401
5402 left_pane
5403 });
5404
5405 cx.focus_view(&left_pane);
5406
5407 // When closing all of the items in the left pane, we should be prompted twice:
5408 // once for project entry 0, and once for project entry 2. Project entries 1,
5409 // 3, and 4 are all still open in the other paten. After those two
5410 // prompts, the task should complete.
5411
5412 let close = left_pane.update(cx, |pane, cx| {
5413 pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
5414 });
5415 cx.executor().run_until_parked();
5416
5417 // Discard "Save all" prompt
5418 cx.simulate_prompt_answer(2);
5419
5420 cx.executor().run_until_parked();
5421 left_pane.update(cx, |pane, cx| {
5422 assert_eq!(
5423 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
5424 &[ProjectEntryId::from_proto(0)]
5425 );
5426 });
5427 cx.simulate_prompt_answer(0);
5428
5429 cx.executor().run_until_parked();
5430 left_pane.update(cx, |pane, cx| {
5431 assert_eq!(
5432 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
5433 &[ProjectEntryId::from_proto(2)]
5434 );
5435 });
5436 cx.simulate_prompt_answer(0);
5437
5438 cx.executor().run_until_parked();
5439 close.await.unwrap();
5440 left_pane.update(cx, |pane, _| {
5441 assert_eq!(pane.items_len(), 0);
5442 });
5443 }
5444
5445 #[gpui::test]
5446 async fn test_autosave(cx: &mut gpui::TestAppContext) {
5447 init_test(cx);
5448
5449 let fs = FakeFs::new(cx.executor());
5450 let project = Project::test(fs, [], cx).await;
5451 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5452 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5453
5454 let item = cx.new_view(|cx| {
5455 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5456 });
5457 let item_id = item.entity_id();
5458 workspace.update(cx, |workspace, cx| {
5459 workspace.add_item_to_active_pane(Box::new(item.clone()), cx);
5460 });
5461
5462 // Autosave on window change.
5463 item.update(cx, |item, cx| {
5464 cx.update_global(|settings: &mut SettingsStore, cx| {
5465 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5466 settings.autosave = Some(AutosaveSetting::OnWindowChange);
5467 })
5468 });
5469 item.is_dirty = true;
5470 });
5471
5472 // Deactivating the window saves the file.
5473 cx.deactivate_window();
5474 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
5475
5476 // Re-activating the window doesn't save the file.
5477 cx.update(|cx| cx.activate_window());
5478 cx.executor().run_until_parked();
5479 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
5480
5481 // Autosave on focus change.
5482 item.update(cx, |item, cx| {
5483 cx.focus_self();
5484 cx.update_global(|settings: &mut SettingsStore, cx| {
5485 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5486 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5487 })
5488 });
5489 item.is_dirty = true;
5490 });
5491
5492 // Blurring the item saves the file.
5493 item.update(cx, |_, cx| cx.blur());
5494 cx.executor().run_until_parked();
5495 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
5496
5497 // Deactivating the window still saves the file.
5498 item.update(cx, |item, cx| {
5499 cx.focus_self();
5500 item.is_dirty = true;
5501 });
5502 cx.deactivate_window();
5503 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
5504
5505 // Autosave after delay.
5506 item.update(cx, |item, cx| {
5507 cx.update_global(|settings: &mut SettingsStore, cx| {
5508 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5509 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
5510 })
5511 });
5512 item.is_dirty = true;
5513 cx.emit(ItemEvent::Edit);
5514 });
5515
5516 // Delay hasn't fully expired, so the file is still dirty and unsaved.
5517 cx.executor().advance_clock(Duration::from_millis(250));
5518 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
5519
5520 // After delay expires, the file is saved.
5521 cx.executor().advance_clock(Duration::from_millis(250));
5522 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
5523
5524 // Autosave on focus change, ensuring closing the tab counts as such.
5525 item.update(cx, |item, cx| {
5526 cx.update_global(|settings: &mut SettingsStore, cx| {
5527 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
5528 settings.autosave = Some(AutosaveSetting::OnFocusChange);
5529 })
5530 });
5531 item.is_dirty = true;
5532 });
5533
5534 pane.update(cx, |pane, cx| {
5535 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5536 })
5537 .await
5538 .unwrap();
5539 assert!(!cx.has_pending_prompt());
5540 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5541
5542 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
5543 workspace.update(cx, |workspace, cx| {
5544 workspace.add_item_to_active_pane(Box::new(item.clone()), cx);
5545 });
5546 item.update(cx, |item, cx| {
5547 item.project_items[0].update(cx, |item, _| {
5548 item.entry_id = None;
5549 });
5550 item.is_dirty = true;
5551 cx.blur();
5552 });
5553 cx.run_until_parked();
5554 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5555
5556 // Ensure autosave is prevented for deleted files also when closing the buffer.
5557 let _close_items = pane.update(cx, |pane, cx| {
5558 pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
5559 });
5560 cx.run_until_parked();
5561 assert!(cx.has_pending_prompt());
5562 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
5563 }
5564
5565 #[gpui::test]
5566 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
5567 init_test(cx);
5568
5569 let fs = FakeFs::new(cx.executor());
5570
5571 let project = Project::test(fs, [], cx).await;
5572 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5573
5574 let item = cx.new_view(|cx| {
5575 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
5576 });
5577 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5578 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
5579 let toolbar_notify_count = Rc::new(RefCell::new(0));
5580
5581 workspace.update(cx, |workspace, cx| {
5582 workspace.add_item_to_active_pane(Box::new(item.clone()), cx);
5583 let toolbar_notification_count = toolbar_notify_count.clone();
5584 cx.observe(&toolbar, move |_, _, _| {
5585 *toolbar_notification_count.borrow_mut() += 1
5586 })
5587 .detach();
5588 });
5589
5590 pane.update(cx, |pane, _| {
5591 assert!(!pane.can_navigate_backward());
5592 assert!(!pane.can_navigate_forward());
5593 });
5594
5595 item.update(cx, |item, cx| {
5596 item.set_state("one".to_string(), cx);
5597 });
5598
5599 // Toolbar must be notified to re-render the navigation buttons
5600 assert_eq!(*toolbar_notify_count.borrow(), 1);
5601
5602 pane.update(cx, |pane, _| {
5603 assert!(pane.can_navigate_backward());
5604 assert!(!pane.can_navigate_forward());
5605 });
5606
5607 workspace
5608 .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
5609 .await
5610 .unwrap();
5611
5612 assert_eq!(*toolbar_notify_count.borrow(), 2);
5613 pane.update(cx, |pane, _| {
5614 assert!(!pane.can_navigate_backward());
5615 assert!(pane.can_navigate_forward());
5616 });
5617 }
5618
5619 #[gpui::test]
5620 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
5621 init_test(cx);
5622 let fs = FakeFs::new(cx.executor());
5623
5624 let project = Project::test(fs, [], cx).await;
5625 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5626
5627 let panel = workspace.update(cx, |workspace, cx| {
5628 let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5629 workspace.add_panel(panel.clone(), cx);
5630
5631 workspace
5632 .right_dock()
5633 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5634
5635 panel
5636 });
5637
5638 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
5639 pane.update(cx, |pane, cx| {
5640 let item = cx.new_view(|cx| TestItem::new(cx));
5641 pane.add_item(Box::new(item), true, true, None, cx);
5642 });
5643
5644 // Transfer focus from center to panel
5645 workspace.update(cx, |workspace, cx| {
5646 workspace.toggle_panel_focus::<TestPanel>(cx);
5647 });
5648
5649 workspace.update(cx, |workspace, cx| {
5650 assert!(workspace.right_dock().read(cx).is_open());
5651 assert!(!panel.is_zoomed(cx));
5652 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5653 });
5654
5655 // Transfer focus from panel to center
5656 workspace.update(cx, |workspace, cx| {
5657 workspace.toggle_panel_focus::<TestPanel>(cx);
5658 });
5659
5660 workspace.update(cx, |workspace, cx| {
5661 assert!(workspace.right_dock().read(cx).is_open());
5662 assert!(!panel.is_zoomed(cx));
5663 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5664 });
5665
5666 // Close the dock
5667 workspace.update(cx, |workspace, cx| {
5668 workspace.toggle_dock(DockPosition::Right, cx);
5669 });
5670
5671 workspace.update(cx, |workspace, cx| {
5672 assert!(!workspace.right_dock().read(cx).is_open());
5673 assert!(!panel.is_zoomed(cx));
5674 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5675 });
5676
5677 // Open the dock
5678 workspace.update(cx, |workspace, cx| {
5679 workspace.toggle_dock(DockPosition::Right, cx);
5680 });
5681
5682 workspace.update(cx, |workspace, cx| {
5683 assert!(workspace.right_dock().read(cx).is_open());
5684 assert!(!panel.is_zoomed(cx));
5685 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5686 });
5687
5688 // Focus and zoom panel
5689 panel.update(cx, |panel, cx| {
5690 cx.focus_self();
5691 panel.set_zoomed(true, cx)
5692 });
5693
5694 workspace.update(cx, |workspace, cx| {
5695 assert!(workspace.right_dock().read(cx).is_open());
5696 assert!(panel.is_zoomed(cx));
5697 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5698 });
5699
5700 // Transfer focus to the center closes the dock
5701 workspace.update(cx, |workspace, cx| {
5702 workspace.toggle_panel_focus::<TestPanel>(cx);
5703 });
5704
5705 workspace.update(cx, |workspace, cx| {
5706 assert!(!workspace.right_dock().read(cx).is_open());
5707 assert!(panel.is_zoomed(cx));
5708 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5709 });
5710
5711 // Transferring focus back to the panel keeps it zoomed
5712 workspace.update(cx, |workspace, cx| {
5713 workspace.toggle_panel_focus::<TestPanel>(cx);
5714 });
5715
5716 workspace.update(cx, |workspace, cx| {
5717 assert!(workspace.right_dock().read(cx).is_open());
5718 assert!(panel.is_zoomed(cx));
5719 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5720 });
5721
5722 // Close the dock while it is zoomed
5723 workspace.update(cx, |workspace, cx| {
5724 workspace.toggle_dock(DockPosition::Right, cx)
5725 });
5726
5727 workspace.update(cx, |workspace, cx| {
5728 assert!(!workspace.right_dock().read(cx).is_open());
5729 assert!(panel.is_zoomed(cx));
5730 assert!(workspace.zoomed.is_none());
5731 assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
5732 });
5733
5734 // Opening the dock, when it's zoomed, retains focus
5735 workspace.update(cx, |workspace, cx| {
5736 workspace.toggle_dock(DockPosition::Right, cx)
5737 });
5738
5739 workspace.update(cx, |workspace, cx| {
5740 assert!(workspace.right_dock().read(cx).is_open());
5741 assert!(panel.is_zoomed(cx));
5742 assert!(workspace.zoomed.is_some());
5743 assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
5744 });
5745
5746 // Unzoom and close the panel, zoom the active pane.
5747 panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
5748 workspace.update(cx, |workspace, cx| {
5749 workspace.toggle_dock(DockPosition::Right, cx)
5750 });
5751 pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
5752
5753 // Opening a dock unzooms the pane.
5754 workspace.update(cx, |workspace, cx| {
5755 workspace.toggle_dock(DockPosition::Right, cx)
5756 });
5757 workspace.update(cx, |workspace, cx| {
5758 let pane = pane.read(cx);
5759 assert!(!pane.is_zoomed());
5760 assert!(!pane.focus_handle(cx).is_focused(cx));
5761 assert!(workspace.right_dock().read(cx).is_open());
5762 assert!(workspace.zoomed.is_none());
5763 });
5764 }
5765
5766 struct TestModal(FocusHandle);
5767
5768 impl TestModal {
5769 fn new(cx: &mut ViewContext<Self>) -> Self {
5770 Self(cx.focus_handle())
5771 }
5772 }
5773
5774 impl EventEmitter<DismissEvent> for TestModal {}
5775
5776 impl FocusableView for TestModal {
5777 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
5778 self.0.clone()
5779 }
5780 }
5781
5782 impl ModalView for TestModal {}
5783
5784 impl Render for TestModal {
5785 fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
5786 div().track_focus(&self.0)
5787 }
5788 }
5789
5790 #[gpui::test]
5791 async fn test_panels(cx: &mut gpui::TestAppContext) {
5792 init_test(cx);
5793 let fs = FakeFs::new(cx.executor());
5794
5795 let project = Project::test(fs, [], cx).await;
5796 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
5797
5798 let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
5799 let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
5800 workspace.add_panel(panel_1.clone(), cx);
5801 workspace
5802 .left_dock()
5803 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
5804 let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
5805 workspace.add_panel(panel_2.clone(), cx);
5806 workspace
5807 .right_dock()
5808 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
5809
5810 let left_dock = workspace.left_dock();
5811 assert_eq!(
5812 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5813 panel_1.panel_id()
5814 );
5815 assert_eq!(
5816 left_dock.read(cx).active_panel_size(cx).unwrap(),
5817 panel_1.size(cx)
5818 );
5819
5820 left_dock.update(cx, |left_dock, cx| {
5821 left_dock.resize_active_panel(Some(px(1337.)), cx)
5822 });
5823 assert_eq!(
5824 workspace
5825 .right_dock()
5826 .read(cx)
5827 .visible_panel()
5828 .unwrap()
5829 .panel_id(),
5830 panel_2.panel_id(),
5831 );
5832
5833 (panel_1, panel_2)
5834 });
5835
5836 // Move panel_1 to the right
5837 panel_1.update(cx, |panel_1, cx| {
5838 panel_1.set_position(DockPosition::Right, cx)
5839 });
5840
5841 workspace.update(cx, |workspace, cx| {
5842 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
5843 // Since it was the only panel on the left, the left dock should now be closed.
5844 assert!(!workspace.left_dock().read(cx).is_open());
5845 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
5846 let right_dock = workspace.right_dock();
5847 assert_eq!(
5848 right_dock.read(cx).visible_panel().unwrap().panel_id(),
5849 panel_1.panel_id()
5850 );
5851 assert_eq!(
5852 right_dock.read(cx).active_panel_size(cx).unwrap(),
5853 px(1337.)
5854 );
5855
5856 // Now we move panel_2 to the left
5857 panel_2.set_position(DockPosition::Left, cx);
5858 });
5859
5860 workspace.update(cx, |workspace, cx| {
5861 // Since panel_2 was not visible on the right, we don't open the left dock.
5862 assert!(!workspace.left_dock().read(cx).is_open());
5863 // And the right dock is unaffected in its displaying of panel_1
5864 assert!(workspace.right_dock().read(cx).is_open());
5865 assert_eq!(
5866 workspace
5867 .right_dock()
5868 .read(cx)
5869 .visible_panel()
5870 .unwrap()
5871 .panel_id(),
5872 panel_1.panel_id(),
5873 );
5874 });
5875
5876 // Move panel_1 back to the left
5877 panel_1.update(cx, |panel_1, cx| {
5878 panel_1.set_position(DockPosition::Left, cx)
5879 });
5880
5881 workspace.update(cx, |workspace, cx| {
5882 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
5883 let left_dock = workspace.left_dock();
5884 assert!(left_dock.read(cx).is_open());
5885 assert_eq!(
5886 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5887 panel_1.panel_id()
5888 );
5889 assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
5890 // And the right dock should be closed as it no longer has any panels.
5891 assert!(!workspace.right_dock().read(cx).is_open());
5892
5893 // Now we move panel_1 to the bottom
5894 panel_1.set_position(DockPosition::Bottom, cx);
5895 });
5896
5897 workspace.update(cx, |workspace, cx| {
5898 // Since panel_1 was visible on the left, we close the left dock.
5899 assert!(!workspace.left_dock().read(cx).is_open());
5900 // The bottom dock is sized based on the panel's default size,
5901 // since the panel orientation changed from vertical to horizontal.
5902 let bottom_dock = workspace.bottom_dock();
5903 assert_eq!(
5904 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
5905 panel_1.size(cx),
5906 );
5907 // Close bottom dock and move panel_1 back to the left.
5908 bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
5909 panel_1.set_position(DockPosition::Left, cx);
5910 });
5911
5912 // Emit activated event on panel 1
5913 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
5914
5915 // Now the left dock is open and panel_1 is active and focused.
5916 workspace.update(cx, |workspace, cx| {
5917 let left_dock = workspace.left_dock();
5918 assert!(left_dock.read(cx).is_open());
5919 assert_eq!(
5920 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5921 panel_1.panel_id(),
5922 );
5923 assert!(panel_1.focus_handle(cx).is_focused(cx));
5924 });
5925
5926 // Emit closed event on panel 2, which is not active
5927 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
5928
5929 // Wo don't close the left dock, because panel_2 wasn't the active panel
5930 workspace.update(cx, |workspace, cx| {
5931 let left_dock = workspace.left_dock();
5932 assert!(left_dock.read(cx).is_open());
5933 assert_eq!(
5934 left_dock.read(cx).visible_panel().unwrap().panel_id(),
5935 panel_1.panel_id(),
5936 );
5937 });
5938
5939 // Emitting a ZoomIn event shows the panel as zoomed.
5940 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
5941 workspace.update(cx, |workspace, _| {
5942 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5943 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
5944 });
5945
5946 // Move panel to another dock while it is zoomed
5947 panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
5948 workspace.update(cx, |workspace, _| {
5949 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5950
5951 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5952 });
5953
5954 // This is a helper for getting a:
5955 // - valid focus on an element,
5956 // - that isn't a part of the panes and panels system of the Workspace,
5957 // - and doesn't trigger the 'on_focus_lost' API.
5958 let focus_other_view = {
5959 let workspace = workspace.clone();
5960 move |cx: &mut VisualTestContext| {
5961 workspace.update(cx, |workspace, cx| {
5962 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
5963 workspace.toggle_modal(cx, TestModal::new);
5964 workspace.toggle_modal(cx, TestModal::new);
5965 } else {
5966 workspace.toggle_modal(cx, TestModal::new);
5967 }
5968 })
5969 }
5970 };
5971
5972 // If focus is transferred to another view that's not a panel or another pane, we still show
5973 // the panel as zoomed.
5974 focus_other_view(cx);
5975 workspace.update(cx, |workspace, _| {
5976 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5977 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
5978 });
5979
5980 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
5981 workspace.update(cx, |_, cx| cx.focus_self());
5982 workspace.update(cx, |workspace, _| {
5983 assert_eq!(workspace.zoomed, None);
5984 assert_eq!(workspace.zoomed_position, None);
5985 });
5986
5987 // If focus is transferred again to another view that's not a panel or a pane, we won't
5988 // show the panel as zoomed because it wasn't zoomed before.
5989 focus_other_view(cx);
5990 workspace.update(cx, |workspace, _| {
5991 assert_eq!(workspace.zoomed, None);
5992 assert_eq!(workspace.zoomed_position, None);
5993 });
5994
5995 // When the panel is activated, it is zoomed again.
5996 cx.dispatch_action(ToggleRightDock);
5997 workspace.update(cx, |workspace, _| {
5998 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
5999 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
6000 });
6001
6002 // Emitting a ZoomOut event unzooms the panel.
6003 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
6004 workspace.update(cx, |workspace, _| {
6005 assert_eq!(workspace.zoomed, None);
6006 assert_eq!(workspace.zoomed_position, None);
6007 });
6008
6009 // Emit closed event on panel 1, which is active
6010 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
6011
6012 // Now the left dock is closed, because panel_1 was the active panel
6013 workspace.update(cx, |workspace, cx| {
6014 let right_dock = workspace.right_dock();
6015 assert!(!right_dock.read(cx).is_open());
6016 });
6017 }
6018
6019 mod register_project_item_tests {
6020 use ui::Context as _;
6021
6022 use super::*;
6023
6024 const TEST_PNG_KIND: &str = "TestPngItemView";
6025 // View
6026 struct TestPngItemView {
6027 focus_handle: FocusHandle,
6028 }
6029 // Model
6030 struct TestPngItem {}
6031
6032 impl project::Item for TestPngItem {
6033 fn try_open(
6034 _project: &Model<Project>,
6035 path: &ProjectPath,
6036 cx: &mut AppContext,
6037 ) -> Option<Task<gpui::Result<Model<Self>>>> {
6038 if path.path.extension().unwrap() == "png" {
6039 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestPngItem {}) }))
6040 } else {
6041 None
6042 }
6043 }
6044
6045 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
6046 None
6047 }
6048
6049 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
6050 None
6051 }
6052 }
6053
6054 impl Item for TestPngItemView {
6055 type Event = ();
6056
6057 fn serialized_item_kind() -> Option<&'static str> {
6058 Some(TEST_PNG_KIND)
6059 }
6060 }
6061 impl EventEmitter<()> for TestPngItemView {}
6062 impl FocusableView for TestPngItemView {
6063 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
6064 self.focus_handle.clone()
6065 }
6066 }
6067
6068 impl Render for TestPngItemView {
6069 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
6070 Empty
6071 }
6072 }
6073
6074 impl ProjectItem for TestPngItemView {
6075 type Item = TestPngItem;
6076
6077 fn for_project_item(
6078 _project: Model<Project>,
6079 _item: Model<Self::Item>,
6080 cx: &mut ViewContext<Self>,
6081 ) -> Self
6082 where
6083 Self: Sized,
6084 {
6085 Self {
6086 focus_handle: cx.focus_handle(),
6087 }
6088 }
6089 }
6090
6091 const TEST_IPYNB_KIND: &str = "TestIpynbItemView";
6092 // View
6093 struct TestIpynbItemView {
6094 focus_handle: FocusHandle,
6095 }
6096 // Model
6097 struct TestIpynbItem {}
6098
6099 impl project::Item for TestIpynbItem {
6100 fn try_open(
6101 _project: &Model<Project>,
6102 path: &ProjectPath,
6103 cx: &mut AppContext,
6104 ) -> Option<Task<gpui::Result<Model<Self>>>> {
6105 if path.path.extension().unwrap() == "ipynb" {
6106 Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestIpynbItem {}) }))
6107 } else {
6108 None
6109 }
6110 }
6111
6112 fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
6113 None
6114 }
6115
6116 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
6117 None
6118 }
6119 }
6120
6121 impl Item for TestIpynbItemView {
6122 type Event = ();
6123
6124 fn serialized_item_kind() -> Option<&'static str> {
6125 Some(TEST_IPYNB_KIND)
6126 }
6127 }
6128 impl EventEmitter<()> for TestIpynbItemView {}
6129 impl FocusableView for TestIpynbItemView {
6130 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
6131 self.focus_handle.clone()
6132 }
6133 }
6134
6135 impl Render for TestIpynbItemView {
6136 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
6137 Empty
6138 }
6139 }
6140
6141 impl ProjectItem for TestIpynbItemView {
6142 type Item = TestIpynbItem;
6143
6144 fn for_project_item(
6145 _project: Model<Project>,
6146 _item: Model<Self::Item>,
6147 cx: &mut ViewContext<Self>,
6148 ) -> Self
6149 where
6150 Self: Sized,
6151 {
6152 Self {
6153 focus_handle: cx.focus_handle(),
6154 }
6155 }
6156 }
6157
6158 struct TestAlternatePngItemView {
6159 focus_handle: FocusHandle,
6160 }
6161
6162 const TEST_ALTERNATE_PNG_KIND: &str = "TestAlternatePngItemView";
6163 impl Item for TestAlternatePngItemView {
6164 type Event = ();
6165
6166 fn serialized_item_kind() -> Option<&'static str> {
6167 Some(TEST_ALTERNATE_PNG_KIND)
6168 }
6169 }
6170 impl EventEmitter<()> for TestAlternatePngItemView {}
6171 impl FocusableView for TestAlternatePngItemView {
6172 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
6173 self.focus_handle.clone()
6174 }
6175 }
6176
6177 impl Render for TestAlternatePngItemView {
6178 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
6179 Empty
6180 }
6181 }
6182
6183 impl ProjectItem for TestAlternatePngItemView {
6184 type Item = TestPngItem;
6185
6186 fn for_project_item(
6187 _project: Model<Project>,
6188 _item: Model<Self::Item>,
6189 cx: &mut ViewContext<Self>,
6190 ) -> Self
6191 where
6192 Self: Sized,
6193 {
6194 Self {
6195 focus_handle: cx.focus_handle(),
6196 }
6197 }
6198 }
6199
6200 #[gpui::test]
6201 async fn test_register_project_item(cx: &mut TestAppContext) {
6202 init_test(cx);
6203
6204 cx.update(|cx| {
6205 register_project_item::<TestPngItemView>(cx);
6206 register_project_item::<TestIpynbItemView>(cx);
6207 });
6208
6209 let fs = FakeFs::new(cx.executor());
6210 fs.insert_tree(
6211 "/root1",
6212 json!({
6213 "one.png": "BINARYDATAHERE",
6214 "two.ipynb": "{ totally a notebook }",
6215 "three.txt": "editing text, sure why not?"
6216 }),
6217 )
6218 .await;
6219
6220 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6221 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6222
6223 let worktree_id = project.update(cx, |project, cx| {
6224 project.worktrees().next().unwrap().read(cx).id()
6225 });
6226
6227 let handle = workspace
6228 .update(cx, |workspace, cx| {
6229 let project_path = (worktree_id, "one.png");
6230 workspace.open_path(project_path, None, true, cx)
6231 })
6232 .await
6233 .unwrap();
6234
6235 // Now we can check if the handle we got back errored or not
6236 assert_eq!(handle.serialized_item_kind().unwrap(), TEST_PNG_KIND);
6237
6238 let handle = workspace
6239 .update(cx, |workspace, cx| {
6240 let project_path = (worktree_id, "two.ipynb");
6241 workspace.open_path(project_path, None, true, cx)
6242 })
6243 .await
6244 .unwrap();
6245
6246 assert_eq!(handle.serialized_item_kind().unwrap(), TEST_IPYNB_KIND);
6247
6248 let handle = workspace
6249 .update(cx, |workspace, cx| {
6250 let project_path = (worktree_id, "three.txt");
6251 workspace.open_path(project_path, None, true, cx)
6252 })
6253 .await;
6254 assert!(handle.is_err());
6255 }
6256
6257 #[gpui::test]
6258 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
6259 init_test(cx);
6260
6261 cx.update(|cx| {
6262 register_project_item::<TestPngItemView>(cx);
6263 register_project_item::<TestAlternatePngItemView>(cx);
6264 });
6265
6266 let fs = FakeFs::new(cx.executor());
6267 fs.insert_tree(
6268 "/root1",
6269 json!({
6270 "one.png": "BINARYDATAHERE",
6271 "two.ipynb": "{ totally a notebook }",
6272 "three.txt": "editing text, sure why not?"
6273 }),
6274 )
6275 .await;
6276
6277 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6278 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6279
6280 let worktree_id = project.update(cx, |project, cx| {
6281 project.worktrees().next().unwrap().read(cx).id()
6282 });
6283
6284 let handle = workspace
6285 .update(cx, |workspace, cx| {
6286 let project_path = (worktree_id, "one.png");
6287 workspace.open_path(project_path, None, true, cx)
6288 })
6289 .await
6290 .unwrap();
6291
6292 // This _must_ be the second item registered
6293 assert_eq!(
6294 handle.serialized_item_kind().unwrap(),
6295 TEST_ALTERNATE_PNG_KIND
6296 );
6297
6298 let handle = workspace
6299 .update(cx, |workspace, cx| {
6300 let project_path = (worktree_id, "three.txt");
6301 workspace.open_path(project_path, None, true, cx)
6302 })
6303 .await;
6304 assert!(handle.is_err());
6305 }
6306 }
6307
6308 pub fn init_test(cx: &mut TestAppContext) {
6309 cx.update(|cx| {
6310 let settings_store = SettingsStore::test(cx);
6311 cx.set_global(settings_store);
6312 theme::init(theme::LoadThemes::JustBase, cx);
6313 language::init(cx);
6314 crate::init_settings(cx);
6315 Project::init_settings(cx);
6316 });
6317 }
6318}