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