diff --git a/assets/settings/default.json b/assets/settings/default.json index f0a2d5e04e..bd540ab9a1 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -115,6 +115,15 @@ "confirm_quit": false, // Whether to restore last closed project when fresh Zed instance is opened. "restore_on_startup": "last_session", + // Whether to attempt to restore previous file's state when opening it again. + // The state is stored per pane. + // When disabled, defaults are applied instead of the state restoration. + // + // E.g. for editors, selections, folds and scroll positions are restored, if the same file is closed and, later, opened again in the same pane. + // When disabled, a single selection in the very beginning of the file, zero scroll position and no folds state is used as a default. + // + // Default: true + "restore_on_file_reopen": true, // Size of the drop target in the editor. "drop_target_size": 0.2, // Whether the window should be closed when using 'close active item' on a window with no tabs. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 46761e3a82..04ecbed2e0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1597,6 +1597,17 @@ impl Editor { } this.tasks_update_task = Some(this.refresh_runnables(window, cx)); this._subscriptions.extend(project_subscriptions); + this._subscriptions + .push(cx.subscribe_self(|editor, e: &EditorEvent, cx| { + if let EditorEvent::SelectionsChanged { local } = e { + if *local { + let new_anchor = editor.scroll_manager.anchor(); + editor.update_restoration_data(cx, move |data| { + data.scroll_anchor = new_anchor; + }); + } + } + })); this.end_selection(window, cx); this.scroll_manager.show_scrollbars(window, cx); @@ -2317,18 +2328,24 @@ impl Editor { if selections.len() == 1 { cx.emit(SearchEvent::ActiveMatchChanged) } - if local - && self.is_singleton(cx) - && WorkspaceSettings::get(None, cx).restore_on_startup != RestoreOnStartupBehavior::None - { - if let Some(workspace_id) = self.workspace.as_ref().and_then(|workspace| workspace.1) { - let background_executor = cx.background_executor().clone(); - let editor_id = cx.entity().entity_id().as_u64() as ItemId; - let snapshot = self.buffer().read(cx).snapshot(cx); - let selections = selections.clone(); - self.serialize_selections = cx.background_spawn(async move { + if local && self.is_singleton(cx) { + let inmemory_selections = selections.iter().map(|s| s.range()).collect(); + self.update_restoration_data(cx, |data| { + data.selections = inmemory_selections; + }); + + if WorkspaceSettings::get(None, cx).restore_on_startup != RestoreOnStartupBehavior::None + { + if let Some(workspace_id) = + self.workspace.as_ref().and_then(|workspace| workspace.1) + { + let snapshot = self.buffer().read(cx).snapshot(cx); + let selections = selections.clone(); + let background_executor = cx.background_executor().clone(); + let editor_id = cx.entity().entity_id().as_u64() as ItemId; + self.serialize_selections = cx.background_spawn(async move { background_executor.timer(SERIALIZATION_THROTTLE_TIME).await; - let selections = selections + let db_selections = selections .iter() .map(|selection| { ( @@ -2338,11 +2355,12 @@ impl Editor { }) .collect(); - DB.save_editor_selections(editor_id, workspace_id, selections) + DB.save_editor_selections(editor_id, workspace_id, db_selections) .await .with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}")) .log_err(); }); + } } } @@ -2356,13 +2374,24 @@ impl Editor { return; } + let snapshot = self.buffer().read(cx).snapshot(cx); + let inmemory_folds = self.display_map.update(cx, |display_map, cx| { + display_map + .snapshot(cx) + .folds_in_range(0..snapshot.len()) + .map(|fold| fold.range.deref().clone()) + .collect() + }); + self.update_restoration_data(cx, |data| { + data.folds = inmemory_folds; + }); + let Some(workspace_id) = self.workspace.as_ref().and_then(|workspace| workspace.1) else { return; }; let background_executor = cx.background_executor().clone(); let editor_id = cx.entity().entity_id().as_u64() as ItemId; - let snapshot = self.buffer().read(cx).snapshot(cx); - let folds = self.display_map.update(cx, |display_map, cx| { + let db_folds = self.display_map.update(cx, |display_map, cx| { display_map .snapshot(cx) .folds_in_range(0..snapshot.len()) @@ -2376,7 +2405,7 @@ impl Editor { }); self.serialize_folds = cx.background_spawn(async move { background_executor.timer(SERIALIZATION_THROTTLE_TIME).await; - DB.save_editor_folds(editor_id, workspace_id, folds) + DB.save_editor_folds(editor_id, workspace_id, db_folds) .await .with_context(|| format!("persisting editor folds for editor {editor_id}, workspace {workspace_id:?}")) .log_err(); @@ -17454,19 +17483,6 @@ impl Editor { { let buffer_snapshot = OnceCell::new(); - if let Some(selections) = DB.get_editor_selections(item_id, workspace_id).log_err() { - if !selections.is_empty() { - let snapshot = - buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); - self.change_selections(None, window, cx, |s| { - s.select_ranges(selections.into_iter().map(|(start, end)| { - snapshot.clip_offset(start, Bias::Left) - ..snapshot.clip_offset(end, Bias::Right) - })); - }); - } - }; - if let Some(folds) = DB.get_editor_folds(item_id, workspace_id).log_err() { if !folds.is_empty() { let snapshot = @@ -17485,6 +17501,19 @@ impl Editor { ); } } + + if let Some(selections) = DB.get_editor_selections(item_id, workspace_id).log_err() { + if !selections.is_empty() { + let snapshot = + buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); + self.change_selections(None, window, cx, |s| { + s.select_ranges(selections.into_iter().map(|(start, end)| { + snapshot.clip_offset(start, Bias::Left) + ..snapshot.clip_offset(end, Bias::Right) + })); + }); + } + }; } self.read_scroll_position_from_db(item_id, workspace_id, window, cx); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 662f8e2535..bf85f786a7 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -52,7 +52,7 @@ use util::{ }; use workspace::{ item::{FollowEvent, FollowableItem, Item, ItemHandle}, - NavigationEntry, ViewId, + CloseAllItems, CloseInactiveItems, NavigationEntry, ViewId, }; #[gpui::test] @@ -18284,6 +18284,396 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex }); } +#[gpui::test] +async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let fs = FakeFs::new(cx.executor()); + let main_text = r#"fn main() { +println!("1"); +println!("2"); +println!("3"); +println!("4"); +println!("5"); +}"#; + let lib_text = "mod foo {}"; + fs.insert_tree( + path!("/a"), + json!({ + "lib.rs": lib_text, + "main.rs": main_text, + }), + ) + .await; + + let project = Project::test(fs, [path!("/a").as_ref()], cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let worktree_id = workspace.update(cx, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); + + let expected_ranges = vec![ + Point::new(0, 0)..Point::new(0, 0), + Point::new(1, 0)..Point::new(1, 1), + Point::new(2, 0)..Point::new(2, 2), + Point::new(3, 0)..Point::new(3, 3), + ]; + + let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone()); + let editor_1 = workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_path( + (worktree_id, "main.rs"), + Some(pane_1.downgrade()), + true, + window, + cx, + ) + }) + .unwrap() + .await + .downcast::() + .unwrap(); + pane_1.update(cx, |pane, cx| { + let open_editor = pane.active_item().unwrap().downcast::().unwrap(); + open_editor.update(cx, |editor, cx| { + assert_eq!( + editor.display_text(cx), + main_text, + "Original main.rs text on initial open", + ); + assert_eq!( + editor + .selections + .all::(cx) + .into_iter() + .map(|s| s.range()) + .collect::>(), + vec![Point::zero()..Point::zero()], + "Default selections on initial open", + ); + }) + }); + editor_1.update_in(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { + s.select_ranges(expected_ranges.clone()); + }); + }); + + let pane_2 = workspace.update_in(cx, |workspace, window, cx| { + workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx) + }); + let editor_2 = workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_path( + (worktree_id, "main.rs"), + Some(pane_2.downgrade()), + true, + window, + cx, + ) + }) + .unwrap() + .await + .downcast::() + .unwrap(); + pane_2.update(cx, |pane, cx| { + let open_editor = pane.active_item().unwrap().downcast::().unwrap(); + open_editor.update(cx, |editor, cx| { + assert_eq!( + editor.display_text(cx), + main_text, + "Original main.rs text on initial open in another panel", + ); + assert_eq!( + editor + .selections + .all::(cx) + .into_iter() + .map(|s| s.range()) + .collect::>(), + vec![Point::zero()..Point::zero()], + "Default selections on initial open in another panel", + ); + }) + }); + + editor_2.update_in(cx, |editor, window, cx| { + editor.fold_ranges(expected_ranges.clone(), false, window, cx); + }); + + let _other_editor_1 = workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_path( + (worktree_id, "lib.rs"), + Some(pane_1.downgrade()), + true, + window, + cx, + ) + }) + .unwrap() + .await + .downcast::() + .unwrap(); + pane_1 + .update_in(cx, |pane, window, cx| { + pane.close_inactive_items(&CloseInactiveItems::default(), window, cx) + .unwrap() + }) + .await + .unwrap(); + drop(editor_1); + pane_1.update(cx, |pane, cx| { + pane.active_item() + .unwrap() + .downcast::() + .unwrap() + .update(cx, |editor, cx| { + assert_eq!( + editor.display_text(cx), + lib_text, + "Other file should be open and active", + ); + }); + assert_eq!(pane.items().count(), 1, "No other editors should be open"); + }); + + let _other_editor_2 = workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_path( + (worktree_id, "lib.rs"), + Some(pane_2.downgrade()), + true, + window, + cx, + ) + }) + .unwrap() + .await + .downcast::() + .unwrap(); + pane_2 + .update_in(cx, |pane, window, cx| { + pane.close_inactive_items(&CloseInactiveItems::default(), window, cx) + .unwrap() + }) + .await + .unwrap(); + drop(editor_2); + pane_2.update(cx, |pane, cx| { + let open_editor = pane.active_item().unwrap().downcast::().unwrap(); + open_editor.update(cx, |editor, cx| { + assert_eq!( + editor.display_text(cx), + lib_text, + "Other file should be open and active in another panel too", + ); + }); + assert_eq!( + pane.items().count(), + 1, + "No other editors should be open in another pane", + ); + }); + + let _editor_1_reopened = workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_path( + (worktree_id, "main.rs"), + Some(pane_1.downgrade()), + true, + window, + cx, + ) + }) + .unwrap() + .await + .downcast::() + .unwrap(); + let _editor_2_reopened = workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_path( + (worktree_id, "main.rs"), + Some(pane_2.downgrade()), + true, + window, + cx, + ) + }) + .unwrap() + .await + .downcast::() + .unwrap(); + pane_1.update(cx, |pane, cx| { + let open_editor = pane.active_item().unwrap().downcast::().unwrap(); + open_editor.update(cx, |editor, cx| { + assert_eq!( + editor.display_text(cx), + main_text, + "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen", + ); + assert_eq!( + editor + .selections + .all::(cx) + .into_iter() + .map(|s| s.range()) + .collect::>(), + expected_ranges, + "Previous editor in the 1st panel had selections and should get them restored on reopen", + ); + }) + }); + pane_2.update(cx, |pane, cx| { + let open_editor = pane.active_item().unwrap().downcast::().unwrap(); + open_editor.update(cx, |editor, cx| { + assert_eq!( + editor.display_text(cx), + r#"fn main() { +⋯rintln!("1"); +⋯intln!("2"); +⋯ntln!("3"); +println!("4"); +println!("5"); +}"#, + "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane", + ); + assert_eq!( + editor + .selections + .all::(cx) + .into_iter() + .map(|s| s.range()) + .collect::>(), + vec![Point::zero()..Point::zero()], + "Previous editor in the 2nd pane had no selections changed hence should restore none", + ); + }) + }); +} + +#[gpui::test] +async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let fs = FakeFs::new(cx.executor()); + let main_text = r#"fn main() { +println!("1"); +println!("2"); +println!("3"); +println!("4"); +println!("5"); +}"#; + let lib_text = "mod foo {}"; + fs.insert_tree( + path!("/a"), + json!({ + "lib.rs": lib_text, + "main.rs": main_text, + }), + ) + .await; + + let project = Project::test(fs, [path!("/a").as_ref()], cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let worktree_id = workspace.update(cx, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); + + let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone()); + let editor = workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_path( + (worktree_id, "main.rs"), + Some(pane.downgrade()), + true, + window, + cx, + ) + }) + .unwrap() + .await + .downcast::() + .unwrap(); + pane.update(cx, |pane, cx| { + let open_editor = pane.active_item().unwrap().downcast::().unwrap(); + open_editor.update(cx, |editor, cx| { + assert_eq!( + editor.display_text(cx), + main_text, + "Original main.rs text on initial open", + ); + }) + }); + editor.update_in(cx, |editor, window, cx| { + editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx); + }); + + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |s| { + s.restore_on_file_reopen = Some(false); + }); + }); + editor.update_in(cx, |editor, window, cx| { + editor.fold_ranges( + vec![ + Point::new(1, 0)..Point::new(1, 1), + Point::new(2, 0)..Point::new(2, 2), + Point::new(3, 0)..Point::new(3, 3), + ], + false, + window, + cx, + ); + }); + pane.update_in(cx, |pane, window, cx| { + pane.close_all_items(&CloseAllItems::default(), window, cx) + .unwrap() + }) + .await + .unwrap(); + pane.update(cx, |pane, _| { + assert!(pane.active_item().is_none()); + }); + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |s| { + s.restore_on_file_reopen = Some(true); + }); + }); + + let _editor_reopened = workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_path( + (worktree_id, "main.rs"), + Some(pane.downgrade()), + true, + window, + cx, + ) + }) + .unwrap() + .await + .downcast::() + .unwrap(); + pane.update(cx, |pane, cx| { + let open_editor = pane.active_item().unwrap().downcast::().unwrap(); + open_editor.update(cx, |editor, cx| { + assert_eq!( + editor.display_text(cx), + main_text, + "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration" + ); + }) + }); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(DisplayRow(row as u32), column as u32); point..point diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b504ede9bc..b89b0df975 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -6,7 +6,8 @@ use crate::{ MultiBuffer, MultiBufferSnapshot, NavigationData, SearchWithinRange, ToPoint as _, }; use anyhow::{anyhow, Context as _, Result}; -use collections::HashSet; +use clock::Global; +use collections::{HashMap, HashSet}; use file_icons::FileIcons; use futures::future::try_join_all; use git::status::GitSummary; @@ -21,7 +22,7 @@ use language::{ use lsp::DiagnosticSeverity; use project::{ lsp_store::FormatTrigger, project_settings::ProjectSettings, search::SearchQuery, Project, - ProjectItem as _, ProjectPath, + ProjectEntryId, ProjectItem as _, ProjectPath, }; use rpc::proto::{self, update_view, PeerId}; use settings::Settings; @@ -29,6 +30,7 @@ use std::{ any::TypeId, borrow::Cow, cmp::{self, Ordering}, + collections::hash_map, iter, ops::Range, path::Path, @@ -39,9 +41,9 @@ use theme::{Theme, ThemeSettings}; use ui::{prelude::*, IconDecorationKind}; use util::{paths::PathExt, ResultExt, TryFutureExt}; use workspace::{ - item::{BreadcrumbText, FollowEvent}, + item::{BreadcrumbText, FollowEvent, ProjectItemKind}, searchable::SearchOptions, - OpenVisible, + OpenVisible, Pane, WorkspaceSettings, }; use workspace::{ item::{Dedup, ItemSettings, SerializableItem, TabContentParams}, @@ -1250,21 +1252,119 @@ impl SerializableItem for Editor { } } +#[derive(Debug, Default)] +struct EditorRestorationData { + entries: HashMap, +} + +#[derive(Debug)] +pub struct RestorationData { + pub scroll_anchor: ScrollAnchor, + pub folds: Vec>, + pub selections: Vec>, + pub buffer_version: Global, +} + +impl Default for RestorationData { + fn default() -> Self { + Self { + scroll_anchor: ScrollAnchor::new(), + folds: Vec::new(), + selections: Vec::new(), + buffer_version: Global::default(), + } + } +} + impl ProjectItem for Editor { type Item = Buffer; + fn project_item_kind() -> Option { + Some(ProjectItemKind("Editor")) + } + fn for_project_item( project: Entity, + pane: &Pane, buffer: Entity, window: &mut Window, cx: &mut Context, ) -> Self { - Self::for_buffer(buffer, Some(project), window, cx) + let mut editor = Self::for_buffer(buffer.clone(), Some(project), window, cx); + + if WorkspaceSettings::get(None, cx).restore_on_file_reopen { + if let Some(restoration_data) = Self::project_item_kind() + .and_then(|kind| pane.project_item_restoration_data.get(&kind)) + .and_then(|data| data.downcast_ref::()) + .and_then(|data| data.entries.get(&buffer.read(cx).entry_id(cx)?)) + .filter(|data| !buffer.read(cx).version.changed_since(&data.buffer_version)) + { + editor.fold_ranges(restoration_data.folds.clone(), false, window, cx); + if !restoration_data.selections.is_empty() { + editor.change_selections(None, window, cx, |s| { + s.select_ranges(restoration_data.selections.clone()); + }); + } + editor.set_scroll_anchor(restoration_data.scroll_anchor, window, cx); + } + } + + editor } } impl EventEmitter for Editor {} +impl Editor { + pub fn update_restoration_data( + &self, + cx: &mut Context, + write: impl for<'a> FnOnce(&'a mut RestorationData) + 'static, + ) { + if !WorkspaceSettings::get(None, cx).restore_on_file_reopen { + return; + } + + let editor = cx.entity(); + cx.defer(move |cx| { + editor.update(cx, |editor, cx| { + let kind = Editor::project_item_kind()?; + let pane = editor.workspace()?.read(cx).pane_for(&cx.entity())?; + let buffer = editor.buffer().read(cx).as_singleton()?; + let entry_id = buffer.read(cx).entry_id(cx)?; + let buffer_version = buffer.read(cx).version(); + pane.update(cx, |pane, _| { + let data = pane + .project_item_restoration_data + .entry(kind) + .or_insert_with(|| Box::new(EditorRestorationData::default()) as Box<_>); + let data = match data.downcast_mut::() { + Some(data) => data, + None => { + *data = Box::new(EditorRestorationData::default()); + data.downcast_mut::() + .expect("just written the type downcasted to") + } + }; + + let data = match data.entries.entry(entry_id) { + hash_map::Entry::Occupied(o) => { + if buffer_version.changed_since(&o.get().buffer_version) { + return None; + } + o.into_mut() + } + hash_map::Entry::Vacant(v) => v.insert(RestorationData::default()), + }; + write(data); + data.buffer_version = buffer_version; + Some(()) + }) + }); + }); + } +} + pub(crate) enum BufferSearchHighlights {} impl SearchableItem for Editor { type Match = Range; diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 228a04c555..81c1e1a665 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -38,7 +38,7 @@ pub struct ScrollAnchor { } impl ScrollAnchor { - fn new() -> Self { + pub(super) fn new() -> Self { Self { offset: gpui::Point::default(), anchor: Anchor::min(), diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 72b4ed22c1..fff096a4d3 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -19,7 +19,7 @@ use ui::prelude::*; use util::paths::PathExt; use workspace::{ item::{BreadcrumbText, Item, ProjectItem, SerializableItem, TabContentParams}, - ItemId, ItemSettings, ToolbarItemLocation, Workspace, WorkspaceId, + ItemId, ItemSettings, Pane, ToolbarItemLocation, Workspace, WorkspaceId, }; pub use crate::image_info::*; @@ -357,6 +357,7 @@ impl ProjectItem for ImageView { fn for_project_item( project: Entity, + _: &Pane, item: Entity, _: &mut Window, cx: &mut Context, diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index 9869562bf2..5dc5278797 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -9,7 +9,7 @@ use std::path::{Path, PathBuf}; use util::{path, separator}; use workspace::{ item::{Item, ProjectItem}, - register_project_item, AppState, + register_project_item, AppState, Pane, }; #[gpui::test] @@ -5146,6 +5146,7 @@ impl ProjectItem for TestProjectItemView { fn for_project_item( _: Entity, + _: &Pane, project_item: Entity, _: &mut Window, cx: &mut Context, diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index 786696a309..c8a1a093bc 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -17,7 +17,7 @@ use project::{Project, ProjectEntryId, ProjectPath}; use ui::{prelude::*, Tooltip}; use workspace::item::{ItemEvent, TabContentParams}; use workspace::searchable::SearchableItemHandle; -use workspace::{Item, ItemHandle, ProjectItem, ToolbarItemLocation}; +use workspace::{Item, ItemHandle, Pane, ProjectItem, ToolbarItemLocation}; use workspace::{ToolbarItemEvent, ToolbarItemView}; use super::{Cell, CellPosition, RenderableCell}; @@ -825,6 +825,7 @@ impl ProjectItem for NotebookEditor { fn for_project_item( project: Entity, + _: &Pane, item: Entity, window: &mut Window, cx: &mut Context, diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 34eec0408e..e163ba499a 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -24,6 +24,7 @@ impl VimTestContext { git_ui::init(cx); crate::init(cx); search::init(cx); + workspace::init_settings(cx); language::init(cx); editor::init_settings(cx); project::Project::init_settings(cx); diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index ff24eab03e..47e7e917dd 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -1031,11 +1031,19 @@ impl WeakItemHandle for WeakEntity { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ProjectItemKind(pub &'static str); + pub trait ProjectItem: Item { type Item: project::ProjectItem; + fn project_item_kind() -> Option { + None + } + fn for_project_item( project: Entity, + pane: &Pane, item: Entity, window: &mut Window, cx: &mut Context, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index df63bdbcac..10667b91a5 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1,7 +1,8 @@ use crate::{ item::{ ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings, - ShowCloseButton, ShowDiagnostics, TabContentParams, TabTooltipContent, WeakItemHandle, + ProjectItemKind, ShowCloseButton, ShowDiagnostics, TabContentParams, TabTooltipContent, + WeakItemHandle, }, move_item, notifications::NotifyResultExt, @@ -9,6 +10,7 @@ use crate::{ workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings}, CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible, SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace, + WorkspaceItemBuilder, }; use anyhow::Result; use collections::{BTreeSet, HashMap, HashSet, VecDeque}; @@ -321,6 +323,8 @@ pub struct Pane { pinned_tab_count: usize, diagnostics: HashMap, zoom_out_on_close: bool, + /// If a certain project item wants to get recreated with specific data, it can persist its data before the recreation here. + pub project_item_restoration_data: HashMap>, } pub struct ActivationHistoryEntry { @@ -526,6 +530,7 @@ impl Pane { pinned_tab_count: 0, diagnostics: Default::default(), zoom_out_on_close: true, + project_item_restoration_data: HashMap::default(), } } @@ -859,7 +864,7 @@ impl Pane { suggested_position: Option, window: &mut Window, cx: &mut Context, - build_item: impl FnOnce(&mut Window, &mut Context) -> Box, + build_item: WorkspaceItemBuilder, ) -> Box { let mut existing_item = None; if let Some(project_entry_id) = project_entry_id { @@ -896,7 +901,7 @@ impl Pane { suggested_position }; - let new_item = build_item(window, cx); + let new_item = build_item(self, window, cx); if allow_preview { self.set_preview_item_id(Some(new_item.item_id()), cx); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d289d52e83..612002e977 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -457,7 +457,8 @@ type ProjectItemOpener = fn( ) -> Option, WorkspaceItemBuilder)>>>; -type WorkspaceItemBuilder = Box) -> Box>; +type WorkspaceItemBuilder = + Box) -> Box>; impl Global for ProjectItemOpeners {} @@ -473,10 +474,13 @@ pub fn register_project_item(cx: &mut App) { let project_item = project_item.await?; let project_entry_id: Option = project_item.read_with(cx, project::ProjectItem::entry_id)?; - let build_workspace_item = Box::new(|window: &mut Window, cx: &mut Context| { - Box::new(cx.new(|cx| I::for_project_item(project, project_item, window, cx))) - as Box - }) as Box<_>; + let build_workspace_item = Box::new( + |pane: &mut Pane, window: &mut Window, cx: &mut Context| { + Box::new( + cx.new(|cx| I::for_project_item(project, pane, project_item, window, cx)), + ) as Box + }, + ) as Box<_>; Ok((project_entry_id, build_workspace_item)) })) }); @@ -3060,8 +3064,9 @@ impl Workspace { return item; } - let item = - cx.new(|cx| T::for_project_item(self.project().clone(), project_item, window, cx)); + let item = pane.update(cx, |pane, cx| { + cx.new(|cx| T::for_project_item(self.project().clone(), pane, project_item, window, cx)) + }); let item_id = item.item_id(); let mut destination_index = None; pane.update(cx, |pane, cx| { @@ -8720,6 +8725,7 @@ mod tests { fn for_project_item( _project: Entity, + _pane: &Pane, _item: Entity, _: &mut Window, cx: &mut Context, @@ -8791,6 +8797,7 @@ mod tests { fn for_project_item( _project: Entity, + _pane: &Pane, _item: Entity, _: &mut Window, cx: &mut Context, @@ -8834,6 +8841,7 @@ mod tests { fn for_project_item( _project: Entity, + _pane: &Pane, _item: Entity, _: &mut Window, cx: &mut Context, diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 8d444ad48f..2dda042288 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -17,6 +17,7 @@ pub struct WorkspaceSettings { pub show_call_status_icon: bool, pub autosave: AutosaveSetting, pub restore_on_startup: RestoreOnStartupBehavior, + pub restore_on_file_reopen: bool, pub drop_target_size: f32, pub use_system_path_prompts: bool, pub use_system_prompts: bool, @@ -134,6 +135,15 @@ pub struct WorkspaceSettingsContent { /// Values: none, last_workspace, last_session /// Default: last_session pub restore_on_startup: Option, + /// Whether to attempt to restore previous file's state when opening it again. + /// The state is stored per pane. + /// When disabled, defaults are applied instead of the state restoration. + /// + /// E.g. for editors, selections, folds and scroll positions are restored, if the same file is closed and, later, opened again in the same pane. + /// When disabled, a single selection in the very beginning of the file, zero scroll position and no folds state is used as a default. + /// + /// Default: true + pub restore_on_file_reopen: Option, /// The size of the workspace split drop targets on the outer edges. /// Given as a fraction that will be multiplied by the smaller dimension of the workspace. ///