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