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