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