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