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