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