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