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