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