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