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