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