Merge pull request #349 from zed-industries/navigation-history

Antonio Scandurra created

Add a navigation history

Change summary

Cargo.lock                            |   1 
crates/diagnostics/src/diagnostics.rs |   5 
crates/editor/src/editor.rs           | 182 +++++++++++++++--
crates/editor/src/items.rs            |  39 +++
crates/editor/src/multi_buffer.rs     |  12 +
crates/go_to_line/src/go_to_line.rs   |  55 ++---
crates/outline/src/outline.rs         |  52 +---
crates/text/src/text.rs               |  12 
crates/workspace/Cargo.toml           |   3 
crates/workspace/src/pane.rs          | 281 +++++++++++++++++++++++++---
crates/workspace/src/workspace.rs     |  83 ++++++--
crates/zed/src/zed.rs                 | 200 +++++++++++++++++++-
12 files changed, 741 insertions(+), 184 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5677,6 +5677,7 @@ dependencies = [
  "project",
  "serde_json",
  "theme",
+ "util",
 ]
 
 [[package]]

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<Self>,
         workspace: &Workspace,
+        _: Rc<Navigation>,
         cx: &mut ViewContext<Self::View>,
     ) -> Self::View {
         ProjectDiagnosticsEditor::new(handle, workspace.weak_handle(), workspace.settings(), cx)

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<Arc<str>>,
     highlighted_rows: Option<Range<u32>>,
+    navigation: Option<Rc<Navigation>>,
 }
 
 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>) -> 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::<Point>(&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::<Point>(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::<usize>(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::<usize>(buffer);
             let mut selections = self.local_selections(cx);
             selections.retain(|selection| selection.id != newest_selection.id);
             self.update_selections::<usize>(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<Self>)
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn select_display_ranges<'a, T>(&mut self, ranges: T, cx: &mut ViewContext<Self>)
     where
         T: IntoIterator<Item = &'a Range<DisplayPoint>>,
     {
@@ -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<Point>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        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<Self>) {
         let mut selection = self.local_selections::<usize>(cx).first().unwrap().clone();
         selection.set_head(self.buffer.read(cx).read(cx).len());
@@ -3205,14 +3261,14 @@ impl Editor {
         &self,
         snapshot: &MultiBufferSnapshot,
     ) -> Selection<D> {
-        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<Anchor>> {
+        self.pending_selection
+            .as_ref()
+            .map(|s| &s.selection)
+            .or_else(|| self.selections.iter().max_by_key(|s| s.id))
     }
 
     pub fn update_selections<T>(
@@ -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<Self>) {
+    pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext<Self>) {
         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);

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<Navigation>,
         cx: &mut MutableAppContext,
     ) -> Box<dyn ItemViewHandle> {
         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<dyn std::any::Any>, cx: &mut ViewContext<Self>) {
+        if let Some(data) = data.downcast_ref::<NavigationData>() {
+            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<Self>) {
+        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()
     }

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<T: ToOffset>(&self, range: Range<T>) -> bool {
         let start = range.start.to_offset(self);
         let end = range.end.to_offset(self);

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<Settings>,
     line_editor: ViewHandle<Editor>,
     active_editor: ViewHandle<Editor>,
-    restore_state: Option<RestoreState>,
-    line_selection_id: Option<usize>,
+    prev_scroll_position: Option<Vector2F>,
     cursor_point: Point,
     max_point: Point,
 }
 
-struct RestoreState {
-    scroll_position: Vector2F,
-    selections: Vec<Selection<usize>>,
-}
-
 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::<usize>(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>) {
-        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::<usize>(&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::<usize>(&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);
             }
         })
     }

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<Editor>,
     outline: Outline<Anchor>,
     selected_match_index: usize,
-    restore_state: Option<RestoreState>,
-    symbol_selection_id: Option<usize>,
+    prev_scroll_position: Option<Vector2F>,
     matches: Vec<StringMatch>,
     query_editor: ViewHandle<Editor>,
     list_state: UniformListState,
     settings: watch::Receiver<Settings>,
 }
 
-struct RestoreState {
-    scroll_position: Vector2F,
-    selections: Vec<Selection<usize>>,
-}
-
 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::<usize>(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::<usize>(&buffer_snapshot).id)
+                active_editor.request_autoscroll(Autoscroll::Center, cx);
             });
         }
         cx.notify();
     }
 
     fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
-        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::<usize>(&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);
             }
         })
     }

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)
     }

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"

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<TabState>,
+pub struct Pane {
+    item_views: Vec<(usize, Box<dyn ItemViewHandle>)>,
+    active_item_index: usize,
+    settings: watch::Receiver<Settings>,
+    navigation: Rc<Navigation>,
+}
+
+#[derive(Default)]
+pub struct Navigation(RefCell<NavigationHistory>);
+
+#[derive(Default)]
+struct NavigationHistory {
+    mode: NavigationMode,
+    backward_stack: VecDeque<NavigationEntry>,
+    forward_stack: VecDeque<NavigationEntry>,
+    paths_by_item: HashMap<usize, ProjectPath>,
 }
 
-#[derive(Debug, Eq, PartialEq)]
-pub struct TabState {
-    pub title: String,
-    pub active: bool,
+#[derive(Copy, Clone)]
+enum NavigationMode {
+    Normal,
+    GoingBack,
+    GoingForward,
 }
 
-pub struct Pane {
-    item_views: Vec<(usize, Box<dyn ItemViewHandle>)>,
-    active_item: usize,
-    settings: watch::Receiver<Settings>,
+impl Default for NavigationMode {
+    fn default() -> Self {
+        Self::Normal
+    }
+}
+
+pub struct NavigationEntry {
+    pub item_view: Box<dyn WeakItemViewHandle>,
+    pub data: Option<Box<dyn Any>>,
 }
 
 impl Pane {
     pub fn new(settings: watch::Receiver<Settings>) -> 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<Workspace>) -> Task<()> {
+        Self::navigate_history(
+            workspace,
+            workspace.active_pane().clone(),
+            NavigationMode::GoingBack,
+            cx,
+        )
+    }
+
+    pub fn go_forward(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Task<()> {
+        Self::navigate_history(
+            workspace,
+            workspace.active_pane().clone(),
+            NavigationMode::GoingForward,
+            cx,
+        )
+    }
+
+    fn navigate_history(
+        workspace: &mut Workspace,
+        pane: ViewHandle<Pane>,
+        mode: NavigationMode,
+        cx: &mut ViewContext<Workspace>,
+    ) -> 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<T>(
         &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<dyn ItemViewHandle>,
+        mut item_view: Box<dyn ItemViewHandle>,
         cx: &mut ViewContext<Self>,
     ) {
         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<Box<dyn ItemViewHandle>> {
         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<Self>) {
         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<Self>) {
-        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<Self>) {
-        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<Self>) {
         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>) {
-        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<Self>) {
+        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::<Tabs, _, _, _>(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<NavigationEntry> {
+        self.0.borrow_mut().backward_stack.pop_back()
+    }
+
+    pub fn pop_forward(&self) -> Option<NavigationEntry> {
+        self.0.borrow_mut().forward_stack.pop_back()
+    }
+
+    fn pop(&self, mode: NavigationMode) -> Option<NavigationEntry> {
+        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<D: 'static + Any, T: ItemView>(&self, data: Option<D>, cx: &mut ViewContext<T>) {
+        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<dyn Any>),
+                });
+                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<dyn Any>),
+                });
+            }
+            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<dyn Any>),
+                });
+            }
+        }
+    }
+}

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<Self>,
         workspace: &Workspace,
+        navigation: Rc<Navigation>,
         cx: &mut ViewContext<Self::View>,
     ) -> Self::View;
 
@@ -144,6 +147,8 @@ pub trait Item: Entity + Sized {
 pub trait ItemView: View {
     type ItemHandle: ItemHandle;
 
+    fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
+    fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) {}
     fn item_handle(&self, cx: &AppContext) -> Self::ItemHandle;
     fn title(&self, cx: &AppContext) -> String;
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
@@ -185,6 +190,7 @@ pub trait ItemHandle: Send + Sync {
         &self,
         window_id: usize,
         workspace: &Workspace,
+        navigation: Rc<Navigation>,
         cx: &mut MutableAppContext,
     ) -> Box<dyn ItemViewHandle>;
     fn boxed_clone(&self) -> Box<dyn ItemHandle>;
@@ -204,7 +210,9 @@ pub trait ItemViewHandle {
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
     fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
     fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>>;
-    fn added_to_pane(&self, cx: &mut ViewContext<Pane>);
+    fn added_to_pane(&mut self, cx: &mut ViewContext<Pane>);
+    fn deactivated(&self, cx: &mut MutableAppContext);
+    fn navigate(&self, data: Box<dyn Any>, 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<anyhow::Result<()>>;
 }
 
+pub trait WeakItemViewHandle {
+    fn id(&self) -> usize;
+    fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemViewHandle>>;
+}
+
 impl<T: Item> ItemHandle for ModelHandle<T> {
     fn id(&self) -> usize {
         self.id()
@@ -229,9 +242,12 @@ impl<T: Item> ItemHandle for ModelHandle<T> {
         &self,
         window_id: usize,
         workspace: &Workspace,
+        navigation: Rc<Navigation>,
         cx: &mut MutableAppContext,
     ) -> Box<dyn ItemViewHandle> {
-        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<dyn ItemHandle> {
@@ -260,9 +276,10 @@ impl ItemHandle for Box<dyn ItemHandle> {
         &self,
         window_id: usize,
         workspace: &Workspace,
+        navigation: Rc<Navigation>,
         cx: &mut MutableAppContext,
     ) -> Box<dyn ItemViewHandle> {
-        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<dyn ItemHandle> {
@@ -330,7 +347,7 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
         .map(|handle| Box::new(handle) as Box<dyn ItemViewHandle>)
     }
 
-    fn added_to_pane(&self, cx: &mut ViewContext<Pane>) {
+    fn added_to_pane(&mut self, cx: &mut ViewContext<Pane>) {
         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<T: ItemView> ItemViewHandle for ViewHandle<T> {
         .detach();
     }
 
+    fn deactivated(&self, cx: &mut MutableAppContext) {
+        self.update(cx, |this, cx| this.deactivated(cx));
+    }
+
+    fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) {
+        self.update(cx, |this, cx| this.navigate(data, cx));
+    }
+
     fn save(&self, cx: &mut MutableAppContext) -> Result<Task<Result<()>>> {
         self.update(cx, |item, cx| item.save(cx))
     }
@@ -399,6 +424,17 @@ impl Clone for Box<dyn ItemHandle> {
     }
 }
 
+impl<T: ItemView> WeakItemViewHandle for WeakViewHandle<T> {
+    fn id(&self) -> usize {
+        self.id()
+    }
+
+    fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemViewHandle>> {
+        self.upgrade(cx)
+            .map(|v| Box::new(v) as Box<dyn ItemViewHandle>)
+    }
+}
+
 #[derive(Clone)]
 pub struct WorkspaceParams {
     pub project: ModelHandle<Project>,
@@ -722,46 +758,49 @@ impl Workspace {
         }
     }
 
-    #[must_use]
     pub fn open_path(
         &mut self,
         path: ProjectPath,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<Box<dyn ItemViewHandle>, Arc<anyhow::Error>>> {
+        let load_task = self.load_path(path, cx);
+        let pane = self.active_pane().clone().downgrade();
+        cx.spawn(|this, mut cx| async move {
+            let item = load_task.await?;
+            this.update(&mut cx, |this, cx| {
+                let pane = pane
+                    .upgrade(&cx)
+                    .ok_or_else(|| anyhow!("could not upgrade pane reference"))?;
+                Ok(this.open_item_in_pane(item, &pane, cx))
+            })
+        })
+    }
+
+    pub fn load_path(
+        &mut self,
+        path: ProjectPath,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<Box<dyn ItemHandle>>> {
         if let Some(existing_item) = self.item_for_path(&path, cx) {
-            return Task::ready(Ok(self.open_item(existing_item, 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(Arc::new(anyhow!(
-                    "worktree {} does not exist",
-                    path.worktree_id
-                ))));
+                return Task::ready(Err(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| {
+        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 pane = self.active_pane().clone().downgrade();
-        cx.spawn(|this, mut cx| async move {
-            let item = open_task.await?;
-            this.update(&mut cx, |this, cx| {
-                let pane = pane
-                    .upgrade(&cx)
-                    .ok_or_else(|| anyhow!("could not upgrade pane reference"))?;
-                Ok(this.open_item_in_pane(item, &pane, cx))
-            })
         })
     }
 

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(&params, 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::<Editor>()
+            .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::<Editor>()
+            .unwrap();
+        let editor3 = workspace
+            .update(&mut cx, |w, cx| w.open_path(file3.clone(), cx))
+            .await
+            .unwrap()
+            .to_any()
+            .downcast::<Editor>()
+            .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<Workspace>,
+            cx: &mut TestAppContext,
+        ) -> (ProjectPath, DisplayPoint) {
+            workspace.update(cx, |workspace, cx| {
+                let item = workspace.active_item(cx).unwrap();
+                let editor = item.to_any().downcast::<Editor>().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);