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

Add a navigation history
This commit is contained in:
Antonio Scandurra 2022-01-19 09:02:23 +01:00 committed by GitHub
commit 7c233ed682
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 751 additions and 194 deletions

1
Cargo.lock generated
View file

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

View file

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

View file

@ -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);

View file

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

View file

@ -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);

View file

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

View file

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

View file

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

View file

@ -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"

View file

@ -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>,
}
#[derive(Debug, Eq, PartialEq)]
pub struct TabState {
pub title: String,
pub active: bool,
}
pub struct Pane {
item_views: Vec<(usize, Box<dyn ItemViewHandle>)>,
active_item: usize,
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(Copy, Clone)]
enum NavigationMode {
Normal,
GoingBack,
GoingForward,
}
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>),
});
}
}
}
}

View file

@ -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,40 +758,15 @@ impl Workspace {
}
}
#[must_use]
pub fn open_path(
&mut self,
path: ProjectPath,
cx: &mut ViewContext<Self>,
) -> Task<Result<Box<dyn ItemViewHandle>, Arc<anyhow::Error>>> {
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<Self>,
) -> Task<Result<Box<dyn ItemHandle>>> {
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<Box<dyn ItemHandle>> {
self.items
.iter()

View file

@ -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);