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