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