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