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