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