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