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