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