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