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