Refactor explore cursor code (#12979)

`explore` has 3 cursor-related structs that are extensively used to
track the currently shown "window" of the data being shown. I was
finding the cursor code quite difficult to follow, so this PR:
- rewrites the base `Cursor` struct from scratch, with some tests
- makes big changes to `WindowCursor`
- renames `XYCursor` to `WindowCursor2D`
- makes some of the cursor functions fallible as a start towards better
error handling
- changes lots of function names to things that I find more intuitive
- adds comments, including ASCII diagrams to explain how the cursors
work

More work could be done (I'd like to review/change more function names
in `WindowCursor` and `WindowCursor2D` and add more tests), but this is
the limit of what I can get done in a weekend. I think this part of the
code is in a better place now.

# Testing performed

I did a lot of manual testing in the record view and binary viewer,
moving around with arrow keys / page up+down / home+end.

This can definitely wait until after the release freeze, this area has
very few automated tests and it'd be good to let the changes bake a bit.
This commit is contained in:
Reilly Wood 2024-06-04 19:50:11 -07:00 committed by GitHub
parent e4104d0792
commit a9c2349ada
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 504 additions and 334 deletions

View File

@ -17,16 +17,20 @@ use crate::{
ConfigMap, Frame, Transition, ViewInfo,
},
util::create_map,
views::cursor::Position,
};
use self::binary_widget::{BinarySettings, BinaryStyle, BinaryWidget};
use super::{cursor::XYCursor, Layout, View, ViewConfig};
use super::{cursor::WindowCursor2D, Layout, View, ViewConfig};
/// An interactive view that displays binary data in a hex dump format.
/// Not finished; many aspects are still WIP.
#[derive(Debug, Clone)]
pub struct BinaryView {
data: Vec<u8>,
cursor: XYCursor,
// HACK: we are only using the vertical dimension of the cursor, should we use a plain old WindowCursor?
cursor: WindowCursor2D,
settings: Settings,
}
@ -40,7 +44,7 @@ impl BinaryView {
pub fn new(data: Vec<u8>) -> Self {
Self {
data,
cursor: XYCursor::default(),
cursor: WindowCursor2D::default(),
settings: Settings::default(),
}
}
@ -95,12 +99,13 @@ impl View for BinaryView {
let count_rows =
BinaryWidget::new(&self.data, self.settings.opts, Default::default()).count_lines();
self.cursor = XYCursor::new(count_rows, 0);
// TODO: refactor View so setup() is fallible and we don't have to panic here
self.cursor = WindowCursor2D::new(count_rows, 1).expect("Failed to create XYCursor");
}
}
fn create_binary_widget(v: &BinaryView) -> BinaryWidget<'_> {
let start_line = v.cursor.row_starts_at();
let start_line = v.cursor.window_origin().row;
let count_elements =
BinaryWidget::new(&[], v.settings.opts, Default::default()).count_elements();
let index = start_line * count_elements;
@ -203,7 +208,7 @@ fn config_get_usize(config: &ConfigMap, key: &str, default: usize) -> usize {
.unwrap_or(default)
}
fn create_report(cursor: XYCursor) -> Report {
fn create_report(cursor: WindowCursor2D) -> Report {
let covered_percent = report_row_position(cursor);
let cursor = report_cursor_position(cursor);
let mode = report_mode_name();
@ -216,8 +221,8 @@ fn report_mode_name() -> String {
String::from("VIEW")
}
fn report_row_position(cursor: XYCursor) -> String {
if cursor.row_starts_at() == 0 {
fn report_row_position(cursor: WindowCursor2D) -> String {
if cursor.window_origin().row == 0 {
return String::from("Top");
}
@ -233,10 +238,9 @@ fn report_row_position(cursor: XYCursor) -> String {
}
}
fn report_cursor_position(cursor: XYCursor) -> String {
let rows_seen = cursor.row_starts_at();
let columns_seen = cursor.column_starts_at();
format!("{rows_seen},{columns_seen}")
fn report_cursor_position(cursor: WindowCursor2D) -> String {
let Position { row, column } = cursor.window_origin();
format!("{row},{column}")
}
fn get_percentage(value: usize, max: usize) -> usize {

View File

@ -1,71 +1,125 @@
mod windowcursor;
mod xycursor;
mod window_cursor;
mod window_cursor_2d;
pub use windowcursor::WindowCursor;
pub use xycursor::XYCursor;
use anyhow::{bail, Result};
pub use window_cursor::WindowCursor;
pub use window_cursor_2d::{Position, WindowCursor2D};
/// A 1-dimensional cursor to track a position from 0 to N
///
/// Say we have a cursor with size=9, at position 3:
/// 0 1 2 3 4 5 6 7 8 9
/// | | | C | | | | | |
///
/// After moving forward by 2 steps:
/// 0 1 2 3 4 5 6 7 8 9
/// | | | | | C | | | |
///
/// After moving backward by 6 steps (clamped to 0):
/// 0 1 2 3 4 5 6 7 8 9
/// C | | | | | | | | |
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Cursor {
index: usize,
limit: usize,
struct Cursor {
/// The current position of the cursor
position: usize,
/// The number of distinct positions the cursor can be at
size: usize,
}
impl Cursor {
pub fn new(limit: usize) -> Self {
Self { index: 0, limit }
}
#[allow(dead_code)]
pub fn get_index(&self) -> usize {
self.index
}
pub fn cap(&self) -> usize {
self.limit - self.index
}
pub fn set(&mut self, i: usize) -> bool {
if i >= self.limit {
return false;
/// Constructor to create a new Cursor
pub fn new(size: usize) -> Result<Self> {
if size == 0 {
bail!("Size cannot be zero");
}
self.index = i;
true
}
pub fn limit(&mut self, i: usize) -> bool {
if self.index > self.limit {
self.index = self.limit.saturating_sub(1);
return false;
}
self.limit = i;
if self.index >= self.limit {
self.index = self.limit.saturating_sub(1);
}
true
Ok(Cursor { position: 0, size })
}
/// The max position the cursor can be at
pub fn end(&self) -> usize {
self.limit
self.size - 1
}
pub fn next(&mut self, i: usize) -> bool {
if self.index + i == self.limit {
return false;
/// Set the position to a specific value within the bounds [0, end]
pub fn set_position(&mut self, pos: usize) {
if pos <= self.end() {
self.position = pos;
} else {
// Clamp the position to end if out of bounds
self.position = self.end();
}
self.index += i;
true
}
pub fn prev(&mut self, i: usize) -> bool {
if self.index < i {
return false;
/// Set the size of the cursor. The position is clamped if it exceeds the new size
pub fn set_size(&mut self, size: usize) -> Result<()> {
if size == 0 {
bail!("Size cannot be zero");
}
self.size = size;
if self.position > self.end() {
self.position = self.end();
}
Ok(())
}
self.index -= i;
true
/// Move the cursor forward by a specified number of steps
pub fn move_forward(&mut self, steps: usize) {
if self.position + steps <= self.end() {
self.position += steps;
} else {
self.position = self.end();
}
}
/// Move the cursor backward by a specified number of steps
pub fn move_backward(&mut self, steps: usize) {
if self.position >= steps {
self.position -= steps;
} else {
self.position = 0;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cursor_set_position() {
// from 0 to 9
let mut cursor = Cursor::new(10).unwrap();
cursor.set_position(5);
assert_eq!(cursor.position, 5);
cursor.set_position(15);
assert_eq!(cursor.position, 9);
}
#[test]
fn test_cursor_move_forward() {
// from 0 to 9
let mut cursor = Cursor::new(10).unwrap();
assert_eq!(cursor.position, 0);
cursor.move_forward(3);
assert_eq!(cursor.position, 3);
cursor.move_forward(10);
assert_eq!(cursor.position, 9);
}
#[test]
fn test_cursor_move_backward() {
// from 0 to 9
let mut cursor = Cursor::new(10).unwrap();
cursor.move_backward(3);
assert_eq!(cursor.position, 0);
cursor.move_forward(5);
assert_eq!(cursor.position, 5);
cursor.move_backward(3);
assert_eq!(cursor.position, 2);
cursor.move_backward(3);
assert_eq!(cursor.position, 0);
}
}

View File

@ -0,0 +1,146 @@
use std::cmp::min;
use super::Cursor;
use anyhow::{bail, Ok, Result};
/// WindowCursor provides a mechanism to navigate through a 1-dimensional range
/// using a smaller movable window within the view.
///
/// View: The larger context or total allowable range for navigation.
/// Window: The smaller, focused subset of the view.
///
/// Example:
/// ```plaintext
/// 1. Initial view of size 20 with a window of size 5. The absolute cursor position starts at 0.
/// View :
/// |--------------------|
/// Window :
/// |X====|
///
/// 2. After advancing the window by 3, the absolute cursor position becomes 3.
/// View :
/// |--------------------|
/// Window :
/// |X====|
///
/// 3. After advancing the cursor inside the window by 2, the absolute cursor position becomes 5.
/// View :
/// |--------------------|
/// Window :
/// |==X==|
/// ```
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct WindowCursor {
view: Cursor,
window: Cursor,
}
impl WindowCursor {
pub fn new(view_size: usize, window_size: usize) -> Result<Self> {
if window_size > view_size {
bail!("Window size cannot be greater than view size");
}
Ok(Self {
view: Cursor::new(view_size)?,
window: Cursor::new(window_size)?,
})
}
pub fn absolute_position(&self) -> usize {
self.window_starts_at() + self.window.position
}
pub fn window_relative_position(&self) -> usize {
self.window.position
}
pub fn window_starts_at(&self) -> usize {
self.view.position
}
pub fn window_size(&self) -> usize {
self.window.size
}
pub fn end(&self) -> usize {
self.view.end()
}
pub fn set_window_start_position(&mut self, i: usize) {
self.view.set_position(i)
}
pub fn move_window_to_end(&mut self) {
self.view.set_position(self.end() - self.window_size() + 1);
}
pub fn set_window_size(&mut self, new_size: usize) -> Result<()> {
if new_size > self.view.size {
// TODO: should we return an error here or clamp? the Ok is copying existing behavior
return Ok(());
}
self.window.set_size(new_size)?;
Ok(())
}
pub fn next_n(&mut self, n: usize) {
for _ in 0..n {
self.next();
}
}
pub fn next(&mut self) {
if self.absolute_position() >= self.end() {
return;
}
if self.window_relative_position() == self.window.end() {
self.view.move_forward(1);
} else {
self.window.move_forward(1);
}
}
pub fn next_window(&mut self) {
self.move_cursor_to_end_of_window();
// move window forward by window size, or less if that would send it off the end of the view
let window_end = self.window_starts_at() + self.window_size() - 1;
let distance_from_window_end_to_view_end = self.end() - window_end;
self.view.move_forward(min(
distance_from_window_end_to_view_end,
self.window_size(),
));
}
pub fn prev_n(&mut self, n: usize) {
for _ in 0..n {
self.prev();
}
}
pub fn prev(&mut self) {
if self.window_relative_position() == 0 {
self.view.move_backward(1);
} else {
self.window.move_backward(1);
}
}
pub fn prev_window(&mut self) {
self.move_cursor_to_start_of_window();
// move the whole window back
self.view.move_backward(self.window_size());
}
pub fn move_cursor_to_start_of_window(&mut self) {
self.window.move_backward(self.window_size());
}
pub fn move_cursor_to_end_of_window(&mut self) {
self.window.move_forward(self.window_size());
}
}

View File

@ -0,0 +1,172 @@
use super::WindowCursor;
use anyhow::Result;
/// `WindowCursor2D` manages a 2-dimensional "window" onto a grid of cells, with a cursor that can point to a specific cell.
/// For example, consider a 3x3 grid of cells:
///
/// +---+---+---+
/// | a | b | c |
/// |---|---|---|
/// | d | e | f |
/// |---|---|---|
/// | g | h | i |
/// +---+---+---+
///
/// A `WindowCursor2D` can be used to track the currently visible section of this grid.
/// For example, a 2x2 window onto this grid could initially show the top left 2x2 section:
///
/// +---+---+
/// | a | b |
/// |---|---|
/// | d | e |
/// +---+---+
///
/// Moving the window down 1 row:
///
/// +---+---+
/// | d | e |
/// |---|---|
/// | g | h |
/// +---+---+
///
/// Inside the window, the cursor can point to a specific cell.
#[derive(Debug, Default, Clone, Copy)]
pub struct WindowCursor2D {
x: WindowCursor,
y: WindowCursor,
}
pub struct Position {
pub row: usize,
pub column: usize,
}
impl WindowCursor2D {
pub fn new(count_rows: usize, count_columns: usize) -> Result<Self> {
Ok(Self {
x: WindowCursor::new(count_columns, count_columns)?,
y: WindowCursor::new(count_rows, count_rows)?,
})
}
pub fn set_window_size(&mut self, count_rows: usize, count_columns: usize) -> Result<()> {
self.x.set_window_size(count_columns)?;
self.y.set_window_size(count_rows)?;
Ok(())
}
pub fn set_window_start_position(&mut self, row: usize, col: usize) {
self.x.set_window_start_position(col);
self.y.set_window_start_position(row);
}
/// The absolute position of the cursor in the grid (0-indexed, row only)
pub fn row(&self) -> usize {
self.y.absolute_position()
}
/// The absolute position of the cursor in the grid (0-indexed, column only)
pub fn column(&self) -> usize {
self.x.absolute_position()
}
/// The absolute position of the cursor in the grid (0-indexed)
pub fn position(&self) -> Position {
Position {
row: self.row(),
column: self.column(),
}
}
pub fn row_limit(&self) -> usize {
self.y.end()
}
pub fn window_origin(&self) -> Position {
Position {
row: self.y.window_starts_at(),
column: self.x.window_starts_at(),
}
}
pub fn window_relative_position(&self) -> Position {
Position {
row: self.y.window_relative_position(),
column: self.x.window_relative_position(),
}
}
pub fn window_width_in_columns(&self) -> usize {
self.x.window_size()
}
pub fn next_row(&mut self) {
self.y.next_n(1)
}
pub fn next_row_page(&mut self) {
self.y.next_window()
}
pub fn row_move_to_end(&mut self) {
self.y.move_window_to_end();
self.y.move_cursor_to_end_of_window();
}
pub fn row_move_to_start(&mut self) {
self.y.move_cursor_to_start_of_window();
self.y.set_window_start_position(0);
}
pub fn prev_row(&mut self) {
self.y.prev()
}
pub fn prev_row_page(&mut self) {
self.y.prev_window()
}
pub fn next_column(&mut self) {
self.x.next()
}
pub fn next_column_by(&mut self, i: usize) {
self.x.next_n(i)
}
pub fn prev_column(&mut self) {
self.x.prev()
}
pub fn prev_column_by(&mut self, i: usize) {
self.x.prev_n(i)
}
pub fn next_column_i(&mut self) {
self.x
.set_window_start_position(self.x.window_starts_at() + 1)
}
pub fn prev_column_i(&mut self) {
if self.x.window_starts_at() == 0 {
return;
}
self.x
.set_window_start_position(self.x.window_starts_at() - 1)
}
pub fn next_row_i(&mut self) {
self.y
.set_window_start_position(self.y.window_starts_at() + 1)
}
pub fn prev_row_i(&mut self) {
if self.y.window_starts_at() == 0 {
return;
}
self.y
.set_window_start_position(self.y.window_starts_at() - 1)
}
}

View File

@ -1,106 +0,0 @@
use super::Cursor;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct WindowCursor {
view: Cursor,
window: Cursor,
}
impl WindowCursor {
pub fn new(limit: usize, window: usize) -> Option<Self> {
if window > limit {
return None;
}
Some(Self {
view: Cursor::new(limit),
window: Cursor::new(window),
})
}
pub fn index(&self) -> usize {
self.view.index + self.window.index
}
pub fn offset(&self) -> usize {
self.window.index
}
pub fn starts_at(&self) -> usize {
self.view.index
}
pub fn cap(&self) -> usize {
self.view.cap()
}
pub fn window(&self) -> usize {
self.window.end()
}
pub fn end(&self) -> usize {
self.view.end()
}
pub fn set_window_at(&mut self, i: usize) -> bool {
self.view.set(i)
}
pub fn set_window(&mut self, i: usize) -> bool {
if i > self.view.end() {
return false;
}
self.window.limit(i)
}
pub fn next(&mut self, i: usize) -> bool {
if i > self.cap() {
return false;
}
let mut rest = 0;
for y in 0..i {
if !self.window.next(1) {
rest = i - y;
break;
}
}
for _ in 0..rest {
if self.index() + 1 == self.end() {
return rest != i;
}
self.view.next(1);
}
true
}
pub fn next_window(&mut self) -> bool {
let end_cursor = self.window() - self.offset();
self.next(end_cursor);
let mut index_move = self.window();
if index_move + self.starts_at() >= self.end() {
index_move = self.end() - self.starts_at();
}
self.next(index_move)
}
pub fn prev(&mut self, i: usize) -> bool {
for _ in 0..i {
if !self.window.prev(1) {
self.view.prev(1);
}
}
true
}
pub fn prev_window(&mut self) -> bool {
self.prev(self.window() + self.offset())
}
}

View File

@ -1,122 +0,0 @@
use super::WindowCursor;
#[derive(Debug, Default, Clone, Copy)]
pub struct XYCursor {
x: WindowCursor,
y: WindowCursor,
}
impl XYCursor {
pub fn new(count_rows: usize, count_columns: usize) -> Self {
Self {
x: WindowCursor::new(count_columns, count_columns).expect("..."),
y: WindowCursor::new(count_rows, count_rows).expect("..."),
}
}
pub fn set_window(&mut self, count_rows: usize, count_columns: usize) {
self.x.set_window(count_columns);
self.y.set_window(count_rows);
}
pub fn set_position(&mut self, row: usize, col: usize) {
self.x.set_window_at(col);
self.y.set_window_at(row);
}
pub fn row(&self) -> usize {
self.y.index()
}
pub fn column(&self) -> usize {
self.x.index()
}
pub fn row_limit(&self) -> usize {
self.y.end()
}
pub fn row_starts_at(&self) -> usize {
self.y.starts_at()
}
pub fn column_starts_at(&self) -> usize {
self.x.starts_at()
}
pub fn row_window(&self) -> usize {
self.y.offset()
}
pub fn column_window(&self) -> usize {
self.x.offset()
}
pub fn column_window_size(&self) -> usize {
self.x.window()
}
pub fn next_row(&mut self) -> bool {
self.y.next(1)
}
pub fn next_row_page(&mut self) -> bool {
self.y.next_window()
}
pub fn row_move_to_end(&mut self) -> bool {
self.y.next(self.y.cap())
}
pub fn row_move_to_start(&mut self) -> bool {
self.y.prev(self.y.index())
}
pub fn prev_row(&mut self) -> bool {
self.y.prev(1)
}
pub fn prev_row_page(&mut self) -> bool {
self.y.prev_window()
}
pub fn next_column(&mut self) -> bool {
self.x.next(1)
}
pub fn next_column_by(&mut self, i: usize) -> bool {
self.x.next(i)
}
pub fn prev_column(&mut self) -> bool {
self.x.prev(1)
}
pub fn prev_column_by(&mut self, i: usize) -> bool {
self.x.prev(i)
}
pub fn next_column_i(&mut self) -> bool {
self.x.set_window_at(self.x.starts_at() + 1)
}
pub fn prev_column_i(&mut self) -> bool {
if self.x.starts_at() == 0 {
return false;
}
self.x.set_window_at(self.x.starts_at() - 1)
}
pub fn next_row_i(&mut self) -> bool {
self.y.set_window_at(self.y.starts_at() + 1)
}
pub fn prev_row_i(&mut self) -> bool {
if self.y.starts_at() == 0 {
return false;
}
self.y.set_window_at(self.y.starts_at() - 1)
}
}

View File

@ -160,7 +160,7 @@ impl View for InteractiveView<'_> {
.as_mut()
.expect("we know that we have a table cause of a flag");
let was_at_the_top = table.get_current_position().0 == 0;
let was_at_the_top = table.get_cursor_position().row == 0;
if was_at_the_top && matches!(key.code, KeyCode::Up | KeyCode::PageUp) {
self.view_mode = false;

View File

@ -1,4 +1,6 @@
use super::{colored_text_widget::ColoredTextWidget, cursor::XYCursor, Layout, View, ViewConfig};
use super::{
colored_text_widget::ColoredTextWidget, cursor::WindowCursor2D, Layout, View, ViewConfig,
};
use crate::{
nu_common::{NuSpan, NuText},
pager::{report::Report, Frame, Transition, ViewInfo},
@ -17,7 +19,7 @@ use std::cmp::max;
pub struct Preview {
underlying_value: Option<Value>,
lines: Vec<String>,
cursor: XYCursor,
cursor: WindowCursor2D,
}
impl Preview {
@ -26,8 +28,9 @@ impl Preview {
.lines()
.map(|line| line.replace('\t', " ")) // tui: doesn't support TAB
.collect();
let cursor = XYCursor::new(lines.len(), usize::MAX);
// TODO: refactor so this is fallible and returns a Result instead of panicking
let cursor = WindowCursor2D::new(lines.len(), usize::MAX).expect("Failed to create cursor");
Self {
lines,
cursor,
@ -38,10 +41,11 @@ impl Preview {
impl View for Preview {
fn draw(&mut self, f: &mut Frame, area: Rect, _: ViewConfig<'_>, layout: &mut Layout) {
self.cursor
.set_window(area.height as usize, area.width as usize);
let _ = self
.cursor
.set_window_size(area.height as usize, area.width as usize);
let lines = &self.lines[self.cursor.row_starts_at()..];
let lines = &self.lines[self.cursor.window_origin().row..];
for (i, line) in lines.iter().enumerate().take(area.height as usize) {
let text_widget = ColoredTextWidget::new(line, self.cursor.column());
let plain_text = text_widget.get_plain_text(area.width as usize);
@ -65,13 +69,13 @@ impl View for Preview {
match key.code {
KeyCode::Left => {
self.cursor
.prev_column_by(max(1, self.cursor.column_window_size() / 2));
.prev_column_by(max(1, self.cursor.window_width_in_columns() / 2));
Some(Transition::Ok)
}
KeyCode::Right => {
self.cursor
.next_column_by(max(1, self.cursor.column_window_size() / 2));
.next_column_by(max(1, self.cursor.window_width_in_columns() / 2));
Some(Transition::Ok)
}
@ -128,7 +132,7 @@ impl View for Preview {
//
// todo: improve somehow?
self.cursor.set_position(row, 0);
self.cursor.set_window_start_position(row, 0);
true
}
@ -152,7 +156,7 @@ fn set_status_end(view: &Preview, info: &mut ViewInfo) {
}
fn set_status_top(view: &Preview, info: &mut ViewInfo) {
if view.cursor.row_starts_at() == 0 {
if view.cursor.window_origin().row == 0 {
info.status = Some(Report::info("TOP"));
} else {
info.status = Some(Report::default());

View File

@ -2,7 +2,7 @@ mod table_widget;
use self::table_widget::{TableStyle, TableWidget, TableWidgetState};
use super::{
cursor::XYCursor,
cursor::{Position, WindowCursor2D},
util::{make_styled_string, nu_style_to_tui},
Layout, View, ViewConfig,
};
@ -133,22 +133,22 @@ impl<'a> RecordView<'a> {
layer.reset_cursor();
}
pub fn get_current_position(&self) -> (usize, usize) {
/// Get the current position of the cursor in the table as a whole
pub fn get_cursor_position(&self) -> Position {
let layer = self.get_layer_last();
(layer.cursor.row(), layer.cursor.column())
layer.cursor.position()
}
pub fn get_current_window(&self) -> (usize, usize) {
/// Get the current position of the cursor in the window being shown
pub fn get_cursor_position_in_window(&self) -> Position {
let layer = self.get_layer_last();
(layer.cursor.row_window(), layer.cursor.column_window())
layer.cursor.window_relative_position()
}
pub fn get_current_offset(&self) -> (usize, usize) {
/// Get the origin of the window being shown. (0,0), top left corner.
pub fn get_window_origin(&self) -> Position {
let layer = self.get_layer_last();
(
layer.cursor.row_starts_at(),
layer.cursor.column_starts_at(),
)
layer.cursor.window_origin()
}
pub fn set_cursor_mode(&mut self) {
@ -160,7 +160,7 @@ impl<'a> RecordView<'a> {
}
pub fn get_current_value(&self) -> Value {
let (row, column) = self.get_current_position();
let Position { row, column } = self.get_cursor_position();
let layer = self.get_layer_last();
let (row, column) = match layer.orientation {
@ -175,15 +175,17 @@ impl<'a> RecordView<'a> {
}
}
fn create_tablew(&'a self, cfg: ViewConfig<'a>) -> TableWidget<'a> {
/// Create a table widget.
/// WARNING: this is currently really slow on large data sets.
/// It creates a string representation of every cell in the table and looks at every row for lscolorize.
fn create_table_widget(&'a self, cfg: ViewConfig<'a>) -> TableWidget<'a> {
let layer = self.get_layer_last();
let mut data = convert_records_to_string(&layer.records, cfg.nu_config, cfg.style_computer);
lscolorize(&layer.columns, &mut data, cfg.lscolors);
let headers = layer.columns.as_ref();
let style_computer = cfg.style_computer;
let (row, column) = self.get_current_offset();
let Position { row, column } = self.get_window_origin();
TableWidget::new(
headers,
@ -199,11 +201,17 @@ impl<'a> RecordView<'a> {
fn update_cursors(&mut self, rows: usize, columns: usize) {
match self.get_layer_last().orientation {
Orientation::Top => {
self.get_layer_last_mut().cursor.set_window(rows, columns);
let _ = self
.get_layer_last_mut()
.cursor
.set_window_size(rows, columns);
}
Orientation::Left => {
self.get_layer_last_mut().cursor.set_window(rows, columns);
let _ = self
.get_layer_last_mut()
.cursor
.set_window_size(rows, columns);
}
}
}
@ -226,7 +234,10 @@ impl<'a> RecordView<'a> {
impl View for RecordView<'_> {
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) {
let mut table_layout = TableWidgetState::default();
let table = self.create_tablew(cfg);
// TODO: creating the table widget is O(N) where N is the number of cells in the grid.
// Way too slow to do on every draw call!
// To make explore work for larger data sets, this needs to be improved.
let table = self.create_table_widget(cfg);
f.render_stateful_widget(table, area, &mut table_layout);
*layout = table_layout.layout;
@ -234,7 +245,7 @@ impl View for RecordView<'_> {
self.update_cursors(table_layout.count_rows, table_layout.count_columns);
if self.mode == UIMode::Cursor {
let (row, column) = self.get_current_window();
let Position { row, column } = self.get_cursor_position_in_window();
let info = get_element_info(
layout,
row,
@ -308,7 +319,9 @@ impl View for RecordView<'_> {
for (column, _) in cells.iter().enumerate() {
if i == pos {
self.get_layer_last_mut().cursor.set_position(row, column);
self.get_layer_last_mut()
.cursor
.set_window_start_position(row, column);
return true;
}
@ -374,7 +387,7 @@ pub struct RecordLayer<'a> {
orientation: Orientation,
name: Option<String>,
was_transposed: bool,
cursor: XYCursor,
cursor: WindowCursor2D,
}
impl<'a> RecordLayer<'a> {
@ -384,7 +397,10 @@ impl<'a> RecordLayer<'a> {
) -> Self {
let columns = columns.into();
let records = records.into();
let cursor = XYCursor::new(records.len(), columns.len());
// TODO: refactor so this is fallible and returns a Result instead of panicking
let cursor =
WindowCursor2D::new(records.len(), columns.len()).expect("Failed to create cursor");
Self {
columns,
@ -420,7 +436,9 @@ impl<'a> RecordLayer<'a> {
}
fn reset_cursor(&mut self) {
self.cursor = XYCursor::new(self.count_rows(), self.count_columns());
// TODO: refactor so this is fallible and returns a Result instead of panicking
self.cursor = WindowCursor2D::new(self.count_rows(), self.count_columns())
.expect("Failed to create cursor");
}
}
@ -634,7 +652,9 @@ fn tail_data(state: &mut RecordView<'_>, page_size: usize) {
let layer = state.get_layer_last_mut();
let count_rows = layer.records.len();
if count_rows > page_size {
layer.cursor.set_position(count_rows - page_size, 0);
layer
.cursor
.set_window_start_position(count_rows - page_size, 0);
}
}
@ -731,20 +751,18 @@ fn build_table_as_record(v: &RecordView) -> Value {
Value::record(record, NuSpan::unknown())
}
fn report_cursor_position(mode: UIMode, cursor: XYCursor) -> String {
fn report_cursor_position(mode: UIMode, cursor: WindowCursor2D) -> String {
if mode == UIMode::Cursor {
let row = cursor.row();
let column = cursor.column();
let Position { row, column } = cursor.position();
format!("{row},{column}")
} else {
let rows_seen = cursor.row_starts_at();
let columns_seen = cursor.column_starts_at();
format!("{rows_seen},{columns_seen}")
let Position { row, column } = cursor.window_origin();
format!("{row},{column}")
}
}
fn report_row_position(cursor: XYCursor) -> String {
if cursor.row_starts_at() == 0 {
fn report_row_position(cursor: WindowCursor2D) -> String {
if cursor.window_origin().row == 0 {
String::from("Top")
} else {
let percent_rows = get_percentage(cursor.row(), cursor.row_limit());