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