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 .with_click_message("File an issue")
5211 .on_click(|_window, cx| cx.open_url(REPORT_ISSUE_URL))
5212 })
5213 },
5214 );
5215 }
5216 })
5217 .log_err();
5218}
5219
5220impl Focusable for Workspace {
5221 fn focus_handle(&self, cx: &App) -> FocusHandle {
5222 self.active_pane.focus_handle(cx)
5223 }
5224}
5225
5226#[derive(Clone)]
5227struct DraggedDock(DockPosition);
5228
5229impl Render for DraggedDock {
5230 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5231 gpui::Empty
5232 }
5233}
5234
5235impl Render for Workspace {
5236 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5237 let mut context = KeyContext::new_with_defaults();
5238 context.add("Workspace");
5239 context.set("keyboard_layout", cx.keyboard_layout().clone());
5240 let centered_layout = self.centered_layout
5241 && self.center.panes().len() == 1
5242 && self.active_item(cx).is_some();
5243 let render_padding = |size| {
5244 (size > 0.0).then(|| {
5245 div()
5246 .h_full()
5247 .w(relative(size))
5248 .bg(cx.theme().colors().editor_background)
5249 .border_color(cx.theme().colors().pane_group_border)
5250 })
5251 };
5252 let paddings = if centered_layout {
5253 let settings = WorkspaceSettings::get_global(cx).centered_layout;
5254 (
5255 render_padding(Self::adjust_padding(settings.left_padding)),
5256 render_padding(Self::adjust_padding(settings.right_padding)),
5257 )
5258 } else {
5259 (None, None)
5260 };
5261 let ui_font = theme::setup_ui_font(window, cx);
5262
5263 let theme = cx.theme().clone();
5264 let colors = theme.colors();
5265
5266 client_side_decorations(
5267 self.actions(div(), window, cx)
5268 .key_context(context)
5269 .relative()
5270 .size_full()
5271 .flex()
5272 .flex_col()
5273 .font(ui_font)
5274 .gap_0()
5275 .justify_start()
5276 .items_start()
5277 .text_color(colors.text)
5278 .overflow_hidden()
5279 .children(self.titlebar_item.clone())
5280 .child(
5281 div()
5282 .size_full()
5283 .relative()
5284 .flex_1()
5285 .flex()
5286 .flex_col()
5287 .child(
5288 div()
5289 .id("workspace")
5290 .bg(colors.background)
5291 .relative()
5292 .flex_1()
5293 .w_full()
5294 .flex()
5295 .flex_col()
5296 .overflow_hidden()
5297 .border_t_1()
5298 .border_b_1()
5299 .border_color(colors.border)
5300 .child({
5301 let this = cx.entity().clone();
5302 canvas(
5303 move |bounds, window, cx| {
5304 this.update(cx, |this, cx| {
5305 let bounds_changed = this.bounds != bounds;
5306 this.bounds = bounds;
5307
5308 if bounds_changed {
5309 this.left_dock.update(cx, |dock, cx| {
5310 dock.clamp_panel_size(
5311 bounds.size.width,
5312 window,
5313 cx,
5314 )
5315 });
5316
5317 this.right_dock.update(cx, |dock, cx| {
5318 dock.clamp_panel_size(
5319 bounds.size.width,
5320 window,
5321 cx,
5322 )
5323 });
5324
5325 this.bottom_dock.update(cx, |dock, cx| {
5326 dock.clamp_panel_size(
5327 bounds.size.height,
5328 window,
5329 cx,
5330 )
5331 });
5332 }
5333 })
5334 },
5335 |_, _, _, _| {},
5336 )
5337 .absolute()
5338 .size_full()
5339 })
5340 .when(self.zoomed.is_none(), |this| {
5341 this.on_drag_move(cx.listener(
5342 move |workspace,
5343 e: &DragMoveEvent<DraggedDock>,
5344 window,
5345 cx| {
5346 if workspace.previous_dock_drag_coordinates
5347 != Some(e.event.position)
5348 {
5349 workspace.previous_dock_drag_coordinates =
5350 Some(e.event.position);
5351 match e.drag(cx).0 {
5352 DockPosition::Left => {
5353 resize_left_dock(
5354 e.event.position.x
5355 - workspace.bounds.left(),
5356 workspace,
5357 window,
5358 cx,
5359 );
5360 }
5361 DockPosition::Right => {
5362 resize_right_dock(
5363 workspace.bounds.right()
5364 - e.event.position.x,
5365 workspace,
5366 window,
5367 cx,
5368 );
5369 }
5370 DockPosition::Bottom => {
5371 resize_bottom_dock(
5372 workspace.bounds.bottom()
5373 - e.event.position.y,
5374 workspace,
5375 window,
5376 cx,
5377 );
5378 }
5379 };
5380 workspace.serialize_workspace(window, cx);
5381 }
5382 },
5383 ))
5384 })
5385 .child(
5386 div()
5387 .flex()
5388 .flex_row()
5389 .h_full()
5390 // Left Dock
5391 .children(self.render_dock(
5392 DockPosition::Left,
5393 &self.left_dock,
5394 window,
5395 cx,
5396 ))
5397 // Panes
5398 .child(
5399 div()
5400 .flex()
5401 .flex_col()
5402 .flex_1()
5403 .overflow_hidden()
5404 .child(
5405 h_flex()
5406 .flex_1()
5407 .when_some(paddings.0, |this, p| {
5408 this.child(p.border_r_1())
5409 })
5410 .child(self.center.render(
5411 &self.project,
5412 &self.follower_states,
5413 self.active_call(),
5414 &self.active_pane,
5415 self.zoomed.as_ref(),
5416 &self.app_state,
5417 window,
5418 cx,
5419 ))
5420 .when_some(paddings.1, |this, p| {
5421 this.child(p.border_l_1())
5422 }),
5423 )
5424 .children(self.render_dock(
5425 DockPosition::Bottom,
5426 &self.bottom_dock,
5427 window,
5428 cx,
5429 )),
5430 )
5431 // Right Dock
5432 .children(self.render_dock(
5433 DockPosition::Right,
5434 &self.right_dock,
5435 window,
5436 cx,
5437 )),
5438 )
5439 .children(self.zoomed.as_ref().and_then(|view| {
5440 let zoomed_view = view.upgrade()?;
5441 let div = div()
5442 .occlude()
5443 .absolute()
5444 .overflow_hidden()
5445 .border_color(colors.border)
5446 .bg(colors.background)
5447 .child(zoomed_view)
5448 .inset_0()
5449 .shadow_lg();
5450
5451 Some(match self.zoomed_position {
5452 Some(DockPosition::Left) => div.right_2().border_r_1(),
5453 Some(DockPosition::Right) => div.left_2().border_l_1(),
5454 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
5455 None => {
5456 div.top_2().bottom_2().left_2().right_2().border_1()
5457 }
5458 })
5459 }))
5460 .children(self.render_notifications(window, cx)),
5461 )
5462 .child(self.status_bar.clone())
5463 .child(self.modal_layer.clone()),
5464 ),
5465 window,
5466 cx,
5467 )
5468 }
5469}
5470
5471fn resize_bottom_dock(
5472 new_size: Pixels,
5473 workspace: &mut Workspace,
5474 window: &mut Window,
5475 cx: &mut App,
5476) {
5477 let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
5478 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
5479 bottom_dock.resize_active_panel(Some(size), window, cx);
5480 });
5481}
5482
5483fn resize_right_dock(
5484 new_size: Pixels,
5485 workspace: &mut Workspace,
5486 window: &mut Window,
5487 cx: &mut App,
5488) {
5489 let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
5490 workspace.right_dock.update(cx, |right_dock, cx| {
5491 right_dock.resize_active_panel(Some(size), window, cx);
5492 });
5493}
5494
5495fn resize_left_dock(
5496 new_size: Pixels,
5497 workspace: &mut Workspace,
5498 window: &mut Window,
5499 cx: &mut App,
5500) {
5501 let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
5502
5503 workspace.left_dock.update(cx, |left_dock, cx| {
5504 left_dock.resize_active_panel(Some(size), window, cx);
5505 });
5506}
5507
5508impl WorkspaceStore {
5509 pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
5510 Self {
5511 workspaces: Default::default(),
5512 _subscriptions: vec![
5513 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
5514 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
5515 ],
5516 client,
5517 }
5518 }
5519
5520 pub fn update_followers(
5521 &self,
5522 project_id: Option<u64>,
5523 update: proto::update_followers::Variant,
5524 cx: &App,
5525 ) -> Option<()> {
5526 let active_call = ActiveCall::try_global(cx)?;
5527 let room_id = active_call.read(cx).room()?.read(cx).id();
5528 self.client
5529 .send(proto::UpdateFollowers {
5530 room_id,
5531 project_id,
5532 variant: Some(update),
5533 })
5534 .log_err()
5535 }
5536
5537 pub async fn handle_follow(
5538 this: Entity<Self>,
5539 envelope: TypedEnvelope<proto::Follow>,
5540 mut cx: AsyncApp,
5541 ) -> Result<proto::FollowResponse> {
5542 this.update(&mut cx, |this, cx| {
5543 let follower = Follower {
5544 project_id: envelope.payload.project_id,
5545 peer_id: envelope.original_sender_id()?,
5546 };
5547
5548 let mut response = proto::FollowResponse::default();
5549 this.workspaces.retain(|workspace| {
5550 workspace
5551 .update(cx, |workspace, window, cx| {
5552 let handler_response =
5553 workspace.handle_follow(follower.project_id, window, cx);
5554 if let Some(active_view) = handler_response.active_view.clone() {
5555 if workspace.project.read(cx).remote_id() == follower.project_id {
5556 response.active_view = Some(active_view)
5557 }
5558 }
5559 })
5560 .is_ok()
5561 });
5562
5563 Ok(response)
5564 })?
5565 }
5566
5567 async fn handle_update_followers(
5568 this: Entity<Self>,
5569 envelope: TypedEnvelope<proto::UpdateFollowers>,
5570 mut cx: AsyncApp,
5571 ) -> Result<()> {
5572 let leader_id = envelope.original_sender_id()?;
5573 let update = envelope.payload;
5574
5575 this.update(&mut cx, |this, cx| {
5576 this.workspaces.retain(|workspace| {
5577 workspace
5578 .update(cx, |workspace, window, cx| {
5579 let project_id = workspace.project.read(cx).remote_id();
5580 if update.project_id != project_id && update.project_id.is_some() {
5581 return;
5582 }
5583 workspace.handle_update_followers(leader_id, update.clone(), window, cx);
5584 })
5585 .is_ok()
5586 });
5587 Ok(())
5588 })?
5589 }
5590}
5591
5592impl ViewId {
5593 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5594 Ok(Self {
5595 creator: message
5596 .creator
5597 .ok_or_else(|| anyhow!("creator is missing"))?,
5598 id: message.id,
5599 })
5600 }
5601
5602 pub(crate) fn to_proto(self) -> proto::ViewId {
5603 proto::ViewId {
5604 creator: Some(self.creator),
5605 id: self.id,
5606 }
5607 }
5608}
5609
5610impl FollowerState {
5611 fn pane(&self) -> &Entity<Pane> {
5612 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5613 }
5614}
5615
5616pub trait WorkspaceHandle {
5617 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
5618}
5619
5620impl WorkspaceHandle for Entity<Workspace> {
5621 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
5622 self.read(cx)
5623 .worktrees(cx)
5624 .flat_map(|worktree| {
5625 let worktree_id = worktree.read(cx).id();
5626 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5627 worktree_id,
5628 path: f.path.clone(),
5629 })
5630 })
5631 .collect::<Vec<_>>()
5632 }
5633}
5634
5635impl std::fmt::Debug for OpenPaths {
5636 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5637 f.debug_struct("OpenPaths")
5638 .field("paths", &self.paths)
5639 .finish()
5640 }
5641}
5642
5643pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5644 DB.last_workspace().await.log_err().flatten()
5645}
5646
5647pub fn last_session_workspace_locations(
5648 last_session_id: &str,
5649 last_session_window_stack: Option<Vec<WindowId>>,
5650) -> Option<Vec<SerializedWorkspaceLocation>> {
5651 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5652 .log_err()
5653}
5654
5655actions!(collab, [OpenChannelNotes]);
5656actions!(zed, [OpenLog]);
5657
5658async fn join_channel_internal(
5659 channel_id: ChannelId,
5660 app_state: &Arc<AppState>,
5661 requesting_window: Option<WindowHandle<Workspace>>,
5662 active_call: &Entity<ActiveCall>,
5663 cx: &mut AsyncApp,
5664) -> Result<bool> {
5665 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5666 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5667 return (false, None);
5668 };
5669
5670 let already_in_channel = room.channel_id() == Some(channel_id);
5671 let should_prompt = room.is_sharing_project()
5672 && !room.remote_participants().is_empty()
5673 && !already_in_channel;
5674 let open_room = if already_in_channel {
5675 active_call.room().cloned()
5676 } else {
5677 None
5678 };
5679 (should_prompt, open_room)
5680 })?;
5681
5682 if let Some(room) = open_room {
5683 let task = room.update(cx, |room, cx| {
5684 if let Some((project, host)) = room.most_active_project(cx) {
5685 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5686 }
5687
5688 None
5689 })?;
5690 if let Some(task) = task {
5691 task.await?;
5692 }
5693 return anyhow::Ok(true);
5694 }
5695
5696 if should_prompt {
5697 if let Some(workspace) = requesting_window {
5698 let answer = workspace
5699 .update(cx, |_, window, cx| {
5700 window.prompt(
5701 PromptLevel::Warning,
5702 "Do you want to switch channels?",
5703 Some("Leaving this call will unshare your current project."),
5704 &["Yes, Join Channel", "Cancel"],
5705 cx,
5706 )
5707 })?
5708 .await;
5709
5710 if answer == Ok(1) {
5711 return Ok(false);
5712 }
5713 } else {
5714 return Ok(false); // unreachable!() hopefully
5715 }
5716 }
5717
5718 let client = cx.update(|cx| active_call.read(cx).client())?;
5719
5720 let mut client_status = client.status();
5721
5722 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5723 'outer: loop {
5724 let Some(status) = client_status.recv().await else {
5725 return Err(anyhow!("error connecting"));
5726 };
5727
5728 match status {
5729 Status::Connecting
5730 | Status::Authenticating
5731 | Status::Reconnecting
5732 | Status::Reauthenticating => continue,
5733 Status::Connected { .. } => break 'outer,
5734 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5735 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5736 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5737 return Err(ErrorCode::Disconnected.into());
5738 }
5739 }
5740 }
5741
5742 let room = active_call
5743 .update(cx, |active_call, cx| {
5744 active_call.join_channel(channel_id, cx)
5745 })?
5746 .await?;
5747
5748 let Some(room) = room else {
5749 return anyhow::Ok(true);
5750 };
5751
5752 room.update(cx, |room, _| room.room_update_completed())?
5753 .await;
5754
5755 let task = room.update(cx, |room, cx| {
5756 if let Some((project, host)) = room.most_active_project(cx) {
5757 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5758 }
5759
5760 // If you are the first to join a channel, see if you should share your project.
5761 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5762 if let Some(workspace) = requesting_window {
5763 let project = workspace.update(cx, |workspace, _, cx| {
5764 let project = workspace.project.read(cx);
5765
5766 if !CallSettings::get_global(cx).share_on_join {
5767 return None;
5768 }
5769
5770 if (project.is_local() || project.is_via_ssh())
5771 && project.visible_worktrees(cx).any(|tree| {
5772 tree.read(cx)
5773 .root_entry()
5774 .map_or(false, |entry| entry.is_dir())
5775 })
5776 {
5777 Some(workspace.project.clone())
5778 } else {
5779 None
5780 }
5781 });
5782 if let Ok(Some(project)) = project {
5783 return Some(cx.spawn(|room, mut cx| async move {
5784 room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5785 .await?;
5786 Ok(())
5787 }));
5788 }
5789 }
5790 }
5791
5792 None
5793 })?;
5794 if let Some(task) = task {
5795 task.await?;
5796 return anyhow::Ok(true);
5797 }
5798 anyhow::Ok(false)
5799}
5800
5801pub fn join_channel(
5802 channel_id: ChannelId,
5803 app_state: Arc<AppState>,
5804 requesting_window: Option<WindowHandle<Workspace>>,
5805 cx: &mut App,
5806) -> Task<Result<()>> {
5807 let active_call = ActiveCall::global(cx);
5808 cx.spawn(|mut cx| async move {
5809 let result = join_channel_internal(
5810 channel_id,
5811 &app_state,
5812 requesting_window,
5813 &active_call,
5814 &mut cx,
5815 )
5816 .await;
5817
5818 // join channel succeeded, and opened a window
5819 if matches!(result, Ok(true)) {
5820 return anyhow::Ok(());
5821 }
5822
5823 // find an existing workspace to focus and show call controls
5824 let mut active_window =
5825 requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5826 if active_window.is_none() {
5827 // no open workspaces, make one to show the error in (blergh)
5828 let (window_handle, _) = cx
5829 .update(|cx| {
5830 Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5831 })?
5832 .await?;
5833
5834 if result.is_ok() {
5835 cx.update(|cx| {
5836 cx.dispatch_action(&OpenChannelNotes);
5837 }).log_err();
5838 }
5839
5840 active_window = Some(window_handle);
5841 }
5842
5843 if let Err(err) = result {
5844 log::error!("failed to join channel: {}", err);
5845 if let Some(active_window) = active_window {
5846 active_window
5847 .update(&mut cx, |_, window, cx| {
5848 let detail: SharedString = match err.error_code() {
5849 ErrorCode::SignedOut => {
5850 "Please sign in to continue.".into()
5851 }
5852 ErrorCode::UpgradeRequired => {
5853 "Your are running an unsupported version of Zed. Please update to continue.".into()
5854 }
5855 ErrorCode::NoSuchChannel => {
5856 "No matching channel was found. Please check the link and try again.".into()
5857 }
5858 ErrorCode::Forbidden => {
5859 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5860 }
5861 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5862 _ => format!("{}\n\nPlease try again.", err).into(),
5863 };
5864 window.prompt(
5865 PromptLevel::Critical,
5866 "Failed to join channel",
5867 Some(&detail),
5868 &["Ok"],
5869 cx)
5870 })?
5871 .await
5872 .ok();
5873 }
5874 }
5875
5876 // return ok, we showed the error to the user.
5877 anyhow::Ok(())
5878 })
5879}
5880
5881pub async fn get_any_active_workspace(
5882 app_state: Arc<AppState>,
5883 mut cx: AsyncApp,
5884) -> anyhow::Result<WindowHandle<Workspace>> {
5885 // find an existing workspace to focus and show call controls
5886 let active_window = activate_any_workspace_window(&mut cx);
5887 if active_window.is_none() {
5888 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5889 .await?;
5890 }
5891 activate_any_workspace_window(&mut cx).context("could not open zed")
5892}
5893
5894fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
5895 cx.update(|cx| {
5896 if let Some(workspace_window) = cx
5897 .active_window()
5898 .and_then(|window| window.downcast::<Workspace>())
5899 {
5900 return Some(workspace_window);
5901 }
5902
5903 for window in cx.windows() {
5904 if let Some(workspace_window) = window.downcast::<Workspace>() {
5905 workspace_window
5906 .update(cx, |_, window, _| window.activate_window())
5907 .ok();
5908 return Some(workspace_window);
5909 }
5910 }
5911 None
5912 })
5913 .ok()
5914 .flatten()
5915}
5916
5917pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
5918 cx.windows()
5919 .into_iter()
5920 .filter_map(|window| window.downcast::<Workspace>())
5921 .filter(|workspace| {
5922 workspace
5923 .read(cx)
5924 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5925 })
5926 .collect()
5927}
5928
5929#[derive(Default)]
5930pub struct OpenOptions {
5931 pub open_new_workspace: Option<bool>,
5932 pub replace_window: Option<WindowHandle<Workspace>>,
5933 pub env: Option<HashMap<String, String>>,
5934}
5935
5936#[allow(clippy::type_complexity)]
5937pub fn open_paths(
5938 abs_paths: &[PathBuf],
5939 app_state: Arc<AppState>,
5940 open_options: OpenOptions,
5941 cx: &mut App,
5942) -> Task<
5943 anyhow::Result<(
5944 WindowHandle<Workspace>,
5945 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5946 )>,
5947> {
5948 let abs_paths = abs_paths.to_vec();
5949 let mut existing = None;
5950 let mut best_match = None;
5951 let mut open_visible = OpenVisible::All;
5952
5953 if open_options.open_new_workspace != Some(true) {
5954 for window in local_workspace_windows(cx) {
5955 if let Ok(workspace) = window.read(cx) {
5956 let m = workspace
5957 .project
5958 .read(cx)
5959 .visibility_for_paths(&abs_paths, cx);
5960 if m > best_match {
5961 existing = Some(window);
5962 best_match = m;
5963 } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
5964 existing = Some(window)
5965 }
5966 }
5967 }
5968 }
5969
5970 cx.spawn(move |mut cx| async move {
5971 if open_options.open_new_workspace.is_none() && existing.is_none() {
5972 let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5973 if futures::future::join_all(all_files)
5974 .await
5975 .into_iter()
5976 .filter_map(|result| result.ok().flatten())
5977 .all(|file| !file.is_dir)
5978 {
5979 cx.update(|cx| {
5980 if let Some(window) = cx
5981 .active_window()
5982 .and_then(|window| window.downcast::<Workspace>())
5983 {
5984 if let Ok(workspace) = window.read(cx) {
5985 let project = workspace.project().read(cx);
5986 if project.is_local() && !project.is_via_collab() {
5987 existing = Some(window);
5988 open_visible = OpenVisible::None;
5989 return;
5990 }
5991 }
5992 }
5993 for window in local_workspace_windows(cx) {
5994 if let Ok(workspace) = window.read(cx) {
5995 let project = workspace.project().read(cx);
5996 if project.is_via_collab() {
5997 continue;
5998 }
5999 existing = Some(window);
6000 open_visible = OpenVisible::None;
6001 break;
6002 }
6003 }
6004 })?;
6005 }
6006 }
6007
6008 if let Some(existing) = existing {
6009 let open_task = existing
6010 .update(&mut cx, |workspace, window, cx| {
6011 window.activate_window();
6012 workspace.open_paths(abs_paths, open_visible, None, window, cx)
6013 })?
6014 .await;
6015
6016 _ = existing.update(&mut cx, |workspace, _, cx| {
6017 for item in open_task.iter().flatten() {
6018 if let Err(e) = item {
6019 workspace.show_error(&e, cx);
6020 }
6021 }
6022 });
6023
6024 Ok((existing, open_task))
6025 } else {
6026 cx.update(move |cx| {
6027 Workspace::new_local(
6028 abs_paths,
6029 app_state.clone(),
6030 open_options.replace_window,
6031 open_options.env,
6032 cx,
6033 )
6034 })?
6035 .await
6036 }
6037 })
6038}
6039
6040pub fn open_new(
6041 open_options: OpenOptions,
6042 app_state: Arc<AppState>,
6043 cx: &mut App,
6044 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6045) -> Task<anyhow::Result<()>> {
6046 let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6047 cx.spawn(|mut cx| async move {
6048 let (workspace, opened_paths) = task.await?;
6049 workspace.update(&mut cx, |workspace, window, cx| {
6050 if opened_paths.is_empty() {
6051 init(workspace, window, cx)
6052 }
6053 })?;
6054 Ok(())
6055 })
6056}
6057
6058pub fn create_and_open_local_file(
6059 path: &'static Path,
6060 window: &mut Window,
6061 cx: &mut Context<Workspace>,
6062 default_content: impl 'static + Send + FnOnce() -> Rope,
6063) -> Task<Result<Box<dyn ItemHandle>>> {
6064 cx.spawn_in(window, |workspace, mut cx| async move {
6065 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
6066 if !fs.is_file(path).await {
6067 fs.create_file(path, Default::default()).await?;
6068 fs.save(path, &default_content(), Default::default())
6069 .await?;
6070 }
6071
6072 let mut items = workspace
6073 .update_in(&mut cx, |workspace, window, cx| {
6074 workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6075 workspace.open_paths(
6076 vec![path.to_path_buf()],
6077 OpenVisible::None,
6078 None,
6079 window,
6080 cx,
6081 )
6082 })
6083 })?
6084 .await?
6085 .await;
6086
6087 let item = items.pop().flatten();
6088 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
6089 })
6090}
6091
6092pub fn open_ssh_project(
6093 window: WindowHandle<Workspace>,
6094 connection_options: SshConnectionOptions,
6095 cancel_rx: oneshot::Receiver<()>,
6096 delegate: Arc<dyn SshClientDelegate>,
6097 app_state: Arc<AppState>,
6098 paths: Vec<PathBuf>,
6099 cx: &mut App,
6100) -> Task<Result<()>> {
6101 cx.spawn(|mut cx| async move {
6102 let (serialized_ssh_project, workspace_id, serialized_workspace) =
6103 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6104
6105 let session = match cx
6106 .update(|cx| {
6107 remote::SshRemoteClient::new(
6108 ConnectionIdentifier::Workspace(workspace_id.0),
6109 connection_options,
6110 cancel_rx,
6111 delegate,
6112 cx,
6113 )
6114 })?
6115 .await?
6116 {
6117 Some(result) => result,
6118 None => return Ok(()),
6119 };
6120
6121 let project = cx.update(|cx| {
6122 project::Project::ssh(
6123 session,
6124 app_state.client.clone(),
6125 app_state.node_runtime.clone(),
6126 app_state.user_store.clone(),
6127 app_state.languages.clone(),
6128 app_state.fs.clone(),
6129 cx,
6130 )
6131 })?;
6132
6133 let toolchains = DB.toolchains(workspace_id).await?;
6134 for (toolchain, worktree_id) in toolchains {
6135 project
6136 .update(&mut cx, |this, cx| {
6137 this.activate_toolchain(worktree_id, toolchain, cx)
6138 })?
6139 .await;
6140 }
6141 let mut project_paths_to_open = vec![];
6142 let mut project_path_errors = vec![];
6143
6144 for path in paths {
6145 let result = cx
6146 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6147 .await;
6148 match result {
6149 Ok((_, project_path)) => {
6150 project_paths_to_open.push((path.clone(), Some(project_path)));
6151 }
6152 Err(error) => {
6153 project_path_errors.push(error);
6154 }
6155 };
6156 }
6157
6158 if project_paths_to_open.is_empty() {
6159 return Err(project_path_errors
6160 .pop()
6161 .unwrap_or_else(|| anyhow!("no paths given")));
6162 }
6163
6164 cx.update_window(window.into(), |_, window, cx| {
6165 window.replace_root(cx, |window, cx| {
6166 telemetry::event!("SSH Project Opened");
6167
6168 let mut workspace =
6169 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
6170 workspace.set_serialized_ssh_project(serialized_ssh_project);
6171 workspace
6172 });
6173 })?;
6174
6175 window
6176 .update(&mut cx, |_, window, cx| {
6177 window.activate_window();
6178
6179 open_items(serialized_workspace, project_paths_to_open, window, cx)
6180 })?
6181 .await?;
6182
6183 window.update(&mut cx, |workspace, _, cx| {
6184 for error in project_path_errors {
6185 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
6186 if let Some(path) = error.error_tag("path") {
6187 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
6188 }
6189 } else {
6190 workspace.show_error(&error, cx)
6191 }
6192 }
6193 })
6194 })
6195}
6196
6197fn serialize_ssh_project(
6198 connection_options: SshConnectionOptions,
6199 paths: Vec<PathBuf>,
6200 cx: &AsyncApp,
6201) -> Task<
6202 Result<(
6203 SerializedSshProject,
6204 WorkspaceId,
6205 Option<SerializedWorkspace>,
6206 )>,
6207> {
6208 cx.background_executor().spawn(async move {
6209 let serialized_ssh_project = persistence::DB
6210 .get_or_create_ssh_project(
6211 connection_options.host.clone(),
6212 connection_options.port,
6213 paths
6214 .iter()
6215 .map(|path| path.to_string_lossy().to_string())
6216 .collect::<Vec<_>>(),
6217 connection_options.username.clone(),
6218 )
6219 .await?;
6220
6221 let serialized_workspace =
6222 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
6223
6224 let workspace_id = if let Some(workspace_id) =
6225 serialized_workspace.as_ref().map(|workspace| workspace.id)
6226 {
6227 workspace_id
6228 } else {
6229 persistence::DB.next_id().await?
6230 };
6231
6232 Ok((serialized_ssh_project, workspace_id, serialized_workspace))
6233 })
6234}
6235
6236pub fn join_in_room_project(
6237 project_id: u64,
6238 follow_user_id: u64,
6239 app_state: Arc<AppState>,
6240 cx: &mut App,
6241) -> Task<Result<()>> {
6242 let windows = cx.windows();
6243 cx.spawn(|mut cx| async move {
6244 let existing_workspace = windows.into_iter().find_map(|window_handle| {
6245 window_handle
6246 .downcast::<Workspace>()
6247 .and_then(|window_handle| {
6248 window_handle
6249 .update(&mut cx, |workspace, _window, cx| {
6250 if workspace.project().read(cx).remote_id() == Some(project_id) {
6251 Some(window_handle)
6252 } else {
6253 None
6254 }
6255 })
6256 .unwrap_or(None)
6257 })
6258 });
6259
6260 let workspace = if let Some(existing_workspace) = existing_workspace {
6261 existing_workspace
6262 } else {
6263 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
6264 let room = active_call
6265 .read_with(&cx, |call, _| call.room().cloned())?
6266 .ok_or_else(|| anyhow!("not in a call"))?;
6267 let project = room
6268 .update(&mut cx, |room, cx| {
6269 room.join_project(
6270 project_id,
6271 app_state.languages.clone(),
6272 app_state.fs.clone(),
6273 cx,
6274 )
6275 })?
6276 .await?;
6277
6278 let window_bounds_override = window_bounds_env_override();
6279 cx.update(|cx| {
6280 let mut options = (app_state.build_window_options)(None, cx);
6281 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
6282 cx.open_window(options, |window, cx| {
6283 cx.new(|cx| {
6284 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
6285 })
6286 })
6287 })??
6288 };
6289
6290 workspace.update(&mut cx, |workspace, window, cx| {
6291 cx.activate(true);
6292 window.activate_window();
6293
6294 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
6295 let follow_peer_id = room
6296 .read(cx)
6297 .remote_participants()
6298 .iter()
6299 .find(|(_, participant)| participant.user.id == follow_user_id)
6300 .map(|(_, p)| p.peer_id)
6301 .or_else(|| {
6302 // If we couldn't follow the given user, follow the host instead.
6303 let collaborator = workspace
6304 .project()
6305 .read(cx)
6306 .collaborators()
6307 .values()
6308 .find(|collaborator| collaborator.is_host)?;
6309 Some(collaborator.peer_id)
6310 });
6311
6312 if let Some(follow_peer_id) = follow_peer_id {
6313 workspace.follow(follow_peer_id, window, cx);
6314 }
6315 }
6316 })?;
6317
6318 anyhow::Ok(())
6319 })
6320}
6321
6322pub fn reload(reload: &Reload, cx: &mut App) {
6323 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
6324 let mut workspace_windows = cx
6325 .windows()
6326 .into_iter()
6327 .filter_map(|window| window.downcast::<Workspace>())
6328 .collect::<Vec<_>>();
6329
6330 // If multiple windows have unsaved changes, and need a save prompt,
6331 // prompt in the active window before switching to a different window.
6332 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
6333
6334 let mut prompt = None;
6335 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
6336 prompt = window
6337 .update(cx, |_, window, cx| {
6338 window.prompt(
6339 PromptLevel::Info,
6340 "Are you sure you want to restart?",
6341 None,
6342 &["Restart", "Cancel"],
6343 cx,
6344 )
6345 })
6346 .ok();
6347 }
6348
6349 let binary_path = reload.binary_path.clone();
6350 cx.spawn(|mut cx| async move {
6351 if let Some(prompt) = prompt {
6352 let answer = prompt.await?;
6353 if answer != 0 {
6354 return Ok(());
6355 }
6356 }
6357
6358 // If the user cancels any save prompt, then keep the app open.
6359 for window in workspace_windows {
6360 if let Ok(should_close) = window.update(&mut cx, |workspace, window, cx| {
6361 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
6362 }) {
6363 if !should_close.await? {
6364 return Ok(());
6365 }
6366 }
6367 }
6368
6369 cx.update(|cx| cx.restart(binary_path))
6370 })
6371 .detach_and_log_err(cx);
6372}
6373
6374fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
6375 let mut parts = value.split(',');
6376 let x: usize = parts.next()?.parse().ok()?;
6377 let y: usize = parts.next()?.parse().ok()?;
6378 Some(point(px(x as f32), px(y as f32)))
6379}
6380
6381fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
6382 let mut parts = value.split(',');
6383 let width: usize = parts.next()?.parse().ok()?;
6384 let height: usize = parts.next()?.parse().ok()?;
6385 Some(size(px(width as f32), px(height as f32)))
6386}
6387
6388pub fn client_side_decorations(
6389 element: impl IntoElement,
6390 window: &mut Window,
6391 cx: &mut App,
6392) -> Stateful<Div> {
6393 const BORDER_SIZE: Pixels = px(1.0);
6394 let decorations = window.window_decorations();
6395
6396 if matches!(decorations, Decorations::Client { .. }) {
6397 window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
6398 }
6399
6400 struct GlobalResizeEdge(ResizeEdge);
6401 impl Global for GlobalResizeEdge {}
6402
6403 div()
6404 .id("window-backdrop")
6405 .bg(transparent_black())
6406 .map(|div| match decorations {
6407 Decorations::Server => div,
6408 Decorations::Client { tiling, .. } => div
6409 .when(!(tiling.top || tiling.right), |div| {
6410 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6411 })
6412 .when(!(tiling.top || tiling.left), |div| {
6413 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6414 })
6415 .when(!(tiling.bottom || tiling.right), |div| {
6416 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6417 })
6418 .when(!(tiling.bottom || tiling.left), |div| {
6419 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6420 })
6421 .when(!tiling.top, |div| {
6422 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
6423 })
6424 .when(!tiling.bottom, |div| {
6425 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
6426 })
6427 .when(!tiling.left, |div| {
6428 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
6429 })
6430 .when(!tiling.right, |div| {
6431 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
6432 })
6433 .on_mouse_move(move |e, window, cx| {
6434 let size = window.window_bounds().get_bounds().size;
6435 let pos = e.position;
6436
6437 let new_edge =
6438 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
6439
6440 let edge = cx.try_global::<GlobalResizeEdge>();
6441 if new_edge != edge.map(|edge| edge.0) {
6442 window
6443 .window_handle()
6444 .update(cx, |workspace, _, cx| {
6445 cx.notify(workspace.entity_id());
6446 })
6447 .ok();
6448 }
6449 })
6450 .on_mouse_down(MouseButton::Left, move |e, window, _| {
6451 let size = window.window_bounds().get_bounds().size;
6452 let pos = e.position;
6453
6454 let edge = match resize_edge(
6455 pos,
6456 theme::CLIENT_SIDE_DECORATION_SHADOW,
6457 size,
6458 tiling,
6459 ) {
6460 Some(value) => value,
6461 None => return,
6462 };
6463
6464 window.start_window_resize(edge);
6465 }),
6466 })
6467 .size_full()
6468 .child(
6469 div()
6470 .cursor(CursorStyle::Arrow)
6471 .map(|div| match decorations {
6472 Decorations::Server => div,
6473 Decorations::Client { tiling } => div
6474 .border_color(cx.theme().colors().border)
6475 .when(!(tiling.top || tiling.right), |div| {
6476 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6477 })
6478 .when(!(tiling.top || tiling.left), |div| {
6479 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6480 })
6481 .when(!(tiling.bottom || tiling.right), |div| {
6482 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6483 })
6484 .when(!(tiling.bottom || tiling.left), |div| {
6485 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6486 })
6487 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6488 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6489 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6490 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6491 .when(!tiling.is_tiled(), |div| {
6492 div.shadow(smallvec::smallvec![gpui::BoxShadow {
6493 color: Hsla {
6494 h: 0.,
6495 s: 0.,
6496 l: 0.,
6497 a: 0.4,
6498 },
6499 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6500 spread_radius: px(0.),
6501 offset: point(px(0.0), px(0.0)),
6502 }])
6503 }),
6504 })
6505 .on_mouse_move(|_e, _, cx| {
6506 cx.stop_propagation();
6507 })
6508 .size_full()
6509 .child(element),
6510 )
6511 .map(|div| match decorations {
6512 Decorations::Server => div,
6513 Decorations::Client { tiling, .. } => div.child(
6514 canvas(
6515 |_bounds, window, _| {
6516 window.insert_hitbox(
6517 Bounds::new(
6518 point(px(0.0), px(0.0)),
6519 window.window_bounds().get_bounds().size,
6520 ),
6521 false,
6522 )
6523 },
6524 move |_bounds, hitbox, window, cx| {
6525 let mouse = window.mouse_position();
6526 let size = window.window_bounds().get_bounds().size;
6527 let Some(edge) =
6528 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6529 else {
6530 return;
6531 };
6532 cx.set_global(GlobalResizeEdge(edge));
6533 window.set_cursor_style(
6534 match edge {
6535 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6536 ResizeEdge::Left | ResizeEdge::Right => {
6537 CursorStyle::ResizeLeftRight
6538 }
6539 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6540 CursorStyle::ResizeUpLeftDownRight
6541 }
6542 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6543 CursorStyle::ResizeUpRightDownLeft
6544 }
6545 },
6546 &hitbox,
6547 );
6548 },
6549 )
6550 .size_full()
6551 .absolute(),
6552 ),
6553 })
6554}
6555
6556fn resize_edge(
6557 pos: Point<Pixels>,
6558 shadow_size: Pixels,
6559 window_size: Size<Pixels>,
6560 tiling: Tiling,
6561) -> Option<ResizeEdge> {
6562 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6563 if bounds.contains(&pos) {
6564 return None;
6565 }
6566
6567 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6568 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6569 if !tiling.top && top_left_bounds.contains(&pos) {
6570 return Some(ResizeEdge::TopLeft);
6571 }
6572
6573 let top_right_bounds = Bounds::new(
6574 Point::new(window_size.width - corner_size.width, px(0.)),
6575 corner_size,
6576 );
6577 if !tiling.top && top_right_bounds.contains(&pos) {
6578 return Some(ResizeEdge::TopRight);
6579 }
6580
6581 let bottom_left_bounds = Bounds::new(
6582 Point::new(px(0.), window_size.height - corner_size.height),
6583 corner_size,
6584 );
6585 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6586 return Some(ResizeEdge::BottomLeft);
6587 }
6588
6589 let bottom_right_bounds = Bounds::new(
6590 Point::new(
6591 window_size.width - corner_size.width,
6592 window_size.height - corner_size.height,
6593 ),
6594 corner_size,
6595 );
6596 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6597 return Some(ResizeEdge::BottomRight);
6598 }
6599
6600 if !tiling.top && pos.y < shadow_size {
6601 Some(ResizeEdge::Top)
6602 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6603 Some(ResizeEdge::Bottom)
6604 } else if !tiling.left && pos.x < shadow_size {
6605 Some(ResizeEdge::Left)
6606 } else if !tiling.right && pos.x > window_size.width - shadow_size {
6607 Some(ResizeEdge::Right)
6608 } else {
6609 None
6610 }
6611}
6612
6613fn join_pane_into_active(
6614 active_pane: &Entity<Pane>,
6615 pane: &Entity<Pane>,
6616 window: &mut Window,
6617 cx: &mut App,
6618) {
6619 if pane == active_pane {
6620 return;
6621 } else if pane.read(cx).items_len() == 0 {
6622 pane.update(cx, |_, cx| {
6623 cx.emit(pane::Event::Remove {
6624 focus_on_pane: None,
6625 });
6626 })
6627 } else {
6628 move_all_items(pane, active_pane, window, cx);
6629 }
6630}
6631
6632fn move_all_items(
6633 from_pane: &Entity<Pane>,
6634 to_pane: &Entity<Pane>,
6635 window: &mut Window,
6636 cx: &mut App,
6637) {
6638 let destination_is_different = from_pane != to_pane;
6639 let mut moved_items = 0;
6640 for (item_ix, item_handle) in from_pane
6641 .read(cx)
6642 .items()
6643 .enumerate()
6644 .map(|(ix, item)| (ix, item.clone()))
6645 .collect::<Vec<_>>()
6646 {
6647 let ix = item_ix - moved_items;
6648 if destination_is_different {
6649 // Close item from previous pane
6650 from_pane.update(cx, |source, cx| {
6651 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
6652 });
6653 moved_items += 1;
6654 }
6655
6656 // This automatically removes duplicate items in the pane
6657 to_pane.update(cx, |destination, cx| {
6658 destination.add_item(item_handle, true, true, None, window, cx);
6659 window.focus(&destination.focus_handle(cx))
6660 });
6661 }
6662}
6663
6664pub fn move_item(
6665 source: &Entity<Pane>,
6666 destination: &Entity<Pane>,
6667 item_id_to_move: EntityId,
6668 destination_index: usize,
6669 window: &mut Window,
6670 cx: &mut App,
6671) {
6672 let Some((item_ix, item_handle)) = source
6673 .read(cx)
6674 .items()
6675 .enumerate()
6676 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6677 .map(|(ix, item)| (ix, item.clone()))
6678 else {
6679 // Tab was closed during drag
6680 return;
6681 };
6682
6683 if source != destination {
6684 // Close item from previous pane
6685 source.update(cx, |source, cx| {
6686 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
6687 });
6688 }
6689
6690 // This automatically removes duplicate items in the pane
6691 destination.update(cx, |destination, cx| {
6692 destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
6693 window.focus(&destination.focus_handle(cx))
6694 });
6695}
6696
6697pub fn move_active_item(
6698 source: &Entity<Pane>,
6699 destination: &Entity<Pane>,
6700 focus_destination: bool,
6701 close_if_empty: bool,
6702 window: &mut Window,
6703 cx: &mut App,
6704) {
6705 if source == destination {
6706 return;
6707 }
6708 let Some(active_item) = source.read(cx).active_item() else {
6709 return;
6710 };
6711 source.update(cx, |source_pane, cx| {
6712 let item_id = active_item.item_id();
6713 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
6714 destination.update(cx, |target_pane, cx| {
6715 target_pane.add_item(
6716 active_item,
6717 focus_destination,
6718 focus_destination,
6719 Some(target_pane.items_len()),
6720 window,
6721 cx,
6722 );
6723 });
6724 });
6725}
6726
6727#[cfg(test)]
6728mod tests {
6729 use std::{cell::RefCell, rc::Rc};
6730
6731 use super::*;
6732 use crate::{
6733 dock::{test::TestPanel, PanelEvent},
6734 item::{
6735 test::{TestItem, TestProjectItem},
6736 ItemEvent,
6737 },
6738 };
6739 use fs::FakeFs;
6740 use gpui::{
6741 px, DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
6742 UpdateGlobal, VisualTestContext,
6743 };
6744 use project::{Project, ProjectEntryId};
6745 use serde_json::json;
6746 use settings::SettingsStore;
6747
6748 #[gpui::test]
6749 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6750 init_test(cx);
6751
6752 let fs = FakeFs::new(cx.executor());
6753 let project = Project::test(fs, [], cx).await;
6754 let (workspace, cx) =
6755 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6756
6757 // Adding an item with no ambiguity renders the tab without detail.
6758 let item1 = cx.new(|cx| {
6759 let mut item = TestItem::new(cx);
6760 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6761 item
6762 });
6763 workspace.update_in(cx, |workspace, window, cx| {
6764 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6765 });
6766 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6767
6768 // Adding an item that creates ambiguity increases the level of detail on
6769 // both tabs.
6770 let item2 = cx.new_window_entity(|_window, cx| {
6771 let mut item = TestItem::new(cx);
6772 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6773 item
6774 });
6775 workspace.update_in(cx, |workspace, window, cx| {
6776 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6777 });
6778 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6779 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6780
6781 // Adding an item that creates ambiguity increases the level of detail only
6782 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6783 // we stop at the highest detail available.
6784 let item3 = cx.new(|cx| {
6785 let mut item = TestItem::new(cx);
6786 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6787 item
6788 });
6789 workspace.update_in(cx, |workspace, window, cx| {
6790 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6791 });
6792 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6793 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6794 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6795 }
6796
6797 #[gpui::test]
6798 async fn test_tracking_active_path(cx: &mut TestAppContext) {
6799 init_test(cx);
6800
6801 let fs = FakeFs::new(cx.executor());
6802 fs.insert_tree(
6803 "/root1",
6804 json!({
6805 "one.txt": "",
6806 "two.txt": "",
6807 }),
6808 )
6809 .await;
6810 fs.insert_tree(
6811 "/root2",
6812 json!({
6813 "three.txt": "",
6814 }),
6815 )
6816 .await;
6817
6818 let project = Project::test(fs, ["root1".as_ref()], cx).await;
6819 let (workspace, cx) =
6820 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6821 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6822 let worktree_id = project.update(cx, |project, cx| {
6823 project.worktrees(cx).next().unwrap().read(cx).id()
6824 });
6825
6826 let item1 = cx.new(|cx| {
6827 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6828 });
6829 let item2 = cx.new(|cx| {
6830 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6831 });
6832
6833 // Add an item to an empty pane
6834 workspace.update_in(cx, |workspace, window, cx| {
6835 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
6836 });
6837 project.update(cx, |project, cx| {
6838 assert_eq!(
6839 project.active_entry(),
6840 project
6841 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6842 .map(|e| e.id)
6843 );
6844 });
6845 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6846
6847 // Add a second item to a non-empty pane
6848 workspace.update_in(cx, |workspace, window, cx| {
6849 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
6850 });
6851 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6852 project.update(cx, |project, cx| {
6853 assert_eq!(
6854 project.active_entry(),
6855 project
6856 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6857 .map(|e| e.id)
6858 );
6859 });
6860
6861 // Close the active item
6862 pane.update_in(cx, |pane, window, cx| {
6863 pane.close_active_item(&Default::default(), window, cx)
6864 .unwrap()
6865 })
6866 .await
6867 .unwrap();
6868 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6869 project.update(cx, |project, cx| {
6870 assert_eq!(
6871 project.active_entry(),
6872 project
6873 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6874 .map(|e| e.id)
6875 );
6876 });
6877
6878 // Add a project folder
6879 project
6880 .update(cx, |project, cx| {
6881 project.find_or_create_worktree("root2", true, cx)
6882 })
6883 .await
6884 .unwrap();
6885 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6886
6887 // Remove a project folder
6888 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6889 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6890 }
6891
6892 #[gpui::test]
6893 async fn test_close_window(cx: &mut TestAppContext) {
6894 init_test(cx);
6895
6896 let fs = FakeFs::new(cx.executor());
6897 fs.insert_tree("/root", json!({ "one": "" })).await;
6898
6899 let project = Project::test(fs, ["root".as_ref()], cx).await;
6900 let (workspace, cx) =
6901 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6902
6903 // When there are no dirty items, there's nothing to do.
6904 let item1 = cx.new(TestItem::new);
6905 workspace.update_in(cx, |w, window, cx| {
6906 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
6907 });
6908 let task = workspace.update_in(cx, |w, window, cx| {
6909 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6910 });
6911 assert!(task.await.unwrap());
6912
6913 // When there are dirty untitled items, prompt to save each one. If the user
6914 // cancels any prompt, then abort.
6915 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
6916 let item3 = cx.new(|cx| {
6917 TestItem::new(cx)
6918 .with_dirty(true)
6919 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6920 });
6921 workspace.update_in(cx, |w, window, cx| {
6922 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6923 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6924 });
6925 let task = workspace.update_in(cx, |w, window, cx| {
6926 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6927 });
6928 cx.executor().run_until_parked();
6929 cx.simulate_prompt_answer(2); // cancel save all
6930 cx.executor().run_until_parked();
6931 cx.simulate_prompt_answer(2); // cancel save all
6932 cx.executor().run_until_parked();
6933 assert!(!cx.has_pending_prompt());
6934 assert!(!task.await.unwrap());
6935 }
6936
6937 #[gpui::test]
6938 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6939 init_test(cx);
6940
6941 // Register TestItem as a serializable item
6942 cx.update(|cx| {
6943 register_serializable_item::<TestItem>(cx);
6944 });
6945
6946 let fs = FakeFs::new(cx.executor());
6947 fs.insert_tree("/root", json!({ "one": "" })).await;
6948
6949 let project = Project::test(fs, ["root".as_ref()], cx).await;
6950 let (workspace, cx) =
6951 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6952
6953 // When there are dirty untitled items, but they can serialize, then there is no prompt.
6954 let item1 = cx.new(|cx| {
6955 TestItem::new(cx)
6956 .with_dirty(true)
6957 .with_serialize(|| Some(Task::ready(Ok(()))))
6958 });
6959 let item2 = cx.new(|cx| {
6960 TestItem::new(cx)
6961 .with_dirty(true)
6962 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6963 .with_serialize(|| Some(Task::ready(Ok(()))))
6964 });
6965 workspace.update_in(cx, |w, window, cx| {
6966 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6967 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6968 });
6969 let task = workspace.update_in(cx, |w, window, cx| {
6970 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6971 });
6972 assert!(task.await.unwrap());
6973 }
6974
6975 #[gpui::test]
6976 async fn test_close_pane_items(cx: &mut TestAppContext) {
6977 init_test(cx);
6978
6979 let fs = FakeFs::new(cx.executor());
6980
6981 let project = Project::test(fs, None, cx).await;
6982 let (workspace, cx) =
6983 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6984
6985 let item1 = cx.new(|cx| {
6986 TestItem::new(cx)
6987 .with_dirty(true)
6988 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
6989 });
6990 let item2 = cx.new(|cx| {
6991 TestItem::new(cx)
6992 .with_dirty(true)
6993 .with_conflict(true)
6994 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
6995 });
6996 let item3 = cx.new(|cx| {
6997 TestItem::new(cx)
6998 .with_dirty(true)
6999 .with_conflict(true)
7000 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
7001 });
7002 let item4 = cx.new(|cx| {
7003 TestItem::new(cx).with_dirty(true).with_project_items(&[{
7004 let project_item = TestProjectItem::new_untitled(cx);
7005 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7006 project_item
7007 }])
7008 });
7009 let pane = workspace.update_in(cx, |workspace, window, cx| {
7010 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7011 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7012 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7013 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
7014 workspace.active_pane().clone()
7015 });
7016
7017 let close_items = pane.update_in(cx, |pane, window, cx| {
7018 pane.activate_item(1, true, true, window, cx);
7019 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7020 let item1_id = item1.item_id();
7021 let item3_id = item3.item_id();
7022 let item4_id = item4.item_id();
7023 pane.close_items(window, cx, SaveIntent::Close, move |id| {
7024 [item1_id, item3_id, item4_id].contains(&id)
7025 })
7026 });
7027 cx.executor().run_until_parked();
7028
7029 assert!(cx.has_pending_prompt());
7030 // Ignore "Save all" prompt
7031 cx.simulate_prompt_answer(2);
7032 cx.executor().run_until_parked();
7033 // There's a prompt to save item 1.
7034 pane.update(cx, |pane, _| {
7035 assert_eq!(pane.items_len(), 4);
7036 assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
7037 });
7038 // Confirm saving item 1.
7039 cx.simulate_prompt_answer(0);
7040 cx.executor().run_until_parked();
7041
7042 // Item 1 is saved. There's a prompt to save item 3.
7043 pane.update(cx, |pane, cx| {
7044 assert_eq!(item1.read(cx).save_count, 1);
7045 assert_eq!(item1.read(cx).save_as_count, 0);
7046 assert_eq!(item1.read(cx).reload_count, 0);
7047 assert_eq!(pane.items_len(), 3);
7048 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7049 });
7050 assert!(cx.has_pending_prompt());
7051
7052 // Cancel saving item 3.
7053 cx.simulate_prompt_answer(1);
7054 cx.executor().run_until_parked();
7055
7056 // Item 3 is reloaded. There's a prompt to save item 4.
7057 pane.update(cx, |pane, cx| {
7058 assert_eq!(item3.read(cx).save_count, 0);
7059 assert_eq!(item3.read(cx).save_as_count, 0);
7060 assert_eq!(item3.read(cx).reload_count, 1);
7061 assert_eq!(pane.items_len(), 2);
7062 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7063 });
7064 assert!(cx.has_pending_prompt());
7065
7066 // Confirm saving item 4.
7067 cx.simulate_prompt_answer(0);
7068 cx.executor().run_until_parked();
7069
7070 // There's a prompt for a path for item 4.
7071 cx.simulate_new_path_selection(|_| Some(Default::default()));
7072 close_items.await.unwrap();
7073
7074 // The requested items are closed.
7075 pane.update(cx, |pane, cx| {
7076 assert_eq!(item4.read(cx).save_count, 0);
7077 assert_eq!(item4.read(cx).save_as_count, 1);
7078 assert_eq!(item4.read(cx).reload_count, 0);
7079 assert_eq!(pane.items_len(), 1);
7080 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7081 });
7082 }
7083
7084 #[gpui::test]
7085 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7086 init_test(cx);
7087
7088 let fs = FakeFs::new(cx.executor());
7089 let project = Project::test(fs, [], cx).await;
7090 let (workspace, cx) =
7091 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7092
7093 // Create several workspace items with single project entries, and two
7094 // workspace items with multiple project entries.
7095 let single_entry_items = (0..=4)
7096 .map(|project_entry_id| {
7097 cx.new(|cx| {
7098 TestItem::new(cx)
7099 .with_dirty(true)
7100 .with_project_items(&[dirty_project_item(
7101 project_entry_id,
7102 &format!("{project_entry_id}.txt"),
7103 cx,
7104 )])
7105 })
7106 })
7107 .collect::<Vec<_>>();
7108 let item_2_3 = cx.new(|cx| {
7109 TestItem::new(cx)
7110 .with_dirty(true)
7111 .with_singleton(false)
7112 .with_project_items(&[
7113 single_entry_items[2].read(cx).project_items[0].clone(),
7114 single_entry_items[3].read(cx).project_items[0].clone(),
7115 ])
7116 });
7117 let item_3_4 = cx.new(|cx| {
7118 TestItem::new(cx)
7119 .with_dirty(true)
7120 .with_singleton(false)
7121 .with_project_items(&[
7122 single_entry_items[3].read(cx).project_items[0].clone(),
7123 single_entry_items[4].read(cx).project_items[0].clone(),
7124 ])
7125 });
7126
7127 // Create two panes that contain the following project entries:
7128 // left pane:
7129 // multi-entry items: (2, 3)
7130 // single-entry items: 0, 1, 2, 3, 4
7131 // right pane:
7132 // single-entry items: 1
7133 // multi-entry items: (3, 4)
7134 let left_pane = workspace.update_in(cx, |workspace, window, cx| {
7135 let left_pane = workspace.active_pane().clone();
7136 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
7137 for item in single_entry_items {
7138 workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
7139 }
7140 left_pane.update(cx, |pane, cx| {
7141 pane.activate_item(2, true, true, window, cx);
7142 });
7143
7144 let right_pane = workspace
7145 .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
7146 .unwrap();
7147
7148 right_pane.update(cx, |pane, cx| {
7149 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
7150 });
7151
7152 left_pane
7153 });
7154
7155 cx.focus(&left_pane);
7156
7157 // When closing all of the items in the left pane, we should be prompted twice:
7158 // once for project entry 0, and once for project entry 2. Project entries 1,
7159 // 3, and 4 are all still open in the other paten. After those two
7160 // prompts, the task should complete.
7161
7162 let close = left_pane.update_in(cx, |pane, window, cx| {
7163 pane.close_all_items(&CloseAllItems::default(), window, cx)
7164 .unwrap()
7165 });
7166 cx.executor().run_until_parked();
7167
7168 // Discard "Save all" prompt
7169 cx.simulate_prompt_answer(2);
7170
7171 cx.executor().run_until_parked();
7172 left_pane.update(cx, |pane, cx| {
7173 assert_eq!(
7174 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
7175 &[ProjectEntryId::from_proto(0)]
7176 );
7177 });
7178 cx.simulate_prompt_answer(0);
7179
7180 cx.executor().run_until_parked();
7181 left_pane.update(cx, |pane, cx| {
7182 assert_eq!(
7183 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
7184 &[ProjectEntryId::from_proto(2)]
7185 );
7186 });
7187 cx.simulate_prompt_answer(0);
7188
7189 cx.executor().run_until_parked();
7190 close.await.unwrap();
7191 left_pane.update(cx, |pane, _| {
7192 assert_eq!(pane.items_len(), 0);
7193 });
7194 }
7195
7196 #[gpui::test]
7197 async fn test_autosave(cx: &mut gpui::TestAppContext) {
7198 init_test(cx);
7199
7200 let fs = FakeFs::new(cx.executor());
7201 let project = Project::test(fs, [], cx).await;
7202 let (workspace, cx) =
7203 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7204 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7205
7206 let item = cx.new(|cx| {
7207 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7208 });
7209 let item_id = item.entity_id();
7210 workspace.update_in(cx, |workspace, window, cx| {
7211 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7212 });
7213
7214 // Autosave on window change.
7215 item.update(cx, |item, cx| {
7216 SettingsStore::update_global(cx, |settings, cx| {
7217 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7218 settings.autosave = Some(AutosaveSetting::OnWindowChange);
7219 })
7220 });
7221 item.is_dirty = true;
7222 });
7223
7224 // Deactivating the window saves the file.
7225 cx.deactivate_window();
7226 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7227
7228 // Re-activating the window doesn't save the file.
7229 cx.update(|window, _| window.activate_window());
7230 cx.executor().run_until_parked();
7231 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7232
7233 // Autosave on focus change.
7234 item.update_in(cx, |item, window, cx| {
7235 cx.focus_self(window);
7236 SettingsStore::update_global(cx, |settings, cx| {
7237 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7238 settings.autosave = Some(AutosaveSetting::OnFocusChange);
7239 })
7240 });
7241 item.is_dirty = true;
7242 });
7243
7244 // Blurring the item saves the file.
7245 item.update_in(cx, |_, window, _| window.blur());
7246 cx.executor().run_until_parked();
7247 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
7248
7249 // Deactivating the window still saves the file.
7250 item.update_in(cx, |item, window, cx| {
7251 cx.focus_self(window);
7252 item.is_dirty = true;
7253 });
7254 cx.deactivate_window();
7255 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7256
7257 // Autosave after delay.
7258 item.update(cx, |item, cx| {
7259 SettingsStore::update_global(cx, |settings, cx| {
7260 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7261 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
7262 })
7263 });
7264 item.is_dirty = true;
7265 cx.emit(ItemEvent::Edit);
7266 });
7267
7268 // Delay hasn't fully expired, so the file is still dirty and unsaved.
7269 cx.executor().advance_clock(Duration::from_millis(250));
7270 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7271
7272 // After delay expires, the file is saved.
7273 cx.executor().advance_clock(Duration::from_millis(250));
7274 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
7275
7276 // Autosave on focus change, ensuring closing the tab counts as such.
7277 item.update(cx, |item, cx| {
7278 SettingsStore::update_global(cx, |settings, cx| {
7279 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7280 settings.autosave = Some(AutosaveSetting::OnFocusChange);
7281 })
7282 });
7283 item.is_dirty = true;
7284 for project_item in &mut item.project_items {
7285 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7286 }
7287 });
7288
7289 pane.update_in(cx, |pane, window, cx| {
7290 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7291 })
7292 .await
7293 .unwrap();
7294 assert!(!cx.has_pending_prompt());
7295 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7296
7297 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
7298 workspace.update_in(cx, |workspace, window, cx| {
7299 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7300 });
7301 item.update_in(cx, |item, window, cx| {
7302 item.project_items[0].update(cx, |item, _| {
7303 item.entry_id = None;
7304 });
7305 item.is_dirty = true;
7306 window.blur();
7307 });
7308 cx.run_until_parked();
7309 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7310
7311 // Ensure autosave is prevented for deleted files also when closing the buffer.
7312 let _close_items = pane.update_in(cx, |pane, window, cx| {
7313 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7314 });
7315 cx.run_until_parked();
7316 assert!(cx.has_pending_prompt());
7317 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7318 }
7319
7320 #[gpui::test]
7321 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
7322 init_test(cx);
7323
7324 let fs = FakeFs::new(cx.executor());
7325
7326 let project = Project::test(fs, [], cx).await;
7327 let (workspace, cx) =
7328 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7329
7330 let item = cx.new(|cx| {
7331 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7332 });
7333 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7334 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
7335 let toolbar_notify_count = Rc::new(RefCell::new(0));
7336
7337 workspace.update_in(cx, |workspace, window, cx| {
7338 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7339 let toolbar_notification_count = toolbar_notify_count.clone();
7340 cx.observe_in(&toolbar, window, move |_, _, _, _| {
7341 *toolbar_notification_count.borrow_mut() += 1
7342 })
7343 .detach();
7344 });
7345
7346 pane.update(cx, |pane, _| {
7347 assert!(!pane.can_navigate_backward());
7348 assert!(!pane.can_navigate_forward());
7349 });
7350
7351 item.update_in(cx, |item, _, cx| {
7352 item.set_state("one".to_string(), cx);
7353 });
7354
7355 // Toolbar must be notified to re-render the navigation buttons
7356 assert_eq!(*toolbar_notify_count.borrow(), 1);
7357
7358 pane.update(cx, |pane, _| {
7359 assert!(pane.can_navigate_backward());
7360 assert!(!pane.can_navigate_forward());
7361 });
7362
7363 workspace
7364 .update_in(cx, |workspace, window, cx| {
7365 workspace.go_back(pane.downgrade(), window, cx)
7366 })
7367 .await
7368 .unwrap();
7369
7370 assert_eq!(*toolbar_notify_count.borrow(), 2);
7371 pane.update(cx, |pane, _| {
7372 assert!(!pane.can_navigate_backward());
7373 assert!(pane.can_navigate_forward());
7374 });
7375 }
7376
7377 #[gpui::test]
7378 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
7379 init_test(cx);
7380 let fs = FakeFs::new(cx.executor());
7381
7382 let project = Project::test(fs, [], cx).await;
7383 let (workspace, cx) =
7384 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7385
7386 let panel = workspace.update_in(cx, |workspace, window, cx| {
7387 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7388 workspace.add_panel(panel.clone(), window, cx);
7389
7390 workspace
7391 .right_dock()
7392 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
7393
7394 panel
7395 });
7396
7397 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7398 pane.update_in(cx, |pane, window, cx| {
7399 let item = cx.new(TestItem::new);
7400 pane.add_item(Box::new(item), true, true, None, window, cx);
7401 });
7402
7403 // Transfer focus from center to panel
7404 workspace.update_in(cx, |workspace, window, cx| {
7405 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7406 });
7407
7408 workspace.update_in(cx, |workspace, window, cx| {
7409 assert!(workspace.right_dock().read(cx).is_open());
7410 assert!(!panel.is_zoomed(window, cx));
7411 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7412 });
7413
7414 // Transfer focus from panel to center
7415 workspace.update_in(cx, |workspace, window, cx| {
7416 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7417 });
7418
7419 workspace.update_in(cx, |workspace, window, cx| {
7420 assert!(workspace.right_dock().read(cx).is_open());
7421 assert!(!panel.is_zoomed(window, cx));
7422 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7423 });
7424
7425 // Close the dock
7426 workspace.update_in(cx, |workspace, window, cx| {
7427 workspace.toggle_dock(DockPosition::Right, window, cx);
7428 });
7429
7430 workspace.update_in(cx, |workspace, window, cx| {
7431 assert!(!workspace.right_dock().read(cx).is_open());
7432 assert!(!panel.is_zoomed(window, cx));
7433 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7434 });
7435
7436 // Open the dock
7437 workspace.update_in(cx, |workspace, window, cx| {
7438 workspace.toggle_dock(DockPosition::Right, window, cx);
7439 });
7440
7441 workspace.update_in(cx, |workspace, window, cx| {
7442 assert!(workspace.right_dock().read(cx).is_open());
7443 assert!(!panel.is_zoomed(window, cx));
7444 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7445 });
7446
7447 // Focus and zoom panel
7448 panel.update_in(cx, |panel, window, cx| {
7449 cx.focus_self(window);
7450 panel.set_zoomed(true, window, cx)
7451 });
7452
7453 workspace.update_in(cx, |workspace, window, cx| {
7454 assert!(workspace.right_dock().read(cx).is_open());
7455 assert!(panel.is_zoomed(window, cx));
7456 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7457 });
7458
7459 // Transfer focus to the center closes the dock
7460 workspace.update_in(cx, |workspace, window, cx| {
7461 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7462 });
7463
7464 workspace.update_in(cx, |workspace, window, cx| {
7465 assert!(!workspace.right_dock().read(cx).is_open());
7466 assert!(panel.is_zoomed(window, cx));
7467 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7468 });
7469
7470 // Transferring focus back to the panel keeps it zoomed
7471 workspace.update_in(cx, |workspace, window, cx| {
7472 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7473 });
7474
7475 workspace.update_in(cx, |workspace, window, cx| {
7476 assert!(workspace.right_dock().read(cx).is_open());
7477 assert!(panel.is_zoomed(window, cx));
7478 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7479 });
7480
7481 // Close the dock while it is zoomed
7482 workspace.update_in(cx, |workspace, window, cx| {
7483 workspace.toggle_dock(DockPosition::Right, window, cx)
7484 });
7485
7486 workspace.update_in(cx, |workspace, window, cx| {
7487 assert!(!workspace.right_dock().read(cx).is_open());
7488 assert!(panel.is_zoomed(window, cx));
7489 assert!(workspace.zoomed.is_none());
7490 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7491 });
7492
7493 // Opening the dock, when it's zoomed, retains focus
7494 workspace.update_in(cx, |workspace, window, cx| {
7495 workspace.toggle_dock(DockPosition::Right, window, cx)
7496 });
7497
7498 workspace.update_in(cx, |workspace, window, cx| {
7499 assert!(workspace.right_dock().read(cx).is_open());
7500 assert!(panel.is_zoomed(window, cx));
7501 assert!(workspace.zoomed.is_some());
7502 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7503 });
7504
7505 // Unzoom and close the panel, zoom the active pane.
7506 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
7507 workspace.update_in(cx, |workspace, window, cx| {
7508 workspace.toggle_dock(DockPosition::Right, window, cx)
7509 });
7510 pane.update_in(cx, |pane, window, cx| {
7511 pane.toggle_zoom(&Default::default(), window, cx)
7512 });
7513
7514 // Opening a dock unzooms the pane.
7515 workspace.update_in(cx, |workspace, window, cx| {
7516 workspace.toggle_dock(DockPosition::Right, window, cx)
7517 });
7518 workspace.update_in(cx, |workspace, window, cx| {
7519 let pane = pane.read(cx);
7520 assert!(!pane.is_zoomed());
7521 assert!(!pane.focus_handle(cx).is_focused(window));
7522 assert!(workspace.right_dock().read(cx).is_open());
7523 assert!(workspace.zoomed.is_none());
7524 });
7525 }
7526
7527 #[gpui::test]
7528 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
7529 init_test(cx);
7530
7531 let fs = FakeFs::new(cx.executor());
7532
7533 let project = Project::test(fs, None, cx).await;
7534 let (workspace, cx) =
7535 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7536
7537 // Let's arrange the panes like this:
7538 //
7539 // +-----------------------+
7540 // | top |
7541 // +------+--------+-------+
7542 // | left | center | right |
7543 // +------+--------+-------+
7544 // | bottom |
7545 // +-----------------------+
7546
7547 let top_item = cx.new(|cx| {
7548 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
7549 });
7550 let bottom_item = cx.new(|cx| {
7551 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7552 });
7553 let left_item = cx.new(|cx| {
7554 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7555 });
7556 let right_item = cx.new(|cx| {
7557 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7558 });
7559 let center_item = cx.new(|cx| {
7560 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7561 });
7562
7563 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7564 let top_pane_id = workspace.active_pane().entity_id();
7565 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
7566 workspace.split_pane(
7567 workspace.active_pane().clone(),
7568 SplitDirection::Down,
7569 window,
7570 cx,
7571 );
7572 top_pane_id
7573 });
7574 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7575 let bottom_pane_id = workspace.active_pane().entity_id();
7576 workspace.add_item_to_active_pane(
7577 Box::new(bottom_item.clone()),
7578 None,
7579 false,
7580 window,
7581 cx,
7582 );
7583 workspace.split_pane(
7584 workspace.active_pane().clone(),
7585 SplitDirection::Up,
7586 window,
7587 cx,
7588 );
7589 bottom_pane_id
7590 });
7591 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7592 let left_pane_id = workspace.active_pane().entity_id();
7593 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
7594 workspace.split_pane(
7595 workspace.active_pane().clone(),
7596 SplitDirection::Right,
7597 window,
7598 cx,
7599 );
7600 left_pane_id
7601 });
7602 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7603 let right_pane_id = workspace.active_pane().entity_id();
7604 workspace.add_item_to_active_pane(
7605 Box::new(right_item.clone()),
7606 None,
7607 false,
7608 window,
7609 cx,
7610 );
7611 workspace.split_pane(
7612 workspace.active_pane().clone(),
7613 SplitDirection::Left,
7614 window,
7615 cx,
7616 );
7617 right_pane_id
7618 });
7619 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7620 let center_pane_id = workspace.active_pane().entity_id();
7621 workspace.add_item_to_active_pane(
7622 Box::new(center_item.clone()),
7623 None,
7624 false,
7625 window,
7626 cx,
7627 );
7628 center_pane_id
7629 });
7630 cx.executor().run_until_parked();
7631
7632 workspace.update_in(cx, |workspace, window, cx| {
7633 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7634
7635 // Join into next from center pane into right
7636 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7637 });
7638
7639 workspace.update_in(cx, |workspace, window, cx| {
7640 let active_pane = workspace.active_pane();
7641 assert_eq!(right_pane_id, active_pane.entity_id());
7642 assert_eq!(2, active_pane.read(cx).items_len());
7643 let item_ids_in_pane =
7644 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7645 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7646 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7647
7648 // Join into next from right pane into bottom
7649 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7650 });
7651
7652 workspace.update_in(cx, |workspace, window, cx| {
7653 let active_pane = workspace.active_pane();
7654 assert_eq!(bottom_pane_id, active_pane.entity_id());
7655 assert_eq!(3, active_pane.read(cx).items_len());
7656 let item_ids_in_pane =
7657 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7658 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7659 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7660 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7661
7662 // Join into next from bottom pane into left
7663 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7664 });
7665
7666 workspace.update_in(cx, |workspace, window, cx| {
7667 let active_pane = workspace.active_pane();
7668 assert_eq!(left_pane_id, active_pane.entity_id());
7669 assert_eq!(4, active_pane.read(cx).items_len());
7670 let item_ids_in_pane =
7671 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7672 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7673 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7674 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7675 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7676
7677 // Join into next from left pane into top
7678 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7679 });
7680
7681 workspace.update_in(cx, |workspace, window, cx| {
7682 let active_pane = workspace.active_pane();
7683 assert_eq!(top_pane_id, active_pane.entity_id());
7684 assert_eq!(5, active_pane.read(cx).items_len());
7685 let item_ids_in_pane =
7686 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7687 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7688 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7689 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7690 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7691 assert!(item_ids_in_pane.contains(&top_item.item_id()));
7692
7693 // Single pane left: no-op
7694 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
7695 });
7696
7697 workspace.update(cx, |workspace, _cx| {
7698 let active_pane = workspace.active_pane();
7699 assert_eq!(top_pane_id, active_pane.entity_id());
7700 });
7701 }
7702
7703 fn add_an_item_to_active_pane(
7704 cx: &mut VisualTestContext,
7705 workspace: &Entity<Workspace>,
7706 item_id: u64,
7707 ) -> Entity<TestItem> {
7708 let item = cx.new(|cx| {
7709 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7710 item_id,
7711 "item{item_id}.txt",
7712 cx,
7713 )])
7714 });
7715 workspace.update_in(cx, |workspace, window, cx| {
7716 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
7717 });
7718 return item;
7719 }
7720
7721 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
7722 return workspace.update_in(cx, |workspace, window, cx| {
7723 let new_pane = workspace.split_pane(
7724 workspace.active_pane().clone(),
7725 SplitDirection::Right,
7726 window,
7727 cx,
7728 );
7729 new_pane
7730 });
7731 }
7732
7733 #[gpui::test]
7734 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7735 init_test(cx);
7736 let fs = FakeFs::new(cx.executor());
7737 let project = Project::test(fs, None, cx).await;
7738 let (workspace, cx) =
7739 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7740
7741 add_an_item_to_active_pane(cx, &workspace, 1);
7742 split_pane(cx, &workspace);
7743 add_an_item_to_active_pane(cx, &workspace, 2);
7744 split_pane(cx, &workspace); // empty pane
7745 split_pane(cx, &workspace);
7746 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7747
7748 cx.executor().run_until_parked();
7749
7750 workspace.update(cx, |workspace, cx| {
7751 let num_panes = workspace.panes().len();
7752 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7753 let active_item = workspace
7754 .active_pane()
7755 .read(cx)
7756 .active_item()
7757 .expect("item is in focus");
7758
7759 assert_eq!(num_panes, 4);
7760 assert_eq!(num_items_in_current_pane, 1);
7761 assert_eq!(active_item.item_id(), last_item.item_id());
7762 });
7763
7764 workspace.update_in(cx, |workspace, window, cx| {
7765 workspace.join_all_panes(window, cx);
7766 });
7767
7768 workspace.update(cx, |workspace, cx| {
7769 let num_panes = workspace.panes().len();
7770 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7771 let active_item = workspace
7772 .active_pane()
7773 .read(cx)
7774 .active_item()
7775 .expect("item is in focus");
7776
7777 assert_eq!(num_panes, 1);
7778 assert_eq!(num_items_in_current_pane, 3);
7779 assert_eq!(active_item.item_id(), last_item.item_id());
7780 });
7781 }
7782 struct TestModal(FocusHandle);
7783
7784 impl TestModal {
7785 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
7786 Self(cx.focus_handle())
7787 }
7788 }
7789
7790 impl EventEmitter<DismissEvent> for TestModal {}
7791
7792 impl Focusable for TestModal {
7793 fn focus_handle(&self, _cx: &App) -> FocusHandle {
7794 self.0.clone()
7795 }
7796 }
7797
7798 impl ModalView for TestModal {}
7799
7800 impl Render for TestModal {
7801 fn render(
7802 &mut self,
7803 _window: &mut Window,
7804 _cx: &mut Context<TestModal>,
7805 ) -> impl IntoElement {
7806 div().track_focus(&self.0)
7807 }
7808 }
7809
7810 #[gpui::test]
7811 async fn test_panels(cx: &mut gpui::TestAppContext) {
7812 init_test(cx);
7813 let fs = FakeFs::new(cx.executor());
7814
7815 let project = Project::test(fs, [], cx).await;
7816 let (workspace, cx) =
7817 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7818
7819 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
7820 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
7821 workspace.add_panel(panel_1.clone(), window, cx);
7822 workspace.toggle_dock(DockPosition::Left, window, cx);
7823 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7824 workspace.add_panel(panel_2.clone(), window, cx);
7825 workspace.toggle_dock(DockPosition::Right, window, cx);
7826
7827 let left_dock = workspace.left_dock();
7828 assert_eq!(
7829 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7830 panel_1.panel_id()
7831 );
7832 assert_eq!(
7833 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7834 panel_1.size(window, cx)
7835 );
7836
7837 left_dock.update(cx, |left_dock, cx| {
7838 left_dock.resize_active_panel(Some(px(1337.)), window, cx)
7839 });
7840 assert_eq!(
7841 workspace
7842 .right_dock()
7843 .read(cx)
7844 .visible_panel()
7845 .unwrap()
7846 .panel_id(),
7847 panel_2.panel_id(),
7848 );
7849
7850 (panel_1, panel_2)
7851 });
7852
7853 // Move panel_1 to the right
7854 panel_1.update_in(cx, |panel_1, window, cx| {
7855 panel_1.set_position(DockPosition::Right, window, cx)
7856 });
7857
7858 workspace.update_in(cx, |workspace, window, cx| {
7859 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7860 // Since it was the only panel on the left, the left dock should now be closed.
7861 assert!(!workspace.left_dock().read(cx).is_open());
7862 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7863 let right_dock = workspace.right_dock();
7864 assert_eq!(
7865 right_dock.read(cx).visible_panel().unwrap().panel_id(),
7866 panel_1.panel_id()
7867 );
7868 assert_eq!(
7869 right_dock.read(cx).active_panel_size(window, cx).unwrap(),
7870 px(1337.)
7871 );
7872
7873 // Now we move panel_2 to the left
7874 panel_2.set_position(DockPosition::Left, window, cx);
7875 });
7876
7877 workspace.update(cx, |workspace, cx| {
7878 // Since panel_2 was not visible on the right, we don't open the left dock.
7879 assert!(!workspace.left_dock().read(cx).is_open());
7880 // And the right dock is unaffected in its displaying of panel_1
7881 assert!(workspace.right_dock().read(cx).is_open());
7882 assert_eq!(
7883 workspace
7884 .right_dock()
7885 .read(cx)
7886 .visible_panel()
7887 .unwrap()
7888 .panel_id(),
7889 panel_1.panel_id(),
7890 );
7891 });
7892
7893 // Move panel_1 back to the left
7894 panel_1.update_in(cx, |panel_1, window, cx| {
7895 panel_1.set_position(DockPosition::Left, window, cx)
7896 });
7897
7898 workspace.update_in(cx, |workspace, window, cx| {
7899 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7900 let left_dock = workspace.left_dock();
7901 assert!(left_dock.read(cx).is_open());
7902 assert_eq!(
7903 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7904 panel_1.panel_id()
7905 );
7906 assert_eq!(
7907 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7908 px(1337.)
7909 );
7910 // And the right dock should be closed as it no longer has any panels.
7911 assert!(!workspace.right_dock().read(cx).is_open());
7912
7913 // Now we move panel_1 to the bottom
7914 panel_1.set_position(DockPosition::Bottom, window, cx);
7915 });
7916
7917 workspace.update_in(cx, |workspace, window, cx| {
7918 // Since panel_1 was visible on the left, we close the left dock.
7919 assert!(!workspace.left_dock().read(cx).is_open());
7920 // The bottom dock is sized based on the panel's default size,
7921 // since the panel orientation changed from vertical to horizontal.
7922 let bottom_dock = workspace.bottom_dock();
7923 assert_eq!(
7924 bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
7925 panel_1.size(window, cx),
7926 );
7927 // Close bottom dock and move panel_1 back to the left.
7928 bottom_dock.update(cx, |bottom_dock, cx| {
7929 bottom_dock.set_open(false, window, cx)
7930 });
7931 panel_1.set_position(DockPosition::Left, window, cx);
7932 });
7933
7934 // Emit activated event on panel 1
7935 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7936
7937 // Now the left dock is open and panel_1 is active and focused.
7938 workspace.update_in(cx, |workspace, window, cx| {
7939 let left_dock = workspace.left_dock();
7940 assert!(left_dock.read(cx).is_open());
7941 assert_eq!(
7942 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7943 panel_1.panel_id(),
7944 );
7945 assert!(panel_1.focus_handle(cx).is_focused(window));
7946 });
7947
7948 // Emit closed event on panel 2, which is not active
7949 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7950
7951 // Wo don't close the left dock, because panel_2 wasn't the active panel
7952 workspace.update(cx, |workspace, cx| {
7953 let left_dock = workspace.left_dock();
7954 assert!(left_dock.read(cx).is_open());
7955 assert_eq!(
7956 left_dock.read(cx).visible_panel().unwrap().panel_id(),
7957 panel_1.panel_id(),
7958 );
7959 });
7960
7961 // Emitting a ZoomIn event shows the panel as zoomed.
7962 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
7963 workspace.update(cx, |workspace, _| {
7964 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7965 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
7966 });
7967
7968 // Move panel to another dock while it is zoomed
7969 panel_1.update_in(cx, |panel, window, cx| {
7970 panel.set_position(DockPosition::Right, window, cx)
7971 });
7972 workspace.update(cx, |workspace, _| {
7973 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7974
7975 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7976 });
7977
7978 // This is a helper for getting a:
7979 // - valid focus on an element,
7980 // - that isn't a part of the panes and panels system of the Workspace,
7981 // - and doesn't trigger the 'on_focus_lost' API.
7982 let focus_other_view = {
7983 let workspace = workspace.clone();
7984 move |cx: &mut VisualTestContext| {
7985 workspace.update_in(cx, |workspace, window, cx| {
7986 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
7987 workspace.toggle_modal(window, cx, TestModal::new);
7988 workspace.toggle_modal(window, cx, TestModal::new);
7989 } else {
7990 workspace.toggle_modal(window, cx, TestModal::new);
7991 }
7992 })
7993 }
7994 };
7995
7996 // If focus is transferred to another view that's not a panel or another pane, we still show
7997 // the panel as zoomed.
7998 focus_other_view(cx);
7999 workspace.update(cx, |workspace, _| {
8000 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8001 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8002 });
8003
8004 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
8005 workspace.update_in(cx, |_workspace, window, cx| {
8006 cx.focus_self(window);
8007 });
8008 workspace.update(cx, |workspace, _| {
8009 assert_eq!(workspace.zoomed, None);
8010 assert_eq!(workspace.zoomed_position, None);
8011 });
8012
8013 // If focus is transferred again to another view that's not a panel or a pane, we won't
8014 // show the panel as zoomed because it wasn't zoomed before.
8015 focus_other_view(cx);
8016 workspace.update(cx, |workspace, _| {
8017 assert_eq!(workspace.zoomed, None);
8018 assert_eq!(workspace.zoomed_position, None);
8019 });
8020
8021 // When the panel is activated, it is zoomed again.
8022 cx.dispatch_action(ToggleRightDock);
8023 workspace.update(cx, |workspace, _| {
8024 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8025 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8026 });
8027
8028 // Emitting a ZoomOut event unzooms the panel.
8029 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
8030 workspace.update(cx, |workspace, _| {
8031 assert_eq!(workspace.zoomed, None);
8032 assert_eq!(workspace.zoomed_position, None);
8033 });
8034
8035 // Emit closed event on panel 1, which is active
8036 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8037
8038 // Now the left dock is closed, because panel_1 was the active panel
8039 workspace.update(cx, |workspace, cx| {
8040 let right_dock = workspace.right_dock();
8041 assert!(!right_dock.read(cx).is_open());
8042 });
8043 }
8044
8045 #[gpui::test]
8046 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8047 init_test(cx);
8048
8049 let fs = FakeFs::new(cx.background_executor.clone());
8050 let project = Project::test(fs, [], cx).await;
8051 let (workspace, cx) =
8052 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8053 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8054
8055 let dirty_regular_buffer = cx.new(|cx| {
8056 TestItem::new(cx)
8057 .with_dirty(true)
8058 .with_label("1.txt")
8059 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8060 });
8061 let dirty_regular_buffer_2 = cx.new(|cx| {
8062 TestItem::new(cx)
8063 .with_dirty(true)
8064 .with_label("2.txt")
8065 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8066 });
8067 let dirty_multi_buffer_with_both = cx.new(|cx| {
8068 TestItem::new(cx)
8069 .with_dirty(true)
8070 .with_singleton(false)
8071 .with_label("Fake Project Search")
8072 .with_project_items(&[
8073 dirty_regular_buffer.read(cx).project_items[0].clone(),
8074 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8075 ])
8076 });
8077 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8078 workspace.update_in(cx, |workspace, window, cx| {
8079 workspace.add_item(
8080 pane.clone(),
8081 Box::new(dirty_regular_buffer.clone()),
8082 None,
8083 false,
8084 false,
8085 window,
8086 cx,
8087 );
8088 workspace.add_item(
8089 pane.clone(),
8090 Box::new(dirty_regular_buffer_2.clone()),
8091 None,
8092 false,
8093 false,
8094 window,
8095 cx,
8096 );
8097 workspace.add_item(
8098 pane.clone(),
8099 Box::new(dirty_multi_buffer_with_both.clone()),
8100 None,
8101 false,
8102 false,
8103 window,
8104 cx,
8105 );
8106 });
8107
8108 pane.update_in(cx, |pane, window, cx| {
8109 pane.activate_item(2, true, true, window, cx);
8110 assert_eq!(
8111 pane.active_item().unwrap().item_id(),
8112 multi_buffer_with_both_files_id,
8113 "Should select the multi buffer in the pane"
8114 );
8115 });
8116 let close_all_but_multi_buffer_task = pane
8117 .update_in(cx, |pane, window, cx| {
8118 pane.close_inactive_items(
8119 &CloseInactiveItems {
8120 save_intent: Some(SaveIntent::Save),
8121 close_pinned: true,
8122 },
8123 window,
8124 cx,
8125 )
8126 })
8127 .expect("should have inactive files to close");
8128 cx.background_executor.run_until_parked();
8129 assert!(
8130 !cx.has_pending_prompt(),
8131 "Multi buffer still has the unsaved buffer inside, so no save prompt should be shown"
8132 );
8133 close_all_but_multi_buffer_task
8134 .await
8135 .expect("Closing all buffers but the multi buffer failed");
8136 pane.update(cx, |pane, cx| {
8137 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8138 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
8139 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8140 assert_eq!(pane.items_len(), 1);
8141 assert_eq!(
8142 pane.active_item().unwrap().item_id(),
8143 multi_buffer_with_both_files_id,
8144 "Should have only the multi buffer left in the pane"
8145 );
8146 assert!(
8147 dirty_multi_buffer_with_both.read(cx).is_dirty,
8148 "The multi buffer containing the unsaved buffer should still be dirty"
8149 );
8150 });
8151
8152 let close_multi_buffer_task = pane
8153 .update_in(cx, |pane, window, cx| {
8154 pane.close_active_item(
8155 &CloseActiveItem {
8156 save_intent: Some(SaveIntent::Close),
8157 },
8158 window,
8159 cx,
8160 )
8161 })
8162 .expect("should have the multi buffer to close");
8163 cx.background_executor.run_until_parked();
8164 assert!(
8165 cx.has_pending_prompt(),
8166 "Dirty multi buffer should prompt a save dialog"
8167 );
8168 cx.simulate_prompt_answer(0);
8169 cx.background_executor.run_until_parked();
8170 close_multi_buffer_task
8171 .await
8172 .expect("Closing the multi buffer failed");
8173 pane.update(cx, |pane, cx| {
8174 assert_eq!(
8175 dirty_multi_buffer_with_both.read(cx).save_count,
8176 1,
8177 "Multi buffer item should get be saved"
8178 );
8179 // Test impl does not save inner items, so we do not assert them
8180 assert_eq!(
8181 pane.items_len(),
8182 0,
8183 "No more items should be left in the pane"
8184 );
8185 assert!(pane.active_item().is_none());
8186 });
8187 }
8188
8189 #[gpui::test]
8190 async fn test_no_save_prompt_when_dirty_singleton_buffer_closed_with_a_multi_buffer_containing_it_present_in_the_pane(
8191 cx: &mut TestAppContext,
8192 ) {
8193 init_test(cx);
8194
8195 let fs = FakeFs::new(cx.background_executor.clone());
8196 let project = Project::test(fs, [], cx).await;
8197 let (workspace, cx) =
8198 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8199 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8200
8201 let dirty_regular_buffer = cx.new(|cx| {
8202 TestItem::new(cx)
8203 .with_dirty(true)
8204 .with_label("1.txt")
8205 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8206 });
8207 let dirty_regular_buffer_2 = cx.new(|cx| {
8208 TestItem::new(cx)
8209 .with_dirty(true)
8210 .with_label("2.txt")
8211 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8212 });
8213 let clear_regular_buffer = cx.new(|cx| {
8214 TestItem::new(cx)
8215 .with_label("3.txt")
8216 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8217 });
8218
8219 let dirty_multi_buffer_with_both = cx.new(|cx| {
8220 TestItem::new(cx)
8221 .with_dirty(true)
8222 .with_singleton(false)
8223 .with_label("Fake Project Search")
8224 .with_project_items(&[
8225 dirty_regular_buffer.read(cx).project_items[0].clone(),
8226 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8227 clear_regular_buffer.read(cx).project_items[0].clone(),
8228 ])
8229 });
8230 workspace.update_in(cx, |workspace, window, cx| {
8231 workspace.add_item(
8232 pane.clone(),
8233 Box::new(dirty_regular_buffer.clone()),
8234 None,
8235 false,
8236 false,
8237 window,
8238 cx,
8239 );
8240 workspace.add_item(
8241 pane.clone(),
8242 Box::new(dirty_multi_buffer_with_both.clone()),
8243 None,
8244 false,
8245 false,
8246 window,
8247 cx,
8248 );
8249 });
8250
8251 pane.update_in(cx, |pane, window, cx| {
8252 pane.activate_item(0, true, true, window, cx);
8253 assert_eq!(
8254 pane.active_item().unwrap().item_id(),
8255 dirty_regular_buffer.item_id(),
8256 "Should select the dirty singleton buffer in the pane"
8257 );
8258 });
8259 let close_singleton_buffer_task = pane
8260 .update_in(cx, |pane, window, cx| {
8261 pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
8262 })
8263 .expect("should have active singleton buffer to close");
8264 cx.background_executor.run_until_parked();
8265 assert!(
8266 !cx.has_pending_prompt(),
8267 "Multi buffer is still in the pane and has the unsaved buffer inside, so no save prompt should be shown"
8268 );
8269
8270 close_singleton_buffer_task
8271 .await
8272 .expect("Should not fail closing the singleton buffer");
8273 pane.update(cx, |pane, cx| {
8274 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8275 assert_eq!(
8276 dirty_multi_buffer_with_both.read(cx).save_count,
8277 0,
8278 "Multi buffer itself should not be saved"
8279 );
8280 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8281 assert_eq!(
8282 pane.items_len(),
8283 1,
8284 "A dirty multi buffer should be present in the pane"
8285 );
8286 assert_eq!(
8287 pane.active_item().unwrap().item_id(),
8288 dirty_multi_buffer_with_both.item_id(),
8289 "Should activate the only remaining item in the pane"
8290 );
8291 });
8292 }
8293
8294 #[gpui::test]
8295 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
8296 cx: &mut TestAppContext,
8297 ) {
8298 init_test(cx);
8299
8300 let fs = FakeFs::new(cx.background_executor.clone());
8301 let project = Project::test(fs, [], cx).await;
8302 let (workspace, cx) =
8303 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8304 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8305
8306 let dirty_regular_buffer = cx.new(|cx| {
8307 TestItem::new(cx)
8308 .with_dirty(true)
8309 .with_label("1.txt")
8310 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8311 });
8312 let dirty_regular_buffer_2 = cx.new(|cx| {
8313 TestItem::new(cx)
8314 .with_dirty(true)
8315 .with_label("2.txt")
8316 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8317 });
8318 let clear_regular_buffer = cx.new(|cx| {
8319 TestItem::new(cx)
8320 .with_label("3.txt")
8321 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8322 });
8323
8324 let dirty_multi_buffer_with_both = cx.new(|cx| {
8325 TestItem::new(cx)
8326 .with_dirty(true)
8327 .with_singleton(false)
8328 .with_label("Fake Project Search")
8329 .with_project_items(&[
8330 dirty_regular_buffer.read(cx).project_items[0].clone(),
8331 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8332 clear_regular_buffer.read(cx).project_items[0].clone(),
8333 ])
8334 });
8335 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8336 workspace.update_in(cx, |workspace, window, cx| {
8337 workspace.add_item(
8338 pane.clone(),
8339 Box::new(dirty_regular_buffer.clone()),
8340 None,
8341 false,
8342 false,
8343 window,
8344 cx,
8345 );
8346 workspace.add_item(
8347 pane.clone(),
8348 Box::new(dirty_multi_buffer_with_both.clone()),
8349 None,
8350 false,
8351 false,
8352 window,
8353 cx,
8354 );
8355 });
8356
8357 pane.update_in(cx, |pane, window, cx| {
8358 pane.activate_item(1, true, true, window, cx);
8359 assert_eq!(
8360 pane.active_item().unwrap().item_id(),
8361 multi_buffer_with_both_files_id,
8362 "Should select the multi buffer in the pane"
8363 );
8364 });
8365 let _close_multi_buffer_task = pane
8366 .update_in(cx, |pane, window, cx| {
8367 pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
8368 })
8369 .expect("should have active multi buffer to close");
8370 cx.background_executor.run_until_parked();
8371 assert!(
8372 cx.has_pending_prompt(),
8373 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
8374 );
8375 }
8376
8377 #[gpui::test]
8378 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
8379 cx: &mut TestAppContext,
8380 ) {
8381 init_test(cx);
8382
8383 let fs = FakeFs::new(cx.background_executor.clone());
8384 let project = Project::test(fs, [], cx).await;
8385 let (workspace, cx) =
8386 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8387 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8388
8389 let dirty_regular_buffer = cx.new(|cx| {
8390 TestItem::new(cx)
8391 .with_dirty(true)
8392 .with_label("1.txt")
8393 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8394 });
8395 let dirty_regular_buffer_2 = cx.new(|cx| {
8396 TestItem::new(cx)
8397 .with_dirty(true)
8398 .with_label("2.txt")
8399 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8400 });
8401 let clear_regular_buffer = cx.new(|cx| {
8402 TestItem::new(cx)
8403 .with_label("3.txt")
8404 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8405 });
8406
8407 let dirty_multi_buffer = cx.new(|cx| {
8408 TestItem::new(cx)
8409 .with_dirty(true)
8410 .with_singleton(false)
8411 .with_label("Fake Project Search")
8412 .with_project_items(&[
8413 dirty_regular_buffer.read(cx).project_items[0].clone(),
8414 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8415 clear_regular_buffer.read(cx).project_items[0].clone(),
8416 ])
8417 });
8418 workspace.update_in(cx, |workspace, window, cx| {
8419 workspace.add_item(
8420 pane.clone(),
8421 Box::new(dirty_regular_buffer.clone()),
8422 None,
8423 false,
8424 false,
8425 window,
8426 cx,
8427 );
8428 workspace.add_item(
8429 pane.clone(),
8430 Box::new(dirty_regular_buffer_2.clone()),
8431 None,
8432 false,
8433 false,
8434 window,
8435 cx,
8436 );
8437 workspace.add_item(
8438 pane.clone(),
8439 Box::new(dirty_multi_buffer.clone()),
8440 None,
8441 false,
8442 false,
8443 window,
8444 cx,
8445 );
8446 });
8447
8448 pane.update_in(cx, |pane, window, cx| {
8449 pane.activate_item(2, true, true, window, cx);
8450 assert_eq!(
8451 pane.active_item().unwrap().item_id(),
8452 dirty_multi_buffer.item_id(),
8453 "Should select the multi buffer in the pane"
8454 );
8455 });
8456 let close_multi_buffer_task = pane
8457 .update_in(cx, |pane, window, cx| {
8458 pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
8459 })
8460 .expect("should have active multi buffer to close");
8461 cx.background_executor.run_until_parked();
8462 assert!(
8463 !cx.has_pending_prompt(),
8464 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
8465 );
8466 close_multi_buffer_task
8467 .await
8468 .expect("Closing multi buffer failed");
8469 pane.update(cx, |pane, cx| {
8470 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8471 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
8472 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8473 assert_eq!(
8474 pane.items()
8475 .map(|item| item.item_id())
8476 .sorted()
8477 .collect::<Vec<_>>(),
8478 vec![
8479 dirty_regular_buffer.item_id(),
8480 dirty_regular_buffer_2.item_id(),
8481 ],
8482 "Should have no multi buffer left in the pane"
8483 );
8484 assert!(dirty_regular_buffer.read(cx).is_dirty);
8485 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
8486 });
8487 }
8488
8489 #[gpui::test]
8490 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
8491 init_test(cx);
8492 let fs = FakeFs::new(cx.executor());
8493 let project = Project::test(fs, [], cx).await;
8494 let (workspace, cx) =
8495 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8496
8497 // Add a new panel to the right dock, opening the dock and setting the
8498 // focus to the new panel.
8499 let panel = workspace.update_in(cx, |workspace, window, cx| {
8500 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8501 workspace.add_panel(panel.clone(), window, cx);
8502
8503 workspace
8504 .right_dock()
8505 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8506
8507 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8508
8509 panel
8510 });
8511
8512 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8513 // panel to the next valid position which, in this case, is the left
8514 // dock.
8515 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8516 workspace.update(cx, |workspace, cx| {
8517 assert!(workspace.left_dock().read(cx).is_open());
8518 assert_eq!(panel.read(cx).position, DockPosition::Left);
8519 });
8520
8521 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8522 // panel to the next valid position which, in this case, is the bottom
8523 // dock.
8524 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8525 workspace.update(cx, |workspace, cx| {
8526 assert!(workspace.bottom_dock().read(cx).is_open());
8527 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
8528 });
8529
8530 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
8531 // around moving the panel to its initial position, the right dock.
8532 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8533 workspace.update(cx, |workspace, cx| {
8534 assert!(workspace.right_dock().read(cx).is_open());
8535 assert_eq!(panel.read(cx).position, DockPosition::Right);
8536 });
8537
8538 // Remove focus from the panel, ensuring that, if the panel is not
8539 // focused, the `MoveFocusedPanelToNextPosition` action does not update
8540 // the panel's position, so the panel is still in the right dock.
8541 workspace.update_in(cx, |workspace, window, cx| {
8542 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8543 });
8544
8545 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8546 workspace.update(cx, |workspace, cx| {
8547 assert!(workspace.right_dock().read(cx).is_open());
8548 assert_eq!(panel.read(cx).position, DockPosition::Right);
8549 });
8550 }
8551
8552 mod register_project_item_tests {
8553
8554 use super::*;
8555
8556 // View
8557 struct TestPngItemView {
8558 focus_handle: FocusHandle,
8559 }
8560 // Model
8561 struct TestPngItem {}
8562
8563 impl project::ProjectItem for TestPngItem {
8564 fn try_open(
8565 _project: &Entity<Project>,
8566 path: &ProjectPath,
8567 cx: &mut App,
8568 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8569 if path.path.extension().unwrap() == "png" {
8570 Some(cx.spawn(|mut cx| async move { cx.new(|_| TestPngItem {}) }))
8571 } else {
8572 None
8573 }
8574 }
8575
8576 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8577 None
8578 }
8579
8580 fn project_path(&self, _: &App) -> Option<ProjectPath> {
8581 None
8582 }
8583
8584 fn is_dirty(&self) -> bool {
8585 false
8586 }
8587 }
8588
8589 impl Item for TestPngItemView {
8590 type Event = ();
8591 }
8592 impl EventEmitter<()> for TestPngItemView {}
8593 impl Focusable for TestPngItemView {
8594 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8595 self.focus_handle.clone()
8596 }
8597 }
8598
8599 impl Render for TestPngItemView {
8600 fn render(
8601 &mut self,
8602 _window: &mut Window,
8603 _cx: &mut Context<Self>,
8604 ) -> impl IntoElement {
8605 Empty
8606 }
8607 }
8608
8609 impl ProjectItem for TestPngItemView {
8610 type Item = TestPngItem;
8611
8612 fn for_project_item(
8613 _project: Entity<Project>,
8614 _item: Entity<Self::Item>,
8615 _: &mut Window,
8616 cx: &mut Context<Self>,
8617 ) -> Self
8618 where
8619 Self: Sized,
8620 {
8621 Self {
8622 focus_handle: cx.focus_handle(),
8623 }
8624 }
8625 }
8626
8627 // View
8628 struct TestIpynbItemView {
8629 focus_handle: FocusHandle,
8630 }
8631 // Model
8632 struct TestIpynbItem {}
8633
8634 impl project::ProjectItem for TestIpynbItem {
8635 fn try_open(
8636 _project: &Entity<Project>,
8637 path: &ProjectPath,
8638 cx: &mut App,
8639 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8640 if path.path.extension().unwrap() == "ipynb" {
8641 Some(cx.spawn(|mut cx| async move { cx.new(|_| TestIpynbItem {}) }))
8642 } else {
8643 None
8644 }
8645 }
8646
8647 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8648 None
8649 }
8650
8651 fn project_path(&self, _: &App) -> Option<ProjectPath> {
8652 None
8653 }
8654
8655 fn is_dirty(&self) -> bool {
8656 false
8657 }
8658 }
8659
8660 impl Item for TestIpynbItemView {
8661 type Event = ();
8662 }
8663 impl EventEmitter<()> for TestIpynbItemView {}
8664 impl Focusable for TestIpynbItemView {
8665 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8666 self.focus_handle.clone()
8667 }
8668 }
8669
8670 impl Render for TestIpynbItemView {
8671 fn render(
8672 &mut self,
8673 _window: &mut Window,
8674 _cx: &mut Context<Self>,
8675 ) -> impl IntoElement {
8676 Empty
8677 }
8678 }
8679
8680 impl ProjectItem for TestIpynbItemView {
8681 type Item = TestIpynbItem;
8682
8683 fn for_project_item(
8684 _project: Entity<Project>,
8685 _item: Entity<Self::Item>,
8686 _: &mut Window,
8687 cx: &mut Context<Self>,
8688 ) -> Self
8689 where
8690 Self: Sized,
8691 {
8692 Self {
8693 focus_handle: cx.focus_handle(),
8694 }
8695 }
8696 }
8697
8698 struct TestAlternatePngItemView {
8699 focus_handle: FocusHandle,
8700 }
8701
8702 impl Item for TestAlternatePngItemView {
8703 type Event = ();
8704 }
8705
8706 impl EventEmitter<()> for TestAlternatePngItemView {}
8707 impl Focusable for TestAlternatePngItemView {
8708 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8709 self.focus_handle.clone()
8710 }
8711 }
8712
8713 impl Render for TestAlternatePngItemView {
8714 fn render(
8715 &mut self,
8716 _window: &mut Window,
8717 _cx: &mut Context<Self>,
8718 ) -> impl IntoElement {
8719 Empty
8720 }
8721 }
8722
8723 impl ProjectItem for TestAlternatePngItemView {
8724 type Item = TestPngItem;
8725
8726 fn for_project_item(
8727 _project: Entity<Project>,
8728 _item: Entity<Self::Item>,
8729 _: &mut Window,
8730 cx: &mut Context<Self>,
8731 ) -> Self
8732 where
8733 Self: Sized,
8734 {
8735 Self {
8736 focus_handle: cx.focus_handle(),
8737 }
8738 }
8739 }
8740
8741 #[gpui::test]
8742 async fn test_register_project_item(cx: &mut TestAppContext) {
8743 init_test(cx);
8744
8745 cx.update(|cx| {
8746 register_project_item::<TestPngItemView>(cx);
8747 register_project_item::<TestIpynbItemView>(cx);
8748 });
8749
8750 let fs = FakeFs::new(cx.executor());
8751 fs.insert_tree(
8752 "/root1",
8753 json!({
8754 "one.png": "BINARYDATAHERE",
8755 "two.ipynb": "{ totally a notebook }",
8756 "three.txt": "editing text, sure why not?"
8757 }),
8758 )
8759 .await;
8760
8761 let project = Project::test(fs, ["root1".as_ref()], cx).await;
8762 let (workspace, cx) =
8763 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8764
8765 let worktree_id = project.update(cx, |project, cx| {
8766 project.worktrees(cx).next().unwrap().read(cx).id()
8767 });
8768
8769 let handle = workspace
8770 .update_in(cx, |workspace, window, cx| {
8771 let project_path = (worktree_id, "one.png");
8772 workspace.open_path(project_path, None, true, window, cx)
8773 })
8774 .await
8775 .unwrap();
8776
8777 // Now we can check if the handle we got back errored or not
8778 assert_eq!(
8779 handle.to_any().entity_type(),
8780 TypeId::of::<TestPngItemView>()
8781 );
8782
8783 let handle = workspace
8784 .update_in(cx, |workspace, window, cx| {
8785 let project_path = (worktree_id, "two.ipynb");
8786 workspace.open_path(project_path, None, true, window, cx)
8787 })
8788 .await
8789 .unwrap();
8790
8791 assert_eq!(
8792 handle.to_any().entity_type(),
8793 TypeId::of::<TestIpynbItemView>()
8794 );
8795
8796 let handle = workspace
8797 .update_in(cx, |workspace, window, cx| {
8798 let project_path = (worktree_id, "three.txt");
8799 workspace.open_path(project_path, None, true, window, cx)
8800 })
8801 .await;
8802 assert!(handle.is_err());
8803 }
8804
8805 #[gpui::test]
8806 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8807 init_test(cx);
8808
8809 cx.update(|cx| {
8810 register_project_item::<TestPngItemView>(cx);
8811 register_project_item::<TestAlternatePngItemView>(cx);
8812 });
8813
8814 let fs = FakeFs::new(cx.executor());
8815 fs.insert_tree(
8816 "/root1",
8817 json!({
8818 "one.png": "BINARYDATAHERE",
8819 "two.ipynb": "{ totally a notebook }",
8820 "three.txt": "editing text, sure why not?"
8821 }),
8822 )
8823 .await;
8824 let project = Project::test(fs, ["root1".as_ref()], cx).await;
8825 let (workspace, cx) =
8826 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8827 let worktree_id = project.update(cx, |project, cx| {
8828 project.worktrees(cx).next().unwrap().read(cx).id()
8829 });
8830
8831 let handle = workspace
8832 .update_in(cx, |workspace, window, cx| {
8833 let project_path = (worktree_id, "one.png");
8834 workspace.open_path(project_path, None, true, window, cx)
8835 })
8836 .await
8837 .unwrap();
8838
8839 // This _must_ be the second item registered
8840 assert_eq!(
8841 handle.to_any().entity_type(),
8842 TypeId::of::<TestAlternatePngItemView>()
8843 );
8844
8845 let handle = workspace
8846 .update_in(cx, |workspace, window, cx| {
8847 let project_path = (worktree_id, "three.txt");
8848 workspace.open_path(project_path, None, true, window, cx)
8849 })
8850 .await;
8851 assert!(handle.is_err());
8852 }
8853 }
8854
8855 pub fn init_test(cx: &mut TestAppContext) {
8856 cx.update(|cx| {
8857 let settings_store = SettingsStore::test(cx);
8858 cx.set_global(settings_store);
8859 theme::init(theme::LoadThemes::JustBase, cx);
8860 language::init(cx);
8861 crate::init_settings(cx);
8862 Project::init_settings(cx);
8863 });
8864 }
8865
8866 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
8867 let item = TestProjectItem::new(id, path, cx);
8868 item.update(cx, |item, _| {
8869 item.is_dirty = true;
8870 });
8871 item
8872 }
8873}