diff --git a/Cargo.lock b/Cargo.lock index 119515de8b..03faaaf561 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5677,6 +5677,7 @@ dependencies = [ "project", "serde_json", "theme", + "util", ] [[package]] diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 47bea529c1..054074fb39 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -15,9 +15,9 @@ use gpui::{ use language::{Bias, Buffer, Diagnostic, DiagnosticEntry, Point, Selection, SelectionGoal}; use postage::watch; use project::{Project, ProjectPath, WorktreeId}; -use std::{cmp::Ordering, mem, ops::Range, sync::Arc}; +use std::{cmp::Ordering, mem, ops::Range, rc::Rc, sync::Arc}; use util::TryFutureExt; -use workspace::Workspace; +use workspace::{Navigation, Workspace}; action!(Deploy); action!(OpenExcerpts); @@ -522,6 +522,7 @@ impl workspace::Item for ProjectDiagnostics { fn build_view( handle: ModelHandle, workspace: &Workspace, + _: Rc, cx: &mut ViewContext, ) -> Self::View { ProjectDiagnosticsEditor::new(handle, workspace.weak_handle(), workspace.settings(), cx) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 898f1fce42..96e9973df5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -41,6 +41,7 @@ use std::{ iter::{self, FromIterator}, mem, ops::{Deref, Range, RangeInclusive, Sub}, + rc::Rc, sync::Arc, time::{Duration, Instant}, }; @@ -48,10 +49,11 @@ use sum_tree::Bias; use text::rope::TextDimension; use theme::{DiagnosticStyle, EditorStyle}; use util::post_inc; -use workspace::{PathOpener, Workspace}; +use workspace::{Navigation, PathOpener, Workspace}; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const MAX_LINE_LEN: usize = 1024; +const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; action!(Cancel); action!(Backspace); @@ -377,6 +379,7 @@ pub struct Editor { mode: EditorMode, placeholder_text: Option>, highlighted_rows: Option>, + navigation: Option>, } pub struct EditorSnapshot { @@ -424,6 +427,11 @@ struct ClipboardSelection { is_entire_line: bool, } +pub struct NavigationData { + anchor: Anchor, + offset: usize, +} + impl Editor { pub fn single_line(build_settings: BuildSettings, cx: &mut ViewContext) -> Self { let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); @@ -457,6 +465,7 @@ impl Editor { let mut clone = Self::new(self.buffer.clone(), self.build_settings.clone(), cx); clone.scroll_position = self.scroll_position; clone.scroll_top_anchor = self.scroll_top_anchor.clone(); + clone.navigation = self.navigation.clone(); clone } @@ -506,6 +515,7 @@ impl Editor { mode: EditorMode::Full, placeholder_text: None, highlighted_rows: None, + navigation: None, }; let selection = Selection { id: post_inc(&mut this.next_selection_id), @@ -628,7 +638,10 @@ impl Editor { let first_cursor_top; let last_cursor_bottom; - if autoscroll == Autoscroll::Newest { + if let Some(highlighted_rows) = &self.highlighted_rows { + first_cursor_top = highlighted_rows.start as f32; + last_cursor_bottom = first_cursor_top + 1.; + } else if autoscroll == Autoscroll::Newest { let newest_selection = self.newest_selection::(&display_map.buffer_snapshot); first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32; last_cursor_bottom = first_cursor_top + 1.; @@ -694,22 +707,33 @@ impl Editor { ) -> bool { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let selections = self.local_selections::(cx); - let mut target_left = std::f32::INFINITY; - let mut target_right = 0.0_f32; - for selection in selections { - let head = selection.head().to_display_point(&display_map); - if head.row() >= start_row && head.row() < start_row + layouts.len() as u32 { - let start_column = head.column().saturating_sub(3); - let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3); - target_left = target_left.min( - layouts[(head.row() - start_row) as usize].x_for_index(start_column as usize), - ); - target_right = target_right.max( - layouts[(head.row() - start_row) as usize].x_for_index(end_column as usize) - + max_glyph_width, - ); + + let mut target_left; + let mut target_right; + + if self.highlighted_rows.is_some() { + target_left = 0.0_f32; + target_right = 0.0_f32; + } else { + target_left = std::f32::INFINITY; + target_right = 0.0_f32; + for selection in selections { + let head = selection.head().to_display_point(&display_map); + if head.row() >= start_row && head.row() < start_row + layouts.len() as u32 { + let start_column = head.column().saturating_sub(3); + let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3); + target_left = target_left.min( + layouts[(head.row() - start_row) as usize] + .x_for_index(start_column as usize), + ); + target_right = target_right.max( + layouts[(head.row() - start_row) as usize].x_for_index(end_column as usize) + + max_glyph_width, + ); + } } } + target_right = target_right.min(scroll_width); if target_right - target_left > viewport_width { @@ -800,6 +824,8 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; + let newest_selection = self.newest_selection_internal().unwrap().clone(); + let start; let end; let mode; @@ -834,6 +860,8 @@ impl Editor { } } + self.push_to_navigation_history(newest_selection.head(), Some(end.to_point(&buffer)), cx); + let selection = Selection { id: post_inc(&mut self.next_selection_id), start, @@ -846,7 +874,6 @@ impl Editor { self.update_selections::(Vec::new(), None, cx); } else if click_count > 1 { // Remove the newest selection since it was only added as part of this multi-click. - let newest_selection = self.newest_selection::(buffer); let mut selections = self.local_selections(cx); selections.retain(|selection| selection.id != newest_selection.id); self.update_selections::(selections, None, cx) @@ -1129,8 +1156,8 @@ impl Editor { self.update_selections(selections, autoscroll, cx); } - #[cfg(test)] - fn select_display_ranges<'a, T>(&mut self, ranges: T, cx: &mut ViewContext) + #[cfg(any(test, feature = "test-support"))] + pub fn select_display_ranges<'a, T>(&mut self, ranges: T, cx: &mut ViewContext) where T: IntoIterator>, { @@ -2428,6 +2455,35 @@ impl Editor { self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); } + fn push_to_navigation_history( + &self, + position: Anchor, + new_position: Option, + cx: &mut ViewContext, + ) { + if let Some(navigation) = &self.navigation { + let buffer = self.buffer.read(cx).read(cx); + let offset = position.to_offset(&buffer); + let point = position.to_point(&buffer); + drop(buffer); + + if let Some(new_position) = new_position { + let row_delta = (new_position.row as i64 - point.row as i64).abs(); + if row_delta < MIN_NAVIGATION_HISTORY_ROW_DELTA { + return; + } + } + + navigation.push( + Some(NavigationData { + anchor: position, + offset, + }), + cx, + ); + } + } + pub fn select_to_end(&mut self, _: &SelectToEnd, cx: &mut ViewContext) { let mut selection = self.local_selections::(cx).first().unwrap().clone(); selection.set_head(self.buffer.read(cx).read(cx).len()); @@ -3205,14 +3261,14 @@ impl Editor { &self, snapshot: &MultiBufferSnapshot, ) -> Selection { - self.pending_selection(snapshot) - .or_else(|| { - self.selections - .iter() - .max_by_key(|s| s.id) - .map(|selection| self.resolve_selection(selection, snapshot)) - }) - .unwrap() + self.resolve_selection(self.newest_selection_internal().unwrap(), snapshot) + } + + pub fn newest_selection_internal(&self) -> Option<&Selection> { + self.pending_selection + .as_ref() + .map(|s| &s.selection) + .or_else(|| self.selections.iter().max_by_key(|s| s.id)) } pub fn update_selections( @@ -3223,10 +3279,11 @@ impl Editor { ) where T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug, { + let buffer = self.buffer.read(cx).snapshot(cx); + let old_cursor_position = self.newest_selection_internal().map(|s| s.head()); selections.sort_unstable_by_key(|s| s.start); // Merge overlapping selections. - let buffer = self.buffer.read(cx).snapshot(cx); let mut i = 1; while i < selections.len() { if selections[i - 1].end >= selections[i].start { @@ -3267,6 +3324,16 @@ impl Editor { } } + if let Some(old_cursor_position) = old_cursor_position { + let new_cursor_position = selections + .iter() + .max_by_key(|s| s.id) + .map(|s| s.head().to_point(&buffer)); + if new_cursor_position.is_some() { + self.push_to_navigation_history(old_cursor_position, new_cursor_position, cx); + } + } + if let Some(autoscroll) = autoscroll { self.request_autoscroll(autoscroll, cx); } @@ -3347,7 +3414,7 @@ impl Editor { }); } - fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { + pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { self.autoscroll_request = Some(autoscroll); cx.notify(); } @@ -4103,6 +4170,63 @@ mod tests { }); } + #[gpui::test] + fn test_navigation_history(cx: &mut gpui::MutableAppContext) { + cx.add_window(Default::default(), |cx| { + use workspace::ItemView; + let navigation = Rc::new(workspace::Navigation::default()); + let settings = EditorSettings::test(&cx); + let buffer = MultiBuffer::build_simple(&sample_text(30, 5, 'a'), cx); + let mut editor = build_editor(buffer.clone(), settings, cx); + editor.navigation = Some(navigation.clone()); + + // Move the cursor a small distance. + // Nothing is added to the navigation history. + editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); + editor.select_display_ranges(&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)], cx); + assert!(navigation.pop_backward().is_none()); + + // Move the cursor a large distance. + // The history can jump back to the previous position. + editor.select_display_ranges(&[DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)], cx); + let nav_entry = navigation.pop_backward().unwrap(); + editor.navigate(nav_entry.data.unwrap(), cx); + assert_eq!(nav_entry.item_view.id(), cx.view_id()); + assert_eq!( + editor.selected_display_ranges(cx), + &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)] + ); + + // Move the cursor a small distance via the mouse. + // Nothing is added to the navigation history. + editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx); + editor.end_selection(cx); + assert_eq!( + editor.selected_display_ranges(cx), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] + ); + assert!(navigation.pop_backward().is_none()); + + // Move the cursor a large distance via the mouse. + // The history can jump back to the previous position. + editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx); + editor.end_selection(cx); + assert_eq!( + editor.selected_display_ranges(cx), + &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)] + ); + let nav_entry = navigation.pop_backward().unwrap(); + editor.navigate(nav_entry.data.unwrap(), cx); + assert_eq!(nav_entry.item_view.id(), cx.view_id()); + assert_eq!( + editor.selected_display_ranges(cx), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] + ); + + editor + }); + } + #[gpui::test] fn test_cancel(cx: &mut gpui::MutableAppContext) { let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 8abcc76ef9..147622c8d4 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,20 +1,20 @@ -use crate::{Autoscroll, Editor, Event}; -use crate::{MultiBuffer, ToPoint as _}; +use crate::{Autoscroll, Editor, Event, MultiBuffer, NavigationData, ToOffset, ToPoint as _}; use anyhow::Result; use gpui::{ elements::*, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakModelHandle, }; -use language::{Buffer, Diagnostic, File as _}; +use language::{Bias, Buffer, Diagnostic, File as _}; use postage::watch; use project::{File, ProjectPath, Worktree}; use std::fmt::Write; use std::path::Path; +use std::rc::Rc; use text::{Point, Selection}; use util::TryFutureExt; use workspace::{ - ItemHandle, ItemView, ItemViewHandle, PathOpener, Settings, StatusItemView, WeakItemHandle, - Workspace, + ItemHandle, ItemView, ItemViewHandle, Navigation, PathOpener, Settings, StatusItemView, + WeakItemHandle, Workspace, }; pub struct BufferOpener; @@ -46,16 +46,19 @@ impl ItemHandle for BufferItemHandle { &self, window_id: usize, workspace: &Workspace, + navigation: Rc, cx: &mut MutableAppContext, ) -> Box { let buffer = cx.add_model(|cx| MultiBuffer::singleton(self.0.clone(), cx)); let weak_buffer = buffer.downgrade(); Box::new(cx.add_view(window_id, |cx| { - Editor::for_buffer( + let mut editor = Editor::for_buffer( buffer, crate::settings_builder(weak_buffer, workspace.settings()), cx, - ) + ); + editor.navigation = Some(navigation); + editor })) } @@ -102,6 +105,22 @@ impl ItemView for Editor { BufferItemHandle(self.buffer.read(cx).as_singleton().unwrap()) } + fn navigate(&mut self, data: Box, cx: &mut ViewContext) { + if let Some(data) = data.downcast_ref::() { + let buffer = self.buffer.read(cx).read(cx); + let offset = if buffer.can_resolve(&data.anchor) { + data.anchor.to_offset(&buffer) + } else { + buffer.clip_offset(data.offset, Bias::Left) + }; + + drop(buffer); + let navigation = self.navigation.take(); + self.select_ranges([offset..offset], Some(Autoscroll::Fit), cx); + self.navigation = navigation; + } + } + fn title(&self, cx: &AppContext) -> String { let filename = self .buffer() @@ -129,6 +148,12 @@ impl ItemView for Editor { Some(self.clone(cx)) } + fn deactivated(&mut self, cx: &mut ViewContext) { + if let Some(selection) = self.newest_selection_internal() { + self.push_to_navigation_history(selection.head(), None, cx); + } + } + fn is_dirty(&self, cx: &AppContext) -> bool { self.buffer().read(cx).read(cx).is_dirty() } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 30020a0d55..7ae7b56867 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1545,6 +1545,18 @@ impl MultiBufferSnapshot { panic!("excerpt not found"); } + pub fn can_resolve(&self, anchor: &Anchor) -> bool { + if anchor.excerpt_id == ExcerptId::min() || anchor.excerpt_id == ExcerptId::max() { + true + } else if let Some((buffer_id, buffer_snapshot)) = + self.buffer_snapshot_for_excerpt(&anchor.excerpt_id) + { + anchor.buffer_id == buffer_id && buffer_snapshot.can_resolve(&anchor.text_anchor) + } else { + false + } + } + pub fn range_contains_excerpt_boundary(&self, range: Range) -> bool { let start = range.start.to_offset(self); let end = range.end.to_offset(self); diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 53669ea2c6..c282cd1c75 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -1,11 +1,11 @@ -use editor::{display_map::ToDisplayPoint, Autoscroll, Editor, EditorSettings}; +use editor::{display_map::ToDisplayPoint, Autoscroll, DisplayPoint, Editor, EditorSettings}; use gpui::{ action, elements::*, geometry::vector::Vector2F, keymap::Binding, Axis, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, }; use postage::watch; use std::sync::Arc; -use text::{Bias, Point, Selection}; +use text::{Bias, Point}; use workspace::{Settings, Workspace}; action!(Toggle); @@ -25,17 +25,11 @@ pub struct GoToLine { settings: watch::Receiver, line_editor: ViewHandle, active_editor: ViewHandle, - restore_state: Option, - line_selection_id: Option, + prev_scroll_position: Option, cursor_point: Point, max_point: Point, } -struct RestoreState { - scroll_position: Vector2F, - selections: Vec>, -} - pub enum Event { Dismissed, } @@ -65,15 +59,11 @@ impl GoToLine { cx.subscribe(&line_editor, Self::on_line_editor_event) .detach(); - let (restore_state, cursor_point, max_point) = active_editor.update(cx, |editor, cx| { - let restore_state = Some(RestoreState { - scroll_position: editor.scroll_position(cx), - selections: editor.local_selections::(cx), - }); - + let (scroll_position, cursor_point, max_point) = active_editor.update(cx, |editor, cx| { + let scroll_position = editor.scroll_position(cx); let buffer = editor.buffer().read(cx).read(cx); ( - restore_state, + Some(scroll_position), editor.newest_selection(&buffer).head(), buffer.max_point(), ) @@ -83,8 +73,7 @@ impl GoToLine { settings: settings.clone(), line_editor, active_editor, - restore_state, - line_selection_id: None, + prev_scroll_position: scroll_position, cursor_point, max_point, } @@ -105,7 +94,14 @@ impl GoToLine { } fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - self.restore_state.take(); + self.prev_scroll_position.take(); + self.active_editor.update(cx, |active_editor, cx| { + if let Some(rows) = active_editor.highlighted_rows() { + let snapshot = active_editor.snapshot(cx).display_snapshot; + let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot); + active_editor.select_ranges([position..position], Some(Autoscroll::Center), cx); + } + }); cx.emit(Event::Dismissed); } @@ -139,18 +135,13 @@ impl GoToLine { column.map(|column| column.saturating_sub(1)).unwrap_or(0), ) }) { - self.line_selection_id = self.active_editor.update(cx, |active_editor, cx| { + self.active_editor.update(cx, |active_editor, cx| { let snapshot = active_editor.snapshot(cx).display_snapshot; let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); let display_point = point.to_display_point(&snapshot); let row = display_point.row(); - active_editor.select_ranges([point..point], Some(Autoscroll::Center), cx); active_editor.set_highlighted_rows(Some(row..row + 1)); - Some( - active_editor - .newest_selection::(&snapshot.buffer_snapshot) - .id, - ) + active_editor.request_autoscroll(Autoscroll::Center, cx); }); cx.notify(); } @@ -164,17 +155,11 @@ impl Entity for GoToLine { type Event = Event; fn release(&mut self, cx: &mut MutableAppContext) { - let line_selection_id = self.line_selection_id.take(); - let restore_state = self.restore_state.take(); + let scroll_position = self.prev_scroll_position.take(); self.active_editor.update(cx, |editor, cx| { editor.set_highlighted_rows(None); - if let Some((line_selection_id, restore_state)) = line_selection_id.zip(restore_state) { - let newest_selection = - editor.newest_selection::(&editor.buffer().read(cx).read(cx)); - if line_selection_id == newest_selection.id { - editor.set_scroll_position(restore_state.scroll_position, cx); - editor.update_selections(restore_state.selections, None, cx); - } + if let Some(scroll_position) = scroll_position { + editor.set_scroll_position(scroll_position, cx); } }) } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index b11abac86d..46bf088c31 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -1,6 +1,6 @@ use editor::{ - display_map::ToDisplayPoint, Anchor, AnchorRangeExt, Autoscroll, Editor, EditorSettings, - ToPoint, + display_map::ToDisplayPoint, Anchor, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, + EditorSettings, ToPoint, }; use fuzzy::StringMatch; use gpui::{ @@ -12,7 +12,7 @@ use gpui::{ AppContext, Axis, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle, }; -use language::{Outline, Selection}; +use language::Outline; use ordered_float::OrderedFloat; use postage::watch; use std::{ @@ -45,19 +45,13 @@ struct OutlineView { active_editor: ViewHandle, outline: Outline, selected_match_index: usize, - restore_state: Option, - symbol_selection_id: Option, + prev_scroll_position: Option, matches: Vec, query_editor: ViewHandle, list_state: UniformListState, settings: watch::Receiver, } -struct RestoreState { - scroll_position: Vector2F, - selections: Vec>, -} - pub enum Event { Dismissed, } @@ -132,20 +126,12 @@ impl OutlineView { cx.subscribe(&query_editor, Self::on_query_editor_event) .detach(); - let restore_state = editor.update(cx, |editor, cx| { - Some(RestoreState { - scroll_position: editor.scroll_position(cx), - selections: editor.local_selections::(cx), - }) - }); - let mut this = Self { handle: cx.weak_handle(), - active_editor: editor, matches: Default::default(), selected_match_index: 0, - restore_state, - symbol_selection_id: None, + prev_scroll_position: Some(editor.update(cx, |editor, cx| editor.scroll_position(cx))), + active_editor: editor, outline, query_editor, list_state: Default::default(), @@ -207,39 +193,37 @@ impl OutlineView { if navigate { let selected_match = &self.matches[self.selected_match_index]; let outline_item = &self.outline.items[selected_match.candidate_id]; - self.symbol_selection_id = self.active_editor.update(cx, |active_editor, cx| { + self.active_editor.update(cx, |active_editor, cx| { let snapshot = active_editor.snapshot(cx).display_snapshot; let buffer_snapshot = &snapshot.buffer_snapshot; let start = outline_item.range.start.to_point(&buffer_snapshot); let end = outline_item.range.end.to_point(&buffer_snapshot); let display_rows = start.to_display_point(&snapshot).row() ..end.to_display_point(&snapshot).row() + 1; - active_editor.select_ranges([start..start], Some(Autoscroll::Center), cx); active_editor.set_highlighted_rows(Some(display_rows)); - Some(active_editor.newest_selection::(&buffer_snapshot).id) + active_editor.request_autoscroll(Autoscroll::Center, cx); }); } cx.notify(); } fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - self.restore_state.take(); + self.prev_scroll_position.take(); + self.active_editor.update(cx, |active_editor, cx| { + if let Some(rows) = active_editor.highlighted_rows() { + let snapshot = active_editor.snapshot(cx).display_snapshot; + let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot); + active_editor.select_ranges([position..position], Some(Autoscroll::Center), cx); + } + }); cx.emit(Event::Dismissed); } fn restore_active_editor(&mut self, cx: &mut MutableAppContext) { - let symbol_selection_id = self.symbol_selection_id.take(); self.active_editor.update(cx, |editor, cx| { editor.set_highlighted_rows(None); - if let Some((symbol_selection_id, restore_state)) = - symbol_selection_id.zip(self.restore_state.as_ref()) - { - let newest_selection = - editor.newest_selection::(&editor.buffer().read(cx).read(cx)); - if symbol_selection_id == newest_selection.id { - editor.set_scroll_position(restore_state.scroll_position, cx); - editor.update_selections(restore_state.selections.clone(), None, cx); - } + if let Some(scroll_position) = self.prev_scroll_position { + editor.set_scroll_position(scroll_position, cx); } }) } diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 895e6ad471..a762b1ca39 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1131,12 +1131,6 @@ impl Buffer { } } - pub fn can_resolve(&self, anchor: &Anchor) -> bool { - *anchor == Anchor::min() - || *anchor == Anchor::max() - || self.version.observed(anchor.timestamp) - } - pub fn peek_undo_stack(&self) -> Option<&Transaction> { self.history.undo_stack.last() } @@ -1648,6 +1642,12 @@ impl BufferSnapshot { } } + pub fn can_resolve(&self, anchor: &Anchor) -> bool { + *anchor == Anchor::min() + || *anchor == Anchor::max() + || self.version.observed(anchor.timestamp) + } + pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { self.visible_text.clip_offset(offset, bias) } diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index e85fefdabd..f3ea330ca3 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "workspace" version = "0.1.0" -edition = "2018" +edition = "2021" [lib] path = "src/workspace.rs" @@ -17,6 +17,7 @@ gpui = { path = "../gpui" } language = { path = "../language" } project = { path = "../project" } theme = { path = "../theme" } +util = { path = "../util" } anyhow = "1.0.38" log = "0.4" parking_lot = "0.11.1" diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 731db29d63..be0e3f05fb 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1,15 +1,18 @@ use super::{ItemViewHandle, SplitDirection}; -use crate::{ItemHandle, Settings, Workspace}; +use crate::{ItemHandle, ItemView, Settings, WeakItemViewHandle, Workspace}; +use collections::{HashMap, VecDeque}; use gpui::{ action, elements::*, geometry::{rect::RectF, vector::vec2f}, keymap::Binding, platform::CursorStyle, - Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, + Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext, ViewHandle, }; use postage::watch; -use std::cmp; +use project::ProjectPath; +use std::{any::Any, cell::RefCell, cmp, mem, rc::Rc}; +use util::ResultExt; action!(Split, SplitDirection); action!(ActivateItem, usize); @@ -17,6 +20,10 @@ action!(ActivatePrevItem); action!(ActivateNextItem); action!(CloseActiveItem); action!(CloseItem, usize); +action!(GoBack); +action!(GoForward); + +const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; pub fn init(cx: &mut MutableAppContext) { cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| { @@ -37,6 +44,12 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(|pane: &mut Pane, action: &Split, cx| { pane.split(action.0, cx); }); + cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| { + Pane::go_back(workspace, cx).detach(); + }); + cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| { + Pane::go_forward(workspace, cx).detach(); + }); cx.add_bindings(vec![ Binding::new("shift-cmd-{", ActivatePrevItem, Some("Pane")), @@ -46,6 +59,8 @@ pub fn init(cx: &mut MutableAppContext) { Binding::new("cmd-k down", Split(SplitDirection::Down), Some("Pane")), Binding::new("cmd-k left", Split(SplitDirection::Left), Some("Pane")), Binding::new("cmd-k right", Split(SplitDirection::Right), Some("Pane")), + Binding::new("ctrl--", GoBack, Some("Pane")), + Binding::new("shift-ctrl-_", GoForward, Some("Pane")), ]); } @@ -57,29 +72,49 @@ pub enum Event { const MAX_TAB_TITLE_LEN: usize = 24; -#[derive(Debug, Eq, PartialEq)] -pub struct State { - pub tabs: Vec, -} - -#[derive(Debug, Eq, PartialEq)] -pub struct TabState { - pub title: String, - pub active: bool, -} - pub struct Pane { item_views: Vec<(usize, Box)>, - active_item: usize, + active_item_index: usize, settings: watch::Receiver, + navigation: Rc, +} + +#[derive(Default)] +pub struct Navigation(RefCell); + +#[derive(Default)] +struct NavigationHistory { + mode: NavigationMode, + backward_stack: VecDeque, + forward_stack: VecDeque, + paths_by_item: HashMap, +} + +#[derive(Copy, Clone)] +enum NavigationMode { + Normal, + GoingBack, + GoingForward, +} + +impl Default for NavigationMode { + fn default() -> Self { + Self::Normal + } +} + +pub struct NavigationEntry { + pub item_view: Box, + pub data: Option>, } impl Pane { pub fn new(settings: watch::Receiver) -> Self { Self { item_views: Vec::new(), - active_item: 0, + active_item_index: 0, settings, + navigation: Default::default(), } } @@ -87,6 +122,98 @@ impl Pane { cx.emit(Event::Activate); } + pub fn go_back(workspace: &mut Workspace, cx: &mut ViewContext) -> Task<()> { + Self::navigate_history( + workspace, + workspace.active_pane().clone(), + NavigationMode::GoingBack, + cx, + ) + } + + pub fn go_forward(workspace: &mut Workspace, cx: &mut ViewContext) -> Task<()> { + Self::navigate_history( + workspace, + workspace.active_pane().clone(), + NavigationMode::GoingForward, + cx, + ) + } + + fn navigate_history( + workspace: &mut Workspace, + pane: ViewHandle, + mode: NavigationMode, + cx: &mut ViewContext, + ) -> Task<()> { + let to_load = pane.update(cx, |pane, cx| { + // Retrieve the weak item handle from the history. + let entry = pane.navigation.pop(mode)?; + + // If the item is still present in this pane, then activate it. + if let Some(index) = entry + .item_view + .upgrade(cx) + .and_then(|v| pane.index_for_item_view(v.as_ref())) + { + if let Some(item_view) = pane.active_item() { + pane.navigation.set_mode(mode); + item_view.deactivated(cx); + pane.navigation.set_mode(NavigationMode::Normal); + } + + pane.active_item_index = index; + pane.focus_active_item(cx); + if let Some(data) = entry.data { + pane.active_item()?.navigate(data, cx); + } + cx.notify(); + None + } + // If the item is no longer present in this pane, then retrieve its + // project path in order to reopen it. + else { + pane.navigation + .0 + .borrow_mut() + .paths_by_item + .get(&entry.item_view.id()) + .cloned() + .map(|project_path| (project_path, entry)) + } + }); + + if let Some((project_path, entry)) = to_load { + // If the item was no longer present, then load it again from its previous path. + let pane = pane.downgrade(); + let task = workspace.load_path(project_path, cx); + cx.spawn(|workspace, mut cx| async move { + let item = task.await; + if let Some(pane) = cx.read(|cx| pane.upgrade(cx)) { + if let Some(item) = item.log_err() { + workspace.update(&mut cx, |workspace, cx| { + pane.update(cx, |p, _| p.navigation.set_mode(mode)); + let item_view = workspace.open_item_in_pane(item, &pane, cx); + pane.update(cx, |p, _| p.navigation.set_mode(NavigationMode::Normal)); + + if let Some(data) = entry.data { + item_view.navigate(data, cx); + } + }); + } else { + workspace + .update(&mut cx, |workspace, cx| { + Self::navigate_history(workspace, pane, mode, cx) + }) + .await; + } + } + }) + } else { + Task::ready(()) + } + } + pub fn open_item( &mut self, item_handle: T, @@ -104,18 +231,19 @@ impl Pane { } } - let item_view = item_handle.add_view(cx.window_id(), workspace, cx); + let item_view = + item_handle.add_view(cx.window_id(), workspace, self.navigation.clone(), cx); self.add_item_view(item_view.boxed_clone(), cx); item_view } pub fn add_item_view( &mut self, - item_view: Box, + mut item_view: Box, cx: &mut ViewContext, ) { item_view.added_to_pane(cx); - let item_idx = cmp::min(self.active_item + 1, self.item_views.len()); + let item_idx = cmp::min(self.active_item_index + 1, self.item_views.len()); self.item_views .insert(item_idx, (item_view.item_handle(cx).id(), item_view)); self.activate_item(item_idx, cx); @@ -135,7 +263,7 @@ impl Pane { pub fn active_item(&self) -> Option> { self.item_views - .get(self.active_item) + .get(self.active_item_index) .map(|(_, view)| view.clone()) } @@ -151,41 +279,68 @@ impl Pane { pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext) { if index < self.item_views.len() { - self.active_item = index; + let prev_active_item_ix = mem::replace(&mut self.active_item_index, index); + if prev_active_item_ix != self.active_item_index { + self.item_views[prev_active_item_ix].1.deactivated(cx); + } self.focus_active_item(cx); cx.notify(); } } pub fn activate_prev_item(&mut self, cx: &mut ViewContext) { - if self.active_item > 0 { - self.active_item -= 1; + let mut index = self.active_item_index; + if index > 0 { + index -= 1; } else if self.item_views.len() > 0 { - self.active_item = self.item_views.len() - 1; + index = self.item_views.len() - 1; } - self.focus_active_item(cx); - cx.notify(); + self.activate_item(index, cx); } pub fn activate_next_item(&mut self, cx: &mut ViewContext) { - if self.active_item + 1 < self.item_views.len() { - self.active_item += 1; + let mut index = self.active_item_index; + if index + 1 < self.item_views.len() { + index += 1; } else { - self.active_item = 0; + index = 0; } - self.focus_active_item(cx); - cx.notify(); + self.activate_item(index, cx); } pub fn close_active_item(&mut self, cx: &mut ViewContext) { if !self.item_views.is_empty() { - self.close_item(self.item_views[self.active_item].1.id(), cx) + self.close_item(self.item_views[self.active_item_index].1.id(), cx) } } - pub fn close_item(&mut self, item_id: usize, cx: &mut ViewContext) { - self.item_views.retain(|(_, item)| item.id() != item_id); - self.active_item = cmp::min(self.active_item, self.item_views.len().saturating_sub(1)); + pub fn close_item(&mut self, item_view_id: usize, cx: &mut ViewContext) { + let mut item_ix = 0; + self.item_views.retain(|(_, item_view)| { + if item_view.id() == item_view_id { + if item_ix == self.active_item_index { + item_view.deactivated(cx); + } + + let mut navigation = self.navigation.0.borrow_mut(); + if let Some(path) = item_view.project_path(cx) { + navigation.paths_by_item.insert(item_view.id(), path); + } else { + navigation.paths_by_item.remove(&item_view.id()); + } + + item_ix += 1; + false + } else { + item_ix += 1; + true + } + }); + self.active_item_index = cmp::min( + self.active_item_index, + self.item_views.len().saturating_sub(1), + ); + if self.item_views.is_empty() { cx.emit(Event::Remove); } @@ -210,7 +365,7 @@ impl Pane { let tabs = MouseEventHandler::new::(cx.view_id(), cx, |mouse_state, cx| { let mut row = Flex::row(); for (ix, (_, item_view)) in self.item_views.iter().enumerate() { - let is_active = ix == self.active_item; + let is_active = ix == self.active_item_index; row.add_child({ let mut title = item_view.title(cx); @@ -380,3 +535,59 @@ impl View for Pane { self.focus_active_item(cx); } } + +impl Navigation { + pub fn pop_backward(&self) -> Option { + self.0.borrow_mut().backward_stack.pop_back() + } + + pub fn pop_forward(&self) -> Option { + self.0.borrow_mut().forward_stack.pop_back() + } + + fn pop(&self, mode: NavigationMode) -> Option { + match mode { + NavigationMode::Normal => None, + NavigationMode::GoingBack => self.pop_backward(), + NavigationMode::GoingForward => self.pop_forward(), + } + } + + fn set_mode(&self, mode: NavigationMode) { + self.0.borrow_mut().mode = mode; + } + + pub fn push(&self, data: Option, cx: &mut ViewContext) { + let mut state = self.0.borrow_mut(); + match state.mode { + NavigationMode::Normal => { + if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { + state.backward_stack.pop_front(); + } + state.backward_stack.push_back(NavigationEntry { + item_view: Box::new(cx.weak_handle()), + data: data.map(|data| Box::new(data) as Box), + }); + state.forward_stack.clear(); + } + NavigationMode::GoingBack => { + if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { + state.forward_stack.pop_front(); + } + state.forward_stack.push_back(NavigationEntry { + item_view: Box::new(cx.weak_handle()), + data: data.map(|data| Box::new(data) as Box), + }); + } + NavigationMode::GoingForward => { + if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { + state.backward_stack.pop_front(); + } + state.backward_stack.push_back(NavigationEntry { + item_view: Box::new(cx.weak_handle()), + data: data.map(|data| Box::new(data) as Box), + }); + } + } + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index fe540186a9..531d976fd5 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -33,9 +33,11 @@ use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItem use status_bar::StatusBar; pub use status_bar::StatusItemView; use std::{ + any::Any, future::Future, hash::{Hash, Hasher}, path::{Path, PathBuf}, + rc::Rc, sync::Arc, }; use theme::{Theme, ThemeRegistry}; @@ -135,6 +137,7 @@ pub trait Item: Entity + Sized { fn build_view( handle: ModelHandle, workspace: &Workspace, + navigation: Rc, cx: &mut ViewContext, ) -> Self::View; @@ -144,6 +147,8 @@ pub trait Item: Entity + Sized { pub trait ItemView: View { type ItemHandle: ItemHandle; + fn deactivated(&mut self, _: &mut ViewContext) {} + fn navigate(&mut self, _: Box, _: &mut ViewContext) {} fn item_handle(&self, cx: &AppContext) -> Self::ItemHandle; fn title(&self, cx: &AppContext) -> String; fn project_path(&self, cx: &AppContext) -> Option; @@ -185,6 +190,7 @@ pub trait ItemHandle: Send + Sync { &self, window_id: usize, workspace: &Workspace, + navigation: Rc, cx: &mut MutableAppContext, ) -> Box; fn boxed_clone(&self) -> Box; @@ -204,7 +210,9 @@ pub trait ItemViewHandle { fn project_path(&self, cx: &AppContext) -> Option; fn boxed_clone(&self) -> Box; fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option>; - fn added_to_pane(&self, cx: &mut ViewContext); + fn added_to_pane(&mut self, cx: &mut ViewContext); + fn deactivated(&self, cx: &mut MutableAppContext); + fn navigate(&self, data: Box, cx: &mut MutableAppContext); fn id(&self) -> usize; fn to_any(&self) -> AnyViewHandle; fn is_dirty(&self, cx: &AppContext) -> bool; @@ -220,6 +228,11 @@ pub trait ItemViewHandle { ) -> Task>; } +pub trait WeakItemViewHandle { + fn id(&self) -> usize; + fn upgrade(&self, cx: &AppContext) -> Option>; +} + impl ItemHandle for ModelHandle { fn id(&self) -> usize { self.id() @@ -229,9 +242,12 @@ impl ItemHandle for ModelHandle { &self, window_id: usize, workspace: &Workspace, + navigation: Rc, cx: &mut MutableAppContext, ) -> Box { - Box::new(cx.add_view(window_id, |cx| T::build_view(self.clone(), workspace, cx))) + Box::new(cx.add_view(window_id, |cx| { + T::build_view(self.clone(), workspace, navigation, cx) + })) } fn boxed_clone(&self) -> Box { @@ -260,9 +276,10 @@ impl ItemHandle for Box { &self, window_id: usize, workspace: &Workspace, + navigation: Rc, cx: &mut MutableAppContext, ) -> Box { - ItemHandle::add_view(self.as_ref(), window_id, workspace, cx) + ItemHandle::add_view(self.as_ref(), window_id, workspace, navigation, cx) } fn boxed_clone(&self) -> Box { @@ -330,7 +347,7 @@ impl ItemViewHandle for ViewHandle { .map(|handle| Box::new(handle) as Box) } - fn added_to_pane(&self, cx: &mut ViewContext) { + fn added_to_pane(&mut self, cx: &mut ViewContext) { cx.subscribe(self, |pane, item, event, cx| { if T::should_close_item_on_event(event) { pane.close_item(item.id(), cx); @@ -349,6 +366,14 @@ impl ItemViewHandle for ViewHandle { .detach(); } + fn deactivated(&self, cx: &mut MutableAppContext) { + self.update(cx, |this, cx| this.deactivated(cx)); + } + + fn navigate(&self, data: Box, cx: &mut MutableAppContext) { + self.update(cx, |this, cx| this.navigate(data, cx)); + } + fn save(&self, cx: &mut MutableAppContext) -> Result>> { self.update(cx, |item, cx| item.save(cx)) } @@ -399,6 +424,17 @@ impl Clone for Box { } } +impl WeakItemViewHandle for WeakViewHandle { + fn id(&self) -> usize { + self.id() + } + + fn upgrade(&self, cx: &AppContext) -> Option> { + self.upgrade(cx) + .map(|v| Box::new(v) as Box) + } +} + #[derive(Clone)] pub struct WorkspaceParams { pub project: ModelHandle, @@ -722,40 +758,15 @@ impl Workspace { } } - #[must_use] pub fn open_path( &mut self, path: ProjectPath, cx: &mut ViewContext, ) -> Task, Arc>> { - if let Some(existing_item) = self.item_for_path(&path, cx) { - return Task::ready(Ok(self.open_item(existing_item, cx))); - } - - let worktree = match self.project.read(cx).worktree_for_id(path.worktree_id, cx) { - Some(worktree) => worktree, - None => { - return Task::ready(Err(Arc::new(anyhow!( - "worktree {} does not exist", - path.worktree_id - )))); - } - }; - - let project_path = path.clone(); - let path_openers = self.path_openers.clone(); - let open_task = worktree.update(cx, |worktree, cx| { - for opener in path_openers.iter() { - if let Some(task) = opener.open(worktree, project_path.clone(), cx) { - return task; - } - } - Task::ready(Err(anyhow!("no opener found for path {:?}", project_path))) - }); - + let load_task = self.load_path(path, cx); let pane = self.active_pane().clone().downgrade(); cx.spawn(|this, mut cx| async move { - let item = open_task.await?; + let item = load_task.await?; this.update(&mut cx, |this, cx| { let pane = pane .upgrade(&cx) @@ -765,6 +776,34 @@ impl Workspace { }) } + pub fn load_path( + &mut self, + path: ProjectPath, + cx: &mut ViewContext, + ) -> Task>> { + if let Some(existing_item) = self.item_for_path(&path, cx) { + return Task::ready(Ok(existing_item)); + } + + let worktree = match self.project.read(cx).worktree_for_id(path.worktree_id, cx) { + Some(worktree) => worktree, + None => { + return Task::ready(Err(anyhow!("worktree {} does not exist", path.worktree_id))); + } + }; + + let project_path = path.clone(); + let path_openers = self.path_openers.clone(); + worktree.update(cx, |worktree, cx| { + for opener in path_openers.iter() { + if let Some(task) = opener.open(worktree, project_path.clone(), cx) { + return task; + } + } + Task::ready(Err(anyhow!("no opener found for path {:?}", project_path))) + }) + } + fn item_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option> { self.items .iter() diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 61300d1f56..f5aeec861a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -124,8 +124,8 @@ fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) { #[cfg(test)] mod tests { use super::*; - use editor::Editor; - use gpui::MutableAppContext; + use editor::{DisplayPoint, Editor}; + use gpui::{MutableAppContext, TestAppContext, ViewHandle}; use project::ProjectPath; use serde_json::json; use std::{ @@ -136,11 +136,11 @@ mod tests { use theme::DEFAULT_THEME_NAME; use util::test::temp_tree; use workspace::{ - open_paths, pane, ItemView, ItemViewHandle, OpenNew, SplitDirection, WorkspaceHandle, + open_paths, pane, ItemView, ItemViewHandle, OpenNew, Pane, SplitDirection, WorkspaceHandle, }; #[gpui::test] - async fn test_open_paths_action(mut cx: gpui::TestAppContext) { + async fn test_open_paths_action(mut cx: TestAppContext) { let app_state = cx.update(test_app_state); let dir = temp_tree(json!({ "a": { @@ -193,7 +193,7 @@ mod tests { } #[gpui::test] - async fn test_new_empty_workspace(mut cx: gpui::TestAppContext) { + async fn test_new_empty_workspace(mut cx: TestAppContext) { let app_state = cx.update(test_app_state); cx.update(|cx| { workspace::init(cx); @@ -230,7 +230,7 @@ mod tests { } #[gpui::test] - async fn test_open_entry(mut cx: gpui::TestAppContext) { + async fn test_open_entry(mut cx: TestAppContext) { let app_state = cx.update(test_app_state); app_state .fs @@ -350,7 +350,7 @@ mod tests { } #[gpui::test] - async fn test_open_paths(mut cx: gpui::TestAppContext) { + async fn test_open_paths(mut cx: TestAppContext) { let app_state = cx.update(test_app_state); let fs = app_state.fs.as_fake(); fs.insert_dir("/dir1").await.unwrap(); @@ -420,7 +420,7 @@ mod tests { } #[gpui::test] - async fn test_save_conflicting_item(mut cx: gpui::TestAppContext) { + async fn test_save_conflicting_item(mut cx: TestAppContext) { let app_state = cx.update(test_app_state); let fs = app_state.fs.as_fake(); fs.insert_tree("/root", json!({ "a.txt": "" })).await; @@ -469,7 +469,7 @@ mod tests { } #[gpui::test] - async fn test_open_and_save_new_file(mut cx: gpui::TestAppContext) { + async fn test_open_and_save_new_file(mut cx: TestAppContext) { let app_state = cx.update(test_app_state); app_state.fs.as_fake().insert_dir("/root").await.unwrap(); let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx)); @@ -585,9 +585,7 @@ mod tests { } #[gpui::test] - async fn test_setting_language_when_saving_as_single_file_worktree( - mut cx: gpui::TestAppContext, - ) { + async fn test_setting_language_when_saving_as_single_file_worktree(mut cx: TestAppContext) { let app_state = cx.update(test_app_state); app_state.fs.as_fake().insert_dir("/root").await.unwrap(); let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx)); @@ -630,7 +628,7 @@ mod tests { } #[gpui::test] - async fn test_pane_actions(mut cx: gpui::TestAppContext) { + async fn test_pane_actions(mut cx: TestAppContext) { cx.update(|cx| pane::init(cx)); let app_state = cx.update(test_app_state); app_state @@ -693,6 +691,182 @@ mod tests { }); } + #[gpui::test] + async fn test_navigation(mut cx: TestAppContext) { + let app_state = cx.update(test_app_state); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": { + "file1": "contents 1\n".repeat(20), + "file2": "contents 2\n".repeat(20), + "file3": "contents 3\n".repeat(20), + }, + }), + ) + .await; + let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx)); + let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); + workspace + .update(&mut cx, |workspace, cx| { + workspace.add_worktree(Path::new("/root"), cx) + }) + .await + .unwrap(); + cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) + .await; + let entries = cx.read(|cx| workspace.file_project_paths(cx)); + let file1 = entries[0].clone(); + let file2 = entries[1].clone(); + let file3 = entries[2].clone(); + + let editor1 = workspace + .update(&mut cx, |w, cx| w.open_path(file1.clone(), cx)) + .await + .unwrap() + .to_any() + .downcast::() + .unwrap(); + editor1.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)], cx); + }); + let editor2 = workspace + .update(&mut cx, |w, cx| w.open_path(file2.clone(), cx)) + .await + .unwrap() + .to_any() + .downcast::() + .unwrap(); + let editor3 = workspace + .update(&mut cx, |w, cx| w.open_path(file3.clone(), cx)) + .await + .unwrap() + .to_any() + .downcast::() + .unwrap(); + editor3.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)], cx); + }); + assert_eq!( + active_location(&workspace, &mut cx), + (file3.clone(), DisplayPoint::new(15, 0)) + ); + + workspace + .update(&mut cx, |w, cx| Pane::go_back(w, cx)) + .await; + assert_eq!( + active_location(&workspace, &mut cx), + (file3.clone(), DisplayPoint::new(0, 0)) + ); + + workspace + .update(&mut cx, |w, cx| Pane::go_back(w, cx)) + .await; + assert_eq!( + active_location(&workspace, &mut cx), + (file2.clone(), DisplayPoint::new(0, 0)) + ); + + workspace + .update(&mut cx, |w, cx| Pane::go_back(w, cx)) + .await; + assert_eq!( + active_location(&workspace, &mut cx), + (file1.clone(), DisplayPoint::new(10, 0)) + ); + + workspace + .update(&mut cx, |w, cx| Pane::go_back(w, cx)) + .await; + assert_eq!( + active_location(&workspace, &mut cx), + (file1.clone(), DisplayPoint::new(0, 0)) + ); + + // Go back one more time and ensure we don't navigate past the first item in the history. + workspace + .update(&mut cx, |w, cx| Pane::go_back(w, cx)) + .await; + assert_eq!( + active_location(&workspace, &mut cx), + (file1.clone(), DisplayPoint::new(0, 0)) + ); + + workspace + .update(&mut cx, |w, cx| Pane::go_forward(w, cx)) + .await; + assert_eq!( + active_location(&workspace, &mut cx), + (file1.clone(), DisplayPoint::new(10, 0)) + ); + + workspace + .update(&mut cx, |w, cx| Pane::go_forward(w, cx)) + .await; + assert_eq!( + active_location(&workspace, &mut cx), + (file2.clone(), DisplayPoint::new(0, 0)) + ); + + // Go forward to an item that has been closed, ensuring it gets re-opened at the same + // location. + workspace.update(&mut cx, |workspace, cx| { + workspace + .active_pane() + .update(cx, |pane, cx| pane.close_item(editor3.id(), cx)); + drop(editor3); + }); + workspace + .update(&mut cx, |w, cx| Pane::go_forward(w, cx)) + .await; + assert_eq!( + active_location(&workspace, &mut cx), + (file3.clone(), DisplayPoint::new(0, 0)) + ); + + // Go back to an item that has been closed and removed from disk, ensuring it gets skipped. + workspace + .update(&mut cx, |workspace, cx| { + workspace + .active_pane() + .update(cx, |pane, cx| pane.close_item(editor2.id(), cx)); + drop(editor2); + app_state.fs.as_fake().remove(Path::new("/root/a/file2")) + }) + .await + .unwrap(); + workspace + .update(&mut cx, |w, cx| Pane::go_back(w, cx)) + .await; + assert_eq!( + active_location(&workspace, &mut cx), + (file1.clone(), DisplayPoint::new(10, 0)) + ); + workspace + .update(&mut cx, |w, cx| Pane::go_forward(w, cx)) + .await; + assert_eq!( + active_location(&workspace, &mut cx), + (file3.clone(), DisplayPoint::new(0, 0)) + ); + + fn active_location( + workspace: &ViewHandle, + cx: &mut TestAppContext, + ) -> (ProjectPath, DisplayPoint) { + workspace.update(cx, |workspace, cx| { + let item = workspace.active_item(cx).unwrap(); + let editor = item.to_any().downcast::().unwrap(); + let selections = editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)); + (item.project_path(cx).unwrap(), selections[0].start) + }) + } + } + #[gpui::test] fn test_bundled_themes(cx: &mut MutableAppContext) { let app_state = test_app_state(cx);