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