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