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