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 fn local_paths(&self, cx: &App) -> Option<Vec<Arc<Path>>> {
4389 let project = self.project().read(cx);
4390
4391 if project.is_local() {
4392 Some(
4393 project
4394 .visible_worktrees(cx)
4395 .map(|worktree| worktree.read(cx).abs_path())
4396 .collect::<Vec<_>>(),
4397 )
4398 } else {
4399 None
4400 }
4401 }
4402
4403 fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
4404 match member {
4405 Member::Axis(PaneAxis { members, .. }) => {
4406 for child in members.iter() {
4407 self.remove_panes(child.clone(), window, cx)
4408 }
4409 }
4410 Member::Pane(pane) => {
4411 self.force_remove_pane(&pane, &None, window, cx);
4412 }
4413 }
4414 }
4415
4416 fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
4417 self.session_id.take();
4418 self.serialize_workspace_internal(window, cx)
4419 }
4420
4421 fn force_remove_pane(
4422 &mut self,
4423 pane: &Entity<Pane>,
4424 focus_on: &Option<Entity<Pane>>,
4425 window: &mut Window,
4426 cx: &mut Context<Workspace>,
4427 ) {
4428 self.panes.retain(|p| p != pane);
4429 if let Some(focus_on) = focus_on {
4430 focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4431 } else {
4432 if self.active_pane() == pane {
4433 self.panes
4434 .last()
4435 .unwrap()
4436 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4437 }
4438 }
4439 if self.last_active_center_pane == Some(pane.downgrade()) {
4440 self.last_active_center_pane = None;
4441 }
4442 cx.notify();
4443 }
4444
4445 fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4446 if self._schedule_serialize.is_none() {
4447 self._schedule_serialize = Some(cx.spawn_in(window, |this, mut cx| async move {
4448 cx.background_executor()
4449 .timer(Duration::from_millis(100))
4450 .await;
4451 this.update_in(&mut cx, |this, window, cx| {
4452 this.serialize_workspace_internal(window, cx).detach();
4453 this._schedule_serialize.take();
4454 })
4455 .log_err();
4456 }));
4457 }
4458 }
4459
4460 fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
4461 let Some(database_id) = self.database_id() else {
4462 return Task::ready(());
4463 };
4464
4465 fn serialize_pane_handle(
4466 pane_handle: &Entity<Pane>,
4467 window: &mut Window,
4468 cx: &mut App,
4469 ) -> SerializedPane {
4470 let (items, active, pinned_count) = {
4471 let pane = pane_handle.read(cx);
4472 let active_item_id = pane.active_item().map(|item| item.item_id());
4473 (
4474 pane.items()
4475 .filter_map(|handle| {
4476 let handle = handle.to_serializable_item_handle(cx)?;
4477
4478 Some(SerializedItem {
4479 kind: Arc::from(handle.serialized_item_kind()),
4480 item_id: handle.item_id().as_u64(),
4481 active: Some(handle.item_id()) == active_item_id,
4482 preview: pane.is_active_preview_item(handle.item_id()),
4483 })
4484 })
4485 .collect::<Vec<_>>(),
4486 pane.has_focus(window, cx),
4487 pane.pinned_count(),
4488 )
4489 };
4490
4491 SerializedPane::new(items, active, pinned_count)
4492 }
4493
4494 fn build_serialized_pane_group(
4495 pane_group: &Member,
4496 window: &mut Window,
4497 cx: &mut App,
4498 ) -> SerializedPaneGroup {
4499 match pane_group {
4500 Member::Axis(PaneAxis {
4501 axis,
4502 members,
4503 flexes,
4504 bounding_boxes: _,
4505 }) => SerializedPaneGroup::Group {
4506 axis: SerializedAxis(*axis),
4507 children: members
4508 .iter()
4509 .map(|member| build_serialized_pane_group(member, window, cx))
4510 .collect::<Vec<_>>(),
4511 flexes: Some(flexes.lock().clone()),
4512 },
4513 Member::Pane(pane_handle) => {
4514 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
4515 }
4516 }
4517 }
4518
4519 fn build_serialized_docks(
4520 this: &Workspace,
4521 window: &mut Window,
4522 cx: &mut App,
4523 ) -> DockStructure {
4524 let left_dock = this.left_dock.read(cx);
4525 let left_visible = left_dock.is_open();
4526 let left_active_panel = left_dock
4527 .active_panel()
4528 .map(|panel| panel.persistent_name().to_string());
4529 let left_dock_zoom = left_dock
4530 .active_panel()
4531 .map(|panel| panel.is_zoomed(window, cx))
4532 .unwrap_or(false);
4533
4534 let right_dock = this.right_dock.read(cx);
4535 let right_visible = right_dock.is_open();
4536 let right_active_panel = right_dock
4537 .active_panel()
4538 .map(|panel| panel.persistent_name().to_string());
4539 let right_dock_zoom = right_dock
4540 .active_panel()
4541 .map(|panel| panel.is_zoomed(window, cx))
4542 .unwrap_or(false);
4543
4544 let bottom_dock = this.bottom_dock.read(cx);
4545 let bottom_visible = bottom_dock.is_open();
4546 let bottom_active_panel = bottom_dock
4547 .active_panel()
4548 .map(|panel| panel.persistent_name().to_string());
4549 let bottom_dock_zoom = bottom_dock
4550 .active_panel()
4551 .map(|panel| panel.is_zoomed(window, cx))
4552 .unwrap_or(false);
4553
4554 DockStructure {
4555 left: DockData {
4556 visible: left_visible,
4557 active_panel: left_active_panel,
4558 zoom: left_dock_zoom,
4559 },
4560 right: DockData {
4561 visible: right_visible,
4562 active_panel: right_active_panel,
4563 zoom: right_dock_zoom,
4564 },
4565 bottom: DockData {
4566 visible: bottom_visible,
4567 active_panel: bottom_active_panel,
4568 zoom: bottom_dock_zoom,
4569 },
4570 }
4571 }
4572
4573 let location = if let Some(ssh_project) = &self.serialized_ssh_project {
4574 Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
4575 } else if let Some(local_paths) = self.local_paths(cx) {
4576 if !local_paths.is_empty() {
4577 Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4578 } else {
4579 None
4580 }
4581 } else {
4582 None
4583 };
4584
4585 if let Some(location) = location {
4586 let center_group = build_serialized_pane_group(&self.center.root, window, cx);
4587 let docks = build_serialized_docks(self, window, cx);
4588 let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
4589 let serialized_workspace = SerializedWorkspace {
4590 id: database_id,
4591 location,
4592 center_group,
4593 window_bounds,
4594 display: Default::default(),
4595 docks,
4596 centered_layout: self.centered_layout,
4597 session_id: self.session_id.clone(),
4598 window_id: Some(window.window_handle().window_id().as_u64()),
4599 };
4600 return window.spawn(cx, |_| persistence::DB.save_workspace(serialized_workspace));
4601 }
4602 Task::ready(())
4603 }
4604
4605 async fn serialize_items(
4606 this: &WeakEntity<Self>,
4607 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4608 cx: &mut AsyncWindowContext,
4609 ) -> Result<()> {
4610 const CHUNK_SIZE: usize = 200;
4611
4612 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4613
4614 while let Some(items_received) = serializable_items.next().await {
4615 let unique_items =
4616 items_received
4617 .into_iter()
4618 .fold(HashMap::default(), |mut acc, item| {
4619 acc.entry(item.item_id()).or_insert(item);
4620 acc
4621 });
4622
4623 // We use into_iter() here so that the references to the items are moved into
4624 // the tasks and not kept alive while we're sleeping.
4625 for (_, item) in unique_items.into_iter() {
4626 if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
4627 item.serialize(workspace, false, window, cx)
4628 }) {
4629 cx.background_spawn(async move { task.await.log_err() })
4630 .detach();
4631 }
4632 }
4633
4634 cx.background_executor()
4635 .timer(SERIALIZATION_THROTTLE_TIME)
4636 .await;
4637 }
4638
4639 Ok(())
4640 }
4641
4642 pub(crate) fn enqueue_item_serialization(
4643 &mut self,
4644 item: Box<dyn SerializableItemHandle>,
4645 ) -> Result<()> {
4646 self.serializable_items_tx
4647 .unbounded_send(item)
4648 .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4649 }
4650
4651 pub(crate) fn load_workspace(
4652 serialized_workspace: SerializedWorkspace,
4653 paths_to_open: Vec<Option<ProjectPath>>,
4654 window: &mut Window,
4655 cx: &mut Context<Workspace>,
4656 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4657 cx.spawn_in(window, |workspace, mut cx| async move {
4658 let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
4659
4660 let mut center_group = None;
4661 let mut center_items = None;
4662
4663 // Traverse the splits tree and add to things
4664 if let Some((group, active_pane, items)) = serialized_workspace
4665 .center_group
4666 .deserialize(
4667 &project,
4668 serialized_workspace.id,
4669 workspace.clone(),
4670 &mut cx,
4671 )
4672 .await
4673 {
4674 center_items = Some(items);
4675 center_group = Some((group, active_pane))
4676 }
4677
4678 let mut items_by_project_path = HashMap::default();
4679 let mut item_ids_by_kind = HashMap::default();
4680 let mut all_deserialized_items = Vec::default();
4681 cx.update(|_, cx| {
4682 for item in center_items.unwrap_or_default().into_iter().flatten() {
4683 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4684 item_ids_by_kind
4685 .entry(serializable_item_handle.serialized_item_kind())
4686 .or_insert(Vec::new())
4687 .push(item.item_id().as_u64() as ItemId);
4688 }
4689
4690 if let Some(project_path) = item.project_path(cx) {
4691 items_by_project_path.insert(project_path, item.clone());
4692 }
4693 all_deserialized_items.push(item);
4694 }
4695 })?;
4696
4697 let opened_items = paths_to_open
4698 .into_iter()
4699 .map(|path_to_open| {
4700 path_to_open
4701 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4702 })
4703 .collect::<Vec<_>>();
4704
4705 // Remove old panes from workspace panes list
4706 workspace.update_in(&mut cx, |workspace, window, cx| {
4707 if let Some((center_group, active_pane)) = center_group {
4708 workspace.remove_panes(workspace.center.root.clone(), window, cx);
4709
4710 // Swap workspace center group
4711 workspace.center = PaneGroup::with_root(center_group);
4712 if let Some(active_pane) = active_pane {
4713 workspace.set_active_pane(&active_pane, window, cx);
4714 cx.focus_self(window);
4715 } else {
4716 workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
4717 }
4718 }
4719
4720 let docks = serialized_workspace.docks;
4721
4722 for (dock, serialized_dock) in [
4723 (&mut workspace.right_dock, docks.right),
4724 (&mut workspace.left_dock, docks.left),
4725 (&mut workspace.bottom_dock, docks.bottom),
4726 ]
4727 .iter_mut()
4728 {
4729 dock.update(cx, |dock, cx| {
4730 dock.serialized_dock = Some(serialized_dock.clone());
4731 dock.restore_state(window, cx);
4732 });
4733 }
4734
4735 cx.notify();
4736 })?;
4737
4738 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4739 // after loading the items, we might have different items and in order to avoid
4740 // the database filling up, we delete items that haven't been loaded now.
4741 //
4742 // The items that have been loaded, have been saved after they've been added to the workspace.
4743 let clean_up_tasks = workspace.update_in(&mut cx, |_, window, cx| {
4744 item_ids_by_kind
4745 .into_iter()
4746 .map(|(item_kind, loaded_items)| {
4747 SerializableItemRegistry::cleanup(
4748 item_kind,
4749 serialized_workspace.id,
4750 loaded_items,
4751 window,
4752 cx,
4753 )
4754 .log_err()
4755 })
4756 .collect::<Vec<_>>()
4757 })?;
4758
4759 futures::future::join_all(clean_up_tasks).await;
4760
4761 workspace
4762 .update_in(&mut cx, |workspace, window, cx| {
4763 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4764 workspace.serialize_workspace_internal(window, cx).detach();
4765
4766 // Ensure that we mark the window as edited if we did load dirty items
4767 workspace.update_window_edited(window, cx);
4768 })
4769 .ok();
4770
4771 Ok(opened_items)
4772 })
4773 }
4774
4775 fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
4776 self.add_workspace_actions_listeners(div, window, cx)
4777 .on_action(cx.listener(Self::close_inactive_items_and_panes))
4778 .on_action(cx.listener(Self::close_all_items_and_panes))
4779 .on_action(cx.listener(Self::save_all))
4780 .on_action(cx.listener(Self::send_keystrokes))
4781 .on_action(cx.listener(Self::add_folder_to_project))
4782 .on_action(cx.listener(Self::follow_next_collaborator))
4783 .on_action(cx.listener(Self::close_window))
4784 .on_action(cx.listener(Self::activate_pane_at_index))
4785 .on_action(cx.listener(Self::move_item_to_pane_at_index))
4786 .on_action(cx.listener(Self::move_focused_panel_to_next_position))
4787 .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
4788 let pane = workspace.active_pane().clone();
4789 workspace.unfollow_in_pane(&pane, window, cx);
4790 }))
4791 .on_action(cx.listener(|workspace, action: &Save, window, cx| {
4792 workspace
4793 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
4794 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4795 }))
4796 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
4797 workspace
4798 .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
4799 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4800 }))
4801 .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
4802 workspace
4803 .save_active_item(SaveIntent::SaveAs, window, cx)
4804 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4805 }))
4806 .on_action(
4807 cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
4808 workspace.activate_previous_pane(window, cx)
4809 }),
4810 )
4811 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
4812 workspace.activate_next_pane(window, cx)
4813 }))
4814 .on_action(
4815 cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
4816 workspace.activate_next_window(cx)
4817 }),
4818 )
4819 .on_action(
4820 cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
4821 workspace.activate_previous_window(cx)
4822 }),
4823 )
4824 .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
4825 workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
4826 }))
4827 .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
4828 workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
4829 }))
4830 .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
4831 workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
4832 }))
4833 .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
4834 workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
4835 }))
4836 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
4837 workspace.activate_next_pane(window, cx)
4838 }))
4839 .on_action(cx.listener(
4840 |workspace, action: &MoveItemToPaneInDirection, window, cx| {
4841 workspace.move_item_to_pane_in_direction(action, window, cx)
4842 },
4843 ))
4844 .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
4845 workspace.swap_pane_in_direction(SplitDirection::Left, cx)
4846 }))
4847 .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
4848 workspace.swap_pane_in_direction(SplitDirection::Right, cx)
4849 }))
4850 .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
4851 workspace.swap_pane_in_direction(SplitDirection::Up, cx)
4852 }))
4853 .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
4854 workspace.swap_pane_in_direction(SplitDirection::Down, cx)
4855 }))
4856 .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
4857 this.toggle_dock(DockPosition::Left, window, cx);
4858 }))
4859 .on_action(cx.listener(
4860 |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
4861 workspace.toggle_dock(DockPosition::Right, window, cx);
4862 },
4863 ))
4864 .on_action(cx.listener(
4865 |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
4866 workspace.toggle_dock(DockPosition::Bottom, window, cx);
4867 },
4868 ))
4869 .on_action(
4870 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
4871 workspace.close_all_docks(window, cx);
4872 }),
4873 )
4874 .on_action(cx.listener(
4875 |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
4876 workspace.clear_all_notifications(cx);
4877 },
4878 ))
4879 .on_action(cx.listener(
4880 |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
4881 workspace.reopen_closed_item(window, cx).detach();
4882 },
4883 ))
4884 .on_action(cx.listener(Workspace::toggle_centered_layout))
4885 }
4886
4887 #[cfg(any(test, feature = "test-support"))]
4888 pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
4889 use node_runtime::NodeRuntime;
4890 use session::Session;
4891
4892 let client = project.read(cx).client();
4893 let user_store = project.read(cx).user_store();
4894
4895 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
4896 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
4897 window.activate_window();
4898 let app_state = Arc::new(AppState {
4899 languages: project.read(cx).languages().clone(),
4900 workspace_store,
4901 client,
4902 user_store,
4903 fs: project.read(cx).fs().clone(),
4904 build_window_options: |_, _| Default::default(),
4905 node_runtime: NodeRuntime::unavailable(),
4906 session,
4907 });
4908 let workspace = Self::new(Default::default(), project, app_state, window, cx);
4909 workspace
4910 .active_pane
4911 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4912 workspace
4913 }
4914
4915 pub fn register_action<A: Action>(
4916 &mut self,
4917 callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
4918 ) -> &mut Self {
4919 let callback = Arc::new(callback);
4920
4921 self.workspace_actions.push(Box::new(move |div, _, cx| {
4922 let callback = callback.clone();
4923 div.on_action(cx.listener(move |workspace, event, window, cx| {
4924 (callback.clone())(workspace, event, window, cx)
4925 }))
4926 }));
4927 self
4928 }
4929
4930 fn add_workspace_actions_listeners(
4931 &self,
4932 mut div: Div,
4933 window: &mut Window,
4934 cx: &mut Context<Self>,
4935 ) -> Div {
4936 for action in self.workspace_actions.iter() {
4937 div = (action)(div, window, cx)
4938 }
4939 div
4940 }
4941
4942 pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
4943 self.modal_layer.read(cx).has_active_modal()
4944 }
4945
4946 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
4947 self.modal_layer.read(cx).active_modal()
4948 }
4949
4950 pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
4951 where
4952 B: FnOnce(&mut Window, &mut Context<V>) -> V,
4953 {
4954 self.modal_layer.update(cx, |modal_layer, cx| {
4955 modal_layer.toggle_modal(window, cx, build)
4956 })
4957 }
4958
4959 pub fn toggle_centered_layout(
4960 &mut self,
4961 _: &ToggleCenteredLayout,
4962 _: &mut Window,
4963 cx: &mut Context<Self>,
4964 ) {
4965 self.centered_layout = !self.centered_layout;
4966 if let Some(database_id) = self.database_id() {
4967 cx.background_spawn(DB.set_centered_layout(database_id, self.centered_layout))
4968 .detach_and_log_err(cx);
4969 }
4970 cx.notify();
4971 }
4972
4973 fn adjust_padding(padding: Option<f32>) -> f32 {
4974 padding
4975 .unwrap_or(Self::DEFAULT_PADDING)
4976 .clamp(0.0, Self::MAX_PADDING)
4977 }
4978
4979 fn render_dock(
4980 &self,
4981 position: DockPosition,
4982 dock: &Entity<Dock>,
4983 window: &mut Window,
4984 cx: &mut App,
4985 ) -> Option<Div> {
4986 if self.zoomed_position == Some(position) {
4987 return None;
4988 }
4989
4990 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
4991 let pane = panel.pane(cx)?;
4992 let follower_states = &self.follower_states;
4993 leader_border_for_pane(follower_states, &pane, window, cx)
4994 });
4995
4996 Some(
4997 div()
4998 .flex()
4999 .flex_none()
5000 .overflow_hidden()
5001 .child(dock.clone())
5002 .children(leader_border),
5003 )
5004 }
5005
5006 pub fn for_window(window: &mut Window, _: &mut App) -> Option<Entity<Workspace>> {
5007 window.root().flatten()
5008 }
5009
5010 pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
5011 self.zoomed.as_ref()
5012 }
5013
5014 pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
5015 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5016 return;
5017 };
5018 let windows = cx.windows();
5019 let Some(next_window) = windows
5020 .iter()
5021 .cycle()
5022 .skip_while(|window| window.window_id() != current_window_id)
5023 .nth(1)
5024 else {
5025 return;
5026 };
5027 next_window
5028 .update(cx, |_, window, _| window.activate_window())
5029 .ok();
5030 }
5031
5032 pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
5033 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5034 return;
5035 };
5036 let windows = cx.windows();
5037 let Some(prev_window) = windows
5038 .iter()
5039 .rev()
5040 .cycle()
5041 .skip_while(|window| window.window_id() != current_window_id)
5042 .nth(1)
5043 else {
5044 return;
5045 };
5046 prev_window
5047 .update(cx, |_, window, _| window.activate_window())
5048 .ok();
5049 }
5050}
5051
5052fn leader_border_for_pane(
5053 follower_states: &HashMap<PeerId, FollowerState>,
5054 pane: &Entity<Pane>,
5055 _: &Window,
5056 cx: &App,
5057) -> Option<Div> {
5058 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
5059 if state.pane() == pane {
5060 Some((*leader_id, state))
5061 } else {
5062 None
5063 }
5064 })?;
5065
5066 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
5067 let leader = room.remote_participant_for_peer_id(leader_id)?;
5068
5069 let mut leader_color = cx
5070 .theme()
5071 .players()
5072 .color_for_participant(leader.participant_index.0)
5073 .cursor;
5074 leader_color.fade_out(0.3);
5075 Some(
5076 div()
5077 .absolute()
5078 .size_full()
5079 .left_0()
5080 .top_0()
5081 .border_2()
5082 .border_color(leader_color),
5083 )
5084}
5085
5086fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
5087 ZED_WINDOW_POSITION
5088 .zip(*ZED_WINDOW_SIZE)
5089 .map(|(position, size)| Bounds {
5090 origin: position,
5091 size,
5092 })
5093}
5094
5095fn open_items(
5096 serialized_workspace: Option<SerializedWorkspace>,
5097 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
5098 window: &mut Window,
5099 cx: &mut Context<Workspace>,
5100) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
5101 let restored_items = serialized_workspace.map(|serialized_workspace| {
5102 Workspace::load_workspace(
5103 serialized_workspace,
5104 project_paths_to_open
5105 .iter()
5106 .map(|(_, project_path)| project_path)
5107 .cloned()
5108 .collect(),
5109 window,
5110 cx,
5111 )
5112 });
5113
5114 cx.spawn_in(window, |workspace, mut cx| async move {
5115 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
5116
5117 if let Some(restored_items) = restored_items {
5118 let restored_items = restored_items.await?;
5119
5120 let restored_project_paths = restored_items
5121 .iter()
5122 .filter_map(|item| {
5123 cx.update(|_, cx| item.as_ref()?.project_path(cx))
5124 .ok()
5125 .flatten()
5126 })
5127 .collect::<HashSet<_>>();
5128
5129 for restored_item in restored_items {
5130 opened_items.push(restored_item.map(Ok));
5131 }
5132
5133 project_paths_to_open
5134 .iter_mut()
5135 .for_each(|(_, project_path)| {
5136 if let Some(project_path_to_open) = project_path {
5137 if restored_project_paths.contains(project_path_to_open) {
5138 *project_path = None;
5139 }
5140 }
5141 });
5142 } else {
5143 for _ in 0..project_paths_to_open.len() {
5144 opened_items.push(None);
5145 }
5146 }
5147 assert!(opened_items.len() == project_paths_to_open.len());
5148
5149 let tasks =
5150 project_paths_to_open
5151 .into_iter()
5152 .enumerate()
5153 .map(|(ix, (abs_path, project_path))| {
5154 let workspace = workspace.clone();
5155 cx.spawn(|mut cx| async move {
5156 let file_project_path = project_path?;
5157 let abs_path_task = workspace.update(&mut cx, |workspace, cx| {
5158 workspace.project().update(cx, |project, cx| {
5159 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
5160 })
5161 });
5162
5163 // We only want to open file paths here. If one of the items
5164 // here is a directory, it was already opened further above
5165 // with a `find_or_create_worktree`.
5166 if let Ok(task) = abs_path_task {
5167 if task.await.map_or(true, |p| p.is_file()) {
5168 return Some((
5169 ix,
5170 workspace
5171 .update_in(&mut cx, |workspace, window, cx| {
5172 workspace.open_path(
5173 file_project_path,
5174 None,
5175 true,
5176 window,
5177 cx,
5178 )
5179 })
5180 .log_err()?
5181 .await,
5182 ));
5183 }
5184 }
5185 None
5186 })
5187 });
5188
5189 let tasks = tasks.collect::<Vec<_>>();
5190
5191 let tasks = futures::future::join_all(tasks);
5192 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
5193 opened_items[ix] = Some(path_open_result);
5194 }
5195
5196 Ok(opened_items)
5197 })
5198}
5199
5200enum ActivateInDirectionTarget {
5201 Pane(Entity<Pane>),
5202 Dock(Entity<Dock>),
5203}
5204
5205fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncApp) {
5206 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";
5207
5208 workspace
5209 .update(cx, |workspace, _, cx| {
5210 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
5211 struct DatabaseFailedNotification;
5212
5213 workspace.show_notification(
5214 NotificationId::unique::<DatabaseFailedNotification>(),
5215 cx,
5216 |cx| {
5217 cx.new(|_| {
5218 MessageNotification::new("Failed to load the database file.")
5219 .primary_message("File an Issue")
5220 .primary_icon(IconName::Plus)
5221 .primary_on_click(|_window, cx| cx.open_url(REPORT_ISSUE_URL))
5222 })
5223 },
5224 );
5225 }
5226 })
5227 .log_err();
5228}
5229
5230impl Focusable for Workspace {
5231 fn focus_handle(&self, cx: &App) -> FocusHandle {
5232 self.active_pane.focus_handle(cx)
5233 }
5234}
5235
5236#[derive(Clone)]
5237struct DraggedDock(DockPosition);
5238
5239impl Render for DraggedDock {
5240 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5241 gpui::Empty
5242 }
5243}
5244
5245impl Render for Workspace {
5246 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5247 let mut context = KeyContext::new_with_defaults();
5248 context.add("Workspace");
5249 context.set("keyboard_layout", cx.keyboard_layout().clone());
5250 let centered_layout = self.centered_layout
5251 && self.center.panes().len() == 1
5252 && self.active_item(cx).is_some();
5253 let render_padding = |size| {
5254 (size > 0.0).then(|| {
5255 div()
5256 .h_full()
5257 .w(relative(size))
5258 .bg(cx.theme().colors().editor_background)
5259 .border_color(cx.theme().colors().pane_group_border)
5260 })
5261 };
5262 let paddings = if centered_layout {
5263 let settings = WorkspaceSettings::get_global(cx).centered_layout;
5264 (
5265 render_padding(Self::adjust_padding(settings.left_padding)),
5266 render_padding(Self::adjust_padding(settings.right_padding)),
5267 )
5268 } else {
5269 (None, None)
5270 };
5271 let ui_font = theme::setup_ui_font(window, cx);
5272
5273 let theme = cx.theme().clone();
5274 let colors = theme.colors();
5275
5276 client_side_decorations(
5277 self.actions(div(), window, cx)
5278 .key_context(context)
5279 .relative()
5280 .size_full()
5281 .flex()
5282 .flex_col()
5283 .font(ui_font)
5284 .gap_0()
5285 .justify_start()
5286 .items_start()
5287 .text_color(colors.text)
5288 .overflow_hidden()
5289 .children(self.titlebar_item.clone())
5290 .child(
5291 div()
5292 .size_full()
5293 .relative()
5294 .flex_1()
5295 .flex()
5296 .flex_col()
5297 .child(
5298 div()
5299 .id("workspace")
5300 .bg(colors.background)
5301 .relative()
5302 .flex_1()
5303 .w_full()
5304 .flex()
5305 .flex_col()
5306 .overflow_hidden()
5307 .border_t_1()
5308 .border_b_1()
5309 .border_color(colors.border)
5310 .child({
5311 let this = cx.entity().clone();
5312 canvas(
5313 move |bounds, window, cx| {
5314 this.update(cx, |this, cx| {
5315 let bounds_changed = this.bounds != bounds;
5316 this.bounds = bounds;
5317
5318 if bounds_changed {
5319 this.left_dock.update(cx, |dock, cx| {
5320 dock.clamp_panel_size(
5321 bounds.size.width,
5322 window,
5323 cx,
5324 )
5325 });
5326
5327 this.right_dock.update(cx, |dock, cx| {
5328 dock.clamp_panel_size(
5329 bounds.size.width,
5330 window,
5331 cx,
5332 )
5333 });
5334
5335 this.bottom_dock.update(cx, |dock, cx| {
5336 dock.clamp_panel_size(
5337 bounds.size.height,
5338 window,
5339 cx,
5340 )
5341 });
5342 }
5343 })
5344 },
5345 |_, _, _, _| {},
5346 )
5347 .absolute()
5348 .size_full()
5349 })
5350 .when(self.zoomed.is_none(), |this| {
5351 this.on_drag_move(cx.listener(
5352 move |workspace,
5353 e: &DragMoveEvent<DraggedDock>,
5354 window,
5355 cx| {
5356 if workspace.previous_dock_drag_coordinates
5357 != Some(e.event.position)
5358 {
5359 workspace.previous_dock_drag_coordinates =
5360 Some(e.event.position);
5361 match e.drag(cx).0 {
5362 DockPosition::Left => {
5363 resize_left_dock(
5364 e.event.position.x
5365 - workspace.bounds.left(),
5366 workspace,
5367 window,
5368 cx,
5369 );
5370 }
5371 DockPosition::Right => {
5372 resize_right_dock(
5373 workspace.bounds.right()
5374 - e.event.position.x,
5375 workspace,
5376 window,
5377 cx,
5378 );
5379 }
5380 DockPosition::Bottom => {
5381 resize_bottom_dock(
5382 workspace.bounds.bottom()
5383 - e.event.position.y,
5384 workspace,
5385 window,
5386 cx,
5387 );
5388 }
5389 };
5390 workspace.serialize_workspace(window, cx);
5391 }
5392 },
5393 ))
5394 })
5395 .child(
5396 div()
5397 .flex()
5398 .flex_row()
5399 .h_full()
5400 // Left Dock
5401 .children(self.render_dock(
5402 DockPosition::Left,
5403 &self.left_dock,
5404 window,
5405 cx,
5406 ))
5407 // Panes
5408 .child(
5409 div()
5410 .flex()
5411 .flex_col()
5412 .flex_1()
5413 .overflow_hidden()
5414 .child(
5415 h_flex()
5416 .flex_1()
5417 .when_some(paddings.0, |this, p| {
5418 this.child(p.border_r_1())
5419 })
5420 .child(self.center.render(
5421 &self.project,
5422 &self.follower_states,
5423 self.active_call(),
5424 &self.active_pane,
5425 self.zoomed.as_ref(),
5426 &self.app_state,
5427 window,
5428 cx,
5429 ))
5430 .when_some(paddings.1, |this, p| {
5431 this.child(p.border_l_1())
5432 }),
5433 )
5434 .children(self.render_dock(
5435 DockPosition::Bottom,
5436 &self.bottom_dock,
5437 window,
5438 cx,
5439 )),
5440 )
5441 // Right Dock
5442 .children(self.render_dock(
5443 DockPosition::Right,
5444 &self.right_dock,
5445 window,
5446 cx,
5447 )),
5448 )
5449 .children(self.zoomed.as_ref().and_then(|view| {
5450 let zoomed_view = view.upgrade()?;
5451 let div = div()
5452 .occlude()
5453 .absolute()
5454 .overflow_hidden()
5455 .border_color(colors.border)
5456 .bg(colors.background)
5457 .child(zoomed_view)
5458 .inset_0()
5459 .shadow_lg();
5460
5461 Some(match self.zoomed_position {
5462 Some(DockPosition::Left) => div.right_2().border_r_1(),
5463 Some(DockPosition::Right) => div.left_2().border_l_1(),
5464 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
5465 None => {
5466 div.top_2().bottom_2().left_2().right_2().border_1()
5467 }
5468 })
5469 }))
5470 .children(self.render_notifications(window, cx)),
5471 )
5472 .child(self.status_bar.clone())
5473 .child(self.modal_layer.clone()),
5474 ),
5475 window,
5476 cx,
5477 )
5478 }
5479}
5480
5481fn resize_bottom_dock(
5482 new_size: Pixels,
5483 workspace: &mut Workspace,
5484 window: &mut Window,
5485 cx: &mut App,
5486) {
5487 let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
5488 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
5489 bottom_dock.resize_active_panel(Some(size), window, cx);
5490 });
5491}
5492
5493fn resize_right_dock(
5494 new_size: Pixels,
5495 workspace: &mut Workspace,
5496 window: &mut Window,
5497 cx: &mut App,
5498) {
5499 let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
5500 workspace.right_dock.update(cx, |right_dock, cx| {
5501 right_dock.resize_active_panel(Some(size), window, cx);
5502 });
5503}
5504
5505fn resize_left_dock(
5506 new_size: Pixels,
5507 workspace: &mut Workspace,
5508 window: &mut Window,
5509 cx: &mut App,
5510) {
5511 let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
5512
5513 workspace.left_dock.update(cx, |left_dock, cx| {
5514 left_dock.resize_active_panel(Some(size), window, cx);
5515 });
5516}
5517
5518impl WorkspaceStore {
5519 pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
5520 Self {
5521 workspaces: Default::default(),
5522 _subscriptions: vec![
5523 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
5524 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
5525 ],
5526 client,
5527 }
5528 }
5529
5530 pub fn update_followers(
5531 &self,
5532 project_id: Option<u64>,
5533 update: proto::update_followers::Variant,
5534 cx: &App,
5535 ) -> Option<()> {
5536 let active_call = ActiveCall::try_global(cx)?;
5537 let room_id = active_call.read(cx).room()?.read(cx).id();
5538 self.client
5539 .send(proto::UpdateFollowers {
5540 room_id,
5541 project_id,
5542 variant: Some(update),
5543 })
5544 .log_err()
5545 }
5546
5547 pub async fn handle_follow(
5548 this: Entity<Self>,
5549 envelope: TypedEnvelope<proto::Follow>,
5550 mut cx: AsyncApp,
5551 ) -> Result<proto::FollowResponse> {
5552 this.update(&mut cx, |this, cx| {
5553 let follower = Follower {
5554 project_id: envelope.payload.project_id,
5555 peer_id: envelope.original_sender_id()?,
5556 };
5557
5558 let mut response = proto::FollowResponse::default();
5559 this.workspaces.retain(|workspace| {
5560 workspace
5561 .update(cx, |workspace, window, cx| {
5562 let handler_response =
5563 workspace.handle_follow(follower.project_id, window, cx);
5564 if let Some(active_view) = handler_response.active_view.clone() {
5565 if workspace.project.read(cx).remote_id() == follower.project_id {
5566 response.active_view = Some(active_view)
5567 }
5568 }
5569 })
5570 .is_ok()
5571 });
5572
5573 Ok(response)
5574 })?
5575 }
5576
5577 async fn handle_update_followers(
5578 this: Entity<Self>,
5579 envelope: TypedEnvelope<proto::UpdateFollowers>,
5580 mut cx: AsyncApp,
5581 ) -> Result<()> {
5582 let leader_id = envelope.original_sender_id()?;
5583 let update = envelope.payload;
5584
5585 this.update(&mut cx, |this, cx| {
5586 this.workspaces.retain(|workspace| {
5587 workspace
5588 .update(cx, |workspace, window, cx| {
5589 let project_id = workspace.project.read(cx).remote_id();
5590 if update.project_id != project_id && update.project_id.is_some() {
5591 return;
5592 }
5593 workspace.handle_update_followers(leader_id, update.clone(), window, cx);
5594 })
5595 .is_ok()
5596 });
5597 Ok(())
5598 })?
5599 }
5600}
5601
5602impl ViewId {
5603 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5604 Ok(Self {
5605 creator: message
5606 .creator
5607 .ok_or_else(|| anyhow!("creator is missing"))?,
5608 id: message.id,
5609 })
5610 }
5611
5612 pub(crate) fn to_proto(self) -> proto::ViewId {
5613 proto::ViewId {
5614 creator: Some(self.creator),
5615 id: self.id,
5616 }
5617 }
5618}
5619
5620impl FollowerState {
5621 fn pane(&self) -> &Entity<Pane> {
5622 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5623 }
5624}
5625
5626pub trait WorkspaceHandle {
5627 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
5628}
5629
5630impl WorkspaceHandle for Entity<Workspace> {
5631 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
5632 self.read(cx)
5633 .worktrees(cx)
5634 .flat_map(|worktree| {
5635 let worktree_id = worktree.read(cx).id();
5636 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5637 worktree_id,
5638 path: f.path.clone(),
5639 })
5640 })
5641 .collect::<Vec<_>>()
5642 }
5643}
5644
5645impl std::fmt::Debug for OpenPaths {
5646 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5647 f.debug_struct("OpenPaths")
5648 .field("paths", &self.paths)
5649 .finish()
5650 }
5651}
5652
5653pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5654 DB.last_workspace().await.log_err().flatten()
5655}
5656
5657pub fn last_session_workspace_locations(
5658 last_session_id: &str,
5659 last_session_window_stack: Option<Vec<WindowId>>,
5660) -> Option<Vec<SerializedWorkspaceLocation>> {
5661 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5662 .log_err()
5663}
5664
5665actions!(collab, [OpenChannelNotes]);
5666actions!(zed, [OpenLog]);
5667
5668async fn join_channel_internal(
5669 channel_id: ChannelId,
5670 app_state: &Arc<AppState>,
5671 requesting_window: Option<WindowHandle<Workspace>>,
5672 active_call: &Entity<ActiveCall>,
5673 cx: &mut AsyncApp,
5674) -> Result<bool> {
5675 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5676 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5677 return (false, None);
5678 };
5679
5680 let already_in_channel = room.channel_id() == Some(channel_id);
5681 let should_prompt = room.is_sharing_project()
5682 && !room.remote_participants().is_empty()
5683 && !already_in_channel;
5684 let open_room = if already_in_channel {
5685 active_call.room().cloned()
5686 } else {
5687 None
5688 };
5689 (should_prompt, open_room)
5690 })?;
5691
5692 if let Some(room) = open_room {
5693 let task = room.update(cx, |room, cx| {
5694 if let Some((project, host)) = room.most_active_project(cx) {
5695 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5696 }
5697
5698 None
5699 })?;
5700 if let Some(task) = task {
5701 task.await?;
5702 }
5703 return anyhow::Ok(true);
5704 }
5705
5706 if should_prompt {
5707 if let Some(workspace) = requesting_window {
5708 let answer = workspace
5709 .update(cx, |_, window, cx| {
5710 window.prompt(
5711 PromptLevel::Warning,
5712 "Do you want to switch channels?",
5713 Some("Leaving this call will unshare your current project."),
5714 &["Yes, Join Channel", "Cancel"],
5715 cx,
5716 )
5717 })?
5718 .await;
5719
5720 if answer == Ok(1) {
5721 return Ok(false);
5722 }
5723 } else {
5724 return Ok(false); // unreachable!() hopefully
5725 }
5726 }
5727
5728 let client = cx.update(|cx| active_call.read(cx).client())?;
5729
5730 let mut client_status = client.status();
5731
5732 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5733 'outer: loop {
5734 let Some(status) = client_status.recv().await else {
5735 return Err(anyhow!("error connecting"));
5736 };
5737
5738 match status {
5739 Status::Connecting
5740 | Status::Authenticating
5741 | Status::Reconnecting
5742 | Status::Reauthenticating => continue,
5743 Status::Connected { .. } => break 'outer,
5744 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5745 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5746 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5747 return Err(ErrorCode::Disconnected.into());
5748 }
5749 }
5750 }
5751
5752 let room = active_call
5753 .update(cx, |active_call, cx| {
5754 active_call.join_channel(channel_id, cx)
5755 })?
5756 .await?;
5757
5758 let Some(room) = room else {
5759 return anyhow::Ok(true);
5760 };
5761
5762 room.update(cx, |room, _| room.room_update_completed())?
5763 .await;
5764
5765 let task = room.update(cx, |room, cx| {
5766 if let Some((project, host)) = room.most_active_project(cx) {
5767 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5768 }
5769
5770 // If you are the first to join a channel, see if you should share your project.
5771 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5772 if let Some(workspace) = requesting_window {
5773 let project = workspace.update(cx, |workspace, _, cx| {
5774 let project = workspace.project.read(cx);
5775
5776 if !CallSettings::get_global(cx).share_on_join {
5777 return None;
5778 }
5779
5780 if (project.is_local() || project.is_via_ssh())
5781 && project.visible_worktrees(cx).any(|tree| {
5782 tree.read(cx)
5783 .root_entry()
5784 .map_or(false, |entry| entry.is_dir())
5785 })
5786 {
5787 Some(workspace.project.clone())
5788 } else {
5789 None
5790 }
5791 });
5792 if let Ok(Some(project)) = project {
5793 return Some(cx.spawn(|room, mut cx| async move {
5794 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5795 .await?;
5796 Ok(())
5797 }));
5798 }
5799 }
5800 }
5801
5802 None
5803 })?;
5804 if let Some(task) = task {
5805 task.await?;
5806 return anyhow::Ok(true);
5807 }
5808 anyhow::Ok(false)
5809}
5810
5811pub fn join_channel(
5812 channel_id: ChannelId,
5813 app_state: Arc<AppState>,
5814 requesting_window: Option<WindowHandle<Workspace>>,
5815 cx: &mut App,
5816) -> Task<Result<()>> {
5817 let active_call = ActiveCall::global(cx);
5818 cx.spawn(|mut cx| async move {
5819 let result = join_channel_internal(
5820 channel_id,
5821 &app_state,
5822 requesting_window,
5823 &active_call,
5824 &mut cx,
5825 )
5826 .await;
5827
5828 // join channel succeeded, and opened a window
5829 if matches!(result, Ok(true)) {
5830 return anyhow::Ok(());
5831 }
5832
5833 // find an existing workspace to focus and show call controls
5834 let mut active_window =
5835 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5836 if active_window.is_none() {
5837 // no open workspaces, make one to show the error in (blergh)
5838 let (window_handle, _) = cx
5839 .update(|cx| {
5840 Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5841 })?
5842 .await?;
5843
5844 if result.is_ok() {
5845 cx.update(|cx| {
5846 cx.dispatch_action(&OpenChannelNotes);
5847 }).log_err();
5848 }
5849
5850 active_window = Some(window_handle);
5851 }
5852
5853 if let Err(err) = result {
5854 log::error!("failed to join channel: {}", err);
5855 if let Some(active_window) = active_window {
5856 active_window
5857 .update(&mut cx, |_, window, cx| {
5858 let detail: SharedString = match err.error_code() {
5859 ErrorCode::SignedOut => {
5860 "Please sign in to continue.".into()
5861 }
5862 ErrorCode::UpgradeRequired => {
5863 "Your are running an unsupported version of Zed. Please update to continue.".into()
5864 }
5865 ErrorCode::NoSuchChannel => {
5866 "No matching channel was found. Please check the link and try again.".into()
5867 }
5868 ErrorCode::Forbidden => {
5869 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5870 }
5871 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5872 _ => format!("{}\n\nPlease try again.", err).into(),
5873 };
5874 window.prompt(
5875 PromptLevel::Critical,
5876 "Failed to join channel",
5877 Some(&detail),
5878 &["Ok"],
5879 cx)
5880 })?
5881 .await
5882 .ok();
5883 }
5884 }
5885
5886 // return ok, we showed the error to the user.
5887 anyhow::Ok(())
5888 })
5889}
5890
5891pub async fn get_any_active_workspace(
5892 app_state: Arc<AppState>,
5893 mut cx: AsyncApp,
5894) -> anyhow::Result<WindowHandle<Workspace>> {
5895 // find an existing workspace to focus and show call controls
5896 let active_window = activate_any_workspace_window(&mut cx);
5897 if active_window.is_none() {
5898 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5899 .await?;
5900 }
5901 activate_any_workspace_window(&mut cx).context("could not open zed")
5902}
5903
5904fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
5905 cx.update(|cx| {
5906 if let Some(workspace_window) = cx
5907 .active_window()
5908 .and_then(|window| window.downcast::<Workspace>())
5909 {
5910 return Some(workspace_window);
5911 }
5912
5913 for window in cx.windows() {
5914 if let Some(workspace_window) = window.downcast::<Workspace>() {
5915 workspace_window
5916 .update(cx, |_, window, _| window.activate_window())
5917 .ok();
5918 return Some(workspace_window);
5919 }
5920 }
5921 None
5922 })
5923 .ok()
5924 .flatten()
5925}
5926
5927pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
5928 cx.windows()
5929 .into_iter()
5930 .filter_map(|window| window.downcast::<Workspace>())
5931 .filter(|workspace| {
5932 workspace
5933 .read(cx)
5934 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5935 })
5936 .collect()
5937}
5938
5939#[derive(Default)]
5940pub struct OpenOptions {
5941 pub open_new_workspace: Option<bool>,
5942 pub replace_window: Option<WindowHandle<Workspace>>,
5943 pub env: Option<HashMap<String, String>>,
5944}
5945#[allow(clippy::type_complexity)]
5946pub fn open_paths(
5947 abs_paths: &[PathBuf],
5948 app_state: Arc<AppState>,
5949 open_options: OpenOptions,
5950 cx: &mut App,
5951) -> Task<
5952 anyhow::Result<(
5953 WindowHandle<Workspace>,
5954 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5955 )>,
5956> {
5957 let abs_paths = abs_paths.to_vec();
5958 let mut existing = None;
5959 let mut best_match = None;
5960 let mut open_visible = OpenVisible::All;
5961
5962 cx.spawn(move |mut cx| async move {
5963 if open_options.open_new_workspace != Some(true) {
5964 let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5965 let all_metadatas = futures::future::join_all(all_paths)
5966 .await
5967 .into_iter()
5968 .filter_map(|result| result.ok().flatten())
5969 .collect::<Vec<_>>();
5970
5971 cx.update(|cx| {
5972 for window in local_workspace_windows(&cx) {
5973 if let Ok(workspace) = window.read(&cx) {
5974 let m = workspace.project.read(&cx).visibility_for_paths(
5975 &abs_paths,
5976 &all_metadatas,
5977 open_options.open_new_workspace == None,
5978 cx,
5979 );
5980 if m > best_match {
5981 existing = Some(window);
5982 best_match = m;
5983 } else if best_match.is_none()
5984 && open_options.open_new_workspace == Some(false)
5985 {
5986 existing = Some(window)
5987 }
5988 }
5989 }
5990 })?;
5991
5992 if open_options.open_new_workspace.is_none() && existing.is_none() {
5993 if all_metadatas.iter().all(|file| !file.is_dir) {
5994 cx.update(|cx| {
5995 if let Some(window) = cx
5996 .active_window()
5997 .and_then(|window| window.downcast::<Workspace>())
5998 {
5999 if let Ok(workspace) = window.read(cx) {
6000 let project = workspace.project().read(cx);
6001 if project.is_local() && !project.is_via_collab() {
6002 existing = Some(window);
6003 open_visible = OpenVisible::None;
6004 return;
6005 }
6006 }
6007 }
6008 for window in local_workspace_windows(cx) {
6009 if let Ok(workspace) = window.read(cx) {
6010 let project = workspace.project().read(cx);
6011 if project.is_via_collab() {
6012 continue;
6013 }
6014 existing = Some(window);
6015 open_visible = OpenVisible::None;
6016 break;
6017 }
6018 }
6019 })?;
6020 }
6021 }
6022 }
6023
6024 if let Some(existing) = existing {
6025 let open_task = existing
6026 .update(&mut cx, |workspace, window, cx| {
6027 window.activate_window();
6028 workspace.open_paths(abs_paths, open_visible, None, window, cx)
6029 })?
6030 .await;
6031
6032 _ = existing.update(&mut cx, |workspace, _, cx| {
6033 for item in open_task.iter().flatten() {
6034 if let Err(e) = item {
6035 workspace.show_error(&e, cx);
6036 }
6037 }
6038 });
6039
6040 Ok((existing, open_task))
6041 } else {
6042 cx.update(move |cx| {
6043 Workspace::new_local(
6044 abs_paths,
6045 app_state.clone(),
6046 open_options.replace_window,
6047 open_options.env,
6048 cx,
6049 )
6050 })?
6051 .await
6052 }
6053 })
6054}
6055
6056pub fn open_new(
6057 open_options: OpenOptions,
6058 app_state: Arc<AppState>,
6059 cx: &mut App,
6060 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6061) -> Task<anyhow::Result<()>> {
6062 let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6063 cx.spawn(|mut cx| async move {
6064 let (workspace, opened_paths) = task.await?;
6065 workspace.update(&mut cx, |workspace, window, cx| {
6066 if opened_paths.is_empty() {
6067 init(workspace, window, cx)
6068 }
6069 })?;
6070 Ok(())
6071 })
6072}
6073
6074pub fn create_and_open_local_file(
6075 path: &'static Path,
6076 window: &mut Window,
6077 cx: &mut Context<Workspace>,
6078 default_content: impl 'static + Send + FnOnce() -> Rope,
6079) -> Task<Result<Box<dyn ItemHandle>>> {
6080 cx.spawn_in(window, |workspace, mut cx| async move {
6081 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
6082 if !fs.is_file(path).await {
6083 fs.create_file(path, Default::default()).await?;
6084 fs.save(path, &default_content(), Default::default())
6085 .await?;
6086 }
6087
6088 let mut items = workspace
6089 .update_in(&mut cx, |workspace, window, cx| {
6090 workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6091 workspace.open_paths(
6092 vec![path.to_path_buf()],
6093 OpenVisible::None,
6094 None,
6095 window,
6096 cx,
6097 )
6098 })
6099 })?
6100 .await?
6101 .await;
6102
6103 let item = items.pop().flatten();
6104 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
6105 })
6106}
6107
6108pub fn open_ssh_project(
6109 window: WindowHandle<Workspace>,
6110 connection_options: SshConnectionOptions,
6111 cancel_rx: oneshot::Receiver<()>,
6112 delegate: Arc<dyn SshClientDelegate>,
6113 app_state: Arc<AppState>,
6114 paths: Vec<PathBuf>,
6115 cx: &mut App,
6116) -> Task<Result<()>> {
6117 cx.spawn(|mut cx| async move {
6118 let (serialized_ssh_project, workspace_id, serialized_workspace) =
6119 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6120
6121 let session = match cx
6122 .update(|cx| {
6123 remote::SshRemoteClient::new(
6124 ConnectionIdentifier::Workspace(workspace_id.0),
6125 connection_options,
6126 cancel_rx,
6127 delegate,
6128 cx,
6129 )
6130 })?
6131 .await?
6132 {
6133 Some(result) => result,
6134 None => return Ok(()),
6135 };
6136
6137 let project = cx.update(|cx| {
6138 project::Project::ssh(
6139 session,
6140 app_state.client.clone(),
6141 app_state.node_runtime.clone(),
6142 app_state.user_store.clone(),
6143 app_state.languages.clone(),
6144 app_state.fs.clone(),
6145 cx,
6146 )
6147 })?;
6148
6149 let toolchains = DB.toolchains(workspace_id).await?;
6150 for (toolchain, worktree_id) in toolchains {
6151 project
6152 .update(&mut cx, |this, cx| {
6153 this.activate_toolchain(worktree_id, toolchain, cx)
6154 })?
6155 .await;
6156 }
6157 let mut project_paths_to_open = vec![];
6158 let mut project_path_errors = vec![];
6159
6160 for path in paths {
6161 let result = cx
6162 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6163 .await;
6164 match result {
6165 Ok((_, project_path)) => {
6166 project_paths_to_open.push((path.clone(), Some(project_path)));
6167 }
6168 Err(error) => {
6169 project_path_errors.push(error);
6170 }
6171 };
6172 }
6173
6174 if project_paths_to_open.is_empty() {
6175 return Err(project_path_errors
6176 .pop()
6177 .unwrap_or_else(|| anyhow!("no paths given")));
6178 }
6179
6180 cx.update_window(window.into(), |_, window, cx| {
6181 window.replace_root(cx, |window, cx| {
6182 telemetry::event!("SSH Project Opened");
6183
6184 let mut workspace =
6185 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
6186 workspace.set_serialized_ssh_project(serialized_ssh_project);
6187 workspace
6188 });
6189 })?;
6190
6191 window
6192 .update(&mut cx, |_, window, cx| {
6193 window.activate_window();
6194
6195 open_items(serialized_workspace, project_paths_to_open, window, cx)
6196 })?
6197 .await?;
6198
6199 window.update(&mut cx, |workspace, _, cx| {
6200 for error in project_path_errors {
6201 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
6202 if let Some(path) = error.error_tag("path") {
6203 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
6204 }
6205 } else {
6206 workspace.show_error(&error, cx)
6207 }
6208 }
6209 })
6210 })
6211}
6212
6213fn serialize_ssh_project(
6214 connection_options: SshConnectionOptions,
6215 paths: Vec<PathBuf>,
6216 cx: &AsyncApp,
6217) -> Task<
6218 Result<(
6219 SerializedSshProject,
6220 WorkspaceId,
6221 Option<SerializedWorkspace>,
6222 )>,
6223> {
6224 cx.background_spawn(async move {
6225 let serialized_ssh_project = persistence::DB
6226 .get_or_create_ssh_project(
6227 connection_options.host.clone(),
6228 connection_options.port,
6229 paths
6230 .iter()
6231 .map(|path| path.to_string_lossy().to_string())
6232 .collect::<Vec<_>>(),
6233 connection_options.username.clone(),
6234 )
6235 .await?;
6236
6237 let serialized_workspace =
6238 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
6239
6240 let workspace_id = if let Some(workspace_id) =
6241 serialized_workspace.as_ref().map(|workspace| workspace.id)
6242 {
6243 workspace_id
6244 } else {
6245 persistence::DB.next_id().await?
6246 };
6247
6248 Ok((serialized_ssh_project, workspace_id, serialized_workspace))
6249 })
6250}
6251
6252pub fn join_in_room_project(
6253 project_id: u64,
6254 follow_user_id: u64,
6255 app_state: Arc<AppState>,
6256 cx: &mut App,
6257) -> Task<Result<()>> {
6258 let windows = cx.windows();
6259 cx.spawn(|mut cx| async move {
6260 let existing_workspace = windows.into_iter().find_map(|window_handle| {
6261 window_handle
6262 .downcast::<Workspace>()
6263 .and_then(|window_handle| {
6264 window_handle
6265 .update(&mut cx, |workspace, _window, cx| {
6266 if workspace.project().read(cx).remote_id() == Some(project_id) {
6267 Some(window_handle)
6268 } else {
6269 None
6270 }
6271 })
6272 .unwrap_or(None)
6273 })
6274 });
6275
6276 let workspace = if let Some(existing_workspace) = existing_workspace {
6277 existing_workspace
6278 } else {
6279 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
6280 let room = active_call
6281 .read_with(&cx, |call, _| call.room().cloned())?
6282 .ok_or_else(|| anyhow!("not in a call"))?;
6283 let project = room
6284 .update(&mut cx, |room, cx| {
6285 room.join_project(
6286 project_id,
6287 app_state.languages.clone(),
6288 app_state.fs.clone(),
6289 cx,
6290 )
6291 })?
6292 .await?;
6293
6294 let window_bounds_override = window_bounds_env_override();
6295 cx.update(|cx| {
6296 let mut options = (app_state.build_window_options)(None, cx);
6297 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
6298 cx.open_window(options, |window, cx| {
6299 cx.new(|cx| {
6300 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
6301 })
6302 })
6303 })??
6304 };
6305
6306 workspace.update(&mut cx, |workspace, window, cx| {
6307 cx.activate(true);
6308 window.activate_window();
6309
6310 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
6311 let follow_peer_id = room
6312 .read(cx)
6313 .remote_participants()
6314 .iter()
6315 .find(|(_, participant)| participant.user.id == follow_user_id)
6316 .map(|(_, p)| p.peer_id)
6317 .or_else(|| {
6318 // If we couldn't follow the given user, follow the host instead.
6319 let collaborator = workspace
6320 .project()
6321 .read(cx)
6322 .collaborators()
6323 .values()
6324 .find(|collaborator| collaborator.is_host)?;
6325 Some(collaborator.peer_id)
6326 });
6327
6328 if let Some(follow_peer_id) = follow_peer_id {
6329 workspace.follow(follow_peer_id, window, cx);
6330 }
6331 }
6332 })?;
6333
6334 anyhow::Ok(())
6335 })
6336}
6337
6338pub fn reload(reload: &Reload, cx: &mut App) {
6339 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
6340 let mut workspace_windows = cx
6341 .windows()
6342 .into_iter()
6343 .filter_map(|window| window.downcast::<Workspace>())
6344 .collect::<Vec<_>>();
6345
6346 // If multiple windows have unsaved changes, and need a save prompt,
6347 // prompt in the active window before switching to a different window.
6348 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
6349
6350 let mut prompt = None;
6351 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
6352 prompt = window
6353 .update(cx, |_, window, cx| {
6354 window.prompt(
6355 PromptLevel::Info,
6356 "Are you sure you want to restart?",
6357 None,
6358 &["Restart", "Cancel"],
6359 cx,
6360 )
6361 })
6362 .ok();
6363 }
6364
6365 let binary_path = reload.binary_path.clone();
6366 cx.spawn(|mut cx| async move {
6367 if let Some(prompt) = prompt {
6368 let answer = prompt.await?;
6369 if answer != 0 {
6370 return Ok(());
6371 }
6372 }
6373
6374 // If the user cancels any save prompt, then keep the app open.
6375 for window in workspace_windows {
6376 if let Ok(should_close) = window.update(&mut cx, |workspace, window, cx| {
6377 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
6378 }) {
6379 if !should_close.await? {
6380 return Ok(());
6381 }
6382 }
6383 }
6384
6385 cx.update(|cx| cx.restart(binary_path))
6386 })
6387 .detach_and_log_err(cx);
6388}
6389
6390fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
6391 let mut parts = value.split(',');
6392 let x: usize = parts.next()?.parse().ok()?;
6393 let y: usize = parts.next()?.parse().ok()?;
6394 Some(point(px(x as f32), px(y as f32)))
6395}
6396
6397fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
6398 let mut parts = value.split(',');
6399 let width: usize = parts.next()?.parse().ok()?;
6400 let height: usize = parts.next()?.parse().ok()?;
6401 Some(size(px(width as f32), px(height as f32)))
6402}
6403
6404pub fn client_side_decorations(
6405 element: impl IntoElement,
6406 window: &mut Window,
6407 cx: &mut App,
6408) -> Stateful<Div> {
6409 const BORDER_SIZE: Pixels = px(1.0);
6410 let decorations = window.window_decorations();
6411
6412 if matches!(decorations, Decorations::Client { .. }) {
6413 window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
6414 }
6415
6416 struct GlobalResizeEdge(ResizeEdge);
6417 impl Global for GlobalResizeEdge {}
6418
6419 div()
6420 .id("window-backdrop")
6421 .bg(transparent_black())
6422 .map(|div| match decorations {
6423 Decorations::Server => div,
6424 Decorations::Client { tiling, .. } => div
6425 .when(!(tiling.top || tiling.right), |div| {
6426 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6427 })
6428 .when(!(tiling.top || tiling.left), |div| {
6429 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6430 })
6431 .when(!(tiling.bottom || tiling.right), |div| {
6432 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6433 })
6434 .when(!(tiling.bottom || tiling.left), |div| {
6435 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6436 })
6437 .when(!tiling.top, |div| {
6438 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
6439 })
6440 .when(!tiling.bottom, |div| {
6441 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
6442 })
6443 .when(!tiling.left, |div| {
6444 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
6445 })
6446 .when(!tiling.right, |div| {
6447 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
6448 })
6449 .on_mouse_move(move |e, window, cx| {
6450 let size = window.window_bounds().get_bounds().size;
6451 let pos = e.position;
6452
6453 let new_edge =
6454 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
6455
6456 let edge = cx.try_global::<GlobalResizeEdge>();
6457 if new_edge != edge.map(|edge| edge.0) {
6458 window
6459 .window_handle()
6460 .update(cx, |workspace, _, cx| {
6461 cx.notify(workspace.entity_id());
6462 })
6463 .ok();
6464 }
6465 })
6466 .on_mouse_down(MouseButton::Left, move |e, window, _| {
6467 let size = window.window_bounds().get_bounds().size;
6468 let pos = e.position;
6469
6470 let edge = match resize_edge(
6471 pos,
6472 theme::CLIENT_SIDE_DECORATION_SHADOW,
6473 size,
6474 tiling,
6475 ) {
6476 Some(value) => value,
6477 None => return,
6478 };
6479
6480 window.start_window_resize(edge);
6481 }),
6482 })
6483 .size_full()
6484 .child(
6485 div()
6486 .cursor(CursorStyle::Arrow)
6487 .map(|div| match decorations {
6488 Decorations::Server => div,
6489 Decorations::Client { tiling } => div
6490 .border_color(cx.theme().colors().border)
6491 .when(!(tiling.top || tiling.right), |div| {
6492 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6493 })
6494 .when(!(tiling.top || tiling.left), |div| {
6495 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6496 })
6497 .when(!(tiling.bottom || tiling.right), |div| {
6498 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6499 })
6500 .when(!(tiling.bottom || tiling.left), |div| {
6501 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6502 })
6503 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6504 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6505 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6506 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6507 .when(!tiling.is_tiled(), |div| {
6508 div.shadow(smallvec::smallvec![gpui::BoxShadow {
6509 color: Hsla {
6510 h: 0.,
6511 s: 0.,
6512 l: 0.,
6513 a: 0.4,
6514 },
6515 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6516 spread_radius: px(0.),
6517 offset: point(px(0.0), px(0.0)),
6518 }])
6519 }),
6520 })
6521 .on_mouse_move(|_e, _, cx| {
6522 cx.stop_propagation();
6523 })
6524 .size_full()
6525 .child(element),
6526 )
6527 .map(|div| match decorations {
6528 Decorations::Server => div,
6529 Decorations::Client { tiling, .. } => div.child(
6530 canvas(
6531 |_bounds, window, _| {
6532 window.insert_hitbox(
6533 Bounds::new(
6534 point(px(0.0), px(0.0)),
6535 window.window_bounds().get_bounds().size,
6536 ),
6537 false,
6538 )
6539 },
6540 move |_bounds, hitbox, window, cx| {
6541 let mouse = window.mouse_position();
6542 let size = window.window_bounds().get_bounds().size;
6543 let Some(edge) =
6544 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6545 else {
6546 return;
6547 };
6548 cx.set_global(GlobalResizeEdge(edge));
6549 window.set_cursor_style(
6550 match edge {
6551 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6552 ResizeEdge::Left | ResizeEdge::Right => {
6553 CursorStyle::ResizeLeftRight
6554 }
6555 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6556 CursorStyle::ResizeUpLeftDownRight
6557 }
6558 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6559 CursorStyle::ResizeUpRightDownLeft
6560 }
6561 },
6562 &hitbox,
6563 );
6564 },
6565 )
6566 .size_full()
6567 .absolute(),
6568 ),
6569 })
6570}
6571
6572fn resize_edge(
6573 pos: Point<Pixels>,
6574 shadow_size: Pixels,
6575 window_size: Size<Pixels>,
6576 tiling: Tiling,
6577) -> Option<ResizeEdge> {
6578 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6579 if bounds.contains(&pos) {
6580 return None;
6581 }
6582
6583 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6584 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6585 if !tiling.top && top_left_bounds.contains(&pos) {
6586 return Some(ResizeEdge::TopLeft);
6587 }
6588
6589 let top_right_bounds = Bounds::new(
6590 Point::new(window_size.width - corner_size.width, px(0.)),
6591 corner_size,
6592 );
6593 if !tiling.top && top_right_bounds.contains(&pos) {
6594 return Some(ResizeEdge::TopRight);
6595 }
6596
6597 let bottom_left_bounds = Bounds::new(
6598 Point::new(px(0.), window_size.height - corner_size.height),
6599 corner_size,
6600 );
6601 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6602 return Some(ResizeEdge::BottomLeft);
6603 }
6604
6605 let bottom_right_bounds = Bounds::new(
6606 Point::new(
6607 window_size.width - corner_size.width,
6608 window_size.height - corner_size.height,
6609 ),
6610 corner_size,
6611 );
6612 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6613 return Some(ResizeEdge::BottomRight);
6614 }
6615
6616 if !tiling.top && pos.y < shadow_size {
6617 Some(ResizeEdge::Top)
6618 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6619 Some(ResizeEdge::Bottom)
6620 } else if !tiling.left && pos.x < shadow_size {
6621 Some(ResizeEdge::Left)
6622 } else if !tiling.right && pos.x > window_size.width - shadow_size {
6623 Some(ResizeEdge::Right)
6624 } else {
6625 None
6626 }
6627}
6628
6629fn join_pane_into_active(
6630 active_pane: &Entity<Pane>,
6631 pane: &Entity<Pane>,
6632 window: &mut Window,
6633 cx: &mut App,
6634) {
6635 if pane == active_pane {
6636 return;
6637 } else if pane.read(cx).items_len() == 0 {
6638 pane.update(cx, |_, cx| {
6639 cx.emit(pane::Event::Remove {
6640 focus_on_pane: None,
6641 });
6642 })
6643 } else {
6644 move_all_items(pane, active_pane, window, cx);
6645 }
6646}
6647
6648fn move_all_items(
6649 from_pane: &Entity<Pane>,
6650 to_pane: &Entity<Pane>,
6651 window: &mut Window,
6652 cx: &mut App,
6653) {
6654 let destination_is_different = from_pane != to_pane;
6655 let mut moved_items = 0;
6656 for (item_ix, item_handle) in from_pane
6657 .read(cx)
6658 .items()
6659 .enumerate()
6660 .map(|(ix, item)| (ix, item.clone()))
6661 .collect::<Vec<_>>()
6662 {
6663 let ix = item_ix - moved_items;
6664 if destination_is_different {
6665 // Close item from previous pane
6666 from_pane.update(cx, |source, cx| {
6667 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
6668 });
6669 moved_items += 1;
6670 }
6671
6672 // This automatically removes duplicate items in the pane
6673 to_pane.update(cx, |destination, cx| {
6674 destination.add_item(item_handle, true, true, None, window, cx);
6675 window.focus(&destination.focus_handle(cx))
6676 });
6677 }
6678}
6679
6680pub fn move_item(
6681 source: &Entity<Pane>,
6682 destination: &Entity<Pane>,
6683 item_id_to_move: EntityId,
6684 destination_index: usize,
6685 window: &mut Window,
6686 cx: &mut App,
6687) {
6688 let Some((item_ix, item_handle)) = source
6689 .read(cx)
6690 .items()
6691 .enumerate()
6692 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6693 .map(|(ix, item)| (ix, item.clone()))
6694 else {
6695 // Tab was closed during drag
6696 return;
6697 };
6698
6699 if source != destination {
6700 // Close item from previous pane
6701 source.update(cx, |source, cx| {
6702 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
6703 });
6704 }
6705
6706 // This automatically removes duplicate items in the pane
6707 destination.update(cx, |destination, cx| {
6708 destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
6709 window.focus(&destination.focus_handle(cx))
6710 });
6711}
6712
6713pub fn move_active_item(
6714 source: &Entity<Pane>,
6715 destination: &Entity<Pane>,
6716 focus_destination: bool,
6717 close_if_empty: bool,
6718 window: &mut Window,
6719 cx: &mut App,
6720) {
6721 if source == destination {
6722 return;
6723 }
6724 let Some(active_item) = source.read(cx).active_item() else {
6725 return;
6726 };
6727 source.update(cx, |source_pane, cx| {
6728 let item_id = active_item.item_id();
6729 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
6730 destination.update(cx, |target_pane, cx| {
6731 target_pane.add_item(
6732 active_item,
6733 focus_destination,
6734 focus_destination,
6735 Some(target_pane.items_len()),
6736 window,
6737 cx,
6738 );
6739 });
6740 });
6741}
6742
6743#[cfg(test)]
6744mod tests {
6745 use std::{cell::RefCell, rc::Rc};
6746
6747 use super::*;
6748 use crate::{
6749 dock::{test::TestPanel, PanelEvent},
6750 item::{
6751 test::{TestItem, TestProjectItem},
6752 ItemEvent,
6753 },
6754 };
6755 use fs::FakeFs;
6756 use gpui::{
6757 px, DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
6758 UpdateGlobal, VisualTestContext,
6759 };
6760 use project::{Project, ProjectEntryId};
6761 use serde_json::json;
6762 use settings::SettingsStore;
6763
6764 #[gpui::test]
6765 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6766 init_test(cx);
6767
6768 let fs = FakeFs::new(cx.executor());
6769 let project = Project::test(fs, [], cx).await;
6770 let (workspace, cx) =
6771 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6772
6773 // Adding an item with no ambiguity renders the tab without detail.
6774 let item1 = cx.new(|cx| {
6775 let mut item = TestItem::new(cx);
6776 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6777 item
6778 });
6779 workspace.update_in(cx, |workspace, window, cx| {
6780 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6781 });
6782 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6783
6784 // Adding an item that creates ambiguity increases the level of detail on
6785 // both tabs.
6786 let item2 = cx.new_window_entity(|_window, cx| {
6787 let mut item = TestItem::new(cx);
6788 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6789 item
6790 });
6791 workspace.update_in(cx, |workspace, window, cx| {
6792 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6793 });
6794 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6795 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6796
6797 // Adding an item that creates ambiguity increases the level of detail only
6798 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6799 // we stop at the highest detail available.
6800 let item3 = cx.new(|cx| {
6801 let mut item = TestItem::new(cx);
6802 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6803 item
6804 });
6805 workspace.update_in(cx, |workspace, window, cx| {
6806 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6807 });
6808 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6809 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6810 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6811 }
6812
6813 #[gpui::test]
6814 async fn test_tracking_active_path(cx: &mut TestAppContext) {
6815 init_test(cx);
6816
6817 let fs = FakeFs::new(cx.executor());
6818 fs.insert_tree(
6819 "/root1",
6820 json!({
6821 "one.txt": "",
6822 "two.txt": "",
6823 }),
6824 )
6825 .await;
6826 fs.insert_tree(
6827 "/root2",
6828 json!({
6829 "three.txt": "",
6830 }),
6831 )
6832 .await;
6833
6834 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6835 let (workspace, cx) =
6836 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6837 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6838 let worktree_id = project.update(cx, |project, cx| {
6839 project.worktrees(cx).next().unwrap().read(cx).id()
6840 });
6841
6842 let item1 = cx.new(|cx| {
6843 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6844 });
6845 let item2 = cx.new(|cx| {
6846 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6847 });
6848
6849 // Add an item to an empty pane
6850 workspace.update_in(cx, |workspace, window, cx| {
6851 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
6852 });
6853 project.update(cx, |project, cx| {
6854 assert_eq!(
6855 project.active_entry(),
6856 project
6857 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6858 .map(|e| e.id)
6859 );
6860 });
6861 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6862
6863 // Add a second item to a non-empty pane
6864 workspace.update_in(cx, |workspace, window, cx| {
6865 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
6866 });
6867 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6868 project.update(cx, |project, cx| {
6869 assert_eq!(
6870 project.active_entry(),
6871 project
6872 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6873 .map(|e| e.id)
6874 );
6875 });
6876
6877 // Close the active item
6878 pane.update_in(cx, |pane, window, cx| {
6879 pane.close_active_item(&Default::default(), window, cx)
6880 .unwrap()
6881 })
6882 .await
6883 .unwrap();
6884 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6885 project.update(cx, |project, cx| {
6886 assert_eq!(
6887 project.active_entry(),
6888 project
6889 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6890 .map(|e| e.id)
6891 );
6892 });
6893
6894 // Add a project folder
6895 project
6896 .update(cx, |project, cx| {
6897 project.find_or_create_worktree("root2", true, cx)
6898 })
6899 .await
6900 .unwrap();
6901 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6902
6903 // Remove a project folder
6904 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6905 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6906 }
6907
6908 #[gpui::test]
6909 async fn test_close_window(cx: &mut TestAppContext) {
6910 init_test(cx);
6911
6912 let fs = FakeFs::new(cx.executor());
6913 fs.insert_tree("/root", json!({ "one": "" })).await;
6914
6915 let project = Project::test(fs, ["root".as_ref()], cx).await;
6916 let (workspace, cx) =
6917 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6918
6919 // When there are no dirty items, there's nothing to do.
6920 let item1 = cx.new(TestItem::new);
6921 workspace.update_in(cx, |w, window, cx| {
6922 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
6923 });
6924 let task = workspace.update_in(cx, |w, window, cx| {
6925 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6926 });
6927 assert!(task.await.unwrap());
6928
6929 // When there are dirty untitled items, prompt to save each one. If the user
6930 // cancels any prompt, then abort.
6931 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
6932 let item3 = cx.new(|cx| {
6933 TestItem::new(cx)
6934 .with_dirty(true)
6935 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6936 });
6937 workspace.update_in(cx, |w, window, cx| {
6938 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6939 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6940 });
6941 let task = workspace.update_in(cx, |w, window, cx| {
6942 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6943 });
6944 cx.executor().run_until_parked();
6945 cx.simulate_prompt_answer("Cancel"); // cancel save all
6946 cx.executor().run_until_parked();
6947 assert!(!cx.has_pending_prompt());
6948 assert!(!task.await.unwrap());
6949 }
6950
6951 #[gpui::test]
6952 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6953 init_test(cx);
6954
6955 // Register TestItem as a serializable item
6956 cx.update(|cx| {
6957 register_serializable_item::<TestItem>(cx);
6958 });
6959
6960 let fs = FakeFs::new(cx.executor());
6961 fs.insert_tree("/root", json!({ "one": "" })).await;
6962
6963 let project = Project::test(fs, ["root".as_ref()], cx).await;
6964 let (workspace, cx) =
6965 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6966
6967 // When there are dirty untitled items, but they can serialize, then there is no prompt.
6968 let item1 = cx.new(|cx| {
6969 TestItem::new(cx)
6970 .with_dirty(true)
6971 .with_serialize(|| Some(Task::ready(Ok(()))))
6972 });
6973 let item2 = cx.new(|cx| {
6974 TestItem::new(cx)
6975 .with_dirty(true)
6976 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6977 .with_serialize(|| Some(Task::ready(Ok(()))))
6978 });
6979 workspace.update_in(cx, |w, window, cx| {
6980 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6981 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6982 });
6983 let task = workspace.update_in(cx, |w, window, cx| {
6984 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6985 });
6986 assert!(task.await.unwrap());
6987 }
6988
6989 #[gpui::test]
6990 async fn test_close_pane_items(cx: &mut TestAppContext) {
6991 init_test(cx);
6992
6993 let fs = FakeFs::new(cx.executor());
6994
6995 let project = Project::test(fs, None, cx).await;
6996 let (workspace, cx) =
6997 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6998
6999 let item1 = cx.new(|cx| {
7000 TestItem::new(cx)
7001 .with_dirty(true)
7002 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7003 });
7004 let item2 = cx.new(|cx| {
7005 TestItem::new(cx)
7006 .with_dirty(true)
7007 .with_conflict(true)
7008 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7009 });
7010 let item3 = cx.new(|cx| {
7011 TestItem::new(cx)
7012 .with_dirty(true)
7013 .with_conflict(true)
7014 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
7015 });
7016 let item4 = cx.new(|cx| {
7017 TestItem::new(cx).with_dirty(true).with_project_items(&[{
7018 let project_item = TestProjectItem::new_untitled(cx);
7019 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7020 project_item
7021 }])
7022 });
7023 let pane = workspace.update_in(cx, |workspace, window, cx| {
7024 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7025 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7026 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7027 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
7028 workspace.active_pane().clone()
7029 });
7030
7031 let close_items = pane.update_in(cx, |pane, window, cx| {
7032 pane.activate_item(1, true, true, window, cx);
7033 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7034 let item1_id = item1.item_id();
7035 let item3_id = item3.item_id();
7036 let item4_id = item4.item_id();
7037 pane.close_items(window, cx, SaveIntent::Close, move |id| {
7038 [item1_id, item3_id, item4_id].contains(&id)
7039 })
7040 });
7041 cx.executor().run_until_parked();
7042
7043 assert!(cx.has_pending_prompt());
7044 cx.simulate_prompt_answer("Save all");
7045
7046 cx.executor().run_until_parked();
7047
7048 // Item 1 is saved. There's a prompt to save item 3.
7049 pane.update(cx, |pane, cx| {
7050 assert_eq!(item1.read(cx).save_count, 1);
7051 assert_eq!(item1.read(cx).save_as_count, 0);
7052 assert_eq!(item1.read(cx).reload_count, 0);
7053 assert_eq!(pane.items_len(), 3);
7054 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7055 });
7056 assert!(cx.has_pending_prompt());
7057
7058 // Cancel saving item 3.
7059 cx.simulate_prompt_answer("Discard");
7060 cx.executor().run_until_parked();
7061
7062 // Item 3 is reloaded. There's a prompt to save item 4.
7063 pane.update(cx, |pane, cx| {
7064 assert_eq!(item3.read(cx).save_count, 0);
7065 assert_eq!(item3.read(cx).save_as_count, 0);
7066 assert_eq!(item3.read(cx).reload_count, 1);
7067 assert_eq!(pane.items_len(), 2);
7068 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7069 });
7070
7071 // There's a prompt for a path for item 4.
7072 cx.simulate_new_path_selection(|_| Some(Default::default()));
7073 close_items.await.unwrap();
7074
7075 // The requested items are closed.
7076 pane.update(cx, |pane, cx| {
7077 assert_eq!(item4.read(cx).save_count, 0);
7078 assert_eq!(item4.read(cx).save_as_count, 1);
7079 assert_eq!(item4.read(cx).reload_count, 0);
7080 assert_eq!(pane.items_len(), 1);
7081 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7082 });
7083 }
7084
7085 #[gpui::test]
7086 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7087 init_test(cx);
7088
7089 let fs = FakeFs::new(cx.executor());
7090 let project = Project::test(fs, [], cx).await;
7091 let (workspace, cx) =
7092 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7093
7094 // Create several workspace items with single project entries, and two
7095 // workspace items with multiple project entries.
7096 let single_entry_items = (0..=4)
7097 .map(|project_entry_id| {
7098 cx.new(|cx| {
7099 TestItem::new(cx)
7100 .with_dirty(true)
7101 .with_project_items(&[dirty_project_item(
7102 project_entry_id,
7103 &format!("{project_entry_id}.txt"),
7104 cx,
7105 )])
7106 })
7107 })
7108 .collect::<Vec<_>>();
7109 let item_2_3 = cx.new(|cx| {
7110 TestItem::new(cx)
7111 .with_dirty(true)
7112 .with_singleton(false)
7113 .with_project_items(&[
7114 single_entry_items[2].read(cx).project_items[0].clone(),
7115 single_entry_items[3].read(cx).project_items[0].clone(),
7116 ])
7117 });
7118 let item_3_4 = cx.new(|cx| {
7119 TestItem::new(cx)
7120 .with_dirty(true)
7121 .with_singleton(false)
7122 .with_project_items(&[
7123 single_entry_items[3].read(cx).project_items[0].clone(),
7124 single_entry_items[4].read(cx).project_items[0].clone(),
7125 ])
7126 });
7127
7128 // Create two panes that contain the following project entries:
7129 // left pane:
7130 // multi-entry items: (2, 3)
7131 // single-entry items: 0, 2, 3, 4
7132 // right pane:
7133 // single-entry items: 4, 1
7134 // multi-entry items: (3, 4)
7135 let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
7136 let left_pane = workspace.active_pane().clone();
7137 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
7138 workspace.add_item_to_active_pane(
7139 single_entry_items[0].boxed_clone(),
7140 None,
7141 true,
7142 window,
7143 cx,
7144 );
7145 workspace.add_item_to_active_pane(
7146 single_entry_items[2].boxed_clone(),
7147 None,
7148 true,
7149 window,
7150 cx,
7151 );
7152 workspace.add_item_to_active_pane(
7153 single_entry_items[3].boxed_clone(),
7154 None,
7155 true,
7156 window,
7157 cx,
7158 );
7159 workspace.add_item_to_active_pane(
7160 single_entry_items[4].boxed_clone(),
7161 None,
7162 true,
7163 window,
7164 cx,
7165 );
7166
7167 let right_pane = workspace
7168 .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
7169 .unwrap();
7170
7171 right_pane.update(cx, |pane, cx| {
7172 pane.add_item(
7173 single_entry_items[1].boxed_clone(),
7174 true,
7175 true,
7176 None,
7177 window,
7178 cx,
7179 );
7180 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
7181 });
7182
7183 (left_pane, right_pane)
7184 });
7185
7186 cx.focus(&right_pane);
7187
7188 let mut close = right_pane.update_in(cx, |pane, window, cx| {
7189 pane.close_all_items(&CloseAllItems::default(), window, cx)
7190 .unwrap()
7191 });
7192 cx.executor().run_until_parked();
7193
7194 let msg = cx.pending_prompt().unwrap().0;
7195 assert!(msg.contains("1.txt"));
7196 assert!(!msg.contains("2.txt"));
7197 assert!(!msg.contains("3.txt"));
7198 assert!(!msg.contains("4.txt"));
7199
7200 cx.simulate_prompt_answer("Cancel");
7201 close.await.unwrap();
7202
7203 left_pane
7204 .update_in(cx, |left_pane, window, cx| {
7205 left_pane.close_item_by_id(
7206 single_entry_items[3].entity_id(),
7207 SaveIntent::Skip,
7208 window,
7209 cx,
7210 )
7211 })
7212 .await
7213 .unwrap();
7214
7215 close = right_pane.update_in(cx, |pane, window, cx| {
7216 pane.close_all_items(&CloseAllItems::default(), window, cx)
7217 .unwrap()
7218 });
7219 cx.executor().run_until_parked();
7220
7221 let details = cx.pending_prompt().unwrap().1;
7222 assert!(details.contains("1.txt"));
7223 assert!(!details.contains("2.txt"));
7224 assert!(details.contains("3.txt"));
7225 // ideally this assertion could be made, but today we can only
7226 // save whole items not project items, so the orphaned item 3 causes
7227 // 4 to be saved too.
7228 // assert!(!details.contains("4.txt"));
7229
7230 cx.simulate_prompt_answer("Save all");
7231
7232 cx.executor().run_until_parked();
7233 close.await.unwrap();
7234 right_pane.update(cx, |pane, _| {
7235 assert_eq!(pane.items_len(), 0);
7236 });
7237 }
7238
7239 #[gpui::test]
7240 async fn test_autosave(cx: &mut gpui::TestAppContext) {
7241 init_test(cx);
7242
7243 let fs = FakeFs::new(cx.executor());
7244 let project = Project::test(fs, [], cx).await;
7245 let (workspace, cx) =
7246 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7247 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7248
7249 let item = cx.new(|cx| {
7250 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7251 });
7252 let item_id = item.entity_id();
7253 workspace.update_in(cx, |workspace, window, cx| {
7254 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7255 });
7256
7257 // Autosave on window change.
7258 item.update(cx, |item, cx| {
7259 SettingsStore::update_global(cx, |settings, cx| {
7260 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7261 settings.autosave = Some(AutosaveSetting::OnWindowChange);
7262 })
7263 });
7264 item.is_dirty = true;
7265 });
7266
7267 // Deactivating the window saves the file.
7268 cx.deactivate_window();
7269 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7270
7271 // Re-activating the window doesn't save the file.
7272 cx.update(|window, _| window.activate_window());
7273 cx.executor().run_until_parked();
7274 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7275
7276 // Autosave on focus change.
7277 item.update_in(cx, |item, window, cx| {
7278 cx.focus_self(window);
7279 SettingsStore::update_global(cx, |settings, cx| {
7280 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7281 settings.autosave = Some(AutosaveSetting::OnFocusChange);
7282 })
7283 });
7284 item.is_dirty = true;
7285 });
7286
7287 // Blurring the item saves the file.
7288 item.update_in(cx, |_, window, _| window.blur());
7289 cx.executor().run_until_parked();
7290 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
7291
7292 // Deactivating the window still saves the file.
7293 item.update_in(cx, |item, window, cx| {
7294 cx.focus_self(window);
7295 item.is_dirty = true;
7296 });
7297 cx.deactivate_window();
7298 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7299
7300 // Autosave after delay.
7301 item.update(cx, |item, cx| {
7302 SettingsStore::update_global(cx, |settings, cx| {
7303 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7304 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
7305 })
7306 });
7307 item.is_dirty = true;
7308 cx.emit(ItemEvent::Edit);
7309 });
7310
7311 // Delay hasn't fully expired, so the file is still dirty and unsaved.
7312 cx.executor().advance_clock(Duration::from_millis(250));
7313 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7314
7315 // After delay expires, the file is saved.
7316 cx.executor().advance_clock(Duration::from_millis(250));
7317 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
7318
7319 // Autosave on focus change, ensuring closing the tab counts as such.
7320 item.update(cx, |item, cx| {
7321 SettingsStore::update_global(cx, |settings, cx| {
7322 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7323 settings.autosave = Some(AutosaveSetting::OnFocusChange);
7324 })
7325 });
7326 item.is_dirty = true;
7327 for project_item in &mut item.project_items {
7328 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7329 }
7330 });
7331
7332 pane.update_in(cx, |pane, window, cx| {
7333 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7334 })
7335 .await
7336 .unwrap();
7337 assert!(!cx.has_pending_prompt());
7338 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7339
7340 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
7341 workspace.update_in(cx, |workspace, window, cx| {
7342 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7343 });
7344 item.update_in(cx, |item, window, cx| {
7345 item.project_items[0].update(cx, |item, _| {
7346 item.entry_id = None;
7347 });
7348 item.is_dirty = true;
7349 window.blur();
7350 });
7351 cx.run_until_parked();
7352 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7353
7354 // Ensure autosave is prevented for deleted files also when closing the buffer.
7355 let _close_items = pane.update_in(cx, |pane, window, cx| {
7356 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7357 });
7358 cx.run_until_parked();
7359 assert!(cx.has_pending_prompt());
7360 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7361 }
7362
7363 #[gpui::test]
7364 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
7365 init_test(cx);
7366
7367 let fs = FakeFs::new(cx.executor());
7368
7369 let project = Project::test(fs, [], cx).await;
7370 let (workspace, cx) =
7371 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7372
7373 let item = cx.new(|cx| {
7374 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7375 });
7376 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7377 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
7378 let toolbar_notify_count = Rc::new(RefCell::new(0));
7379
7380 workspace.update_in(cx, |workspace, window, cx| {
7381 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7382 let toolbar_notification_count = toolbar_notify_count.clone();
7383 cx.observe_in(&toolbar, window, move |_, _, _, _| {
7384 *toolbar_notification_count.borrow_mut() += 1
7385 })
7386 .detach();
7387 });
7388
7389 pane.update(cx, |pane, _| {
7390 assert!(!pane.can_navigate_backward());
7391 assert!(!pane.can_navigate_forward());
7392 });
7393
7394 item.update_in(cx, |item, _, cx| {
7395 item.set_state("one".to_string(), cx);
7396 });
7397
7398 // Toolbar must be notified to re-render the navigation buttons
7399 assert_eq!(*toolbar_notify_count.borrow(), 1);
7400
7401 pane.update(cx, |pane, _| {
7402 assert!(pane.can_navigate_backward());
7403 assert!(!pane.can_navigate_forward());
7404 });
7405
7406 workspace
7407 .update_in(cx, |workspace, window, cx| {
7408 workspace.go_back(pane.downgrade(), window, cx)
7409 })
7410 .await
7411 .unwrap();
7412
7413 assert_eq!(*toolbar_notify_count.borrow(), 2);
7414 pane.update(cx, |pane, _| {
7415 assert!(!pane.can_navigate_backward());
7416 assert!(pane.can_navigate_forward());
7417 });
7418 }
7419
7420 #[gpui::test]
7421 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
7422 init_test(cx);
7423 let fs = FakeFs::new(cx.executor());
7424
7425 let project = Project::test(fs, [], cx).await;
7426 let (workspace, cx) =
7427 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7428
7429 let panel = workspace.update_in(cx, |workspace, window, cx| {
7430 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7431 workspace.add_panel(panel.clone(), window, cx);
7432
7433 workspace
7434 .right_dock()
7435 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
7436
7437 panel
7438 });
7439
7440 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7441 pane.update_in(cx, |pane, window, cx| {
7442 let item = cx.new(TestItem::new);
7443 pane.add_item(Box::new(item), true, true, None, window, cx);
7444 });
7445
7446 // Transfer focus from center to panel
7447 workspace.update_in(cx, |workspace, window, cx| {
7448 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7449 });
7450
7451 workspace.update_in(cx, |workspace, window, cx| {
7452 assert!(workspace.right_dock().read(cx).is_open());
7453 assert!(!panel.is_zoomed(window, cx));
7454 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7455 });
7456
7457 // Transfer focus from panel to center
7458 workspace.update_in(cx, |workspace, window, cx| {
7459 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7460 });
7461
7462 workspace.update_in(cx, |workspace, window, cx| {
7463 assert!(workspace.right_dock().read(cx).is_open());
7464 assert!(!panel.is_zoomed(window, cx));
7465 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7466 });
7467
7468 // Close the dock
7469 workspace.update_in(cx, |workspace, window, cx| {
7470 workspace.toggle_dock(DockPosition::Right, window, cx);
7471 });
7472
7473 workspace.update_in(cx, |workspace, window, cx| {
7474 assert!(!workspace.right_dock().read(cx).is_open());
7475 assert!(!panel.is_zoomed(window, cx));
7476 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7477 });
7478
7479 // Open the dock
7480 workspace.update_in(cx, |workspace, window, cx| {
7481 workspace.toggle_dock(DockPosition::Right, window, cx);
7482 });
7483
7484 workspace.update_in(cx, |workspace, window, cx| {
7485 assert!(workspace.right_dock().read(cx).is_open());
7486 assert!(!panel.is_zoomed(window, cx));
7487 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7488 });
7489
7490 // Focus and zoom panel
7491 panel.update_in(cx, |panel, window, cx| {
7492 cx.focus_self(window);
7493 panel.set_zoomed(true, window, cx)
7494 });
7495
7496 workspace.update_in(cx, |workspace, window, cx| {
7497 assert!(workspace.right_dock().read(cx).is_open());
7498 assert!(panel.is_zoomed(window, cx));
7499 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7500 });
7501
7502 // Transfer focus to the center closes the dock
7503 workspace.update_in(cx, |workspace, window, cx| {
7504 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7505 });
7506
7507 workspace.update_in(cx, |workspace, window, cx| {
7508 assert!(!workspace.right_dock().read(cx).is_open());
7509 assert!(panel.is_zoomed(window, cx));
7510 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7511 });
7512
7513 // Transferring focus back to the panel keeps it zoomed
7514 workspace.update_in(cx, |workspace, window, cx| {
7515 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7516 });
7517
7518 workspace.update_in(cx, |workspace, window, cx| {
7519 assert!(workspace.right_dock().read(cx).is_open());
7520 assert!(panel.is_zoomed(window, cx));
7521 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7522 });
7523
7524 // Close the dock while it is zoomed
7525 workspace.update_in(cx, |workspace, window, cx| {
7526 workspace.toggle_dock(DockPosition::Right, window, cx)
7527 });
7528
7529 workspace.update_in(cx, |workspace, window, cx| {
7530 assert!(!workspace.right_dock().read(cx).is_open());
7531 assert!(panel.is_zoomed(window, cx));
7532 assert!(workspace.zoomed.is_none());
7533 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7534 });
7535
7536 // Opening the dock, when it's zoomed, retains focus
7537 workspace.update_in(cx, |workspace, window, cx| {
7538 workspace.toggle_dock(DockPosition::Right, window, cx)
7539 });
7540
7541 workspace.update_in(cx, |workspace, window, cx| {
7542 assert!(workspace.right_dock().read(cx).is_open());
7543 assert!(panel.is_zoomed(window, cx));
7544 assert!(workspace.zoomed.is_some());
7545 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7546 });
7547
7548 // Unzoom and close the panel, zoom the active pane.
7549 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
7550 workspace.update_in(cx, |workspace, window, cx| {
7551 workspace.toggle_dock(DockPosition::Right, window, cx)
7552 });
7553 pane.update_in(cx, |pane, window, cx| {
7554 pane.toggle_zoom(&Default::default(), window, cx)
7555 });
7556
7557 // Opening a dock unzooms the pane.
7558 workspace.update_in(cx, |workspace, window, cx| {
7559 workspace.toggle_dock(DockPosition::Right, window, cx)
7560 });
7561 workspace.update_in(cx, |workspace, window, cx| {
7562 let pane = pane.read(cx);
7563 assert!(!pane.is_zoomed());
7564 assert!(!pane.focus_handle(cx).is_focused(window));
7565 assert!(workspace.right_dock().read(cx).is_open());
7566 assert!(workspace.zoomed.is_none());
7567 });
7568 }
7569
7570 #[gpui::test]
7571 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
7572 init_test(cx);
7573
7574 let fs = FakeFs::new(cx.executor());
7575
7576 let project = Project::test(fs, None, cx).await;
7577 let (workspace, cx) =
7578 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7579
7580 // Let's arrange the panes like this:
7581 //
7582 // +-----------------------+
7583 // | top |
7584 // +------+--------+-------+
7585 // | left | center | right |
7586 // +------+--------+-------+
7587 // | bottom |
7588 // +-----------------------+
7589
7590 let top_item = cx.new(|cx| {
7591 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
7592 });
7593 let bottom_item = cx.new(|cx| {
7594 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7595 });
7596 let left_item = cx.new(|cx| {
7597 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7598 });
7599 let right_item = cx.new(|cx| {
7600 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7601 });
7602 let center_item = cx.new(|cx| {
7603 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7604 });
7605
7606 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7607 let top_pane_id = workspace.active_pane().entity_id();
7608 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
7609 workspace.split_pane(
7610 workspace.active_pane().clone(),
7611 SplitDirection::Down,
7612 window,
7613 cx,
7614 );
7615 top_pane_id
7616 });
7617 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7618 let bottom_pane_id = workspace.active_pane().entity_id();
7619 workspace.add_item_to_active_pane(
7620 Box::new(bottom_item.clone()),
7621 None,
7622 false,
7623 window,
7624 cx,
7625 );
7626 workspace.split_pane(
7627 workspace.active_pane().clone(),
7628 SplitDirection::Up,
7629 window,
7630 cx,
7631 );
7632 bottom_pane_id
7633 });
7634 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7635 let left_pane_id = workspace.active_pane().entity_id();
7636 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
7637 workspace.split_pane(
7638 workspace.active_pane().clone(),
7639 SplitDirection::Right,
7640 window,
7641 cx,
7642 );
7643 left_pane_id
7644 });
7645 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7646 let right_pane_id = workspace.active_pane().entity_id();
7647 workspace.add_item_to_active_pane(
7648 Box::new(right_item.clone()),
7649 None,
7650 false,
7651 window,
7652 cx,
7653 );
7654 workspace.split_pane(
7655 workspace.active_pane().clone(),
7656 SplitDirection::Left,
7657 window,
7658 cx,
7659 );
7660 right_pane_id
7661 });
7662 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7663 let center_pane_id = workspace.active_pane().entity_id();
7664 workspace.add_item_to_active_pane(
7665 Box::new(center_item.clone()),
7666 None,
7667 false,
7668 window,
7669 cx,
7670 );
7671 center_pane_id
7672 });
7673 cx.executor().run_until_parked();
7674
7675 workspace.update_in(cx, |workspace, window, cx| {
7676 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7677
7678 // Join into next from center pane into right
7679 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7680 });
7681
7682 workspace.update_in(cx, |workspace, window, cx| {
7683 let active_pane = workspace.active_pane();
7684 assert_eq!(right_pane_id, active_pane.entity_id());
7685 assert_eq!(2, active_pane.read(cx).items_len());
7686 let item_ids_in_pane =
7687 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7688 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7689 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7690
7691 // Join into next from right pane into bottom
7692 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7693 });
7694
7695 workspace.update_in(cx, |workspace, window, cx| {
7696 let active_pane = workspace.active_pane();
7697 assert_eq!(bottom_pane_id, active_pane.entity_id());
7698 assert_eq!(3, active_pane.read(cx).items_len());
7699 let item_ids_in_pane =
7700 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7701 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7702 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7703 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7704
7705 // Join into next from bottom pane into left
7706 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7707 });
7708
7709 workspace.update_in(cx, |workspace, window, cx| {
7710 let active_pane = workspace.active_pane();
7711 assert_eq!(left_pane_id, active_pane.entity_id());
7712 assert_eq!(4, active_pane.read(cx).items_len());
7713 let item_ids_in_pane =
7714 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7715 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7716 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7717 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7718 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7719
7720 // Join into next from left pane into top
7721 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7722 });
7723
7724 workspace.update_in(cx, |workspace, window, cx| {
7725 let active_pane = workspace.active_pane();
7726 assert_eq!(top_pane_id, active_pane.entity_id());
7727 assert_eq!(5, active_pane.read(cx).items_len());
7728 let item_ids_in_pane =
7729 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7730 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7731 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7732 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7733 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7734 assert!(item_ids_in_pane.contains(&top_item.item_id()));
7735
7736 // Single pane left: no-op
7737 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
7738 });
7739
7740 workspace.update(cx, |workspace, _cx| {
7741 let active_pane = workspace.active_pane();
7742 assert_eq!(top_pane_id, active_pane.entity_id());
7743 });
7744 }
7745
7746 fn add_an_item_to_active_pane(
7747 cx: &mut VisualTestContext,
7748 workspace: &Entity<Workspace>,
7749 item_id: u64,
7750 ) -> Entity<TestItem> {
7751 let item = cx.new(|cx| {
7752 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7753 item_id,
7754 "item{item_id}.txt",
7755 cx,
7756 )])
7757 });
7758 workspace.update_in(cx, |workspace, window, cx| {
7759 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
7760 });
7761 return item;
7762 }
7763
7764 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
7765 return workspace.update_in(cx, |workspace, window, cx| {
7766 let new_pane = workspace.split_pane(
7767 workspace.active_pane().clone(),
7768 SplitDirection::Right,
7769 window,
7770 cx,
7771 );
7772 new_pane
7773 });
7774 }
7775
7776 #[gpui::test]
7777 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7778 init_test(cx);
7779 let fs = FakeFs::new(cx.executor());
7780 let project = Project::test(fs, None, cx).await;
7781 let (workspace, cx) =
7782 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7783
7784 add_an_item_to_active_pane(cx, &workspace, 1);
7785 split_pane(cx, &workspace);
7786 add_an_item_to_active_pane(cx, &workspace, 2);
7787 split_pane(cx, &workspace); // empty pane
7788 split_pane(cx, &workspace);
7789 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7790
7791 cx.executor().run_until_parked();
7792
7793 workspace.update(cx, |workspace, cx| {
7794 let num_panes = workspace.panes().len();
7795 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7796 let active_item = workspace
7797 .active_pane()
7798 .read(cx)
7799 .active_item()
7800 .expect("item is in focus");
7801
7802 assert_eq!(num_panes, 4);
7803 assert_eq!(num_items_in_current_pane, 1);
7804 assert_eq!(active_item.item_id(), last_item.item_id());
7805 });
7806
7807 workspace.update_in(cx, |workspace, window, cx| {
7808 workspace.join_all_panes(window, cx);
7809 });
7810
7811 workspace.update(cx, |workspace, cx| {
7812 let num_panes = workspace.panes().len();
7813 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7814 let active_item = workspace
7815 .active_pane()
7816 .read(cx)
7817 .active_item()
7818 .expect("item is in focus");
7819
7820 assert_eq!(num_panes, 1);
7821 assert_eq!(num_items_in_current_pane, 3);
7822 assert_eq!(active_item.item_id(), last_item.item_id());
7823 });
7824 }
7825 struct TestModal(FocusHandle);
7826
7827 impl TestModal {
7828 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
7829 Self(cx.focus_handle())
7830 }
7831 }
7832
7833 impl EventEmitter<DismissEvent> for TestModal {}
7834
7835 impl Focusable for TestModal {
7836 fn focus_handle(&self, _cx: &App) -> FocusHandle {
7837 self.0.clone()
7838 }
7839 }
7840
7841 impl ModalView for TestModal {}
7842
7843 impl Render for TestModal {
7844 fn render(
7845 &mut self,
7846 _window: &mut Window,
7847 _cx: &mut Context<TestModal>,
7848 ) -> impl IntoElement {
7849 div().track_focus(&self.0)
7850 }
7851 }
7852
7853 #[gpui::test]
7854 async fn test_panels(cx: &mut gpui::TestAppContext) {
7855 init_test(cx);
7856 let fs = FakeFs::new(cx.executor());
7857
7858 let project = Project::test(fs, [], cx).await;
7859 let (workspace, cx) =
7860 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7861
7862 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
7863 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
7864 workspace.add_panel(panel_1.clone(), window, cx);
7865 workspace.toggle_dock(DockPosition::Left, window, cx);
7866 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7867 workspace.add_panel(panel_2.clone(), window, cx);
7868 workspace.toggle_dock(DockPosition::Right, window, cx);
7869
7870 let left_dock = workspace.left_dock();
7871 assert_eq!(
7872 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7873 panel_1.panel_id()
7874 );
7875 assert_eq!(
7876 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7877 panel_1.size(window, cx)
7878 );
7879
7880 left_dock.update(cx, |left_dock, cx| {
7881 left_dock.resize_active_panel(Some(px(1337.)), window, cx)
7882 });
7883 assert_eq!(
7884 workspace
7885 .right_dock()
7886 .read(cx)
7887 .visible_panel()
7888 .unwrap()
7889 .panel_id(),
7890 panel_2.panel_id(),
7891 );
7892
7893 (panel_1, panel_2)
7894 });
7895
7896 // Move panel_1 to the right
7897 panel_1.update_in(cx, |panel_1, window, cx| {
7898 panel_1.set_position(DockPosition::Right, window, cx)
7899 });
7900
7901 workspace.update_in(cx, |workspace, window, cx| {
7902 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7903 // Since it was the only panel on the left, the left dock should now be closed.
7904 assert!(!workspace.left_dock().read(cx).is_open());
7905 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7906 let right_dock = workspace.right_dock();
7907 assert_eq!(
7908 right_dock.read(cx).visible_panel().unwrap().panel_id(),
7909 panel_1.panel_id()
7910 );
7911 assert_eq!(
7912 right_dock.read(cx).active_panel_size(window, cx).unwrap(),
7913 px(1337.)
7914 );
7915
7916 // Now we move panel_2 to the left
7917 panel_2.set_position(DockPosition::Left, window, cx);
7918 });
7919
7920 workspace.update(cx, |workspace, cx| {
7921 // Since panel_2 was not visible on the right, we don't open the left dock.
7922 assert!(!workspace.left_dock().read(cx).is_open());
7923 // And the right dock is unaffected in its displaying of panel_1
7924 assert!(workspace.right_dock().read(cx).is_open());
7925 assert_eq!(
7926 workspace
7927 .right_dock()
7928 .read(cx)
7929 .visible_panel()
7930 .unwrap()
7931 .panel_id(),
7932 panel_1.panel_id(),
7933 );
7934 });
7935
7936 // Move panel_1 back to the left
7937 panel_1.update_in(cx, |panel_1, window, cx| {
7938 panel_1.set_position(DockPosition::Left, window, cx)
7939 });
7940
7941 workspace.update_in(cx, |workspace, window, cx| {
7942 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7943 let left_dock = workspace.left_dock();
7944 assert!(left_dock.read(cx).is_open());
7945 assert_eq!(
7946 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7947 panel_1.panel_id()
7948 );
7949 assert_eq!(
7950 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7951 px(1337.)
7952 );
7953 // And the right dock should be closed as it no longer has any panels.
7954 assert!(!workspace.right_dock().read(cx).is_open());
7955
7956 // Now we move panel_1 to the bottom
7957 panel_1.set_position(DockPosition::Bottom, window, cx);
7958 });
7959
7960 workspace.update_in(cx, |workspace, window, cx| {
7961 // Since panel_1 was visible on the left, we close the left dock.
7962 assert!(!workspace.left_dock().read(cx).is_open());
7963 // The bottom dock is sized based on the panel's default size,
7964 // since the panel orientation changed from vertical to horizontal.
7965 let bottom_dock = workspace.bottom_dock();
7966 assert_eq!(
7967 bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
7968 panel_1.size(window, cx),
7969 );
7970 // Close bottom dock and move panel_1 back to the left.
7971 bottom_dock.update(cx, |bottom_dock, cx| {
7972 bottom_dock.set_open(false, window, cx)
7973 });
7974 panel_1.set_position(DockPosition::Left, window, cx);
7975 });
7976
7977 // Emit activated event on panel 1
7978 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7979
7980 // Now the left dock is open and panel_1 is active and focused.
7981 workspace.update_in(cx, |workspace, window, cx| {
7982 let left_dock = workspace.left_dock();
7983 assert!(left_dock.read(cx).is_open());
7984 assert_eq!(
7985 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7986 panel_1.panel_id(),
7987 );
7988 assert!(panel_1.focus_handle(cx).is_focused(window));
7989 });
7990
7991 // Emit closed event on panel 2, which is not active
7992 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7993
7994 // Wo don't close the left dock, because panel_2 wasn't the active panel
7995 workspace.update(cx, |workspace, cx| {
7996 let left_dock = workspace.left_dock();
7997 assert!(left_dock.read(cx).is_open());
7998 assert_eq!(
7999 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8000 panel_1.panel_id(),
8001 );
8002 });
8003
8004 // Emitting a ZoomIn event shows the panel as zoomed.
8005 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
8006 workspace.update(cx, |workspace, _| {
8007 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8008 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
8009 });
8010
8011 // Move panel to another dock while it is zoomed
8012 panel_1.update_in(cx, |panel, window, cx| {
8013 panel.set_position(DockPosition::Right, window, cx)
8014 });
8015 workspace.update(cx, |workspace, _| {
8016 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8017
8018 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8019 });
8020
8021 // This is a helper for getting a:
8022 // - valid focus on an element,
8023 // - that isn't a part of the panes and panels system of the Workspace,
8024 // - and doesn't trigger the 'on_focus_lost' API.
8025 let focus_other_view = {
8026 let workspace = workspace.clone();
8027 move |cx: &mut VisualTestContext| {
8028 workspace.update_in(cx, |workspace, window, cx| {
8029 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
8030 workspace.toggle_modal(window, cx, TestModal::new);
8031 workspace.toggle_modal(window, cx, TestModal::new);
8032 } else {
8033 workspace.toggle_modal(window, cx, TestModal::new);
8034 }
8035 })
8036 }
8037 };
8038
8039 // If focus is transferred to another view that's not a panel or another pane, we still show
8040 // the panel as zoomed.
8041 focus_other_view(cx);
8042 workspace.update(cx, |workspace, _| {
8043 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8044 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8045 });
8046
8047 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
8048 workspace.update_in(cx, |_workspace, window, cx| {
8049 cx.focus_self(window);
8050 });
8051 workspace.update(cx, |workspace, _| {
8052 assert_eq!(workspace.zoomed, None);
8053 assert_eq!(workspace.zoomed_position, None);
8054 });
8055
8056 // If focus is transferred again to another view that's not a panel or a pane, we won't
8057 // show the panel as zoomed because it wasn't zoomed before.
8058 focus_other_view(cx);
8059 workspace.update(cx, |workspace, _| {
8060 assert_eq!(workspace.zoomed, None);
8061 assert_eq!(workspace.zoomed_position, None);
8062 });
8063
8064 // When the panel is activated, it is zoomed again.
8065 cx.dispatch_action(ToggleRightDock);
8066 workspace.update(cx, |workspace, _| {
8067 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8068 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8069 });
8070
8071 // Emitting a ZoomOut event unzooms the panel.
8072 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
8073 workspace.update(cx, |workspace, _| {
8074 assert_eq!(workspace.zoomed, None);
8075 assert_eq!(workspace.zoomed_position, None);
8076 });
8077
8078 // Emit closed event on panel 1, which is active
8079 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8080
8081 // Now the left dock is closed, because panel_1 was the active panel
8082 workspace.update(cx, |workspace, cx| {
8083 let right_dock = workspace.right_dock();
8084 assert!(!right_dock.read(cx).is_open());
8085 });
8086 }
8087
8088 #[gpui::test]
8089 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8090 init_test(cx);
8091
8092 let fs = FakeFs::new(cx.background_executor.clone());
8093 let project = Project::test(fs, [], cx).await;
8094 let (workspace, cx) =
8095 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8096 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8097
8098 let dirty_regular_buffer = cx.new(|cx| {
8099 TestItem::new(cx)
8100 .with_dirty(true)
8101 .with_label("1.txt")
8102 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8103 });
8104 let dirty_regular_buffer_2 = cx.new(|cx| {
8105 TestItem::new(cx)
8106 .with_dirty(true)
8107 .with_label("2.txt")
8108 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8109 });
8110 let dirty_multi_buffer_with_both = cx.new(|cx| {
8111 TestItem::new(cx)
8112 .with_dirty(true)
8113 .with_singleton(false)
8114 .with_label("Fake Project Search")
8115 .with_project_items(&[
8116 dirty_regular_buffer.read(cx).project_items[0].clone(),
8117 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8118 ])
8119 });
8120 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8121 workspace.update_in(cx, |workspace, window, cx| {
8122 workspace.add_item(
8123 pane.clone(),
8124 Box::new(dirty_regular_buffer.clone()),
8125 None,
8126 false,
8127 false,
8128 window,
8129 cx,
8130 );
8131 workspace.add_item(
8132 pane.clone(),
8133 Box::new(dirty_regular_buffer_2.clone()),
8134 None,
8135 false,
8136 false,
8137 window,
8138 cx,
8139 );
8140 workspace.add_item(
8141 pane.clone(),
8142 Box::new(dirty_multi_buffer_with_both.clone()),
8143 None,
8144 false,
8145 false,
8146 window,
8147 cx,
8148 );
8149 });
8150
8151 pane.update_in(cx, |pane, window, cx| {
8152 pane.activate_item(2, true, true, window, cx);
8153 assert_eq!(
8154 pane.active_item().unwrap().item_id(),
8155 multi_buffer_with_both_files_id,
8156 "Should select the multi buffer in the pane"
8157 );
8158 });
8159 let close_all_but_multi_buffer_task = pane
8160 .update_in(cx, |pane, window, cx| {
8161 pane.close_inactive_items(
8162 &CloseInactiveItems {
8163 save_intent: Some(SaveIntent::Save),
8164 close_pinned: true,
8165 },
8166 window,
8167 cx,
8168 )
8169 })
8170 .expect("should have inactive files to close");
8171 cx.background_executor.run_until_parked();
8172 assert!(!cx.has_pending_prompt());
8173 close_all_but_multi_buffer_task
8174 .await
8175 .expect("Closing all buffers but the multi buffer failed");
8176 pane.update(cx, |pane, cx| {
8177 assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
8178 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
8179 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
8180 assert_eq!(pane.items_len(), 1);
8181 assert_eq!(
8182 pane.active_item().unwrap().item_id(),
8183 multi_buffer_with_both_files_id,
8184 "Should have only the multi buffer left in the pane"
8185 );
8186 assert!(
8187 dirty_multi_buffer_with_both.read(cx).is_dirty,
8188 "The multi buffer containing the unsaved buffer should still be dirty"
8189 );
8190 });
8191
8192 dirty_regular_buffer.update(cx, |buffer, cx| {
8193 buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
8194 });
8195
8196 let close_multi_buffer_task = pane
8197 .update_in(cx, |pane, window, cx| {
8198 pane.close_active_item(
8199 &CloseActiveItem {
8200 save_intent: Some(SaveIntent::Close),
8201 close_pinned: false,
8202 },
8203 window,
8204 cx,
8205 )
8206 })
8207 .expect("should have the multi buffer to close");
8208 cx.background_executor.run_until_parked();
8209 assert!(
8210 cx.has_pending_prompt(),
8211 "Dirty multi buffer should prompt a save dialog"
8212 );
8213 cx.simulate_prompt_answer("Save");
8214 cx.background_executor.run_until_parked();
8215 close_multi_buffer_task
8216 .await
8217 .expect("Closing the multi buffer failed");
8218 pane.update(cx, |pane, cx| {
8219 assert_eq!(
8220 dirty_multi_buffer_with_both.read(cx).save_count,
8221 1,
8222 "Multi buffer item should get be saved"
8223 );
8224 // Test impl does not save inner items, so we do not assert them
8225 assert_eq!(
8226 pane.items_len(),
8227 0,
8228 "No more items should be left in the pane"
8229 );
8230 assert!(pane.active_item().is_none());
8231 });
8232 }
8233
8234 #[gpui::test]
8235 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
8236 cx: &mut TestAppContext,
8237 ) {
8238 init_test(cx);
8239
8240 let fs = FakeFs::new(cx.background_executor.clone());
8241 let project = Project::test(fs, [], cx).await;
8242 let (workspace, cx) =
8243 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8244 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8245
8246 let dirty_regular_buffer = cx.new(|cx| {
8247 TestItem::new(cx)
8248 .with_dirty(true)
8249 .with_label("1.txt")
8250 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8251 });
8252 let dirty_regular_buffer_2 = cx.new(|cx| {
8253 TestItem::new(cx)
8254 .with_dirty(true)
8255 .with_label("2.txt")
8256 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8257 });
8258 let clear_regular_buffer = cx.new(|cx| {
8259 TestItem::new(cx)
8260 .with_label("3.txt")
8261 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8262 });
8263
8264 let dirty_multi_buffer_with_both = cx.new(|cx| {
8265 TestItem::new(cx)
8266 .with_dirty(true)
8267 .with_singleton(false)
8268 .with_label("Fake Project Search")
8269 .with_project_items(&[
8270 dirty_regular_buffer.read(cx).project_items[0].clone(),
8271 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8272 clear_regular_buffer.read(cx).project_items[0].clone(),
8273 ])
8274 });
8275 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8276 workspace.update_in(cx, |workspace, window, cx| {
8277 workspace.add_item(
8278 pane.clone(),
8279 Box::new(dirty_regular_buffer.clone()),
8280 None,
8281 false,
8282 false,
8283 window,
8284 cx,
8285 );
8286 workspace.add_item(
8287 pane.clone(),
8288 Box::new(dirty_multi_buffer_with_both.clone()),
8289 None,
8290 false,
8291 false,
8292 window,
8293 cx,
8294 );
8295 });
8296
8297 pane.update_in(cx, |pane, window, cx| {
8298 pane.activate_item(1, true, true, window, cx);
8299 assert_eq!(
8300 pane.active_item().unwrap().item_id(),
8301 multi_buffer_with_both_files_id,
8302 "Should select the multi buffer in the pane"
8303 );
8304 });
8305 let _close_multi_buffer_task = pane
8306 .update_in(cx, |pane, window, cx| {
8307 pane.close_active_item(
8308 &CloseActiveItem {
8309 save_intent: None,
8310 close_pinned: false,
8311 },
8312 window,
8313 cx,
8314 )
8315 })
8316 .expect("should have active multi buffer to close");
8317 cx.background_executor.run_until_parked();
8318 assert!(
8319 cx.has_pending_prompt(),
8320 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
8321 );
8322 }
8323
8324 #[gpui::test]
8325 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
8326 cx: &mut TestAppContext,
8327 ) {
8328 init_test(cx);
8329
8330 let fs = FakeFs::new(cx.background_executor.clone());
8331 let project = Project::test(fs, [], cx).await;
8332 let (workspace, cx) =
8333 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8334 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8335
8336 let dirty_regular_buffer = cx.new(|cx| {
8337 TestItem::new(cx)
8338 .with_dirty(true)
8339 .with_label("1.txt")
8340 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8341 });
8342 let dirty_regular_buffer_2 = cx.new(|cx| {
8343 TestItem::new(cx)
8344 .with_dirty(true)
8345 .with_label("2.txt")
8346 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8347 });
8348 let clear_regular_buffer = cx.new(|cx| {
8349 TestItem::new(cx)
8350 .with_label("3.txt")
8351 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8352 });
8353
8354 let dirty_multi_buffer = cx.new(|cx| {
8355 TestItem::new(cx)
8356 .with_dirty(true)
8357 .with_singleton(false)
8358 .with_label("Fake Project Search")
8359 .with_project_items(&[
8360 dirty_regular_buffer.read(cx).project_items[0].clone(),
8361 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8362 clear_regular_buffer.read(cx).project_items[0].clone(),
8363 ])
8364 });
8365 workspace.update_in(cx, |workspace, window, cx| {
8366 workspace.add_item(
8367 pane.clone(),
8368 Box::new(dirty_regular_buffer.clone()),
8369 None,
8370 false,
8371 false,
8372 window,
8373 cx,
8374 );
8375 workspace.add_item(
8376 pane.clone(),
8377 Box::new(dirty_regular_buffer_2.clone()),
8378 None,
8379 false,
8380 false,
8381 window,
8382 cx,
8383 );
8384 workspace.add_item(
8385 pane.clone(),
8386 Box::new(dirty_multi_buffer.clone()),
8387 None,
8388 false,
8389 false,
8390 window,
8391 cx,
8392 );
8393 });
8394
8395 pane.update_in(cx, |pane, window, cx| {
8396 pane.activate_item(2, true, true, window, cx);
8397 assert_eq!(
8398 pane.active_item().unwrap().item_id(),
8399 dirty_multi_buffer.item_id(),
8400 "Should select the multi buffer in the pane"
8401 );
8402 });
8403 let close_multi_buffer_task = pane
8404 .update_in(cx, |pane, window, cx| {
8405 pane.close_active_item(
8406 &CloseActiveItem {
8407 save_intent: None,
8408 close_pinned: false,
8409 },
8410 window,
8411 cx,
8412 )
8413 })
8414 .expect("should have active multi buffer to close");
8415 cx.background_executor.run_until_parked();
8416 assert!(
8417 !cx.has_pending_prompt(),
8418 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
8419 );
8420 close_multi_buffer_task
8421 .await
8422 .expect("Closing multi buffer failed");
8423 pane.update(cx, |pane, cx| {
8424 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8425 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
8426 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8427 assert_eq!(
8428 pane.items()
8429 .map(|item| item.item_id())
8430 .sorted()
8431 .collect::<Vec<_>>(),
8432 vec![
8433 dirty_regular_buffer.item_id(),
8434 dirty_regular_buffer_2.item_id(),
8435 ],
8436 "Should have no multi buffer left in the pane"
8437 );
8438 assert!(dirty_regular_buffer.read(cx).is_dirty);
8439 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
8440 });
8441 }
8442
8443 #[gpui::test]
8444 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
8445 init_test(cx);
8446 let fs = FakeFs::new(cx.executor());
8447 let project = Project::test(fs, [], cx).await;
8448 let (workspace, cx) =
8449 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8450
8451 // Add a new panel to the right dock, opening the dock and setting the
8452 // focus to the new panel.
8453 let panel = workspace.update_in(cx, |workspace, window, cx| {
8454 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8455 workspace.add_panel(panel.clone(), window, cx);
8456
8457 workspace
8458 .right_dock()
8459 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8460
8461 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8462
8463 panel
8464 });
8465
8466 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8467 // panel to the next valid position which, in this case, is the left
8468 // dock.
8469 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8470 workspace.update(cx, |workspace, cx| {
8471 assert!(workspace.left_dock().read(cx).is_open());
8472 assert_eq!(panel.read(cx).position, DockPosition::Left);
8473 });
8474
8475 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8476 // panel to the next valid position which, in this case, is the bottom
8477 // dock.
8478 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8479 workspace.update(cx, |workspace, cx| {
8480 assert!(workspace.bottom_dock().read(cx).is_open());
8481 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
8482 });
8483
8484 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
8485 // around moving the panel to its initial position, the right dock.
8486 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8487 workspace.update(cx, |workspace, cx| {
8488 assert!(workspace.right_dock().read(cx).is_open());
8489 assert_eq!(panel.read(cx).position, DockPosition::Right);
8490 });
8491
8492 // Remove focus from the panel, ensuring that, if the panel is not
8493 // focused, the `MoveFocusedPanelToNextPosition` action does not update
8494 // the panel's position, so the panel is still in the right dock.
8495 workspace.update_in(cx, |workspace, window, cx| {
8496 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8497 });
8498
8499 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8500 workspace.update(cx, |workspace, cx| {
8501 assert!(workspace.right_dock().read(cx).is_open());
8502 assert_eq!(panel.read(cx).position, DockPosition::Right);
8503 });
8504 }
8505
8506 mod register_project_item_tests {
8507
8508 use super::*;
8509
8510 // View
8511 struct TestPngItemView {
8512 focus_handle: FocusHandle,
8513 }
8514 // Model
8515 struct TestPngItem {}
8516
8517 impl project::ProjectItem for TestPngItem {
8518 fn try_open(
8519 _project: &Entity<Project>,
8520 path: &ProjectPath,
8521 cx: &mut App,
8522 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8523 if path.path.extension().unwrap() == "png" {
8524 Some(cx.spawn(|mut cx| async move { cx.new(|_| TestPngItem {}) }))
8525 } else {
8526 None
8527 }
8528 }
8529
8530 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8531 None
8532 }
8533
8534 fn project_path(&self, _: &App) -> Option<ProjectPath> {
8535 None
8536 }
8537
8538 fn is_dirty(&self) -> bool {
8539 false
8540 }
8541 }
8542
8543 impl Item for TestPngItemView {
8544 type Event = ();
8545 }
8546 impl EventEmitter<()> for TestPngItemView {}
8547 impl Focusable for TestPngItemView {
8548 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8549 self.focus_handle.clone()
8550 }
8551 }
8552
8553 impl Render for TestPngItemView {
8554 fn render(
8555 &mut self,
8556 _window: &mut Window,
8557 _cx: &mut Context<Self>,
8558 ) -> impl IntoElement {
8559 Empty
8560 }
8561 }
8562
8563 impl ProjectItem for TestPngItemView {
8564 type Item = TestPngItem;
8565
8566 fn for_project_item(
8567 _project: Entity<Project>,
8568 _item: Entity<Self::Item>,
8569 _: &mut Window,
8570 cx: &mut Context<Self>,
8571 ) -> Self
8572 where
8573 Self: Sized,
8574 {
8575 Self {
8576 focus_handle: cx.focus_handle(),
8577 }
8578 }
8579 }
8580
8581 // View
8582 struct TestIpynbItemView {
8583 focus_handle: FocusHandle,
8584 }
8585 // Model
8586 struct TestIpynbItem {}
8587
8588 impl project::ProjectItem for TestIpynbItem {
8589 fn try_open(
8590 _project: &Entity<Project>,
8591 path: &ProjectPath,
8592 cx: &mut App,
8593 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8594 if path.path.extension().unwrap() == "ipynb" {
8595 Some(cx.spawn(|mut cx| async move { cx.new(|_| TestIpynbItem {}) }))
8596 } else {
8597 None
8598 }
8599 }
8600
8601 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8602 None
8603 }
8604
8605 fn project_path(&self, _: &App) -> Option<ProjectPath> {
8606 None
8607 }
8608
8609 fn is_dirty(&self) -> bool {
8610 false
8611 }
8612 }
8613
8614 impl Item for TestIpynbItemView {
8615 type Event = ();
8616 }
8617 impl EventEmitter<()> for TestIpynbItemView {}
8618 impl Focusable for TestIpynbItemView {
8619 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8620 self.focus_handle.clone()
8621 }
8622 }
8623
8624 impl Render for TestIpynbItemView {
8625 fn render(
8626 &mut self,
8627 _window: &mut Window,
8628 _cx: &mut Context<Self>,
8629 ) -> impl IntoElement {
8630 Empty
8631 }
8632 }
8633
8634 impl ProjectItem for TestIpynbItemView {
8635 type Item = TestIpynbItem;
8636
8637 fn for_project_item(
8638 _project: Entity<Project>,
8639 _item: Entity<Self::Item>,
8640 _: &mut Window,
8641 cx: &mut Context<Self>,
8642 ) -> Self
8643 where
8644 Self: Sized,
8645 {
8646 Self {
8647 focus_handle: cx.focus_handle(),
8648 }
8649 }
8650 }
8651
8652 struct TestAlternatePngItemView {
8653 focus_handle: FocusHandle,
8654 }
8655
8656 impl Item for TestAlternatePngItemView {
8657 type Event = ();
8658 }
8659
8660 impl EventEmitter<()> for TestAlternatePngItemView {}
8661 impl Focusable for TestAlternatePngItemView {
8662 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8663 self.focus_handle.clone()
8664 }
8665 }
8666
8667 impl Render for TestAlternatePngItemView {
8668 fn render(
8669 &mut self,
8670 _window: &mut Window,
8671 _cx: &mut Context<Self>,
8672 ) -> impl IntoElement {
8673 Empty
8674 }
8675 }
8676
8677 impl ProjectItem for TestAlternatePngItemView {
8678 type Item = TestPngItem;
8679
8680 fn for_project_item(
8681 _project: Entity<Project>,
8682 _item: Entity<Self::Item>,
8683 _: &mut Window,
8684 cx: &mut Context<Self>,
8685 ) -> Self
8686 where
8687 Self: Sized,
8688 {
8689 Self {
8690 focus_handle: cx.focus_handle(),
8691 }
8692 }
8693 }
8694
8695 #[gpui::test]
8696 async fn test_register_project_item(cx: &mut TestAppContext) {
8697 init_test(cx);
8698
8699 cx.update(|cx| {
8700 register_project_item::<TestPngItemView>(cx);
8701 register_project_item::<TestIpynbItemView>(cx);
8702 });
8703
8704 let fs = FakeFs::new(cx.executor());
8705 fs.insert_tree(
8706 "/root1",
8707 json!({
8708 "one.png": "BINARYDATAHERE",
8709 "two.ipynb": "{ totally a notebook }",
8710 "three.txt": "editing text, sure why not?"
8711 }),
8712 )
8713 .await;
8714
8715 let project = Project::test(fs, ["root1".as_ref()], cx).await;
8716 let (workspace, cx) =
8717 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8718
8719 let worktree_id = project.update(cx, |project, cx| {
8720 project.worktrees(cx).next().unwrap().read(cx).id()
8721 });
8722
8723 let handle = workspace
8724 .update_in(cx, |workspace, window, cx| {
8725 let project_path = (worktree_id, "one.png");
8726 workspace.open_path(project_path, None, true, window, cx)
8727 })
8728 .await
8729 .unwrap();
8730
8731 // Now we can check if the handle we got back errored or not
8732 assert_eq!(
8733 handle.to_any().entity_type(),
8734 TypeId::of::<TestPngItemView>()
8735 );
8736
8737 let handle = workspace
8738 .update_in(cx, |workspace, window, cx| {
8739 let project_path = (worktree_id, "two.ipynb");
8740 workspace.open_path(project_path, None, true, window, cx)
8741 })
8742 .await
8743 .unwrap();
8744
8745 assert_eq!(
8746 handle.to_any().entity_type(),
8747 TypeId::of::<TestIpynbItemView>()
8748 );
8749
8750 let handle = workspace
8751 .update_in(cx, |workspace, window, cx| {
8752 let project_path = (worktree_id, "three.txt");
8753 workspace.open_path(project_path, None, true, window, cx)
8754 })
8755 .await;
8756 assert!(handle.is_err());
8757 }
8758
8759 #[gpui::test]
8760 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8761 init_test(cx);
8762
8763 cx.update(|cx| {
8764 register_project_item::<TestPngItemView>(cx);
8765 register_project_item::<TestAlternatePngItemView>(cx);
8766 });
8767
8768 let fs = FakeFs::new(cx.executor());
8769 fs.insert_tree(
8770 "/root1",
8771 json!({
8772 "one.png": "BINARYDATAHERE",
8773 "two.ipynb": "{ totally a notebook }",
8774 "three.txt": "editing text, sure why not?"
8775 }),
8776 )
8777 .await;
8778 let project = Project::test(fs, ["root1".as_ref()], cx).await;
8779 let (workspace, cx) =
8780 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8781 let worktree_id = project.update(cx, |project, cx| {
8782 project.worktrees(cx).next().unwrap().read(cx).id()
8783 });
8784
8785 let handle = workspace
8786 .update_in(cx, |workspace, window, cx| {
8787 let project_path = (worktree_id, "one.png");
8788 workspace.open_path(project_path, None, true, window, cx)
8789 })
8790 .await
8791 .unwrap();
8792
8793 // This _must_ be the second item registered
8794 assert_eq!(
8795 handle.to_any().entity_type(),
8796 TypeId::of::<TestAlternatePngItemView>()
8797 );
8798
8799 let handle = workspace
8800 .update_in(cx, |workspace, window, cx| {
8801 let project_path = (worktree_id, "three.txt");
8802 workspace.open_path(project_path, None, true, window, cx)
8803 })
8804 .await;
8805 assert!(handle.is_err());
8806 }
8807 }
8808
8809 pub fn init_test(cx: &mut TestAppContext) {
8810 cx.update(|cx| {
8811 let settings_store = SettingsStore::test(cx);
8812 cx.set_global(settings_store);
8813 theme::init(theme::LoadThemes::JustBase, cx);
8814 language::init(cx);
8815 crate::init_settings(cx);
8816 Project::init_settings(cx);
8817 });
8818 }
8819
8820 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
8821 let item = TestProjectItem::new(id, path, cx);
8822 item.update(cx, |item, _| {
8823 item.is_dirty = true;
8824 });
8825 item
8826 }
8827}