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