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