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