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