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