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