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