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