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 fn shared_screen_for_peer(
4425 &self,
4426 peer_id: PeerId,
4427 pane: &Entity<Pane>,
4428 window: &mut Window,
4429 cx: &mut App,
4430 ) -> Option<Entity<SharedScreen>> {
4431 let call = self.active_call()?;
4432 let room = call.read(cx).room()?.clone();
4433 let participant = room.read(cx).remote_participant_for_peer_id(peer_id)?;
4434 let track = participant.video_tracks.values().next()?.clone();
4435 let user = participant.user.clone();
4436
4437 for item in pane.read(cx).items_of_type::<SharedScreen>() {
4438 if item.read(cx).peer_id == peer_id {
4439 return Some(item);
4440 }
4441 }
4442
4443 Some(cx.new(|cx| SharedScreen::new(track, peer_id, user.clone(), room.clone(), window, cx)))
4444 }
4445
4446 pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4447 if window.is_window_active() {
4448 self.update_active_view_for_followers(window, cx);
4449
4450 if let Some(database_id) = self.database_id {
4451 cx.background_spawn(persistence::DB.update_timestamp(database_id))
4452 .detach();
4453 }
4454 } else {
4455 for pane in &self.panes {
4456 pane.update(cx, |pane, cx| {
4457 if let Some(item) = pane.active_item() {
4458 item.workspace_deactivated(window, cx);
4459 }
4460 for item in pane.items() {
4461 if matches!(
4462 item.workspace_settings(cx).autosave,
4463 AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
4464 ) {
4465 Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
4466 .detach_and_log_err(cx);
4467 }
4468 }
4469 });
4470 }
4471 }
4472 }
4473
4474 pub fn active_call(&self) -> Option<&Entity<ActiveCall>> {
4475 self.active_call.as_ref().map(|(call, _)| call)
4476 }
4477
4478 fn on_active_call_event(
4479 &mut self,
4480 _: &Entity<ActiveCall>,
4481 event: &call::room::Event,
4482 window: &mut Window,
4483 cx: &mut Context<Self>,
4484 ) {
4485 match event {
4486 call::room::Event::ParticipantLocationChanged { participant_id }
4487 | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
4488 self.leader_updated(*participant_id, window, cx);
4489 }
4490 _ => {}
4491 }
4492 }
4493
4494 pub fn database_id(&self) -> Option<WorkspaceId> {
4495 self.database_id
4496 }
4497
4498 pub fn session_id(&self) -> Option<String> {
4499 self.session_id.clone()
4500 }
4501
4502 fn local_paths(&self, cx: &App) -> Option<Vec<Arc<Path>>> {
4503 let project = self.project().read(cx);
4504
4505 if project.is_local() {
4506 Some(
4507 project
4508 .visible_worktrees(cx)
4509 .map(|worktree| worktree.read(cx).abs_path())
4510 .collect::<Vec<_>>(),
4511 )
4512 } else {
4513 None
4514 }
4515 }
4516
4517 fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
4518 match member {
4519 Member::Axis(PaneAxis { members, .. }) => {
4520 for child in members.iter() {
4521 self.remove_panes(child.clone(), window, cx)
4522 }
4523 }
4524 Member::Pane(pane) => {
4525 self.force_remove_pane(&pane, &None, window, cx);
4526 }
4527 }
4528 }
4529
4530 fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
4531 self.session_id.take();
4532 self.serialize_workspace_internal(window, cx)
4533 }
4534
4535 fn force_remove_pane(
4536 &mut self,
4537 pane: &Entity<Pane>,
4538 focus_on: &Option<Entity<Pane>>,
4539 window: &mut Window,
4540 cx: &mut Context<Workspace>,
4541 ) {
4542 self.panes.retain(|p| p != pane);
4543 if let Some(focus_on) = focus_on {
4544 focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4545 } else {
4546 if self.active_pane() == pane {
4547 self.panes
4548 .last()
4549 .unwrap()
4550 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4551 }
4552 }
4553 if self.last_active_center_pane == Some(pane.downgrade()) {
4554 self.last_active_center_pane = None;
4555 }
4556 cx.notify();
4557 }
4558
4559 fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4560 if self._schedule_serialize.is_none() {
4561 self._schedule_serialize = Some(cx.spawn_in(window, async move |this, cx| {
4562 cx.background_executor()
4563 .timer(Duration::from_millis(100))
4564 .await;
4565 this.update_in(cx, |this, window, cx| {
4566 this.serialize_workspace_internal(window, cx).detach();
4567 this._schedule_serialize.take();
4568 })
4569 .log_err();
4570 }));
4571 }
4572 }
4573
4574 fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
4575 let Some(database_id) = self.database_id() else {
4576 return Task::ready(());
4577 };
4578
4579 fn serialize_pane_handle(
4580 pane_handle: &Entity<Pane>,
4581 window: &mut Window,
4582 cx: &mut App,
4583 ) -> SerializedPane {
4584 let (items, active, pinned_count) = {
4585 let pane = pane_handle.read(cx);
4586 let active_item_id = pane.active_item().map(|item| item.item_id());
4587 (
4588 pane.items()
4589 .filter_map(|handle| {
4590 let handle = handle.to_serializable_item_handle(cx)?;
4591
4592 Some(SerializedItem {
4593 kind: Arc::from(handle.serialized_item_kind()),
4594 item_id: handle.item_id().as_u64(),
4595 active: Some(handle.item_id()) == active_item_id,
4596 preview: pane.is_active_preview_item(handle.item_id()),
4597 })
4598 })
4599 .collect::<Vec<_>>(),
4600 pane.has_focus(window, cx),
4601 pane.pinned_count(),
4602 )
4603 };
4604
4605 SerializedPane::new(items, active, pinned_count)
4606 }
4607
4608 fn build_serialized_pane_group(
4609 pane_group: &Member,
4610 window: &mut Window,
4611 cx: &mut App,
4612 ) -> SerializedPaneGroup {
4613 match pane_group {
4614 Member::Axis(PaneAxis {
4615 axis,
4616 members,
4617 flexes,
4618 bounding_boxes: _,
4619 }) => SerializedPaneGroup::Group {
4620 axis: SerializedAxis(*axis),
4621 children: members
4622 .iter()
4623 .map(|member| build_serialized_pane_group(member, window, cx))
4624 .collect::<Vec<_>>(),
4625 flexes: Some(flexes.lock().clone()),
4626 },
4627 Member::Pane(pane_handle) => {
4628 SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
4629 }
4630 }
4631 }
4632
4633 fn build_serialized_docks(
4634 this: &Workspace,
4635 window: &mut Window,
4636 cx: &mut App,
4637 ) -> DockStructure {
4638 let left_dock = this.left_dock.read(cx);
4639 let left_visible = left_dock.is_open();
4640 let left_active_panel = left_dock
4641 .active_panel()
4642 .map(|panel| panel.persistent_name().to_string());
4643 let left_dock_zoom = left_dock
4644 .active_panel()
4645 .map(|panel| panel.is_zoomed(window, cx))
4646 .unwrap_or(false);
4647
4648 let right_dock = this.right_dock.read(cx);
4649 let right_visible = right_dock.is_open();
4650 let right_active_panel = right_dock
4651 .active_panel()
4652 .map(|panel| panel.persistent_name().to_string());
4653 let right_dock_zoom = right_dock
4654 .active_panel()
4655 .map(|panel| panel.is_zoomed(window, cx))
4656 .unwrap_or(false);
4657
4658 let bottom_dock = this.bottom_dock.read(cx);
4659 let bottom_visible = bottom_dock.is_open();
4660 let bottom_active_panel = bottom_dock
4661 .active_panel()
4662 .map(|panel| panel.persistent_name().to_string());
4663 let bottom_dock_zoom = bottom_dock
4664 .active_panel()
4665 .map(|panel| panel.is_zoomed(window, cx))
4666 .unwrap_or(false);
4667
4668 DockStructure {
4669 left: DockData {
4670 visible: left_visible,
4671 active_panel: left_active_panel,
4672 zoom: left_dock_zoom,
4673 },
4674 right: DockData {
4675 visible: right_visible,
4676 active_panel: right_active_panel,
4677 zoom: right_dock_zoom,
4678 },
4679 bottom: DockData {
4680 visible: bottom_visible,
4681 active_panel: bottom_active_panel,
4682 zoom: bottom_dock_zoom,
4683 },
4684 }
4685 }
4686
4687 let location = if let Some(ssh_project) = &self.serialized_ssh_project {
4688 Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
4689 } else if let Some(local_paths) = self.local_paths(cx) {
4690 if !local_paths.is_empty() {
4691 Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4692 } else {
4693 None
4694 }
4695 } else {
4696 None
4697 };
4698
4699 if let Some(location) = location {
4700 let breakpoints = self.project.update(cx, |project, cx| {
4701 project.breakpoint_store().read(cx).all_breakpoints(cx)
4702 });
4703
4704 let center_group = build_serialized_pane_group(&self.center.root, window, cx);
4705 let docks = build_serialized_docks(self, window, cx);
4706 let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
4707 let serialized_workspace = SerializedWorkspace {
4708 id: database_id,
4709 location,
4710 center_group,
4711 window_bounds,
4712 display: Default::default(),
4713 docks,
4714 centered_layout: self.centered_layout,
4715 session_id: self.session_id.clone(),
4716 breakpoints,
4717 window_id: Some(window.window_handle().window_id().as_u64()),
4718 };
4719 return window.spawn(cx, async move |_| {
4720 persistence::DB.save_workspace(serialized_workspace).await
4721 });
4722 }
4723 Task::ready(())
4724 }
4725
4726 async fn serialize_items(
4727 this: &WeakEntity<Self>,
4728 items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4729 cx: &mut AsyncWindowContext,
4730 ) -> Result<()> {
4731 const CHUNK_SIZE: usize = 200;
4732
4733 let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4734
4735 while let Some(items_received) = serializable_items.next().await {
4736 let unique_items =
4737 items_received
4738 .into_iter()
4739 .fold(HashMap::default(), |mut acc, item| {
4740 acc.entry(item.item_id()).or_insert(item);
4741 acc
4742 });
4743
4744 // We use into_iter() here so that the references to the items are moved into
4745 // the tasks and not kept alive while we're sleeping.
4746 for (_, item) in unique_items.into_iter() {
4747 if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
4748 item.serialize(workspace, false, window, cx)
4749 }) {
4750 cx.background_spawn(async move { task.await.log_err() })
4751 .detach();
4752 }
4753 }
4754
4755 cx.background_executor()
4756 .timer(SERIALIZATION_THROTTLE_TIME)
4757 .await;
4758 }
4759
4760 Ok(())
4761 }
4762
4763 pub(crate) fn enqueue_item_serialization(
4764 &mut self,
4765 item: Box<dyn SerializableItemHandle>,
4766 ) -> Result<()> {
4767 self.serializable_items_tx
4768 .unbounded_send(item)
4769 .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4770 }
4771
4772 pub(crate) fn load_workspace(
4773 serialized_workspace: SerializedWorkspace,
4774 paths_to_open: Vec<Option<ProjectPath>>,
4775 window: &mut Window,
4776 cx: &mut Context<Workspace>,
4777 ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4778 cx.spawn_in(window, async move |workspace, cx| {
4779 let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
4780
4781 let mut center_group = None;
4782 let mut center_items = None;
4783
4784 // Traverse the splits tree and add to things
4785 if let Some((group, active_pane, items)) = serialized_workspace
4786 .center_group
4787 .deserialize(&project, serialized_workspace.id, workspace.clone(), cx)
4788 .await
4789 {
4790 center_items = Some(items);
4791 center_group = Some((group, active_pane))
4792 }
4793
4794 let mut items_by_project_path = HashMap::default();
4795 let mut item_ids_by_kind = HashMap::default();
4796 let mut all_deserialized_items = Vec::default();
4797 cx.update(|_, cx| {
4798 for item in center_items.unwrap_or_default().into_iter().flatten() {
4799 if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4800 item_ids_by_kind
4801 .entry(serializable_item_handle.serialized_item_kind())
4802 .or_insert(Vec::new())
4803 .push(item.item_id().as_u64() as ItemId);
4804 }
4805
4806 if let Some(project_path) = item.project_path(cx) {
4807 items_by_project_path.insert(project_path, item.clone());
4808 }
4809 all_deserialized_items.push(item);
4810 }
4811 })?;
4812
4813 let opened_items = paths_to_open
4814 .into_iter()
4815 .map(|path_to_open| {
4816 path_to_open
4817 .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4818 })
4819 .collect::<Vec<_>>();
4820
4821 // Remove old panes from workspace panes list
4822 workspace.update_in(cx, |workspace, window, cx| {
4823 if let Some((center_group, active_pane)) = center_group {
4824 workspace.remove_panes(workspace.center.root.clone(), window, cx);
4825
4826 // Swap workspace center group
4827 workspace.center = PaneGroup::with_root(center_group);
4828 if let Some(active_pane) = active_pane {
4829 workspace.set_active_pane(&active_pane, window, cx);
4830 cx.focus_self(window);
4831 } else {
4832 workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
4833 }
4834 }
4835
4836 let docks = serialized_workspace.docks;
4837
4838 for (dock, serialized_dock) in [
4839 (&mut workspace.right_dock, docks.right),
4840 (&mut workspace.left_dock, docks.left),
4841 (&mut workspace.bottom_dock, docks.bottom),
4842 ]
4843 .iter_mut()
4844 {
4845 dock.update(cx, |dock, cx| {
4846 dock.serialized_dock = Some(serialized_dock.clone());
4847 dock.restore_state(window, cx);
4848 });
4849 }
4850
4851 cx.notify();
4852 })?;
4853
4854 let _ = project
4855 .update(cx, |project, cx| {
4856 project
4857 .breakpoint_store()
4858 .update(cx, |breakpoint_store, cx| {
4859 breakpoint_store
4860 .with_serialized_breakpoints(serialized_workspace.breakpoints, cx)
4861 })
4862 })?
4863 .await;
4864
4865 // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4866 // after loading the items, we might have different items and in order to avoid
4867 // the database filling up, we delete items that haven't been loaded now.
4868 //
4869 // The items that have been loaded, have been saved after they've been added to the workspace.
4870 let clean_up_tasks = workspace.update_in(cx, |_, window, cx| {
4871 item_ids_by_kind
4872 .into_iter()
4873 .map(|(item_kind, loaded_items)| {
4874 SerializableItemRegistry::cleanup(
4875 item_kind,
4876 serialized_workspace.id,
4877 loaded_items,
4878 window,
4879 cx,
4880 )
4881 .log_err()
4882 })
4883 .collect::<Vec<_>>()
4884 })?;
4885
4886 futures::future::join_all(clean_up_tasks).await;
4887
4888 workspace
4889 .update_in(cx, |workspace, window, cx| {
4890 // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4891 workspace.serialize_workspace_internal(window, cx).detach();
4892
4893 // Ensure that we mark the window as edited if we did load dirty items
4894 workspace.update_window_edited(window, cx);
4895 })
4896 .ok();
4897
4898 Ok(opened_items)
4899 })
4900 }
4901
4902 fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
4903 self.add_workspace_actions_listeners(div, window, cx)
4904 .on_action(cx.listener(Self::close_inactive_items_and_panes))
4905 .on_action(cx.listener(Self::close_all_items_and_panes))
4906 .on_action(cx.listener(Self::save_all))
4907 .on_action(cx.listener(Self::send_keystrokes))
4908 .on_action(cx.listener(Self::add_folder_to_project))
4909 .on_action(cx.listener(Self::follow_next_collaborator))
4910 .on_action(cx.listener(Self::close_window))
4911 .on_action(cx.listener(Self::activate_pane_at_index))
4912 .on_action(cx.listener(Self::move_item_to_pane_at_index))
4913 .on_action(cx.listener(Self::move_focused_panel_to_next_position))
4914 .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
4915 let pane = workspace.active_pane().clone();
4916 workspace.unfollow_in_pane(&pane, window, cx);
4917 }))
4918 .on_action(cx.listener(|workspace, action: &Save, window, cx| {
4919 workspace
4920 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
4921 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4922 }))
4923 .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
4924 workspace
4925 .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
4926 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4927 }))
4928 .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
4929 workspace
4930 .save_active_item(SaveIntent::SaveAs, window, cx)
4931 .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4932 }))
4933 .on_action(
4934 cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
4935 workspace.activate_previous_pane(window, cx)
4936 }),
4937 )
4938 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
4939 workspace.activate_next_pane(window, cx)
4940 }))
4941 .on_action(
4942 cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
4943 workspace.activate_next_window(cx)
4944 }),
4945 )
4946 .on_action(
4947 cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
4948 workspace.activate_previous_window(cx)
4949 }),
4950 )
4951 .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
4952 workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
4953 }))
4954 .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
4955 workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
4956 }))
4957 .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
4958 workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
4959 }))
4960 .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
4961 workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
4962 }))
4963 .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
4964 workspace.activate_next_pane(window, cx)
4965 }))
4966 .on_action(cx.listener(
4967 |workspace, action: &MoveItemToPaneInDirection, window, cx| {
4968 workspace.move_item_to_pane_in_direction(action, window, cx)
4969 },
4970 ))
4971 .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
4972 workspace.swap_pane_in_direction(SplitDirection::Left, cx)
4973 }))
4974 .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
4975 workspace.swap_pane_in_direction(SplitDirection::Right, cx)
4976 }))
4977 .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
4978 workspace.swap_pane_in_direction(SplitDirection::Up, cx)
4979 }))
4980 .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
4981 workspace.swap_pane_in_direction(SplitDirection::Down, cx)
4982 }))
4983 .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
4984 this.toggle_dock(DockPosition::Left, window, cx);
4985 }))
4986 .on_action(cx.listener(
4987 |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
4988 workspace.toggle_dock(DockPosition::Right, window, cx);
4989 },
4990 ))
4991 .on_action(cx.listener(
4992 |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
4993 workspace.toggle_dock(DockPosition::Bottom, window, cx);
4994 },
4995 ))
4996 .on_action(
4997 cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
4998 workspace.close_all_docks(window, cx);
4999 }),
5000 )
5001 .on_action(cx.listener(
5002 |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
5003 workspace.clear_all_notifications(cx);
5004 },
5005 ))
5006 .on_action(cx.listener(
5007 |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
5008 workspace.reopen_closed_item(window, cx).detach();
5009 },
5010 ))
5011 .on_action(cx.listener(Workspace::toggle_centered_layout))
5012 }
5013
5014 #[cfg(any(test, feature = "test-support"))]
5015 pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
5016 use node_runtime::NodeRuntime;
5017 use session::Session;
5018
5019 let client = project.read(cx).client();
5020 let user_store = project.read(cx).user_store();
5021
5022 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
5023 let session = cx.new(|cx| AppSession::new(Session::test(), cx));
5024 window.activate_window();
5025 let app_state = Arc::new(AppState {
5026 languages: project.read(cx).languages().clone(),
5027 debug_adapters: project.read(cx).debug_adapters().clone(),
5028 workspace_store,
5029 client,
5030 user_store,
5031 fs: project.read(cx).fs().clone(),
5032 build_window_options: |_, _| Default::default(),
5033 node_runtime: NodeRuntime::unavailable(),
5034 session,
5035 });
5036 let workspace = Self::new(Default::default(), project, app_state, window, cx);
5037 workspace
5038 .active_pane
5039 .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
5040 workspace
5041 }
5042
5043 pub fn register_action<A: Action>(
5044 &mut self,
5045 callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
5046 ) -> &mut Self {
5047 let callback = Arc::new(callback);
5048
5049 self.workspace_actions.push(Box::new(move |div, _, cx| {
5050 let callback = callback.clone();
5051 div.on_action(cx.listener(move |workspace, event, window, cx| {
5052 (callback.clone())(workspace, event, window, cx)
5053 }))
5054 }));
5055 self
5056 }
5057
5058 fn add_workspace_actions_listeners(
5059 &self,
5060 mut div: Div,
5061 window: &mut Window,
5062 cx: &mut Context<Self>,
5063 ) -> Div {
5064 for action in self.workspace_actions.iter() {
5065 div = (action)(div, window, cx)
5066 }
5067 div
5068 }
5069
5070 pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
5071 self.modal_layer.read(cx).has_active_modal()
5072 }
5073
5074 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
5075 self.modal_layer.read(cx).active_modal()
5076 }
5077
5078 pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
5079 where
5080 B: FnOnce(&mut Window, &mut Context<V>) -> V,
5081 {
5082 self.modal_layer.update(cx, |modal_layer, cx| {
5083 modal_layer.toggle_modal(window, cx, build)
5084 })
5085 }
5086
5087 pub fn toggle_status_toast<V: ToastView>(&mut self, entity: Entity<V>, cx: &mut App) {
5088 self.toast_layer
5089 .update(cx, |toast_layer, cx| toast_layer.toggle_toast(cx, entity))
5090 }
5091
5092 pub fn toggle_centered_layout(
5093 &mut self,
5094 _: &ToggleCenteredLayout,
5095 _: &mut Window,
5096 cx: &mut Context<Self>,
5097 ) {
5098 self.centered_layout = !self.centered_layout;
5099 if let Some(database_id) = self.database_id() {
5100 cx.background_spawn(DB.set_centered_layout(database_id, self.centered_layout))
5101 .detach_and_log_err(cx);
5102 }
5103 cx.notify();
5104 }
5105
5106 fn adjust_padding(padding: Option<f32>) -> f32 {
5107 padding
5108 .unwrap_or(Self::DEFAULT_PADDING)
5109 .clamp(0.0, Self::MAX_PADDING)
5110 }
5111
5112 fn render_dock(
5113 &self,
5114 position: DockPosition,
5115 dock: &Entity<Dock>,
5116 window: &mut Window,
5117 cx: &mut App,
5118 ) -> Option<Div> {
5119 if self.zoomed_position == Some(position) {
5120 return None;
5121 }
5122
5123 let leader_border = dock.read(cx).active_panel().and_then(|panel| {
5124 let pane = panel.pane(cx)?;
5125 let follower_states = &self.follower_states;
5126 leader_border_for_pane(follower_states, &pane, window, cx)
5127 });
5128
5129 Some(
5130 div()
5131 .flex()
5132 .flex_none()
5133 .overflow_hidden()
5134 .child(dock.clone())
5135 .children(leader_border),
5136 )
5137 }
5138
5139 pub fn for_window(window: &mut Window, _: &mut App) -> Option<Entity<Workspace>> {
5140 window.root().flatten()
5141 }
5142
5143 pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
5144 self.zoomed.as_ref()
5145 }
5146
5147 pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
5148 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5149 return;
5150 };
5151 let windows = cx.windows();
5152 let Some(next_window) = windows
5153 .iter()
5154 .cycle()
5155 .skip_while(|window| window.window_id() != current_window_id)
5156 .nth(1)
5157 else {
5158 return;
5159 };
5160 next_window
5161 .update(cx, |_, window, _| window.activate_window())
5162 .ok();
5163 }
5164
5165 pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
5166 let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5167 return;
5168 };
5169 let windows = cx.windows();
5170 let Some(prev_window) = windows
5171 .iter()
5172 .rev()
5173 .cycle()
5174 .skip_while(|window| window.window_id() != current_window_id)
5175 .nth(1)
5176 else {
5177 return;
5178 };
5179 prev_window
5180 .update(cx, |_, window, _| window.activate_window())
5181 .ok();
5182 }
5183
5184 pub fn debug_task_ready(&mut self, task_id: &TaskId, cx: &mut App) {
5185 if let Some(debug_config) = self.debug_task_queue.remove(task_id) {
5186 self.project.update(cx, |project, cx| {
5187 project
5188 .start_debug_session(debug_config, cx)
5189 .detach_and_log_err(cx);
5190 })
5191 }
5192 }
5193}
5194
5195fn leader_border_for_pane(
5196 follower_states: &HashMap<PeerId, FollowerState>,
5197 pane: &Entity<Pane>,
5198 _: &Window,
5199 cx: &App,
5200) -> Option<Div> {
5201 let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
5202 if state.pane() == pane {
5203 Some((*leader_id, state))
5204 } else {
5205 None
5206 }
5207 })?;
5208
5209 let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
5210 let leader = room.remote_participant_for_peer_id(leader_id)?;
5211
5212 let mut leader_color = cx
5213 .theme()
5214 .players()
5215 .color_for_participant(leader.participant_index.0)
5216 .cursor;
5217 leader_color.fade_out(0.3);
5218 Some(
5219 div()
5220 .absolute()
5221 .size_full()
5222 .left_0()
5223 .top_0()
5224 .border_2()
5225 .border_color(leader_color),
5226 )
5227}
5228
5229fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
5230 ZED_WINDOW_POSITION
5231 .zip(*ZED_WINDOW_SIZE)
5232 .map(|(position, size)| Bounds {
5233 origin: position,
5234 size,
5235 })
5236}
5237
5238fn open_items(
5239 serialized_workspace: Option<SerializedWorkspace>,
5240 mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
5241 window: &mut Window,
5242 cx: &mut Context<Workspace>,
5243) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> + use<> {
5244 let restored_items = serialized_workspace.map(|serialized_workspace| {
5245 Workspace::load_workspace(
5246 serialized_workspace,
5247 project_paths_to_open
5248 .iter()
5249 .map(|(_, project_path)| project_path)
5250 .cloned()
5251 .collect(),
5252 window,
5253 cx,
5254 )
5255 });
5256
5257 cx.spawn_in(window, async move |workspace, cx| {
5258 let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
5259
5260 if let Some(restored_items) = restored_items {
5261 let restored_items = restored_items.await?;
5262
5263 let restored_project_paths = restored_items
5264 .iter()
5265 .filter_map(|item| {
5266 cx.update(|_, cx| item.as_ref()?.project_path(cx))
5267 .ok()
5268 .flatten()
5269 })
5270 .collect::<HashSet<_>>();
5271
5272 for restored_item in restored_items {
5273 opened_items.push(restored_item.map(Ok));
5274 }
5275
5276 project_paths_to_open
5277 .iter_mut()
5278 .for_each(|(_, project_path)| {
5279 if let Some(project_path_to_open) = project_path {
5280 if restored_project_paths.contains(project_path_to_open) {
5281 *project_path = None;
5282 }
5283 }
5284 });
5285 } else {
5286 for _ in 0..project_paths_to_open.len() {
5287 opened_items.push(None);
5288 }
5289 }
5290 assert!(opened_items.len() == project_paths_to_open.len());
5291
5292 let tasks =
5293 project_paths_to_open
5294 .into_iter()
5295 .enumerate()
5296 .map(|(ix, (abs_path, project_path))| {
5297 let workspace = workspace.clone();
5298 cx.spawn(async move |cx| {
5299 let file_project_path = project_path?;
5300 let abs_path_task = workspace.update(cx, |workspace, cx| {
5301 workspace.project().update(cx, |project, cx| {
5302 project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
5303 })
5304 });
5305
5306 // We only want to open file paths here. If one of the items
5307 // here is a directory, it was already opened further above
5308 // with a `find_or_create_worktree`.
5309 if let Ok(task) = abs_path_task {
5310 if task.await.map_or(true, |p| p.is_file()) {
5311 return Some((
5312 ix,
5313 workspace
5314 .update_in(cx, |workspace, window, cx| {
5315 workspace.open_path(
5316 file_project_path,
5317 None,
5318 true,
5319 window,
5320 cx,
5321 )
5322 })
5323 .log_err()?
5324 .await,
5325 ));
5326 }
5327 }
5328 None
5329 })
5330 });
5331
5332 let tasks = tasks.collect::<Vec<_>>();
5333
5334 let tasks = futures::future::join_all(tasks);
5335 for (ix, path_open_result) in tasks.await.into_iter().flatten() {
5336 opened_items[ix] = Some(path_open_result);
5337 }
5338
5339 Ok(opened_items)
5340 })
5341}
5342
5343enum ActivateInDirectionTarget {
5344 Pane(Entity<Pane>),
5345 Dock(Entity<Dock>),
5346}
5347
5348fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncApp) {
5349 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";
5350
5351 workspace
5352 .update(cx, |workspace, _, cx| {
5353 if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
5354 struct DatabaseFailedNotification;
5355
5356 workspace.show_notification(
5357 NotificationId::unique::<DatabaseFailedNotification>(),
5358 cx,
5359 |cx| {
5360 cx.new(|cx| {
5361 MessageNotification::new("Failed to load the database file.", cx)
5362 .primary_message("File an Issue")
5363 .primary_icon(IconName::Plus)
5364 .primary_on_click(|_window, cx| cx.open_url(REPORT_ISSUE_URL))
5365 })
5366 },
5367 );
5368 }
5369 })
5370 .log_err();
5371}
5372
5373impl Focusable for Workspace {
5374 fn focus_handle(&self, cx: &App) -> FocusHandle {
5375 self.active_pane.focus_handle(cx)
5376 }
5377}
5378
5379#[derive(Clone)]
5380struct DraggedDock(DockPosition);
5381
5382impl Render for DraggedDock {
5383 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5384 gpui::Empty
5385 }
5386}
5387
5388impl Render for Workspace {
5389 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5390 let mut context = KeyContext::new_with_defaults();
5391 context.add("Workspace");
5392 context.set("keyboard_layout", cx.keyboard_layout().clone());
5393 let centered_layout = self.centered_layout
5394 && self.center.panes().len() == 1
5395 && self.active_item(cx).is_some();
5396 let render_padding = |size| {
5397 (size > 0.0).then(|| {
5398 div()
5399 .h_full()
5400 .w(relative(size))
5401 .bg(cx.theme().colors().editor_background)
5402 .border_color(cx.theme().colors().pane_group_border)
5403 })
5404 };
5405 let paddings = if centered_layout {
5406 let settings = WorkspaceSettings::get_global(cx).centered_layout;
5407 (
5408 render_padding(Self::adjust_padding(settings.left_padding)),
5409 render_padding(Self::adjust_padding(settings.right_padding)),
5410 )
5411 } else {
5412 (None, None)
5413 };
5414 let ui_font = theme::setup_ui_font(window, cx);
5415
5416 let theme = cx.theme().clone();
5417 let colors = theme.colors();
5418
5419 client_side_decorations(
5420 self.actions(div(), window, cx)
5421 .key_context(context)
5422 .relative()
5423 .size_full()
5424 .flex()
5425 .flex_col()
5426 .font(ui_font)
5427 .gap_0()
5428 .justify_start()
5429 .items_start()
5430 .text_color(colors.text)
5431 .overflow_hidden()
5432 .children(self.titlebar_item.clone())
5433 .child(
5434 div()
5435 .size_full()
5436 .relative()
5437 .flex_1()
5438 .flex()
5439 .flex_col()
5440 .child(
5441 div()
5442 .id("workspace")
5443 .bg(colors.background)
5444 .relative()
5445 .flex_1()
5446 .w_full()
5447 .flex()
5448 .flex_col()
5449 .overflow_hidden()
5450 .border_t_1()
5451 .border_b_1()
5452 .border_color(colors.border)
5453 .child({
5454 let this = cx.entity().clone();
5455 canvas(
5456 move |bounds, window, cx| {
5457 this.update(cx, |this, cx| {
5458 let bounds_changed = this.bounds != bounds;
5459 this.bounds = bounds;
5460
5461 if bounds_changed {
5462 this.left_dock.update(cx, |dock, cx| {
5463 dock.clamp_panel_size(
5464 bounds.size.width,
5465 window,
5466 cx,
5467 )
5468 });
5469
5470 this.right_dock.update(cx, |dock, cx| {
5471 dock.clamp_panel_size(
5472 bounds.size.width,
5473 window,
5474 cx,
5475 )
5476 });
5477
5478 this.bottom_dock.update(cx, |dock, cx| {
5479 dock.clamp_panel_size(
5480 bounds.size.height,
5481 window,
5482 cx,
5483 )
5484 });
5485 }
5486 })
5487 },
5488 |_, _, _, _| {},
5489 )
5490 .absolute()
5491 .size_full()
5492 })
5493 .when(self.zoomed.is_none(), |this| {
5494 this.on_drag_move(cx.listener(
5495 move |workspace,
5496 e: &DragMoveEvent<DraggedDock>,
5497 window,
5498 cx| {
5499 if workspace.previous_dock_drag_coordinates
5500 != Some(e.event.position)
5501 {
5502 workspace.previous_dock_drag_coordinates =
5503 Some(e.event.position);
5504 match e.drag(cx).0 {
5505 DockPosition::Left => {
5506 resize_left_dock(
5507 e.event.position.x
5508 - workspace.bounds.left(),
5509 workspace,
5510 window,
5511 cx,
5512 );
5513 }
5514 DockPosition::Right => {
5515 resize_right_dock(
5516 workspace.bounds.right()
5517 - e.event.position.x,
5518 workspace,
5519 window,
5520 cx,
5521 );
5522 }
5523 DockPosition::Bottom => {
5524 resize_bottom_dock(
5525 workspace.bounds.bottom()
5526 - e.event.position.y,
5527 workspace,
5528 window,
5529 cx,
5530 );
5531 }
5532 };
5533 workspace.serialize_workspace(window, cx);
5534 }
5535 },
5536 ))
5537 })
5538 .child(
5539 div()
5540 .flex()
5541 .flex_row()
5542 .h_full()
5543 // Left Dock
5544 .children(self.render_dock(
5545 DockPosition::Left,
5546 &self.left_dock,
5547 window,
5548 cx,
5549 ))
5550 // Panes
5551 .child(
5552 div()
5553 .flex()
5554 .flex_col()
5555 .flex_1()
5556 .overflow_hidden()
5557 .child(
5558 h_flex()
5559 .flex_1()
5560 .when_some(paddings.0, |this, p| {
5561 this.child(p.border_r_1())
5562 })
5563 .child(self.center.render(
5564 &self.project,
5565 &self.follower_states,
5566 self.active_call(),
5567 &self.active_pane,
5568 self.zoomed.as_ref(),
5569 &self.app_state,
5570 window,
5571 cx,
5572 ))
5573 .when_some(paddings.1, |this, p| {
5574 this.child(p.border_l_1())
5575 }),
5576 )
5577 .children(self.render_dock(
5578 DockPosition::Bottom,
5579 &self.bottom_dock,
5580 window,
5581 cx,
5582 )),
5583 )
5584 // Right Dock
5585 .children(self.render_dock(
5586 DockPosition::Right,
5587 &self.right_dock,
5588 window,
5589 cx,
5590 )),
5591 )
5592 .children(self.zoomed.as_ref().and_then(|view| {
5593 let zoomed_view = view.upgrade()?;
5594 let div = div()
5595 .occlude()
5596 .absolute()
5597 .overflow_hidden()
5598 .border_color(colors.border)
5599 .bg(colors.background)
5600 .child(zoomed_view)
5601 .inset_0()
5602 .shadow_lg();
5603
5604 Some(match self.zoomed_position {
5605 Some(DockPosition::Left) => div.right_2().border_r_1(),
5606 Some(DockPosition::Right) => div.left_2().border_l_1(),
5607 Some(DockPosition::Bottom) => div.top_2().border_t_1(),
5608 None => {
5609 div.top_2().bottom_2().left_2().right_2().border_1()
5610 }
5611 })
5612 }))
5613 .children(self.render_notifications(window, cx)),
5614 )
5615 .child(self.status_bar.clone())
5616 .child(self.modal_layer.clone())
5617 .child(self.toast_layer.clone()),
5618 ),
5619 window,
5620 cx,
5621 )
5622 }
5623}
5624
5625fn resize_bottom_dock(
5626 new_size: Pixels,
5627 workspace: &mut Workspace,
5628 window: &mut Window,
5629 cx: &mut App,
5630) {
5631 let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
5632 workspace.bottom_dock.update(cx, |bottom_dock, cx| {
5633 bottom_dock.resize_active_panel(Some(size), window, cx);
5634 });
5635}
5636
5637fn resize_right_dock(
5638 new_size: Pixels,
5639 workspace: &mut Workspace,
5640 window: &mut Window,
5641 cx: &mut App,
5642) {
5643 let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
5644 workspace.right_dock.update(cx, |right_dock, cx| {
5645 right_dock.resize_active_panel(Some(size), window, cx);
5646 });
5647}
5648
5649fn resize_left_dock(
5650 new_size: Pixels,
5651 workspace: &mut Workspace,
5652 window: &mut Window,
5653 cx: &mut App,
5654) {
5655 let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
5656
5657 workspace.left_dock.update(cx, |left_dock, cx| {
5658 left_dock.resize_active_panel(Some(size), window, cx);
5659 });
5660}
5661
5662impl WorkspaceStore {
5663 pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
5664 Self {
5665 workspaces: Default::default(),
5666 _subscriptions: vec![
5667 client.add_request_handler(cx.weak_entity(), Self::handle_follow),
5668 client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
5669 ],
5670 client,
5671 }
5672 }
5673
5674 pub fn update_followers(
5675 &self,
5676 project_id: Option<u64>,
5677 update: proto::update_followers::Variant,
5678 cx: &App,
5679 ) -> Option<()> {
5680 let active_call = ActiveCall::try_global(cx)?;
5681 let room_id = active_call.read(cx).room()?.read(cx).id();
5682 self.client
5683 .send(proto::UpdateFollowers {
5684 room_id,
5685 project_id,
5686 variant: Some(update),
5687 })
5688 .log_err()
5689 }
5690
5691 pub async fn handle_follow(
5692 this: Entity<Self>,
5693 envelope: TypedEnvelope<proto::Follow>,
5694 mut cx: AsyncApp,
5695 ) -> Result<proto::FollowResponse> {
5696 this.update(&mut cx, |this, cx| {
5697 let follower = Follower {
5698 project_id: envelope.payload.project_id,
5699 peer_id: envelope.original_sender_id()?,
5700 };
5701
5702 let mut response = proto::FollowResponse::default();
5703 this.workspaces.retain(|workspace| {
5704 workspace
5705 .update(cx, |workspace, window, cx| {
5706 let handler_response =
5707 workspace.handle_follow(follower.project_id, window, cx);
5708 if let Some(active_view) = handler_response.active_view.clone() {
5709 if workspace.project.read(cx).remote_id() == follower.project_id {
5710 response.active_view = Some(active_view)
5711 }
5712 }
5713 })
5714 .is_ok()
5715 });
5716
5717 Ok(response)
5718 })?
5719 }
5720
5721 async fn handle_update_followers(
5722 this: Entity<Self>,
5723 envelope: TypedEnvelope<proto::UpdateFollowers>,
5724 mut cx: AsyncApp,
5725 ) -> Result<()> {
5726 let leader_id = envelope.original_sender_id()?;
5727 let update = envelope.payload;
5728
5729 this.update(&mut cx, |this, cx| {
5730 this.workspaces.retain(|workspace| {
5731 workspace
5732 .update(cx, |workspace, window, cx| {
5733 let project_id = workspace.project.read(cx).remote_id();
5734 if update.project_id != project_id && update.project_id.is_some() {
5735 return;
5736 }
5737 workspace.handle_update_followers(leader_id, update.clone(), window, cx);
5738 })
5739 .is_ok()
5740 });
5741 Ok(())
5742 })?
5743 }
5744}
5745
5746impl ViewId {
5747 pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5748 Ok(Self {
5749 creator: message
5750 .creator
5751 .ok_or_else(|| anyhow!("creator is missing"))?,
5752 id: message.id,
5753 })
5754 }
5755
5756 pub(crate) fn to_proto(self) -> proto::ViewId {
5757 proto::ViewId {
5758 creator: Some(self.creator),
5759 id: self.id,
5760 }
5761 }
5762}
5763
5764impl FollowerState {
5765 fn pane(&self) -> &Entity<Pane> {
5766 self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5767 }
5768}
5769
5770pub trait WorkspaceHandle {
5771 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
5772}
5773
5774impl WorkspaceHandle for Entity<Workspace> {
5775 fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
5776 self.read(cx)
5777 .worktrees(cx)
5778 .flat_map(|worktree| {
5779 let worktree_id = worktree.read(cx).id();
5780 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5781 worktree_id,
5782 path: f.path.clone(),
5783 })
5784 })
5785 .collect::<Vec<_>>()
5786 }
5787}
5788
5789impl std::fmt::Debug for OpenPaths {
5790 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5791 f.debug_struct("OpenPaths")
5792 .field("paths", &self.paths)
5793 .finish()
5794 }
5795}
5796
5797pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5798 DB.last_workspace().await.log_err().flatten()
5799}
5800
5801pub fn last_session_workspace_locations(
5802 last_session_id: &str,
5803 last_session_window_stack: Option<Vec<WindowId>>,
5804) -> Option<Vec<SerializedWorkspaceLocation>> {
5805 DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5806 .log_err()
5807}
5808
5809actions!(
5810 collab,
5811 [
5812 OpenChannelNotes,
5813 Mute,
5814 Deafen,
5815 LeaveCall,
5816 ShareProject,
5817 ScreenShare
5818 ]
5819);
5820actions!(zed, [OpenLog]);
5821
5822async fn join_channel_internal(
5823 channel_id: ChannelId,
5824 app_state: &Arc<AppState>,
5825 requesting_window: Option<WindowHandle<Workspace>>,
5826 active_call: &Entity<ActiveCall>,
5827 cx: &mut AsyncApp,
5828) -> Result<bool> {
5829 let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5830 let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5831 return (false, None);
5832 };
5833
5834 let already_in_channel = room.channel_id() == Some(channel_id);
5835 let should_prompt = room.is_sharing_project()
5836 && !room.remote_participants().is_empty()
5837 && !already_in_channel;
5838 let open_room = if already_in_channel {
5839 active_call.room().cloned()
5840 } else {
5841 None
5842 };
5843 (should_prompt, open_room)
5844 })?;
5845
5846 if let Some(room) = open_room {
5847 let task = room.update(cx, |room, cx| {
5848 if let Some((project, host)) = room.most_active_project(cx) {
5849 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5850 }
5851
5852 None
5853 })?;
5854 if let Some(task) = task {
5855 task.await?;
5856 }
5857 return anyhow::Ok(true);
5858 }
5859
5860 if should_prompt {
5861 if let Some(workspace) = requesting_window {
5862 let answer = workspace
5863 .update(cx, |_, window, cx| {
5864 window.prompt(
5865 PromptLevel::Warning,
5866 "Do you want to switch channels?",
5867 Some("Leaving this call will unshare your current project."),
5868 &["Yes, Join Channel", "Cancel"],
5869 cx,
5870 )
5871 })?
5872 .await;
5873
5874 if answer == Ok(1) {
5875 return Ok(false);
5876 }
5877 } else {
5878 return Ok(false); // unreachable!() hopefully
5879 }
5880 }
5881
5882 let client = cx.update(|cx| active_call.read(cx).client())?;
5883
5884 let mut client_status = client.status();
5885
5886 // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5887 'outer: loop {
5888 let Some(status) = client_status.recv().await else {
5889 return Err(anyhow!("error connecting"));
5890 };
5891
5892 match status {
5893 Status::Connecting
5894 | Status::Authenticating
5895 | Status::Reconnecting
5896 | Status::Reauthenticating => continue,
5897 Status::Connected { .. } => break 'outer,
5898 Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5899 Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5900 Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5901 return Err(ErrorCode::Disconnected.into());
5902 }
5903 }
5904 }
5905
5906 let room = active_call
5907 .update(cx, |active_call, cx| {
5908 active_call.join_channel(channel_id, cx)
5909 })?
5910 .await?;
5911
5912 let Some(room) = room else {
5913 return anyhow::Ok(true);
5914 };
5915
5916 room.update(cx, |room, _| room.room_update_completed())?
5917 .await;
5918
5919 let task = room.update(cx, |room, cx| {
5920 if let Some((project, host)) = room.most_active_project(cx) {
5921 return Some(join_in_room_project(project, host, app_state.clone(), cx));
5922 }
5923
5924 // If you are the first to join a channel, see if you should share your project.
5925 if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5926 if let Some(workspace) = requesting_window {
5927 let project = workspace.update(cx, |workspace, _, cx| {
5928 let project = workspace.project.read(cx);
5929
5930 if !CallSettings::get_global(cx).share_on_join {
5931 return None;
5932 }
5933
5934 if (project.is_local() || project.is_via_ssh())
5935 && project.visible_worktrees(cx).any(|tree| {
5936 tree.read(cx)
5937 .root_entry()
5938 .map_or(false, |entry| entry.is_dir())
5939 })
5940 {
5941 Some(workspace.project.clone())
5942 } else {
5943 None
5944 }
5945 });
5946 if let Ok(Some(project)) = project {
5947 return Some(cx.spawn(async move |room, cx| {
5948 room.update(cx, |room, cx| room.share_project(project, cx))?
5949 .await?;
5950 Ok(())
5951 }));
5952 }
5953 }
5954 }
5955
5956 None
5957 })?;
5958 if let Some(task) = task {
5959 task.await?;
5960 return anyhow::Ok(true);
5961 }
5962 anyhow::Ok(false)
5963}
5964
5965pub fn join_channel(
5966 channel_id: ChannelId,
5967 app_state: Arc<AppState>,
5968 requesting_window: Option<WindowHandle<Workspace>>,
5969 cx: &mut App,
5970) -> Task<Result<()>> {
5971 let active_call = ActiveCall::global(cx);
5972 cx.spawn(async move |cx| {
5973 let result = join_channel_internal(
5974 channel_id,
5975 &app_state,
5976 requesting_window,
5977 &active_call,
5978 cx,
5979 )
5980 .await;
5981
5982 // join channel succeeded, and opened a window
5983 if matches!(result, Ok(true)) {
5984 return anyhow::Ok(());
5985 }
5986
5987 // find an existing workspace to focus and show call controls
5988 let mut active_window =
5989 requesting_window.or_else(|| activate_any_workspace_window( cx));
5990 if active_window.is_none() {
5991 // no open workspaces, make one to show the error in (blergh)
5992 let (window_handle, _) = cx
5993 .update(|cx| {
5994 Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5995 })?
5996 .await?;
5997
5998 if result.is_ok() {
5999 cx.update(|cx| {
6000 cx.dispatch_action(&OpenChannelNotes);
6001 }).log_err();
6002 }
6003
6004 active_window = Some(window_handle);
6005 }
6006
6007 if let Err(err) = result {
6008 log::error!("failed to join channel: {}", err);
6009 if let Some(active_window) = active_window {
6010 active_window
6011 .update(cx, |_, window, cx| {
6012 let detail: SharedString = match err.error_code() {
6013 ErrorCode::SignedOut => {
6014 "Please sign in to continue.".into()
6015 }
6016 ErrorCode::UpgradeRequired => {
6017 "Your are running an unsupported version of Zed. Please update to continue.".into()
6018 }
6019 ErrorCode::NoSuchChannel => {
6020 "No matching channel was found. Please check the link and try again.".into()
6021 }
6022 ErrorCode::Forbidden => {
6023 "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
6024 }
6025 ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
6026 _ => format!("{}\n\nPlease try again.", err).into(),
6027 };
6028 window.prompt(
6029 PromptLevel::Critical,
6030 "Failed to join channel",
6031 Some(&detail),
6032 &["Ok"],
6033 cx)
6034 })?
6035 .await
6036 .ok();
6037 }
6038 }
6039
6040 // return ok, we showed the error to the user.
6041 anyhow::Ok(())
6042 })
6043}
6044
6045pub async fn get_any_active_workspace(
6046 app_state: Arc<AppState>,
6047 mut cx: AsyncApp,
6048) -> anyhow::Result<WindowHandle<Workspace>> {
6049 // find an existing workspace to focus and show call controls
6050 let active_window = activate_any_workspace_window(&mut cx);
6051 if active_window.is_none() {
6052 cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
6053 .await?;
6054 }
6055 activate_any_workspace_window(&mut cx).context("could not open zed")
6056}
6057
6058fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
6059 cx.update(|cx| {
6060 if let Some(workspace_window) = cx
6061 .active_window()
6062 .and_then(|window| window.downcast::<Workspace>())
6063 {
6064 return Some(workspace_window);
6065 }
6066
6067 for window in cx.windows() {
6068 if let Some(workspace_window) = window.downcast::<Workspace>() {
6069 workspace_window
6070 .update(cx, |_, window, _| window.activate_window())
6071 .ok();
6072 return Some(workspace_window);
6073 }
6074 }
6075 None
6076 })
6077 .ok()
6078 .flatten()
6079}
6080
6081pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
6082 cx.windows()
6083 .into_iter()
6084 .filter_map(|window| window.downcast::<Workspace>())
6085 .filter(|workspace| {
6086 workspace
6087 .read(cx)
6088 .is_ok_and(|workspace| workspace.project.read(cx).is_local())
6089 })
6090 .collect()
6091}
6092
6093#[derive(Default)]
6094pub struct OpenOptions {
6095 pub visible: Option<OpenVisible>,
6096 pub focus: Option<bool>,
6097 pub open_new_workspace: Option<bool>,
6098 pub replace_window: Option<WindowHandle<Workspace>>,
6099 pub env: Option<HashMap<String, String>>,
6100}
6101
6102#[allow(clippy::type_complexity)]
6103pub fn open_paths(
6104 abs_paths: &[PathBuf],
6105 app_state: Arc<AppState>,
6106 open_options: OpenOptions,
6107 cx: &mut App,
6108) -> Task<
6109 anyhow::Result<(
6110 WindowHandle<Workspace>,
6111 Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
6112 )>,
6113> {
6114 let abs_paths = abs_paths.to_vec();
6115 let mut existing = None;
6116 let mut best_match = None;
6117 let mut open_visible = OpenVisible::All;
6118
6119 cx.spawn(async move |cx| {
6120 if open_options.open_new_workspace != Some(true) {
6121 let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
6122 let all_metadatas = futures::future::join_all(all_paths)
6123 .await
6124 .into_iter()
6125 .filter_map(|result| result.ok().flatten())
6126 .collect::<Vec<_>>();
6127
6128 cx.update(|cx| {
6129 for window in local_workspace_windows(&cx) {
6130 if let Ok(workspace) = window.read(&cx) {
6131 let m = workspace.project.read(&cx).visibility_for_paths(
6132 &abs_paths,
6133 &all_metadatas,
6134 open_options.open_new_workspace == None,
6135 cx,
6136 );
6137 if m > best_match {
6138 existing = Some(window);
6139 best_match = m;
6140 } else if best_match.is_none()
6141 && open_options.open_new_workspace == Some(false)
6142 {
6143 existing = Some(window)
6144 }
6145 }
6146 }
6147 })?;
6148
6149 if open_options.open_new_workspace.is_none() && existing.is_none() {
6150 if all_metadatas.iter().all(|file| !file.is_dir) {
6151 cx.update(|cx| {
6152 if let Some(window) = cx
6153 .active_window()
6154 .and_then(|window| window.downcast::<Workspace>())
6155 {
6156 if let Ok(workspace) = window.read(cx) {
6157 let project = workspace.project().read(cx);
6158 if project.is_local() && !project.is_via_collab() {
6159 existing = Some(window);
6160 open_visible = OpenVisible::None;
6161 return;
6162 }
6163 }
6164 }
6165 for window in local_workspace_windows(cx) {
6166 if let Ok(workspace) = window.read(cx) {
6167 let project = workspace.project().read(cx);
6168 if project.is_via_collab() {
6169 continue;
6170 }
6171 existing = Some(window);
6172 open_visible = OpenVisible::None;
6173 break;
6174 }
6175 }
6176 })?;
6177 }
6178 }
6179 }
6180
6181 if let Some(existing) = existing {
6182 let open_task = existing
6183 .update(cx, |workspace, window, cx| {
6184 window.activate_window();
6185 workspace.open_paths(
6186 abs_paths,
6187 OpenOptions {
6188 visible: Some(open_visible),
6189 ..Default::default()
6190 },
6191 None,
6192 window,
6193 cx,
6194 )
6195 })?
6196 .await;
6197
6198 _ = existing.update(cx, |workspace, _, cx| {
6199 for item in open_task.iter().flatten() {
6200 if let Err(e) = item {
6201 workspace.show_error(&e, cx);
6202 }
6203 }
6204 });
6205
6206 Ok((existing, open_task))
6207 } else {
6208 cx.update(move |cx| {
6209 Workspace::new_local(
6210 abs_paths,
6211 app_state.clone(),
6212 open_options.replace_window,
6213 open_options.env,
6214 cx,
6215 )
6216 })?
6217 .await
6218 }
6219 })
6220}
6221
6222pub fn open_new(
6223 open_options: OpenOptions,
6224 app_state: Arc<AppState>,
6225 cx: &mut App,
6226 init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6227) -> Task<anyhow::Result<()>> {
6228 let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6229 cx.spawn(async move |cx| {
6230 let (workspace, opened_paths) = task.await?;
6231 workspace.update(cx, |workspace, window, cx| {
6232 if opened_paths.is_empty() {
6233 init(workspace, window, cx)
6234 }
6235 })?;
6236 Ok(())
6237 })
6238}
6239
6240pub fn create_and_open_local_file(
6241 path: &'static Path,
6242 window: &mut Window,
6243 cx: &mut Context<Workspace>,
6244 default_content: impl 'static + Send + FnOnce() -> Rope,
6245) -> Task<Result<Box<dyn ItemHandle>>> {
6246 cx.spawn_in(window, async move |workspace, cx| {
6247 let fs = workspace.update(cx, |workspace, _| workspace.app_state().fs.clone())?;
6248 if !fs.is_file(path).await {
6249 fs.create_file(path, Default::default()).await?;
6250 fs.save(path, &default_content(), Default::default())
6251 .await?;
6252 }
6253
6254 let mut items = workspace
6255 .update_in(cx, |workspace, window, cx| {
6256 workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6257 workspace.open_paths(
6258 vec![path.to_path_buf()],
6259 OpenOptions {
6260 visible: Some(OpenVisible::None),
6261 ..Default::default()
6262 },
6263 None,
6264 window,
6265 cx,
6266 )
6267 })
6268 })?
6269 .await?
6270 .await;
6271
6272 let item = items.pop().flatten();
6273 item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
6274 })
6275}
6276
6277pub fn open_ssh_project_with_new_connection(
6278 window: WindowHandle<Workspace>,
6279 connection_options: SshConnectionOptions,
6280 cancel_rx: oneshot::Receiver<()>,
6281 delegate: Arc<dyn SshClientDelegate>,
6282 app_state: Arc<AppState>,
6283 paths: Vec<PathBuf>,
6284 cx: &mut App,
6285) -> Task<Result<()>> {
6286 cx.spawn(async move |cx| {
6287 let (serialized_ssh_project, workspace_id, serialized_workspace) =
6288 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6289
6290 let session = match cx
6291 .update(|cx| {
6292 remote::SshRemoteClient::new(
6293 ConnectionIdentifier::Workspace(workspace_id.0),
6294 connection_options,
6295 cancel_rx,
6296 delegate,
6297 cx,
6298 )
6299 })?
6300 .await?
6301 {
6302 Some(result) => result,
6303 None => return Ok(()),
6304 };
6305
6306 let project = cx.update(|cx| {
6307 project::Project::ssh(
6308 session,
6309 app_state.client.clone(),
6310 app_state.node_runtime.clone(),
6311 app_state.user_store.clone(),
6312 app_state.languages.clone(),
6313 app_state.fs.clone(),
6314 cx,
6315 )
6316 })?;
6317
6318 open_ssh_project_inner(
6319 project,
6320 paths,
6321 serialized_ssh_project,
6322 workspace_id,
6323 serialized_workspace,
6324 app_state,
6325 window,
6326 cx,
6327 )
6328 .await
6329 })
6330}
6331
6332pub fn open_ssh_project_with_existing_connection(
6333 connection_options: SshConnectionOptions,
6334 project: Entity<Project>,
6335 paths: Vec<PathBuf>,
6336 app_state: Arc<AppState>,
6337 window: WindowHandle<Workspace>,
6338 cx: &mut AsyncApp,
6339) -> Task<Result<()>> {
6340 cx.spawn(async move |cx| {
6341 let (serialized_ssh_project, workspace_id, serialized_workspace) =
6342 serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6343
6344 open_ssh_project_inner(
6345 project,
6346 paths,
6347 serialized_ssh_project,
6348 workspace_id,
6349 serialized_workspace,
6350 app_state,
6351 window,
6352 cx,
6353 )
6354 .await
6355 })
6356}
6357
6358async fn open_ssh_project_inner(
6359 project: Entity<Project>,
6360 paths: Vec<PathBuf>,
6361 serialized_ssh_project: SerializedSshProject,
6362 workspace_id: WorkspaceId,
6363 serialized_workspace: Option<SerializedWorkspace>,
6364 app_state: Arc<AppState>,
6365 window: WindowHandle<Workspace>,
6366 cx: &mut AsyncApp,
6367) -> Result<()> {
6368 let toolchains = DB.toolchains(workspace_id).await?;
6369 for (toolchain, worktree_id, path) in toolchains {
6370 project
6371 .update(cx, |this, cx| {
6372 this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
6373 })?
6374 .await;
6375 }
6376 let mut project_paths_to_open = vec![];
6377 let mut project_path_errors = vec![];
6378
6379 for path in paths {
6380 let result = cx
6381 .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6382 .await;
6383 match result {
6384 Ok((_, project_path)) => {
6385 project_paths_to_open.push((path.clone(), Some(project_path)));
6386 }
6387 Err(error) => {
6388 project_path_errors.push(error);
6389 }
6390 };
6391 }
6392
6393 if project_paths_to_open.is_empty() {
6394 return Err(project_path_errors
6395 .pop()
6396 .unwrap_or_else(|| anyhow!("no paths given")));
6397 }
6398
6399 cx.update_window(window.into(), |_, window, cx| {
6400 window.replace_root(cx, |window, cx| {
6401 telemetry::event!("SSH Project Opened");
6402
6403 let mut workspace =
6404 Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
6405 workspace.set_serialized_ssh_project(serialized_ssh_project);
6406 workspace
6407 });
6408 })?;
6409
6410 window
6411 .update(cx, |_, window, cx| {
6412 window.activate_window();
6413 open_items(serialized_workspace, project_paths_to_open, window, cx)
6414 })?
6415 .await?;
6416
6417 window.update(cx, |workspace, _, cx| {
6418 for error in project_path_errors {
6419 if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
6420 if let Some(path) = error.error_tag("path") {
6421 workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
6422 }
6423 } else {
6424 workspace.show_error(&error, cx)
6425 }
6426 }
6427 })?;
6428
6429 Ok(())
6430}
6431
6432fn serialize_ssh_project(
6433 connection_options: SshConnectionOptions,
6434 paths: Vec<PathBuf>,
6435 cx: &AsyncApp,
6436) -> Task<
6437 Result<(
6438 SerializedSshProject,
6439 WorkspaceId,
6440 Option<SerializedWorkspace>,
6441 )>,
6442> {
6443 cx.background_spawn(async move {
6444 let serialized_ssh_project = persistence::DB
6445 .get_or_create_ssh_project(
6446 connection_options.host.clone(),
6447 connection_options.port,
6448 paths
6449 .iter()
6450 .map(|path| path.to_string_lossy().to_string())
6451 .collect::<Vec<_>>(),
6452 connection_options.username.clone(),
6453 )
6454 .await?;
6455
6456 let serialized_workspace =
6457 persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
6458
6459 let workspace_id = if let Some(workspace_id) =
6460 serialized_workspace.as_ref().map(|workspace| workspace.id)
6461 {
6462 workspace_id
6463 } else {
6464 persistence::DB.next_id().await?
6465 };
6466
6467 Ok((serialized_ssh_project, workspace_id, serialized_workspace))
6468 })
6469}
6470
6471pub fn join_in_room_project(
6472 project_id: u64,
6473 follow_user_id: u64,
6474 app_state: Arc<AppState>,
6475 cx: &mut App,
6476) -> Task<Result<()>> {
6477 let windows = cx.windows();
6478 cx.spawn(async move |cx| {
6479 let existing_workspace = windows.into_iter().find_map(|window_handle| {
6480 window_handle
6481 .downcast::<Workspace>()
6482 .and_then(|window_handle| {
6483 window_handle
6484 .update(cx, |workspace, _window, cx| {
6485 if workspace.project().read(cx).remote_id() == Some(project_id) {
6486 Some(window_handle)
6487 } else {
6488 None
6489 }
6490 })
6491 .unwrap_or(None)
6492 })
6493 });
6494
6495 let workspace = if let Some(existing_workspace) = existing_workspace {
6496 existing_workspace
6497 } else {
6498 let active_call = cx.update(|cx| ActiveCall::global(cx))?;
6499 let room = active_call
6500 .read_with(cx, |call, _| call.room().cloned())?
6501 .ok_or_else(|| anyhow!("not in a call"))?;
6502 let project = room
6503 .update(cx, |room, cx| {
6504 room.join_project(
6505 project_id,
6506 app_state.languages.clone(),
6507 app_state.fs.clone(),
6508 cx,
6509 )
6510 })?
6511 .await?;
6512
6513 let window_bounds_override = window_bounds_env_override();
6514 cx.update(|cx| {
6515 let mut options = (app_state.build_window_options)(None, cx);
6516 options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
6517 cx.open_window(options, |window, cx| {
6518 cx.new(|cx| {
6519 Workspace::new(Default::default(), project, app_state.clone(), window, cx)
6520 })
6521 })
6522 })??
6523 };
6524
6525 workspace.update(cx, |workspace, window, cx| {
6526 cx.activate(true);
6527 window.activate_window();
6528
6529 if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
6530 let follow_peer_id = room
6531 .read(cx)
6532 .remote_participants()
6533 .iter()
6534 .find(|(_, participant)| participant.user.id == follow_user_id)
6535 .map(|(_, p)| p.peer_id)
6536 .or_else(|| {
6537 // If we couldn't follow the given user, follow the host instead.
6538 let collaborator = workspace
6539 .project()
6540 .read(cx)
6541 .collaborators()
6542 .values()
6543 .find(|collaborator| collaborator.is_host)?;
6544 Some(collaborator.peer_id)
6545 });
6546
6547 if let Some(follow_peer_id) = follow_peer_id {
6548 workspace.follow(follow_peer_id, window, cx);
6549 }
6550 }
6551 })?;
6552
6553 anyhow::Ok(())
6554 })
6555}
6556
6557pub fn reload(reload: &Reload, cx: &mut App) {
6558 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
6559 let mut workspace_windows = cx
6560 .windows()
6561 .into_iter()
6562 .filter_map(|window| window.downcast::<Workspace>())
6563 .collect::<Vec<_>>();
6564
6565 // If multiple windows have unsaved changes, and need a save prompt,
6566 // prompt in the active window before switching to a different window.
6567 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
6568
6569 let mut prompt = None;
6570 if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
6571 prompt = window
6572 .update(cx, |_, window, cx| {
6573 window.prompt(
6574 PromptLevel::Info,
6575 "Are you sure you want to restart?",
6576 None,
6577 &["Restart", "Cancel"],
6578 cx,
6579 )
6580 })
6581 .ok();
6582 }
6583
6584 let binary_path = reload.binary_path.clone();
6585 cx.spawn(async move |cx| {
6586 if let Some(prompt) = prompt {
6587 let answer = prompt.await?;
6588 if answer != 0 {
6589 return Ok(());
6590 }
6591 }
6592
6593 // If the user cancels any save prompt, then keep the app open.
6594 for window in workspace_windows {
6595 if let Ok(should_close) = window.update(cx, |workspace, window, cx| {
6596 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
6597 }) {
6598 if !should_close.await? {
6599 return Ok(());
6600 }
6601 }
6602 }
6603
6604 cx.update(|cx| cx.restart(binary_path))
6605 })
6606 .detach_and_log_err(cx);
6607}
6608
6609fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
6610 let mut parts = value.split(',');
6611 let x: usize = parts.next()?.parse().ok()?;
6612 let y: usize = parts.next()?.parse().ok()?;
6613 Some(point(px(x as f32), px(y as f32)))
6614}
6615
6616fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
6617 let mut parts = value.split(',');
6618 let width: usize = parts.next()?.parse().ok()?;
6619 let height: usize = parts.next()?.parse().ok()?;
6620 Some(size(px(width as f32), px(height as f32)))
6621}
6622
6623pub fn client_side_decorations(
6624 element: impl IntoElement,
6625 window: &mut Window,
6626 cx: &mut App,
6627) -> Stateful<Div> {
6628 const BORDER_SIZE: Pixels = px(1.0);
6629 let decorations = window.window_decorations();
6630
6631 if matches!(decorations, Decorations::Client { .. }) {
6632 window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
6633 }
6634
6635 struct GlobalResizeEdge(ResizeEdge);
6636 impl Global for GlobalResizeEdge {}
6637
6638 div()
6639 .id("window-backdrop")
6640 .bg(transparent_black())
6641 .map(|div| match decorations {
6642 Decorations::Server => div,
6643 Decorations::Client { tiling, .. } => div
6644 .when(!(tiling.top || tiling.right), |div| {
6645 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6646 })
6647 .when(!(tiling.top || tiling.left), |div| {
6648 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6649 })
6650 .when(!(tiling.bottom || tiling.right), |div| {
6651 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6652 })
6653 .when(!(tiling.bottom || tiling.left), |div| {
6654 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6655 })
6656 .when(!tiling.top, |div| {
6657 div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
6658 })
6659 .when(!tiling.bottom, |div| {
6660 div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
6661 })
6662 .when(!tiling.left, |div| {
6663 div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
6664 })
6665 .when(!tiling.right, |div| {
6666 div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
6667 })
6668 .on_mouse_move(move |e, window, cx| {
6669 let size = window.window_bounds().get_bounds().size;
6670 let pos = e.position;
6671
6672 let new_edge =
6673 resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
6674
6675 let edge = cx.try_global::<GlobalResizeEdge>();
6676 if new_edge != edge.map(|edge| edge.0) {
6677 window
6678 .window_handle()
6679 .update(cx, |workspace, _, cx| {
6680 cx.notify(workspace.entity_id());
6681 })
6682 .ok();
6683 }
6684 })
6685 .on_mouse_down(MouseButton::Left, move |e, window, _| {
6686 let size = window.window_bounds().get_bounds().size;
6687 let pos = e.position;
6688
6689 let edge = match resize_edge(
6690 pos,
6691 theme::CLIENT_SIDE_DECORATION_SHADOW,
6692 size,
6693 tiling,
6694 ) {
6695 Some(value) => value,
6696 None => return,
6697 };
6698
6699 window.start_window_resize(edge);
6700 }),
6701 })
6702 .size_full()
6703 .child(
6704 div()
6705 .cursor(CursorStyle::Arrow)
6706 .map(|div| match decorations {
6707 Decorations::Server => div,
6708 Decorations::Client { tiling } => div
6709 .border_color(cx.theme().colors().border)
6710 .when(!(tiling.top || tiling.right), |div| {
6711 div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6712 })
6713 .when(!(tiling.top || tiling.left), |div| {
6714 div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6715 })
6716 .when(!(tiling.bottom || tiling.right), |div| {
6717 div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6718 })
6719 .when(!(tiling.bottom || tiling.left), |div| {
6720 div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6721 })
6722 .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6723 .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6724 .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6725 .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6726 .when(!tiling.is_tiled(), |div| {
6727 div.shadow(smallvec::smallvec![gpui::BoxShadow {
6728 color: Hsla {
6729 h: 0.,
6730 s: 0.,
6731 l: 0.,
6732 a: 0.4,
6733 },
6734 blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6735 spread_radius: px(0.),
6736 offset: point(px(0.0), px(0.0)),
6737 }])
6738 }),
6739 })
6740 .on_mouse_move(|_e, _, cx| {
6741 cx.stop_propagation();
6742 })
6743 .size_full()
6744 .child(element),
6745 )
6746 .map(|div| match decorations {
6747 Decorations::Server => div,
6748 Decorations::Client { tiling, .. } => div.child(
6749 canvas(
6750 |_bounds, window, _| {
6751 window.insert_hitbox(
6752 Bounds::new(
6753 point(px(0.0), px(0.0)),
6754 window.window_bounds().get_bounds().size,
6755 ),
6756 false,
6757 )
6758 },
6759 move |_bounds, hitbox, window, cx| {
6760 let mouse = window.mouse_position();
6761 let size = window.window_bounds().get_bounds().size;
6762 let Some(edge) =
6763 resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6764 else {
6765 return;
6766 };
6767 cx.set_global(GlobalResizeEdge(edge));
6768 window.set_cursor_style(
6769 match edge {
6770 ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6771 ResizeEdge::Left | ResizeEdge::Right => {
6772 CursorStyle::ResizeLeftRight
6773 }
6774 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6775 CursorStyle::ResizeUpLeftDownRight
6776 }
6777 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6778 CursorStyle::ResizeUpRightDownLeft
6779 }
6780 },
6781 Some(&hitbox),
6782 );
6783 },
6784 )
6785 .size_full()
6786 .absolute(),
6787 ),
6788 })
6789}
6790
6791fn resize_edge(
6792 pos: Point<Pixels>,
6793 shadow_size: Pixels,
6794 window_size: Size<Pixels>,
6795 tiling: Tiling,
6796) -> Option<ResizeEdge> {
6797 let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6798 if bounds.contains(&pos) {
6799 return None;
6800 }
6801
6802 let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6803 let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6804 if !tiling.top && top_left_bounds.contains(&pos) {
6805 return Some(ResizeEdge::TopLeft);
6806 }
6807
6808 let top_right_bounds = Bounds::new(
6809 Point::new(window_size.width - corner_size.width, px(0.)),
6810 corner_size,
6811 );
6812 if !tiling.top && top_right_bounds.contains(&pos) {
6813 return Some(ResizeEdge::TopRight);
6814 }
6815
6816 let bottom_left_bounds = Bounds::new(
6817 Point::new(px(0.), window_size.height - corner_size.height),
6818 corner_size,
6819 );
6820 if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6821 return Some(ResizeEdge::BottomLeft);
6822 }
6823
6824 let bottom_right_bounds = Bounds::new(
6825 Point::new(
6826 window_size.width - corner_size.width,
6827 window_size.height - corner_size.height,
6828 ),
6829 corner_size,
6830 );
6831 if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6832 return Some(ResizeEdge::BottomRight);
6833 }
6834
6835 if !tiling.top && pos.y < shadow_size {
6836 Some(ResizeEdge::Top)
6837 } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6838 Some(ResizeEdge::Bottom)
6839 } else if !tiling.left && pos.x < shadow_size {
6840 Some(ResizeEdge::Left)
6841 } else if !tiling.right && pos.x > window_size.width - shadow_size {
6842 Some(ResizeEdge::Right)
6843 } else {
6844 None
6845 }
6846}
6847
6848fn join_pane_into_active(
6849 active_pane: &Entity<Pane>,
6850 pane: &Entity<Pane>,
6851 window: &mut Window,
6852 cx: &mut App,
6853) {
6854 if pane == active_pane {
6855 return;
6856 } else if pane.read(cx).items_len() == 0 {
6857 pane.update(cx, |_, cx| {
6858 cx.emit(pane::Event::Remove {
6859 focus_on_pane: None,
6860 });
6861 })
6862 } else {
6863 move_all_items(pane, active_pane, window, cx);
6864 }
6865}
6866
6867fn move_all_items(
6868 from_pane: &Entity<Pane>,
6869 to_pane: &Entity<Pane>,
6870 window: &mut Window,
6871 cx: &mut App,
6872) {
6873 let destination_is_different = from_pane != to_pane;
6874 let mut moved_items = 0;
6875 for (item_ix, item_handle) in from_pane
6876 .read(cx)
6877 .items()
6878 .enumerate()
6879 .map(|(ix, item)| (ix, item.clone()))
6880 .collect::<Vec<_>>()
6881 {
6882 let ix = item_ix - moved_items;
6883 if destination_is_different {
6884 // Close item from previous pane
6885 from_pane.update(cx, |source, cx| {
6886 source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
6887 });
6888 moved_items += 1;
6889 }
6890
6891 // This automatically removes duplicate items in the pane
6892 to_pane.update(cx, |destination, cx| {
6893 destination.add_item(item_handle, true, true, None, window, cx);
6894 window.focus(&destination.focus_handle(cx))
6895 });
6896 }
6897}
6898
6899pub fn move_item(
6900 source: &Entity<Pane>,
6901 destination: &Entity<Pane>,
6902 item_id_to_move: EntityId,
6903 destination_index: usize,
6904 window: &mut Window,
6905 cx: &mut App,
6906) {
6907 let Some((item_ix, item_handle)) = source
6908 .read(cx)
6909 .items()
6910 .enumerate()
6911 .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6912 .map(|(ix, item)| (ix, item.clone()))
6913 else {
6914 // Tab was closed during drag
6915 return;
6916 };
6917
6918 if source != destination {
6919 // Close item from previous pane
6920 source.update(cx, |source, cx| {
6921 source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
6922 });
6923 }
6924
6925 // This automatically removes duplicate items in the pane
6926 destination.update(cx, |destination, cx| {
6927 destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
6928 window.focus(&destination.focus_handle(cx))
6929 });
6930}
6931
6932pub fn move_active_item(
6933 source: &Entity<Pane>,
6934 destination: &Entity<Pane>,
6935 focus_destination: bool,
6936 close_if_empty: bool,
6937 window: &mut Window,
6938 cx: &mut App,
6939) {
6940 if source == destination {
6941 return;
6942 }
6943 let Some(active_item) = source.read(cx).active_item() else {
6944 return;
6945 };
6946 source.update(cx, |source_pane, cx| {
6947 let item_id = active_item.item_id();
6948 source_pane.remove_item(item_id, false, close_if_empty, window, cx);
6949 destination.update(cx, |target_pane, cx| {
6950 target_pane.add_item(
6951 active_item,
6952 focus_destination,
6953 focus_destination,
6954 Some(target_pane.items_len()),
6955 window,
6956 cx,
6957 );
6958 });
6959 });
6960}
6961
6962#[cfg(test)]
6963mod tests {
6964 use std::{cell::RefCell, rc::Rc};
6965
6966 use super::*;
6967 use crate::{
6968 dock::{PanelEvent, test::TestPanel},
6969 item::{
6970 ItemEvent,
6971 test::{TestItem, TestProjectItem},
6972 },
6973 };
6974 use fs::FakeFs;
6975 use gpui::{
6976 DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
6977 UpdateGlobal, VisualTestContext, px,
6978 };
6979 use project::{Project, ProjectEntryId};
6980 use serde_json::json;
6981 use settings::SettingsStore;
6982
6983 #[gpui::test]
6984 async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6985 init_test(cx);
6986
6987 let fs = FakeFs::new(cx.executor());
6988 let project = Project::test(fs, [], cx).await;
6989 let (workspace, cx) =
6990 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6991
6992 // Adding an item with no ambiguity renders the tab without detail.
6993 let item1 = cx.new(|cx| {
6994 let mut item = TestItem::new(cx);
6995 item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6996 item
6997 });
6998 workspace.update_in(cx, |workspace, window, cx| {
6999 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7000 });
7001 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
7002
7003 // Adding an item that creates ambiguity increases the level of detail on
7004 // both tabs.
7005 let item2 = cx.new_window_entity(|_window, cx| {
7006 let mut item = TestItem::new(cx);
7007 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
7008 item
7009 });
7010 workspace.update_in(cx, |workspace, window, cx| {
7011 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7012 });
7013 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7014 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7015
7016 // Adding an item that creates ambiguity increases the level of detail only
7017 // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
7018 // we stop at the highest detail available.
7019 let item3 = cx.new(|cx| {
7020 let mut item = TestItem::new(cx);
7021 item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
7022 item
7023 });
7024 workspace.update_in(cx, |workspace, window, cx| {
7025 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7026 });
7027 item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
7028 item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
7029 item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
7030 }
7031
7032 #[gpui::test]
7033 async fn test_tracking_active_path(cx: &mut TestAppContext) {
7034 init_test(cx);
7035
7036 let fs = FakeFs::new(cx.executor());
7037 fs.insert_tree(
7038 "/root1",
7039 json!({
7040 "one.txt": "",
7041 "two.txt": "",
7042 }),
7043 )
7044 .await;
7045 fs.insert_tree(
7046 "/root2",
7047 json!({
7048 "three.txt": "",
7049 }),
7050 )
7051 .await;
7052
7053 let project = Project::test(fs, ["root1".as_ref()], cx).await;
7054 let (workspace, cx) =
7055 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7056 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7057 let worktree_id = project.update(cx, |project, cx| {
7058 project.worktrees(cx).next().unwrap().read(cx).id()
7059 });
7060
7061 let item1 = cx.new(|cx| {
7062 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
7063 });
7064 let item2 = cx.new(|cx| {
7065 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
7066 });
7067
7068 // Add an item to an empty pane
7069 workspace.update_in(cx, |workspace, window, cx| {
7070 workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
7071 });
7072 project.update(cx, |project, cx| {
7073 assert_eq!(
7074 project.active_entry(),
7075 project
7076 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7077 .map(|e| e.id)
7078 );
7079 });
7080 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7081
7082 // Add a second item to a non-empty pane
7083 workspace.update_in(cx, |workspace, window, cx| {
7084 workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
7085 });
7086 assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
7087 project.update(cx, |project, cx| {
7088 assert_eq!(
7089 project.active_entry(),
7090 project
7091 .entry_for_path(&(worktree_id, "two.txt").into(), cx)
7092 .map(|e| e.id)
7093 );
7094 });
7095
7096 // Close the active item
7097 pane.update_in(cx, |pane, window, cx| {
7098 pane.close_active_item(&Default::default(), window, cx)
7099 .unwrap()
7100 })
7101 .await
7102 .unwrap();
7103 assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
7104 project.update(cx, |project, cx| {
7105 assert_eq!(
7106 project.active_entry(),
7107 project
7108 .entry_for_path(&(worktree_id, "one.txt").into(), cx)
7109 .map(|e| e.id)
7110 );
7111 });
7112
7113 // Add a project folder
7114 project
7115 .update(cx, |project, cx| {
7116 project.find_or_create_worktree("root2", true, cx)
7117 })
7118 .await
7119 .unwrap();
7120 assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
7121
7122 // Remove a project folder
7123 project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
7124 assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
7125 }
7126
7127 #[gpui::test]
7128 async fn test_close_window(cx: &mut TestAppContext) {
7129 init_test(cx);
7130
7131 let fs = FakeFs::new(cx.executor());
7132 fs.insert_tree("/root", json!({ "one": "" })).await;
7133
7134 let project = Project::test(fs, ["root".as_ref()], cx).await;
7135 let (workspace, cx) =
7136 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7137
7138 // When there are no dirty items, there's nothing to do.
7139 let item1 = cx.new(TestItem::new);
7140 workspace.update_in(cx, |w, window, cx| {
7141 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
7142 });
7143 let task = workspace.update_in(cx, |w, window, cx| {
7144 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7145 });
7146 assert!(task.await.unwrap());
7147
7148 // When there are dirty untitled items, prompt to save each one. If the user
7149 // cancels any prompt, then abort.
7150 let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
7151 let item3 = cx.new(|cx| {
7152 TestItem::new(cx)
7153 .with_dirty(true)
7154 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7155 });
7156 workspace.update_in(cx, |w, window, cx| {
7157 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7158 w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7159 });
7160 let task = workspace.update_in(cx, |w, window, cx| {
7161 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7162 });
7163 cx.executor().run_until_parked();
7164 cx.simulate_prompt_answer("Cancel"); // cancel save all
7165 cx.executor().run_until_parked();
7166 assert!(!cx.has_pending_prompt());
7167 assert!(!task.await.unwrap());
7168 }
7169
7170 #[gpui::test]
7171 async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
7172 init_test(cx);
7173
7174 // Register TestItem as a serializable item
7175 cx.update(|cx| {
7176 register_serializable_item::<TestItem>(cx);
7177 });
7178
7179 let fs = FakeFs::new(cx.executor());
7180 fs.insert_tree("/root", json!({ "one": "" })).await;
7181
7182 let project = Project::test(fs, ["root".as_ref()], cx).await;
7183 let (workspace, cx) =
7184 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7185
7186 // When there are dirty untitled items, but they can serialize, then there is no prompt.
7187 let item1 = cx.new(|cx| {
7188 TestItem::new(cx)
7189 .with_dirty(true)
7190 .with_serialize(|| Some(Task::ready(Ok(()))))
7191 });
7192 let item2 = cx.new(|cx| {
7193 TestItem::new(cx)
7194 .with_dirty(true)
7195 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7196 .with_serialize(|| Some(Task::ready(Ok(()))))
7197 });
7198 workspace.update_in(cx, |w, window, cx| {
7199 w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7200 w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7201 });
7202 let task = workspace.update_in(cx, |w, window, cx| {
7203 w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7204 });
7205 assert!(task.await.unwrap());
7206 }
7207
7208 #[gpui::test]
7209 async fn test_close_pane_items(cx: &mut TestAppContext) {
7210 init_test(cx);
7211
7212 let fs = FakeFs::new(cx.executor());
7213
7214 let project = Project::test(fs, None, cx).await;
7215 let (workspace, cx) =
7216 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7217
7218 let item1 = cx.new(|cx| {
7219 TestItem::new(cx)
7220 .with_dirty(true)
7221 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7222 });
7223 let item2 = cx.new(|cx| {
7224 TestItem::new(cx)
7225 .with_dirty(true)
7226 .with_conflict(true)
7227 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7228 });
7229 let item3 = cx.new(|cx| {
7230 TestItem::new(cx)
7231 .with_dirty(true)
7232 .with_conflict(true)
7233 .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
7234 });
7235 let item4 = cx.new(|cx| {
7236 TestItem::new(cx).with_dirty(true).with_project_items(&[{
7237 let project_item = TestProjectItem::new_untitled(cx);
7238 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7239 project_item
7240 }])
7241 });
7242 let pane = workspace.update_in(cx, |workspace, window, cx| {
7243 workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7244 workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7245 workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7246 workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
7247 workspace.active_pane().clone()
7248 });
7249
7250 let close_items = pane.update_in(cx, |pane, window, cx| {
7251 pane.activate_item(1, true, true, window, cx);
7252 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7253 let item1_id = item1.item_id();
7254 let item3_id = item3.item_id();
7255 let item4_id = item4.item_id();
7256 pane.close_items(window, cx, SaveIntent::Close, move |id| {
7257 [item1_id, item3_id, item4_id].contains(&id)
7258 })
7259 });
7260 cx.executor().run_until_parked();
7261
7262 assert!(cx.has_pending_prompt());
7263 cx.simulate_prompt_answer("Save all");
7264
7265 cx.executor().run_until_parked();
7266
7267 // Item 1 is saved. There's a prompt to save item 3.
7268 pane.update(cx, |pane, cx| {
7269 assert_eq!(item1.read(cx).save_count, 1);
7270 assert_eq!(item1.read(cx).save_as_count, 0);
7271 assert_eq!(item1.read(cx).reload_count, 0);
7272 assert_eq!(pane.items_len(), 3);
7273 assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7274 });
7275 assert!(cx.has_pending_prompt());
7276
7277 // Cancel saving item 3.
7278 cx.simulate_prompt_answer("Discard");
7279 cx.executor().run_until_parked();
7280
7281 // Item 3 is reloaded. There's a prompt to save item 4.
7282 pane.update(cx, |pane, cx| {
7283 assert_eq!(item3.read(cx).save_count, 0);
7284 assert_eq!(item3.read(cx).save_as_count, 0);
7285 assert_eq!(item3.read(cx).reload_count, 1);
7286 assert_eq!(pane.items_len(), 2);
7287 assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7288 });
7289
7290 // There's a prompt for a path for item 4.
7291 cx.simulate_new_path_selection(|_| Some(Default::default()));
7292 close_items.await.unwrap();
7293
7294 // The requested items are closed.
7295 pane.update(cx, |pane, cx| {
7296 assert_eq!(item4.read(cx).save_count, 0);
7297 assert_eq!(item4.read(cx).save_as_count, 1);
7298 assert_eq!(item4.read(cx).reload_count, 0);
7299 assert_eq!(pane.items_len(), 1);
7300 assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7301 });
7302 }
7303
7304 #[gpui::test]
7305 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7306 init_test(cx);
7307
7308 let fs = FakeFs::new(cx.executor());
7309 let project = Project::test(fs, [], cx).await;
7310 let (workspace, cx) =
7311 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7312
7313 // Create several workspace items with single project entries, and two
7314 // workspace items with multiple project entries.
7315 let single_entry_items = (0..=4)
7316 .map(|project_entry_id| {
7317 cx.new(|cx| {
7318 TestItem::new(cx)
7319 .with_dirty(true)
7320 .with_project_items(&[dirty_project_item(
7321 project_entry_id,
7322 &format!("{project_entry_id}.txt"),
7323 cx,
7324 )])
7325 })
7326 })
7327 .collect::<Vec<_>>();
7328 let item_2_3 = cx.new(|cx| {
7329 TestItem::new(cx)
7330 .with_dirty(true)
7331 .with_singleton(false)
7332 .with_project_items(&[
7333 single_entry_items[2].read(cx).project_items[0].clone(),
7334 single_entry_items[3].read(cx).project_items[0].clone(),
7335 ])
7336 });
7337 let item_3_4 = cx.new(|cx| {
7338 TestItem::new(cx)
7339 .with_dirty(true)
7340 .with_singleton(false)
7341 .with_project_items(&[
7342 single_entry_items[3].read(cx).project_items[0].clone(),
7343 single_entry_items[4].read(cx).project_items[0].clone(),
7344 ])
7345 });
7346
7347 // Create two panes that contain the following project entries:
7348 // left pane:
7349 // multi-entry items: (2, 3)
7350 // single-entry items: 0, 2, 3, 4
7351 // right pane:
7352 // single-entry items: 4, 1
7353 // multi-entry items: (3, 4)
7354 let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
7355 let left_pane = workspace.active_pane().clone();
7356 workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
7357 workspace.add_item_to_active_pane(
7358 single_entry_items[0].boxed_clone(),
7359 None,
7360 true,
7361 window,
7362 cx,
7363 );
7364 workspace.add_item_to_active_pane(
7365 single_entry_items[2].boxed_clone(),
7366 None,
7367 true,
7368 window,
7369 cx,
7370 );
7371 workspace.add_item_to_active_pane(
7372 single_entry_items[3].boxed_clone(),
7373 None,
7374 true,
7375 window,
7376 cx,
7377 );
7378 workspace.add_item_to_active_pane(
7379 single_entry_items[4].boxed_clone(),
7380 None,
7381 true,
7382 window,
7383 cx,
7384 );
7385
7386 let right_pane = workspace
7387 .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
7388 .unwrap();
7389
7390 right_pane.update(cx, |pane, cx| {
7391 pane.add_item(
7392 single_entry_items[1].boxed_clone(),
7393 true,
7394 true,
7395 None,
7396 window,
7397 cx,
7398 );
7399 pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
7400 });
7401
7402 (left_pane, right_pane)
7403 });
7404
7405 cx.focus(&right_pane);
7406
7407 let mut close = right_pane.update_in(cx, |pane, window, cx| {
7408 pane.close_all_items(&CloseAllItems::default(), window, cx)
7409 .unwrap()
7410 });
7411 cx.executor().run_until_parked();
7412
7413 let msg = cx.pending_prompt().unwrap().0;
7414 assert!(msg.contains("1.txt"));
7415 assert!(!msg.contains("2.txt"));
7416 assert!(!msg.contains("3.txt"));
7417 assert!(!msg.contains("4.txt"));
7418
7419 cx.simulate_prompt_answer("Cancel");
7420 close.await.unwrap();
7421
7422 left_pane
7423 .update_in(cx, |left_pane, window, cx| {
7424 left_pane.close_item_by_id(
7425 single_entry_items[3].entity_id(),
7426 SaveIntent::Skip,
7427 window,
7428 cx,
7429 )
7430 })
7431 .await
7432 .unwrap();
7433
7434 close = right_pane.update_in(cx, |pane, window, cx| {
7435 pane.close_all_items(&CloseAllItems::default(), window, cx)
7436 .unwrap()
7437 });
7438 cx.executor().run_until_parked();
7439
7440 let details = cx.pending_prompt().unwrap().1;
7441 assert!(details.contains("1.txt"));
7442 assert!(!details.contains("2.txt"));
7443 assert!(details.contains("3.txt"));
7444 // ideally this assertion could be made, but today we can only
7445 // save whole items not project items, so the orphaned item 3 causes
7446 // 4 to be saved too.
7447 // assert!(!details.contains("4.txt"));
7448
7449 cx.simulate_prompt_answer("Save all");
7450
7451 cx.executor().run_until_parked();
7452 close.await.unwrap();
7453 right_pane.update(cx, |pane, _| {
7454 assert_eq!(pane.items_len(), 0);
7455 });
7456 }
7457
7458 #[gpui::test]
7459 async fn test_autosave(cx: &mut gpui::TestAppContext) {
7460 init_test(cx);
7461
7462 let fs = FakeFs::new(cx.executor());
7463 let project = Project::test(fs, [], cx).await;
7464 let (workspace, cx) =
7465 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7466 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7467
7468 let item = cx.new(|cx| {
7469 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7470 });
7471 let item_id = item.entity_id();
7472 workspace.update_in(cx, |workspace, window, cx| {
7473 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7474 });
7475
7476 // Autosave on window change.
7477 item.update(cx, |item, cx| {
7478 SettingsStore::update_global(cx, |settings, cx| {
7479 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7480 settings.autosave = Some(AutosaveSetting::OnWindowChange);
7481 })
7482 });
7483 item.is_dirty = true;
7484 });
7485
7486 // Deactivating the window saves the file.
7487 cx.deactivate_window();
7488 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7489
7490 // Re-activating the window doesn't save the file.
7491 cx.update(|window, _| window.activate_window());
7492 cx.executor().run_until_parked();
7493 item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7494
7495 // Autosave on focus change.
7496 item.update_in(cx, |item, window, cx| {
7497 cx.focus_self(window);
7498 SettingsStore::update_global(cx, |settings, cx| {
7499 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7500 settings.autosave = Some(AutosaveSetting::OnFocusChange);
7501 })
7502 });
7503 item.is_dirty = true;
7504 });
7505
7506 // Blurring the item saves the file.
7507 item.update_in(cx, |_, window, _| window.blur());
7508 cx.executor().run_until_parked();
7509 item.update(cx, |item, _| assert_eq!(item.save_count, 2));
7510
7511 // Deactivating the window still saves the file.
7512 item.update_in(cx, |item, window, cx| {
7513 cx.focus_self(window);
7514 item.is_dirty = true;
7515 });
7516 cx.deactivate_window();
7517 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7518
7519 // Autosave after delay.
7520 item.update(cx, |item, cx| {
7521 SettingsStore::update_global(cx, |settings, cx| {
7522 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7523 settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
7524 })
7525 });
7526 item.is_dirty = true;
7527 cx.emit(ItemEvent::Edit);
7528 });
7529
7530 // Delay hasn't fully expired, so the file is still dirty and unsaved.
7531 cx.executor().advance_clock(Duration::from_millis(250));
7532 item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7533
7534 // After delay expires, the file is saved.
7535 cx.executor().advance_clock(Duration::from_millis(250));
7536 item.update(cx, |item, _| assert_eq!(item.save_count, 4));
7537
7538 // Autosave on focus change, ensuring closing the tab counts as such.
7539 item.update(cx, |item, cx| {
7540 SettingsStore::update_global(cx, |settings, cx| {
7541 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7542 settings.autosave = Some(AutosaveSetting::OnFocusChange);
7543 })
7544 });
7545 item.is_dirty = true;
7546 for project_item in &mut item.project_items {
7547 project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7548 }
7549 });
7550
7551 pane.update_in(cx, |pane, window, cx| {
7552 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7553 })
7554 .await
7555 .unwrap();
7556 assert!(!cx.has_pending_prompt());
7557 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7558
7559 // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
7560 workspace.update_in(cx, |workspace, window, cx| {
7561 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7562 });
7563 item.update_in(cx, |item, window, cx| {
7564 item.project_items[0].update(cx, |item, _| {
7565 item.entry_id = None;
7566 });
7567 item.is_dirty = true;
7568 window.blur();
7569 });
7570 cx.run_until_parked();
7571 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7572
7573 // Ensure autosave is prevented for deleted files also when closing the buffer.
7574 let _close_items = pane.update_in(cx, |pane, window, cx| {
7575 pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7576 });
7577 cx.run_until_parked();
7578 assert!(cx.has_pending_prompt());
7579 item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7580 }
7581
7582 #[gpui::test]
7583 async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
7584 init_test(cx);
7585
7586 let fs = FakeFs::new(cx.executor());
7587
7588 let project = Project::test(fs, [], cx).await;
7589 let (workspace, cx) =
7590 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7591
7592 let item = cx.new(|cx| {
7593 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7594 });
7595 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7596 let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
7597 let toolbar_notify_count = Rc::new(RefCell::new(0));
7598
7599 workspace.update_in(cx, |workspace, window, cx| {
7600 workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7601 let toolbar_notification_count = toolbar_notify_count.clone();
7602 cx.observe_in(&toolbar, window, move |_, _, _, _| {
7603 *toolbar_notification_count.borrow_mut() += 1
7604 })
7605 .detach();
7606 });
7607
7608 pane.update(cx, |pane, _| {
7609 assert!(!pane.can_navigate_backward());
7610 assert!(!pane.can_navigate_forward());
7611 });
7612
7613 item.update_in(cx, |item, _, cx| {
7614 item.set_state("one".to_string(), cx);
7615 });
7616
7617 // Toolbar must be notified to re-render the navigation buttons
7618 assert_eq!(*toolbar_notify_count.borrow(), 1);
7619
7620 pane.update(cx, |pane, _| {
7621 assert!(pane.can_navigate_backward());
7622 assert!(!pane.can_navigate_forward());
7623 });
7624
7625 workspace
7626 .update_in(cx, |workspace, window, cx| {
7627 workspace.go_back(pane.downgrade(), window, cx)
7628 })
7629 .await
7630 .unwrap();
7631
7632 assert_eq!(*toolbar_notify_count.borrow(), 2);
7633 pane.update(cx, |pane, _| {
7634 assert!(!pane.can_navigate_backward());
7635 assert!(pane.can_navigate_forward());
7636 });
7637 }
7638
7639 #[gpui::test]
7640 async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
7641 init_test(cx);
7642 let fs = FakeFs::new(cx.executor());
7643
7644 let project = Project::test(fs, [], cx).await;
7645 let (workspace, cx) =
7646 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7647
7648 let panel = workspace.update_in(cx, |workspace, window, cx| {
7649 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7650 workspace.add_panel(panel.clone(), window, cx);
7651
7652 workspace
7653 .right_dock()
7654 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
7655
7656 panel
7657 });
7658
7659 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7660 pane.update_in(cx, |pane, window, cx| {
7661 let item = cx.new(TestItem::new);
7662 pane.add_item(Box::new(item), true, true, None, window, cx);
7663 });
7664
7665 // Transfer focus from center to panel
7666 workspace.update_in(cx, |workspace, window, cx| {
7667 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7668 });
7669
7670 workspace.update_in(cx, |workspace, window, cx| {
7671 assert!(workspace.right_dock().read(cx).is_open());
7672 assert!(!panel.is_zoomed(window, cx));
7673 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7674 });
7675
7676 // Transfer focus from panel to center
7677 workspace.update_in(cx, |workspace, window, cx| {
7678 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7679 });
7680
7681 workspace.update_in(cx, |workspace, window, cx| {
7682 assert!(workspace.right_dock().read(cx).is_open());
7683 assert!(!panel.is_zoomed(window, cx));
7684 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7685 });
7686
7687 // Close the dock
7688 workspace.update_in(cx, |workspace, window, cx| {
7689 workspace.toggle_dock(DockPosition::Right, window, cx);
7690 });
7691
7692 workspace.update_in(cx, |workspace, window, cx| {
7693 assert!(!workspace.right_dock().read(cx).is_open());
7694 assert!(!panel.is_zoomed(window, cx));
7695 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7696 });
7697
7698 // Open the dock
7699 workspace.update_in(cx, |workspace, window, cx| {
7700 workspace.toggle_dock(DockPosition::Right, window, cx);
7701 });
7702
7703 workspace.update_in(cx, |workspace, window, cx| {
7704 assert!(workspace.right_dock().read(cx).is_open());
7705 assert!(!panel.is_zoomed(window, cx));
7706 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7707 });
7708
7709 // Focus and zoom panel
7710 panel.update_in(cx, |panel, window, cx| {
7711 cx.focus_self(window);
7712 panel.set_zoomed(true, 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 // Transfer focus to the center closes the dock
7722 workspace.update_in(cx, |workspace, window, cx| {
7723 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7724 });
7725
7726 workspace.update_in(cx, |workspace, window, cx| {
7727 assert!(!workspace.right_dock().read(cx).is_open());
7728 assert!(panel.is_zoomed(window, cx));
7729 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7730 });
7731
7732 // Transferring focus back to the panel keeps it zoomed
7733 workspace.update_in(cx, |workspace, window, cx| {
7734 workspace.toggle_panel_focus::<TestPanel>(window, cx);
7735 });
7736
7737 workspace.update_in(cx, |workspace, window, cx| {
7738 assert!(workspace.right_dock().read(cx).is_open());
7739 assert!(panel.is_zoomed(window, cx));
7740 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7741 });
7742
7743 // Close the dock while it is zoomed
7744 workspace.update_in(cx, |workspace, window, cx| {
7745 workspace.toggle_dock(DockPosition::Right, window, cx)
7746 });
7747
7748 workspace.update_in(cx, |workspace, window, cx| {
7749 assert!(!workspace.right_dock().read(cx).is_open());
7750 assert!(panel.is_zoomed(window, cx));
7751 assert!(workspace.zoomed.is_none());
7752 assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7753 });
7754
7755 // Opening the dock, when it's zoomed, retains focus
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_some());
7764 assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7765 });
7766
7767 // Unzoom and close the panel, zoom the active pane.
7768 panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
7769 workspace.update_in(cx, |workspace, window, cx| {
7770 workspace.toggle_dock(DockPosition::Right, window, cx)
7771 });
7772 pane.update_in(cx, |pane, window, cx| {
7773 pane.toggle_zoom(&Default::default(), window, cx)
7774 });
7775
7776 // Opening a dock unzooms the pane.
7777 workspace.update_in(cx, |workspace, window, cx| {
7778 workspace.toggle_dock(DockPosition::Right, window, cx)
7779 });
7780 workspace.update_in(cx, |workspace, window, cx| {
7781 let pane = pane.read(cx);
7782 assert!(!pane.is_zoomed());
7783 assert!(!pane.focus_handle(cx).is_focused(window));
7784 assert!(workspace.right_dock().read(cx).is_open());
7785 assert!(workspace.zoomed.is_none());
7786 });
7787 }
7788
7789 #[gpui::test]
7790 async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
7791 init_test(cx);
7792
7793 let fs = FakeFs::new(cx.executor());
7794
7795 let project = Project::test(fs, None, cx).await;
7796 let (workspace, cx) =
7797 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7798
7799 // Let's arrange the panes like this:
7800 //
7801 // +-----------------------+
7802 // | top |
7803 // +------+--------+-------+
7804 // | left | center | right |
7805 // +------+--------+-------+
7806 // | bottom |
7807 // +-----------------------+
7808
7809 let top_item = cx.new(|cx| {
7810 TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
7811 });
7812 let bottom_item = cx.new(|cx| {
7813 TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7814 });
7815 let left_item = cx.new(|cx| {
7816 TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7817 });
7818 let right_item = cx.new(|cx| {
7819 TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7820 });
7821 let center_item = cx.new(|cx| {
7822 TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7823 });
7824
7825 let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7826 let top_pane_id = workspace.active_pane().entity_id();
7827 workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
7828 workspace.split_pane(
7829 workspace.active_pane().clone(),
7830 SplitDirection::Down,
7831 window,
7832 cx,
7833 );
7834 top_pane_id
7835 });
7836 let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7837 let bottom_pane_id = workspace.active_pane().entity_id();
7838 workspace.add_item_to_active_pane(
7839 Box::new(bottom_item.clone()),
7840 None,
7841 false,
7842 window,
7843 cx,
7844 );
7845 workspace.split_pane(
7846 workspace.active_pane().clone(),
7847 SplitDirection::Up,
7848 window,
7849 cx,
7850 );
7851 bottom_pane_id
7852 });
7853 let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7854 let left_pane_id = workspace.active_pane().entity_id();
7855 workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
7856 workspace.split_pane(
7857 workspace.active_pane().clone(),
7858 SplitDirection::Right,
7859 window,
7860 cx,
7861 );
7862 left_pane_id
7863 });
7864 let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7865 let right_pane_id = workspace.active_pane().entity_id();
7866 workspace.add_item_to_active_pane(
7867 Box::new(right_item.clone()),
7868 None,
7869 false,
7870 window,
7871 cx,
7872 );
7873 workspace.split_pane(
7874 workspace.active_pane().clone(),
7875 SplitDirection::Left,
7876 window,
7877 cx,
7878 );
7879 right_pane_id
7880 });
7881 let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7882 let center_pane_id = workspace.active_pane().entity_id();
7883 workspace.add_item_to_active_pane(
7884 Box::new(center_item.clone()),
7885 None,
7886 false,
7887 window,
7888 cx,
7889 );
7890 center_pane_id
7891 });
7892 cx.executor().run_until_parked();
7893
7894 workspace.update_in(cx, |workspace, window, cx| {
7895 assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7896
7897 // Join into next from center pane into right
7898 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7899 });
7900
7901 workspace.update_in(cx, |workspace, window, cx| {
7902 let active_pane = workspace.active_pane();
7903 assert_eq!(right_pane_id, active_pane.entity_id());
7904 assert_eq!(2, active_pane.read(cx).items_len());
7905 let item_ids_in_pane =
7906 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7907 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7908 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7909
7910 // Join into next from right pane into bottom
7911 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7912 });
7913
7914 workspace.update_in(cx, |workspace, window, cx| {
7915 let active_pane = workspace.active_pane();
7916 assert_eq!(bottom_pane_id, active_pane.entity_id());
7917 assert_eq!(3, active_pane.read(cx).items_len());
7918 let item_ids_in_pane =
7919 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7920 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7921 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7922 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7923
7924 // Join into next from bottom pane into left
7925 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7926 });
7927
7928 workspace.update_in(cx, |workspace, window, cx| {
7929 let active_pane = workspace.active_pane();
7930 assert_eq!(left_pane_id, active_pane.entity_id());
7931 assert_eq!(4, active_pane.read(cx).items_len());
7932 let item_ids_in_pane =
7933 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7934 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7935 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7936 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7937 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7938
7939 // Join into next from left pane into top
7940 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7941 });
7942
7943 workspace.update_in(cx, |workspace, window, cx| {
7944 let active_pane = workspace.active_pane();
7945 assert_eq!(top_pane_id, active_pane.entity_id());
7946 assert_eq!(5, active_pane.read(cx).items_len());
7947 let item_ids_in_pane =
7948 HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7949 assert!(item_ids_in_pane.contains(¢er_item.item_id()));
7950 assert!(item_ids_in_pane.contains(&right_item.item_id()));
7951 assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7952 assert!(item_ids_in_pane.contains(&left_item.item_id()));
7953 assert!(item_ids_in_pane.contains(&top_item.item_id()));
7954
7955 // Single pane left: no-op
7956 workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
7957 });
7958
7959 workspace.update(cx, |workspace, _cx| {
7960 let active_pane = workspace.active_pane();
7961 assert_eq!(top_pane_id, active_pane.entity_id());
7962 });
7963 }
7964
7965 fn add_an_item_to_active_pane(
7966 cx: &mut VisualTestContext,
7967 workspace: &Entity<Workspace>,
7968 item_id: u64,
7969 ) -> Entity<TestItem> {
7970 let item = cx.new(|cx| {
7971 TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7972 item_id,
7973 "item{item_id}.txt",
7974 cx,
7975 )])
7976 });
7977 workspace.update_in(cx, |workspace, window, cx| {
7978 workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
7979 });
7980 return item;
7981 }
7982
7983 fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
7984 return workspace.update_in(cx, |workspace, window, cx| {
7985 let new_pane = workspace.split_pane(
7986 workspace.active_pane().clone(),
7987 SplitDirection::Right,
7988 window,
7989 cx,
7990 );
7991 new_pane
7992 });
7993 }
7994
7995 #[gpui::test]
7996 async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7997 init_test(cx);
7998 let fs = FakeFs::new(cx.executor());
7999 let project = Project::test(fs, None, cx).await;
8000 let (workspace, cx) =
8001 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8002
8003 add_an_item_to_active_pane(cx, &workspace, 1);
8004 split_pane(cx, &workspace);
8005 add_an_item_to_active_pane(cx, &workspace, 2);
8006 split_pane(cx, &workspace); // empty pane
8007 split_pane(cx, &workspace);
8008 let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
8009
8010 cx.executor().run_until_parked();
8011
8012 workspace.update(cx, |workspace, cx| {
8013 let num_panes = workspace.panes().len();
8014 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
8015 let active_item = workspace
8016 .active_pane()
8017 .read(cx)
8018 .active_item()
8019 .expect("item is in focus");
8020
8021 assert_eq!(num_panes, 4);
8022 assert_eq!(num_items_in_current_pane, 1);
8023 assert_eq!(active_item.item_id(), last_item.item_id());
8024 });
8025
8026 workspace.update_in(cx, |workspace, window, cx| {
8027 workspace.join_all_panes(window, cx);
8028 });
8029
8030 workspace.update(cx, |workspace, cx| {
8031 let num_panes = workspace.panes().len();
8032 let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
8033 let active_item = workspace
8034 .active_pane()
8035 .read(cx)
8036 .active_item()
8037 .expect("item is in focus");
8038
8039 assert_eq!(num_panes, 1);
8040 assert_eq!(num_items_in_current_pane, 3);
8041 assert_eq!(active_item.item_id(), last_item.item_id());
8042 });
8043 }
8044 struct TestModal(FocusHandle);
8045
8046 impl TestModal {
8047 fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
8048 Self(cx.focus_handle())
8049 }
8050 }
8051
8052 impl EventEmitter<DismissEvent> for TestModal {}
8053
8054 impl Focusable for TestModal {
8055 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8056 self.0.clone()
8057 }
8058 }
8059
8060 impl ModalView for TestModal {}
8061
8062 impl Render for TestModal {
8063 fn render(
8064 &mut self,
8065 _window: &mut Window,
8066 _cx: &mut Context<TestModal>,
8067 ) -> impl IntoElement {
8068 div().track_focus(&self.0)
8069 }
8070 }
8071
8072 #[gpui::test]
8073 async fn test_panels(cx: &mut gpui::TestAppContext) {
8074 init_test(cx);
8075 let fs = FakeFs::new(cx.executor());
8076
8077 let project = Project::test(fs, [], cx).await;
8078 let (workspace, cx) =
8079 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8080
8081 let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
8082 let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
8083 workspace.add_panel(panel_1.clone(), window, cx);
8084 workspace.toggle_dock(DockPosition::Left, window, cx);
8085 let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8086 workspace.add_panel(panel_2.clone(), window, cx);
8087 workspace.toggle_dock(DockPosition::Right, window, cx);
8088
8089 let left_dock = workspace.left_dock();
8090 assert_eq!(
8091 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8092 panel_1.panel_id()
8093 );
8094 assert_eq!(
8095 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8096 panel_1.size(window, cx)
8097 );
8098
8099 left_dock.update(cx, |left_dock, cx| {
8100 left_dock.resize_active_panel(Some(px(1337.)), window, cx)
8101 });
8102 assert_eq!(
8103 workspace
8104 .right_dock()
8105 .read(cx)
8106 .visible_panel()
8107 .unwrap()
8108 .panel_id(),
8109 panel_2.panel_id(),
8110 );
8111
8112 (panel_1, panel_2)
8113 });
8114
8115 // Move panel_1 to the right
8116 panel_1.update_in(cx, |panel_1, window, cx| {
8117 panel_1.set_position(DockPosition::Right, window, cx)
8118 });
8119
8120 workspace.update_in(cx, |workspace, window, cx| {
8121 // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
8122 // Since it was the only panel on the left, the left dock should now be closed.
8123 assert!(!workspace.left_dock().read(cx).is_open());
8124 assert!(workspace.left_dock().read(cx).visible_panel().is_none());
8125 let right_dock = workspace.right_dock();
8126 assert_eq!(
8127 right_dock.read(cx).visible_panel().unwrap().panel_id(),
8128 panel_1.panel_id()
8129 );
8130 assert_eq!(
8131 right_dock.read(cx).active_panel_size(window, cx).unwrap(),
8132 px(1337.)
8133 );
8134
8135 // Now we move panel_2 to the left
8136 panel_2.set_position(DockPosition::Left, window, cx);
8137 });
8138
8139 workspace.update(cx, |workspace, cx| {
8140 // Since panel_2 was not visible on the right, we don't open the left dock.
8141 assert!(!workspace.left_dock().read(cx).is_open());
8142 // And the right dock is unaffected in its displaying of panel_1
8143 assert!(workspace.right_dock().read(cx).is_open());
8144 assert_eq!(
8145 workspace
8146 .right_dock()
8147 .read(cx)
8148 .visible_panel()
8149 .unwrap()
8150 .panel_id(),
8151 panel_1.panel_id(),
8152 );
8153 });
8154
8155 // Move panel_1 back to the left
8156 panel_1.update_in(cx, |panel_1, window, cx| {
8157 panel_1.set_position(DockPosition::Left, window, cx)
8158 });
8159
8160 workspace.update_in(cx, |workspace, window, cx| {
8161 // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
8162 let left_dock = workspace.left_dock();
8163 assert!(left_dock.read(cx).is_open());
8164 assert_eq!(
8165 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8166 panel_1.panel_id()
8167 );
8168 assert_eq!(
8169 left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8170 px(1337.)
8171 );
8172 // And the right dock should be closed as it no longer has any panels.
8173 assert!(!workspace.right_dock().read(cx).is_open());
8174
8175 // Now we move panel_1 to the bottom
8176 panel_1.set_position(DockPosition::Bottom, window, cx);
8177 });
8178
8179 workspace.update_in(cx, |workspace, window, cx| {
8180 // Since panel_1 was visible on the left, we close the left dock.
8181 assert!(!workspace.left_dock().read(cx).is_open());
8182 // The bottom dock is sized based on the panel's default size,
8183 // since the panel orientation changed from vertical to horizontal.
8184 let bottom_dock = workspace.bottom_dock();
8185 assert_eq!(
8186 bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
8187 panel_1.size(window, cx),
8188 );
8189 // Close bottom dock and move panel_1 back to the left.
8190 bottom_dock.update(cx, |bottom_dock, cx| {
8191 bottom_dock.set_open(false, window, cx)
8192 });
8193 panel_1.set_position(DockPosition::Left, window, cx);
8194 });
8195
8196 // Emit activated event on panel 1
8197 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
8198
8199 // Now the left dock is open and panel_1 is active and focused.
8200 workspace.update_in(cx, |workspace, window, cx| {
8201 let left_dock = workspace.left_dock();
8202 assert!(left_dock.read(cx).is_open());
8203 assert_eq!(
8204 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8205 panel_1.panel_id(),
8206 );
8207 assert!(panel_1.focus_handle(cx).is_focused(window));
8208 });
8209
8210 // Emit closed event on panel 2, which is not active
8211 panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8212
8213 // Wo don't close the left dock, because panel_2 wasn't the active panel
8214 workspace.update(cx, |workspace, cx| {
8215 let left_dock = workspace.left_dock();
8216 assert!(left_dock.read(cx).is_open());
8217 assert_eq!(
8218 left_dock.read(cx).visible_panel().unwrap().panel_id(),
8219 panel_1.panel_id(),
8220 );
8221 });
8222
8223 // Emitting a ZoomIn event shows the panel as zoomed.
8224 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
8225 workspace.update(cx, |workspace, _| {
8226 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8227 assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
8228 });
8229
8230 // Move panel to another dock while it is zoomed
8231 panel_1.update_in(cx, |panel, window, cx| {
8232 panel.set_position(DockPosition::Right, window, cx)
8233 });
8234 workspace.update(cx, |workspace, _| {
8235 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8236
8237 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8238 });
8239
8240 // This is a helper for getting a:
8241 // - valid focus on an element,
8242 // - that isn't a part of the panes and panels system of the Workspace,
8243 // - and doesn't trigger the 'on_focus_lost' API.
8244 let focus_other_view = {
8245 let workspace = workspace.clone();
8246 move |cx: &mut VisualTestContext| {
8247 workspace.update_in(cx, |workspace, window, cx| {
8248 if let Some(_) = workspace.active_modal::<TestModal>(cx) {
8249 workspace.toggle_modal(window, cx, TestModal::new);
8250 workspace.toggle_modal(window, cx, TestModal::new);
8251 } else {
8252 workspace.toggle_modal(window, cx, TestModal::new);
8253 }
8254 })
8255 }
8256 };
8257
8258 // If focus is transferred to another view that's not a panel or another pane, we still show
8259 // the panel as zoomed.
8260 focus_other_view(cx);
8261 workspace.update(cx, |workspace, _| {
8262 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8263 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8264 });
8265
8266 // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
8267 workspace.update_in(cx, |_workspace, window, cx| {
8268 cx.focus_self(window);
8269 });
8270 workspace.update(cx, |workspace, _| {
8271 assert_eq!(workspace.zoomed, None);
8272 assert_eq!(workspace.zoomed_position, None);
8273 });
8274
8275 // If focus is transferred again to another view that's not a panel or a pane, we won't
8276 // show the panel as zoomed because it wasn't zoomed before.
8277 focus_other_view(cx);
8278 workspace.update(cx, |workspace, _| {
8279 assert_eq!(workspace.zoomed, None);
8280 assert_eq!(workspace.zoomed_position, None);
8281 });
8282
8283 // When the panel is activated, it is zoomed again.
8284 cx.dispatch_action(ToggleRightDock);
8285 workspace.update(cx, |workspace, _| {
8286 assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8287 assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8288 });
8289
8290 // Emitting a ZoomOut event unzooms the panel.
8291 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
8292 workspace.update(cx, |workspace, _| {
8293 assert_eq!(workspace.zoomed, None);
8294 assert_eq!(workspace.zoomed_position, None);
8295 });
8296
8297 // Emit closed event on panel 1, which is active
8298 panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8299
8300 // Now the left dock is closed, because panel_1 was the active panel
8301 workspace.update(cx, |workspace, cx| {
8302 let right_dock = workspace.right_dock();
8303 assert!(!right_dock.read(cx).is_open());
8304 });
8305 }
8306
8307 #[gpui::test]
8308 async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8309 init_test(cx);
8310
8311 let fs = FakeFs::new(cx.background_executor.clone());
8312 let project = Project::test(fs, [], cx).await;
8313 let (workspace, cx) =
8314 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8315 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8316
8317 let dirty_regular_buffer = cx.new(|cx| {
8318 TestItem::new(cx)
8319 .with_dirty(true)
8320 .with_label("1.txt")
8321 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8322 });
8323 let dirty_regular_buffer_2 = cx.new(|cx| {
8324 TestItem::new(cx)
8325 .with_dirty(true)
8326 .with_label("2.txt")
8327 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8328 });
8329 let dirty_multi_buffer_with_both = cx.new(|cx| {
8330 TestItem::new(cx)
8331 .with_dirty(true)
8332 .with_singleton(false)
8333 .with_label("Fake Project Search")
8334 .with_project_items(&[
8335 dirty_regular_buffer.read(cx).project_items[0].clone(),
8336 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8337 ])
8338 });
8339 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8340 workspace.update_in(cx, |workspace, window, cx| {
8341 workspace.add_item(
8342 pane.clone(),
8343 Box::new(dirty_regular_buffer.clone()),
8344 None,
8345 false,
8346 false,
8347 window,
8348 cx,
8349 );
8350 workspace.add_item(
8351 pane.clone(),
8352 Box::new(dirty_regular_buffer_2.clone()),
8353 None,
8354 false,
8355 false,
8356 window,
8357 cx,
8358 );
8359 workspace.add_item(
8360 pane.clone(),
8361 Box::new(dirty_multi_buffer_with_both.clone()),
8362 None,
8363 false,
8364 false,
8365 window,
8366 cx,
8367 );
8368 });
8369
8370 pane.update_in(cx, |pane, window, cx| {
8371 pane.activate_item(2, true, true, window, cx);
8372 assert_eq!(
8373 pane.active_item().unwrap().item_id(),
8374 multi_buffer_with_both_files_id,
8375 "Should select the multi buffer in the pane"
8376 );
8377 });
8378 let close_all_but_multi_buffer_task = pane
8379 .update_in(cx, |pane, window, cx| {
8380 pane.close_inactive_items(
8381 &CloseInactiveItems {
8382 save_intent: Some(SaveIntent::Save),
8383 close_pinned: true,
8384 },
8385 window,
8386 cx,
8387 )
8388 })
8389 .expect("should have inactive files to close");
8390 cx.background_executor.run_until_parked();
8391 assert!(!cx.has_pending_prompt());
8392 close_all_but_multi_buffer_task
8393 .await
8394 .expect("Closing all buffers but the multi buffer failed");
8395 pane.update(cx, |pane, cx| {
8396 assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
8397 assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
8398 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
8399 assert_eq!(pane.items_len(), 1);
8400 assert_eq!(
8401 pane.active_item().unwrap().item_id(),
8402 multi_buffer_with_both_files_id,
8403 "Should have only the multi buffer left in the pane"
8404 );
8405 assert!(
8406 dirty_multi_buffer_with_both.read(cx).is_dirty,
8407 "The multi buffer containing the unsaved buffer should still be dirty"
8408 );
8409 });
8410
8411 dirty_regular_buffer.update(cx, |buffer, cx| {
8412 buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
8413 });
8414
8415 let close_multi_buffer_task = pane
8416 .update_in(cx, |pane, window, cx| {
8417 pane.close_active_item(
8418 &CloseActiveItem {
8419 save_intent: Some(SaveIntent::Close),
8420 close_pinned: false,
8421 },
8422 window,
8423 cx,
8424 )
8425 })
8426 .expect("should have the multi buffer to close");
8427 cx.background_executor.run_until_parked();
8428 assert!(
8429 cx.has_pending_prompt(),
8430 "Dirty multi buffer should prompt a save dialog"
8431 );
8432 cx.simulate_prompt_answer("Save");
8433 cx.background_executor.run_until_parked();
8434 close_multi_buffer_task
8435 .await
8436 .expect("Closing the multi buffer failed");
8437 pane.update(cx, |pane, cx| {
8438 assert_eq!(
8439 dirty_multi_buffer_with_both.read(cx).save_count,
8440 1,
8441 "Multi buffer item should get be saved"
8442 );
8443 // Test impl does not save inner items, so we do not assert them
8444 assert_eq!(
8445 pane.items_len(),
8446 0,
8447 "No more items should be left in the pane"
8448 );
8449 assert!(pane.active_item().is_none());
8450 });
8451 }
8452
8453 #[gpui::test]
8454 async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
8455 cx: &mut TestAppContext,
8456 ) {
8457 init_test(cx);
8458
8459 let fs = FakeFs::new(cx.background_executor.clone());
8460 let project = Project::test(fs, [], cx).await;
8461 let (workspace, cx) =
8462 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8463 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8464
8465 let dirty_regular_buffer = cx.new(|cx| {
8466 TestItem::new(cx)
8467 .with_dirty(true)
8468 .with_label("1.txt")
8469 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8470 });
8471 let dirty_regular_buffer_2 = cx.new(|cx| {
8472 TestItem::new(cx)
8473 .with_dirty(true)
8474 .with_label("2.txt")
8475 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8476 });
8477 let clear_regular_buffer = cx.new(|cx| {
8478 TestItem::new(cx)
8479 .with_label("3.txt")
8480 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8481 });
8482
8483 let dirty_multi_buffer_with_both = cx.new(|cx| {
8484 TestItem::new(cx)
8485 .with_dirty(true)
8486 .with_singleton(false)
8487 .with_label("Fake Project Search")
8488 .with_project_items(&[
8489 dirty_regular_buffer.read(cx).project_items[0].clone(),
8490 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8491 clear_regular_buffer.read(cx).project_items[0].clone(),
8492 ])
8493 });
8494 let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8495 workspace.update_in(cx, |workspace, window, cx| {
8496 workspace.add_item(
8497 pane.clone(),
8498 Box::new(dirty_regular_buffer.clone()),
8499 None,
8500 false,
8501 false,
8502 window,
8503 cx,
8504 );
8505 workspace.add_item(
8506 pane.clone(),
8507 Box::new(dirty_multi_buffer_with_both.clone()),
8508 None,
8509 false,
8510 false,
8511 window,
8512 cx,
8513 );
8514 });
8515
8516 pane.update_in(cx, |pane, window, cx| {
8517 pane.activate_item(1, true, true, window, cx);
8518 assert_eq!(
8519 pane.active_item().unwrap().item_id(),
8520 multi_buffer_with_both_files_id,
8521 "Should select the multi buffer in the pane"
8522 );
8523 });
8524 let _close_multi_buffer_task = pane
8525 .update_in(cx, |pane, window, cx| {
8526 pane.close_active_item(
8527 &CloseActiveItem {
8528 save_intent: None,
8529 close_pinned: false,
8530 },
8531 window,
8532 cx,
8533 )
8534 })
8535 .expect("should have active multi buffer to close");
8536 cx.background_executor.run_until_parked();
8537 assert!(
8538 cx.has_pending_prompt(),
8539 "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
8540 );
8541 }
8542
8543 #[gpui::test]
8544 async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
8545 cx: &mut TestAppContext,
8546 ) {
8547 init_test(cx);
8548
8549 let fs = FakeFs::new(cx.background_executor.clone());
8550 let project = Project::test(fs, [], cx).await;
8551 let (workspace, cx) =
8552 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8553 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8554
8555 let dirty_regular_buffer = cx.new(|cx| {
8556 TestItem::new(cx)
8557 .with_dirty(true)
8558 .with_label("1.txt")
8559 .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8560 });
8561 let dirty_regular_buffer_2 = cx.new(|cx| {
8562 TestItem::new(cx)
8563 .with_dirty(true)
8564 .with_label("2.txt")
8565 .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8566 });
8567 let clear_regular_buffer = cx.new(|cx| {
8568 TestItem::new(cx)
8569 .with_label("3.txt")
8570 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8571 });
8572
8573 let dirty_multi_buffer = cx.new(|cx| {
8574 TestItem::new(cx)
8575 .with_dirty(true)
8576 .with_singleton(false)
8577 .with_label("Fake Project Search")
8578 .with_project_items(&[
8579 dirty_regular_buffer.read(cx).project_items[0].clone(),
8580 dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8581 clear_regular_buffer.read(cx).project_items[0].clone(),
8582 ])
8583 });
8584 workspace.update_in(cx, |workspace, window, cx| {
8585 workspace.add_item(
8586 pane.clone(),
8587 Box::new(dirty_regular_buffer.clone()),
8588 None,
8589 false,
8590 false,
8591 window,
8592 cx,
8593 );
8594 workspace.add_item(
8595 pane.clone(),
8596 Box::new(dirty_regular_buffer_2.clone()),
8597 None,
8598 false,
8599 false,
8600 window,
8601 cx,
8602 );
8603 workspace.add_item(
8604 pane.clone(),
8605 Box::new(dirty_multi_buffer.clone()),
8606 None,
8607 false,
8608 false,
8609 window,
8610 cx,
8611 );
8612 });
8613
8614 pane.update_in(cx, |pane, window, cx| {
8615 pane.activate_item(2, true, true, window, cx);
8616 assert_eq!(
8617 pane.active_item().unwrap().item_id(),
8618 dirty_multi_buffer.item_id(),
8619 "Should select the multi buffer in the pane"
8620 );
8621 });
8622 let close_multi_buffer_task = pane
8623 .update_in(cx, |pane, window, cx| {
8624 pane.close_active_item(
8625 &CloseActiveItem {
8626 save_intent: None,
8627 close_pinned: false,
8628 },
8629 window,
8630 cx,
8631 )
8632 })
8633 .expect("should have active multi buffer to close");
8634 cx.background_executor.run_until_parked();
8635 assert!(
8636 !cx.has_pending_prompt(),
8637 "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
8638 );
8639 close_multi_buffer_task
8640 .await
8641 .expect("Closing multi buffer failed");
8642 pane.update(cx, |pane, cx| {
8643 assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8644 assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
8645 assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8646 assert_eq!(
8647 pane.items()
8648 .map(|item| item.item_id())
8649 .sorted()
8650 .collect::<Vec<_>>(),
8651 vec![
8652 dirty_regular_buffer.item_id(),
8653 dirty_regular_buffer_2.item_id(),
8654 ],
8655 "Should have no multi buffer left in the pane"
8656 );
8657 assert!(dirty_regular_buffer.read(cx).is_dirty);
8658 assert!(dirty_regular_buffer_2.read(cx).is_dirty);
8659 });
8660 }
8661
8662 #[gpui::test]
8663 async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
8664 init_test(cx);
8665 let fs = FakeFs::new(cx.executor());
8666 let project = Project::test(fs, [], cx).await;
8667 let (workspace, cx) =
8668 cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8669
8670 // Add a new panel to the right dock, opening the dock and setting the
8671 // focus to the new panel.
8672 let panel = workspace.update_in(cx, |workspace, window, cx| {
8673 let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8674 workspace.add_panel(panel.clone(), window, cx);
8675
8676 workspace
8677 .right_dock()
8678 .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8679
8680 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8681
8682 panel
8683 });
8684
8685 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8686 // panel to the next valid position which, in this case, is the left
8687 // dock.
8688 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8689 workspace.update(cx, |workspace, cx| {
8690 assert!(workspace.left_dock().read(cx).is_open());
8691 assert_eq!(panel.read(cx).position, DockPosition::Left);
8692 });
8693
8694 // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8695 // panel to the next valid position which, in this case, is the bottom
8696 // dock.
8697 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8698 workspace.update(cx, |workspace, cx| {
8699 assert!(workspace.bottom_dock().read(cx).is_open());
8700 assert_eq!(panel.read(cx).position, DockPosition::Bottom);
8701 });
8702
8703 // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
8704 // around moving the panel to its initial position, the right dock.
8705 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8706 workspace.update(cx, |workspace, cx| {
8707 assert!(workspace.right_dock().read(cx).is_open());
8708 assert_eq!(panel.read(cx).position, DockPosition::Right);
8709 });
8710
8711 // Remove focus from the panel, ensuring that, if the panel is not
8712 // focused, the `MoveFocusedPanelToNextPosition` action does not update
8713 // the panel's position, so the panel is still in the right dock.
8714 workspace.update_in(cx, |workspace, window, cx| {
8715 workspace.toggle_panel_focus::<TestPanel>(window, cx);
8716 });
8717
8718 cx.dispatch_action(MoveFocusedPanelToNextPosition);
8719 workspace.update(cx, |workspace, cx| {
8720 assert!(workspace.right_dock().read(cx).is_open());
8721 assert_eq!(panel.read(cx).position, DockPosition::Right);
8722 });
8723 }
8724
8725 mod register_project_item_tests {
8726
8727 use super::*;
8728
8729 // View
8730 struct TestPngItemView {
8731 focus_handle: FocusHandle,
8732 }
8733 // Model
8734 struct TestPngItem {}
8735
8736 impl project::ProjectItem for TestPngItem {
8737 fn try_open(
8738 _project: &Entity<Project>,
8739 path: &ProjectPath,
8740 cx: &mut App,
8741 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8742 if path.path.extension().unwrap() == "png" {
8743 Some(cx.spawn(async move |cx| cx.new(|_| TestPngItem {})))
8744 } else {
8745 None
8746 }
8747 }
8748
8749 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8750 None
8751 }
8752
8753 fn project_path(&self, _: &App) -> Option<ProjectPath> {
8754 None
8755 }
8756
8757 fn is_dirty(&self) -> bool {
8758 false
8759 }
8760 }
8761
8762 impl Item for TestPngItemView {
8763 type Event = ();
8764 }
8765 impl EventEmitter<()> for TestPngItemView {}
8766 impl Focusable for TestPngItemView {
8767 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8768 self.focus_handle.clone()
8769 }
8770 }
8771
8772 impl Render for TestPngItemView {
8773 fn render(
8774 &mut self,
8775 _window: &mut Window,
8776 _cx: &mut Context<Self>,
8777 ) -> impl IntoElement {
8778 Empty
8779 }
8780 }
8781
8782 impl ProjectItem for TestPngItemView {
8783 type Item = TestPngItem;
8784
8785 fn for_project_item(
8786 _project: Entity<Project>,
8787 _pane: &Pane,
8788 _item: Entity<Self::Item>,
8789 _: &mut Window,
8790 cx: &mut Context<Self>,
8791 ) -> Self
8792 where
8793 Self: Sized,
8794 {
8795 Self {
8796 focus_handle: cx.focus_handle(),
8797 }
8798 }
8799 }
8800
8801 // View
8802 struct TestIpynbItemView {
8803 focus_handle: FocusHandle,
8804 }
8805 // Model
8806 struct TestIpynbItem {}
8807
8808 impl project::ProjectItem for TestIpynbItem {
8809 fn try_open(
8810 _project: &Entity<Project>,
8811 path: &ProjectPath,
8812 cx: &mut App,
8813 ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8814 if path.path.extension().unwrap() == "ipynb" {
8815 Some(cx.spawn(async move |cx| cx.new(|_| TestIpynbItem {})))
8816 } else {
8817 None
8818 }
8819 }
8820
8821 fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8822 None
8823 }
8824
8825 fn project_path(&self, _: &App) -> Option<ProjectPath> {
8826 None
8827 }
8828
8829 fn is_dirty(&self) -> bool {
8830 false
8831 }
8832 }
8833
8834 impl Item for TestIpynbItemView {
8835 type Event = ();
8836 }
8837 impl EventEmitter<()> for TestIpynbItemView {}
8838 impl Focusable for TestIpynbItemView {
8839 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8840 self.focus_handle.clone()
8841 }
8842 }
8843
8844 impl Render for TestIpynbItemView {
8845 fn render(
8846 &mut self,
8847 _window: &mut Window,
8848 _cx: &mut Context<Self>,
8849 ) -> impl IntoElement {
8850 Empty
8851 }
8852 }
8853
8854 impl ProjectItem for TestIpynbItemView {
8855 type Item = TestIpynbItem;
8856
8857 fn for_project_item(
8858 _project: Entity<Project>,
8859 _pane: &Pane,
8860 _item: Entity<Self::Item>,
8861 _: &mut Window,
8862 cx: &mut Context<Self>,
8863 ) -> Self
8864 where
8865 Self: Sized,
8866 {
8867 Self {
8868 focus_handle: cx.focus_handle(),
8869 }
8870 }
8871 }
8872
8873 struct TestAlternatePngItemView {
8874 focus_handle: FocusHandle,
8875 }
8876
8877 impl Item for TestAlternatePngItemView {
8878 type Event = ();
8879 }
8880
8881 impl EventEmitter<()> for TestAlternatePngItemView {}
8882 impl Focusable for TestAlternatePngItemView {
8883 fn focus_handle(&self, _cx: &App) -> FocusHandle {
8884 self.focus_handle.clone()
8885 }
8886 }
8887
8888 impl Render for TestAlternatePngItemView {
8889 fn render(
8890 &mut self,
8891 _window: &mut Window,
8892 _cx: &mut Context<Self>,
8893 ) -> impl IntoElement {
8894 Empty
8895 }
8896 }
8897
8898 impl ProjectItem for TestAlternatePngItemView {
8899 type Item = TestPngItem;
8900
8901 fn for_project_item(
8902 _project: Entity<Project>,
8903 _pane: &Pane,
8904 _item: Entity<Self::Item>,
8905 _: &mut Window,
8906 cx: &mut Context<Self>,
8907 ) -> Self
8908 where
8909 Self: Sized,
8910 {
8911 Self {
8912 focus_handle: cx.focus_handle(),
8913 }
8914 }
8915 }
8916
8917 #[gpui::test]
8918 async fn test_register_project_item(cx: &mut TestAppContext) {
8919 init_test(cx);
8920
8921 cx.update(|cx| {
8922 register_project_item::<TestPngItemView>(cx);
8923 register_project_item::<TestIpynbItemView>(cx);
8924 });
8925
8926 let fs = FakeFs::new(cx.executor());
8927 fs.insert_tree(
8928 "/root1",
8929 json!({
8930 "one.png": "BINARYDATAHERE",
8931 "two.ipynb": "{ totally a notebook }",
8932 "three.txt": "editing text, sure why not?"
8933 }),
8934 )
8935 .await;
8936
8937 let project = Project::test(fs, ["root1".as_ref()], cx).await;
8938 let (workspace, cx) =
8939 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8940
8941 let worktree_id = project.update(cx, |project, cx| {
8942 project.worktrees(cx).next().unwrap().read(cx).id()
8943 });
8944
8945 let handle = workspace
8946 .update_in(cx, |workspace, window, cx| {
8947 let project_path = (worktree_id, "one.png");
8948 workspace.open_path(project_path, None, true, window, cx)
8949 })
8950 .await
8951 .unwrap();
8952
8953 // Now we can check if the handle we got back errored or not
8954 assert_eq!(
8955 handle.to_any().entity_type(),
8956 TypeId::of::<TestPngItemView>()
8957 );
8958
8959 let handle = workspace
8960 .update_in(cx, |workspace, window, cx| {
8961 let project_path = (worktree_id, "two.ipynb");
8962 workspace.open_path(project_path, None, true, window, cx)
8963 })
8964 .await
8965 .unwrap();
8966
8967 assert_eq!(
8968 handle.to_any().entity_type(),
8969 TypeId::of::<TestIpynbItemView>()
8970 );
8971
8972 let handle = workspace
8973 .update_in(cx, |workspace, window, cx| {
8974 let project_path = (worktree_id, "three.txt");
8975 workspace.open_path(project_path, None, true, window, cx)
8976 })
8977 .await;
8978 assert!(handle.is_err());
8979 }
8980
8981 #[gpui::test]
8982 async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8983 init_test(cx);
8984
8985 cx.update(|cx| {
8986 register_project_item::<TestPngItemView>(cx);
8987 register_project_item::<TestAlternatePngItemView>(cx);
8988 });
8989
8990 let fs = FakeFs::new(cx.executor());
8991 fs.insert_tree(
8992 "/root1",
8993 json!({
8994 "one.png": "BINARYDATAHERE",
8995 "two.ipynb": "{ totally a notebook }",
8996 "three.txt": "editing text, sure why not?"
8997 }),
8998 )
8999 .await;
9000 let project = Project::test(fs, ["root1".as_ref()], cx).await;
9001 let (workspace, cx) =
9002 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
9003 let worktree_id = project.update(cx, |project, cx| {
9004 project.worktrees(cx).next().unwrap().read(cx).id()
9005 });
9006
9007 let handle = workspace
9008 .update_in(cx, |workspace, window, cx| {
9009 let project_path = (worktree_id, "one.png");
9010 workspace.open_path(project_path, None, true, window, cx)
9011 })
9012 .await
9013 .unwrap();
9014
9015 // This _must_ be the second item registered
9016 assert_eq!(
9017 handle.to_any().entity_type(),
9018 TypeId::of::<TestAlternatePngItemView>()
9019 );
9020
9021 let handle = workspace
9022 .update_in(cx, |workspace, window, cx| {
9023 let project_path = (worktree_id, "three.txt");
9024 workspace.open_path(project_path, None, true, window, cx)
9025 })
9026 .await;
9027 assert!(handle.is_err());
9028 }
9029 }
9030
9031 pub fn init_test(cx: &mut TestAppContext) {
9032 cx.update(|cx| {
9033 let settings_store = SettingsStore::test(cx);
9034 cx.set_global(settings_store);
9035 theme::init(theme::LoadThemes::JustBase, cx);
9036 language::init(cx);
9037 crate::init_settings(cx);
9038 Project::init_settings(cx);
9039 });
9040 }
9041
9042 fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
9043 let item = TestProjectItem::new(id, path, cx);
9044 item.update(cx, |item, _| {
9045 item.is_dirty = true;
9046 });
9047 item
9048 }
9049}