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