use clap::ArgEnum;
use crate::flatjson::{FlatJson, Index, OptionIndex};
use crate::types::TTYDimensions;
#[derive(PartialEq, Eq, Copy, Clone, Debug, ArgEnum)]
pub enum Mode {
Line,
Data,
}
const DEFAULT_SCROLLOFF: u16 = 3;
pub struct JsonViewer {
pub flatjson: FlatJson,
pub top_row: Index,
pub focused_row: Index,
desired_depth: usize,
jump_distance: Option<usize>,
pub dimensions: TTYDimensions,
pub scrolloff_setting: u16,
pub mode: Mode,
}
impl JsonViewer {
pub fn new(flatjson: FlatJson, mode: Mode) -> JsonViewer {
JsonViewer {
flatjson,
top_row: 0,
focused_row: 0,
desired_depth: 0,
jump_distance: None,
dimensions: TTYDimensions::default(),
scrolloff_setting: DEFAULT_SCROLLOFF,
mode,
}
}
}
#[derive(Debug, Copy, Clone)]
pub enum Action {
#[allow(dead_code)]
NoOp,
MoveUp(usize),
MoveDown(usize),
MoveLeft,
MoveRight,
MoveTo(Index),
MoveUpUntilDepthChange,
MoveDownUntilDepthChange,
FocusParent,
FocusPrevSibling(usize),
FocusNextSibling(usize),
FocusFirstSibling,
FocusLastSibling,
FocusTop,
FocusBottom,
FocusMatchingPair,
ScrollUp(usize),
ScrollDown(usize),
JumpUp(Option<usize>),
JumpDown(Option<usize>),
PageUp(usize),
PageDown(usize),
MoveFocusedLineToTop,
MoveFocusedLineToCenter,
MoveFocusedLineToBottom,
Click(u16),
ToggleCollapsed,
CollapseNodeAndSiblings,
ExpandNodeAndSiblings,
ToggleMode,
ResizeViewerDimensions(TTYDimensions),
}
impl JsonViewer {
pub fn perform_action(&mut self, action: Action) {
let track_window = JsonViewer::should_refocus_window(&action);
let reset_desired_depth = JsonViewer::should_reset_desired_depth(&action);
match action {
Action::NoOp => {}
Action::MoveUp(n) => self.move_up(n),
Action::MoveDown(n) => self.move_down(n),
Action::MoveLeft => self.move_left(),
Action::MoveRight => self.move_right(),
Action::MoveTo(index) => self.focused_row = index,
Action::MoveUpUntilDepthChange => self.move_up_until_depth_change(),
Action::MoveDownUntilDepthChange => self.move_down_until_depth_change(),
Action::FocusParent => self.focus_parent(),
Action::FocusPrevSibling(n) => self.focus_prev_sibling(n),
Action::FocusNextSibling(n) => self.focus_next_sibling(n),
Action::FocusFirstSibling => self.focus_first_sibling(),
Action::FocusLastSibling => self.focus_last_sibling(),
Action::FocusTop => self.focus_top(),
Action::FocusBottom => self.focus_bottom(),
Action::FocusMatchingPair => self.focus_matching_pair(),
Action::ScrollUp(n) => self.scroll_up(n),
Action::ScrollDown(n) => self.scroll_down(n),
Action::JumpUp(option_n) => self.jump_up(option_n),
Action::JumpDown(option_n) => self.jump_down(option_n),
Action::PageUp(n) => self.scroll_up(self.dimensions.height as usize * n),
Action::PageDown(n) => self.scroll_down(self.dimensions.height as usize * n),
Action::MoveFocusedLineToTop => self.move_focused_line_to_top(),
Action::MoveFocusedLineToCenter => self.move_focused_line_to_center(),
Action::MoveFocusedLineToBottom => self.move_focused_line_to_bottom(),
Action::Click(n) => self.click_row(n),
Action::ToggleCollapsed => self.toggle_collapsed(),
Action::CollapseNodeAndSiblings => self.collapse_node_and_siblings(),
Action::ExpandNodeAndSiblings => self.expand_node_and_siblings(),
Action::ToggleMode => self.toggle_mode(),
Action::ResizeViewerDimensions(dims) => self.dimensions = dims,
}
if reset_desired_depth {
self.desired_depth = self.flatjson[self.focused_row].depth;
}
if track_window {
self.ensure_focused_row_is_visible();
}
}
fn should_refocus_window(action: &Action) -> bool {
match action {
Action::NoOp => false,
Action::MoveUp(_) => true,
Action::MoveDown(_) => true,
Action::MoveLeft => true,
Action::MoveRight => true,
Action::MoveTo(_) => true,
Action::MoveUpUntilDepthChange => true,
Action::MoveDownUntilDepthChange => true,
Action::FocusParent => true,
Action::FocusPrevSibling(_) => true,
Action::FocusNextSibling(_) => true,
Action::FocusFirstSibling => true,
Action::FocusLastSibling => true,
Action::FocusTop => false, Action::FocusBottom => true,
Action::FocusMatchingPair => true,
Action::ScrollUp(_) => false,
Action::ScrollDown(_) => false,
Action::JumpUp(_) => false,
Action::JumpDown(_) => false,
Action::PageUp(_) => false,
Action::PageDown(_) => false,
Action::MoveFocusedLineToTop => false,
Action::MoveFocusedLineToCenter => false,
Action::MoveFocusedLineToBottom => false,
Action::Click(_) => true,
Action::CollapseNodeAndSiblings => true,
Action::ExpandNodeAndSiblings => true,
Action::ToggleMode => false,
Action::ResizeViewerDimensions(_) => true,
_ => false,
}
}
fn should_reset_desired_depth(action: &Action) -> bool {
!matches!(
action,
Action::NoOp
| Action::FocusPrevSibling(_)
| Action::FocusNextSibling(_)
| Action::ScrollUp(_)
| Action::ScrollDown(_)
| Action::MoveFocusedLineToTop
| Action::MoveFocusedLineToCenter
| Action::MoveFocusedLineToBottom
| Action::ToggleMode
| Action::ResizeViewerDimensions(_)
)
}
fn move_up(&mut self, rows: usize) {
let mut row = self.focused_row;
for _ in 0..rows {
let prev_row = match self.mode {
Mode::Line => self.flatjson.prev_visible_row(row),
Mode::Data => self.flatjson.prev_item(row),
};
match prev_row {
OptionIndex::Nil => break,
OptionIndex::Index(prev_row_index) => {
row = prev_row_index;
}
}
}
self.focused_row = row;
}
fn move_down(&mut self, rows: usize) {
let mut row = self.focused_row;
for _ in 0..rows {
let next_row = match self.mode {
Mode::Line => self.flatjson.next_visible_row(row),
Mode::Data => self.flatjson.next_item(row),
};
match next_row {
OptionIndex::Nil => break,
OptionIndex::Index(next_row_index) => {
row = next_row_index;
}
}
}
self.focused_row = row;
}
fn move_right(&mut self) {
let focused_row = &self.flatjson[self.focused_row];
if focused_row.is_primitive() {
return;
}
if focused_row.is_collapsed() {
self.flatjson.expand(self.focused_row);
return;
}
if focused_row.is_opening_of_container() {
self.focused_row = focused_row.first_child().unwrap();
} else {
debug_assert!(
self.mode == Mode::Line,
"Can't be focused on closing char in Data mode"
);
self.focused_row = self.flatjson.prev_visible_row(self.focused_row).unwrap();
}
}
fn move_left(&mut self) {
if self.flatjson[self.focused_row].is_container()
&& self.flatjson[self.focused_row].is_expanded()
{
self.flatjson.collapse(self.focused_row);
if self.flatjson[self.focused_row].is_closing_of_container() {
self.focused_row = self.flatjson[self.focused_row].pair_index().unwrap();
}
return;
}
self.focus_parent();
}
fn move_up_until_depth_change(&mut self) {
let mut row = self.focused_row;
let mut current_depth = self.flatjson[row].depth;
let mut moved_yet = false;
loop {
let prev_row = match self.mode {
Mode::Line => self.flatjson.prev_visible_row(row),
Mode::Data => self.flatjson.prev_item(row),
};
match prev_row {
OptionIndex::Nil => break,
OptionIndex::Index(prev_row_index) => {
let prev_row_depth = self.flatjson[prev_row_index].depth;
if prev_row_depth != current_depth {
if !moved_yet && prev_row_depth > current_depth {
current_depth = prev_row_depth;
} else {
if !moved_yet {
row = prev_row_index;
}
break;
}
}
row = prev_row_index;
moved_yet = true;
}
}
}
self.focused_row = row;
}
fn move_down_until_depth_change(&mut self) {
let mut row = self.focused_row;
let current_depth = self.flatjson[row].depth;
let mut moved_yet = false;
loop {
let next_row = match self.mode {
Mode::Line => self.flatjson.next_visible_row(row),
Mode::Data => self.flatjson.next_item(row),
};
match next_row {
OptionIndex::Nil => break,
OptionIndex::Index(next_row_index) => {
let next_row_depth = self.flatjson[next_row_index].depth;
if next_row_depth != current_depth {
if next_row_depth < current_depth || !moved_yet {
row = next_row_index;
}
break;
}
row = next_row_index;
moved_yet = true;
}
}
}
self.focused_row = row;
}
fn focus_parent(&mut self) {
if let OptionIndex::Index(parent) = self.flatjson[self.focused_row].parent {
self.focused_row = parent;
}
}
fn focus_prev_sibling(&mut self, rows: usize) {
for _ in 0..rows {
self.move_up(1);
let mut focused_row = &self.flatjson[self.focused_row];
while focused_row.depth > self.desired_depth {
self.focused_row = focused_row.parent.unwrap();
focused_row = &self.flatjson[self.focused_row];
}
}
}
fn focus_next_sibling(&mut self, rows: usize) {
for _ in 0..rows {
let current_row = &self.flatjson[self.focused_row];
if current_row.depth == self.desired_depth
&& current_row.is_opening_of_container()
&& current_row.is_expanded()
{
let closing_brace = current_row.pair_index().unwrap();
self.focused_row = if self.mode == Mode::Data {
match self.flatjson.next_item(closing_brace) {
OptionIndex::Nil => self.focused_row,
OptionIndex::Index(i) => i,
}
} else {
closing_brace
}
} else {
self.move_down(1);
}
}
}
fn focus_first_sibling(&mut self) {
match &self.flatjson[self.focused_row].parent {
OptionIndex::Index(parent_index) => {
self.focused_row = self.flatjson[*parent_index].first_child().unwrap();
}
OptionIndex::Nil => self.focus_top(),
}
}
fn focus_last_sibling(&mut self) {
match &self.flatjson[self.focused_row].parent {
OptionIndex::Index(parent_index) => {
let closing_parent_index = self.flatjson[*parent_index].pair_index().unwrap();
self.focused_row = self.flatjson[closing_parent_index].last_child().unwrap();
}
OptionIndex::Nil => {
let last_index = self.flatjson.last_visible_index();
if self.flatjson[last_index].is_container() {
self.focused_row = self.flatjson[last_index].pair_index().unwrap();
} else {
self.focused_row = last_index;
}
}
}
}
fn focus_top(&mut self) {
self.top_row = 0;
self.focused_row = 0;
}
fn focus_bottom(&mut self) {
self.focused_row = match self.mode {
Mode::Line => self.flatjson.last_visible_index(),
Mode::Data => self.flatjson.last_visible_item(),
};
}
fn focus_matching_pair(&mut self) {
if self.mode == Mode::Data {
return;
}
let current_row = &self.flatjson[self.focused_row];
if current_row.is_collapsed() {
return;
}
match current_row.pair_index() {
OptionIndex::Nil => {}
OptionIndex::Index(matching_pair_index) => {
self.focused_row = matching_pair_index;
}
}
}
fn scroll_up(&mut self, rows: usize) {
self.top_row = self.count_n_lines_before(self.top_row, rows, self.mode);
let max_focused_row = self.count_n_lines_past(
self.top_row,
(self.dimensions.height - self.scrolloff() - 1) as usize,
self.mode,
);
if self.focused_row > max_focused_row {
self.focused_row = max_focused_row;
}
}
fn scroll_down(&mut self, rows: usize) {
self.top_row = self.count_n_lines_past(self.top_row, rows, self.mode);
let first_focusable_row =
self.count_n_lines_past(self.top_row, self.scrolloff() as usize, self.mode);
if self.focused_row < first_focusable_row {
self.focused_row = first_focusable_row;
}
}
fn jump_up(&mut self, distance: Option<usize>) {
let lines = self.determine_jump_distance(distance);
let original_top_row = self.top_row;
let num_visible_before_focused = self.index_of_focused_row_on_screen();
self.top_row = self.count_n_lines_before(self.top_row, lines, self.mode);
if original_top_row != self.top_row {
self.focused_row = self.count_n_lines_past(
self.top_row,
num_visible_before_focused as usize,
self.mode,
);
} else {
self.focused_row = self.count_n_lines_before(self.focused_row, lines, self.mode);
}
}
fn jump_down(&mut self, distance: Option<usize>) {
let lines = self.determine_jump_distance(distance);
let original_top_row = self.top_row;
let num_visible_before_focused = self.index_of_focused_row_on_screen();
self.top_row = self.count_n_lines_past(self.top_row, lines, self.mode);
let last_line = match self.mode {
Mode::Line => self.flatjson.last_visible_index(),
Mode::Data => self.flatjson.last_visible_item(),
};
let top_row_if_last_row_is_at_bottom =
self.count_n_lines_before(last_line, self.dimensions.height as usize - 1, self.mode);
if self.top_row > top_row_if_last_row_is_at_bottom {
self.top_row = top_row_if_last_row_is_at_bottom.max(original_top_row);
}
if original_top_row != self.top_row {
self.focused_row = self.count_n_lines_past(
self.top_row,
num_visible_before_focused as usize,
self.mode,
);
} else {
self.focused_row = self.count_n_lines_past(self.focused_row, lines, self.mode);
}
}
fn determine_jump_distance(&mut self, distance: Option<usize>) -> usize {
self.jump_distance = distance.or(self.jump_distance);
match self.jump_distance {
Some(n) => n,
None => (self.dimensions.height as usize / 2).max(1),
}
}
fn move_focused_line_to_top(&mut self) {
let padding = self.scrolloff() as usize;
self.top_row = self.count_n_lines_before(self.focused_row, padding, self.mode);
}
fn move_focused_line_to_center(&mut self) {
let padding = (self.dimensions.height / 2) as usize;
self.top_row = self.count_n_lines_before(self.focused_row, padding, self.mode);
}
fn move_focused_line_to_bottom(&mut self) {
let padding = (self.dimensions.height - self.scrolloff() - 1) as usize;
self.top_row = self.count_n_lines_before(self.focused_row, padding, self.mode);
}
fn click_row(&mut self, row: u16) {
self.focused_row = self.count_n_lines_past(self.top_row, (row - 1) as usize, self.mode);
if self.flatjson[self.focused_row].is_opening_of_container() {
self.toggle_collapsed();
}
}
fn toggle_collapsed(&mut self) {
let focused_row = &mut self.flatjson[self.focused_row];
if focused_row.is_primitive() {
return;
}
if focused_row.is_closing_of_container() {
debug_assert!(
focused_row.is_expanded(),
"Focused on closing char when row is collapsed",
);
self.focused_row = self.flatjson[self.focused_row].pair_index().unwrap();
}
self.flatjson.toggle_collapsed(self.focused_row);
}
fn collapse_node_and_siblings(&mut self) {
let focused_row = &mut self.flatjson[self.focused_row];
if focused_row.is_closing_of_container() {
debug_assert!(
focused_row.is_expanded(),
"Focused on closing char when row is collapsed",
);
self.focused_row = self.flatjson[self.focused_row].pair_index().unwrap();
}
self.set_collapse_state_on_node_and_siblings(true);
}
fn expand_node_and_siblings(&mut self) {
self.set_collapse_state_on_node_and_siblings(false);
}
fn set_collapse_state_on_node_and_siblings(&mut self, collapsed: bool) {
let first_sibling =
if let OptionIndex::Index(parent) = self.flatjson[self.focused_row].parent {
self.flatjson[parent].first_child().unwrap()
} else {
0
};
let mut next_sibling = OptionIndex::Index(first_sibling);
while let OptionIndex::Index(next) = next_sibling {
if collapsed {
self.flatjson.collapse(next);
} else {
self.flatjson.expand(next);
}
next_sibling = self.flatjson[next].next_sibling;
}
}
fn toggle_mode(&mut self) {
let index_of_focused_row = self.index_of_focused_row_on_screen();
if self.mode == Mode::Line && self.flatjson[self.focused_row].is_closing_of_container() {
if let OptionIndex::Index(next) = self.flatjson.next_item(self.focused_row) {
self.focused_row = next;
} else {
self.focused_row = self.flatjson.prev_item(self.focused_row).unwrap();
}
}
self.mode = match self.mode {
Mode::Line => Mode::Data,
Mode::Data => Mode::Line,
};
self.top_row =
self.count_n_lines_before(self.focused_row, index_of_focused_row as usize, self.mode);
}
fn scrolloff(&self) -> u16 {
self.scrolloff_setting.min((self.dimensions.height - 1) / 2)
}
fn ensure_focused_row_is_visible(&mut self) {
self.ensure_top_row_is_visible();
let scrolloff = self.scrolloff();
let max_padding = self.dimensions.height - scrolloff - 1;
let recenter_distance = self.dimensions.height + (self.dimensions.height / 3);
let num_visible_before_focused = self.count_visible_rows_before(
self.top_row,
self.focused_row,
recenter_distance + 1,
self.mode,
);
if self.focused_row < self.top_row || num_visible_before_focused < scrolloff {
self.top_row =
self.count_n_lines_before(self.focused_row, scrolloff as usize, self.mode);
} else if num_visible_before_focused > max_padding {
let refocus_padding = if num_visible_before_focused > recenter_distance {
let bottom_padding = self.dimensions.height * 2 / 3;
bottom_padding.min(max_padding)
} else {
scrolloff
};
let last_line = match self.mode {
Mode::Line => self.flatjson.last_visible_index(),
Mode::Data => self.flatjson.last_visible_item(),
};
let lines_visible_before_eof = self.count_visible_rows_before(
self.focused_row,
last_line,
refocus_padding + 1,
self.mode,
);
let bottom_padding = refocus_padding.min(lines_visible_before_eof);
self.top_row = self.count_n_lines_before(
self.focused_row,
(self.dimensions.height - bottom_padding - 1) as usize,
self.mode,
);
}
}
fn ensure_top_row_is_visible(&mut self) {
if self.flatjson[self.top_row].is_closing_of_container() {
let opening = self.flatjson[self.top_row].pair_index().unwrap();
if self.flatjson[opening].is_collapsed() {
self.top_row = opening;
}
}
let mut ancestor = self.top_row;
while let OptionIndex::Index(ancestor_index) = self.flatjson[ancestor].parent {
if self.flatjson[ancestor_index].is_collapsed() {
self.top_row = ancestor_index;
}
ancestor = ancestor_index;
}
}
fn count_n_lines_before(&self, mut start: Index, mut lines: usize, mode: Mode) -> Index {
while lines != 0 && start != 0 {
start = match mode {
Mode::Line => self.flatjson.prev_visible_row(start).unwrap(),
Mode::Data => self.flatjson.prev_item(start).unwrap(),
};
lines -= 1;
}
start
}
fn count_n_lines_past(&self, mut start: Index, mut lines: usize, mode: Mode) -> Index {
while lines != 0 {
let next = match mode {
Mode::Line => self.flatjson.next_visible_row(start),
Mode::Data => self.flatjson.next_item(start),
};
match next {
OptionIndex::Nil => break,
OptionIndex::Index(n) => start = n,
};
lines -= 1;
}
start
}
fn count_visible_rows_before(&self, mut start: Index, end: Index, max: u16, mode: Mode) -> u16 {
let mut num_visible: u16 = 0;
while start < end && num_visible < max {
num_visible += 1;
start = match mode {
Mode::Line => self.flatjson.next_visible_row(start).unwrap(),
Mode::Data => self.flatjson.next_item(start).unwrap(),
};
}
num_visible
}
fn index_of_focused_row_on_screen(&self) -> u16 {
self.count_visible_rows_before(
self.top_row,
self.focused_row,
self.dimensions.height,
self.mode,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::flatjson::{parse_top_level_json, NIL};
impl OptionIndex {
pub fn to_usize(&self) -> usize {
match self {
OptionIndex::Nil => NIL,
OptionIndex::Index(i) => *i,
}
}
}
const OBJECT: &str = r#"{
"1": 1,
"2": [
3,
"4"
],
"6": {
"7": null,
"8": true,
"9": 9
},
"11": 11
}"#;
const DATA_OBJECT: &str = r#"{
"1": 1,
"2": [
3,
"4"],
"6": {
"7": null,
"8": true,
"9": 9},
"11": 11}"#;
#[test]
fn test_move_up_down_line_mode() {
let fj = parse_top_level_json(OBJECT.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Line);
assert_movements(
&mut viewer,
vec![(Action::MoveDown(1), 1), (Action::MoveDown(2), 3)],
);
viewer.flatjson.collapse(6);
viewer.focused_row = 6;
assert_movements(
&mut viewer,
vec![
(Action::MoveDown(1), 11),
(Action::MoveDown(1), 12),
(Action::MoveDown(1), 12),
],
);
assert_movements(
&mut viewer,
vec![
(Action::MoveUp(2), 6),
(Action::MoveUp(1), 5),
(Action::MoveUp(5), 0),
(Action::MoveUp(2), 0),
],
);
viewer.flatjson.collapse(0);
assert_movements(
&mut viewer,
vec![(Action::MoveUp(1), 0), (Action::MoveDown(1), 0)],
);
}
#[test]
fn test_move_up_down_data_mode() {
let fj = parse_top_level_json(DATA_OBJECT.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Data);
assert_movements(
&mut viewer,
vec![
(Action::MoveDown(1), 1),
(Action::MoveDown(3), 4),
(Action::MoveDown(1), 6),
],
);
viewer.flatjson.collapse(6);
assert_movements(
&mut viewer,
vec![(Action::MoveDown(1), 11), (Action::MoveDown(1), 11)],
);
assert_movements(
&mut viewer,
vec![
(Action::MoveUp(1), 6),
(Action::MoveUp(3), 2),
(Action::MoveUp(1), 1),
(Action::MoveUp(1), 0),
(Action::MoveUp(1), 0),
],
);
}
#[test]
fn test_move_left_right_line_mode() {
let fj = parse_top_level_json(OBJECT.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Line);
assert_movements(
&mut viewer,
vec![
(Action::MoveRight, 1),
(Action::MoveRight, 1),
(Action::MoveDown(1), 2),
(Action::MoveRight, 3),
(Action::MoveLeft, 2),
(Action::MoveLeft, 2),
],
);
assert!(viewer.flatjson[2].is_collapsed());
viewer.focused_row = 10;
assert_movements(
&mut viewer,
vec![
(Action::MoveRight, 9),
(Action::MoveLeft, 6),
(Action::MoveDown(4), 10),
(Action::MoveLeft, 6),
],
);
assert!(viewer.flatjson[6].is_collapsed());
assert_movements(
&mut viewer,
vec![
(Action::MoveLeft, 0),
(Action::MoveLeft, 0),
(Action::MoveDown(1), 0),
],
);
assert!(viewer.flatjson[0].is_collapsed());
assert_movements(&mut viewer, vec![(Action::MoveRight, 0)]);
assert!(viewer.flatjson[0].is_expanded());
assert_movements(&mut viewer, vec![(Action::MoveRight, 1)]);
}
#[test]
fn test_move_left_right_data_mode() {
let fj = parse_top_level_json(DATA_OBJECT.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Data);
assert_movements(
&mut viewer,
vec![
(Action::MoveRight, 1),
(Action::MoveRight, 1),
(Action::MoveDown(5), 7),
(Action::MoveLeft, 6),
(Action::MoveLeft, 6),
],
);
assert!(viewer.flatjson[6].is_collapsed());
assert_movements(
&mut viewer,
vec![
(Action::MoveLeft, 0),
(Action::MoveRight, 1),
(Action::MoveLeft, 0),
(Action::MoveLeft, 0),
],
);
assert!(viewer.flatjson[0].is_collapsed());
assert_movements(
&mut viewer,
vec![(Action::MoveDown(1), 0), (Action::MoveRight, 0)],
);
assert!(viewer.flatjson[0].is_expanded());
assert_movements(&mut viewer, vec![(Action::MoveLeft, 0)]);
}
#[test]
fn test_move_up_down_until_depth_change_line_mode() {
let fj = parse_top_level_json(OBJECT.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Line);
assert_movements(
&mut viewer,
vec![
(Action::MoveDownUntilDepthChange, 1),
(Action::MoveDownUntilDepthChange, 2),
(Action::MoveDownUntilDepthChange, 3),
(Action::MoveDownUntilDepthChange, 5),
(Action::MoveDownUntilDepthChange, 6),
(Action::MoveDownUntilDepthChange, 7),
(Action::MoveDownUntilDepthChange, 10),
(Action::MoveDownUntilDepthChange, 12),
(Action::MoveDownUntilDepthChange, 12),
(Action::MoveUpUntilDepthChange, 10),
(Action::MoveUpUntilDepthChange, 7),
(Action::MoveUpUntilDepthChange, 6),
(Action::MoveUpUntilDepthChange, 5),
(Action::MoveUpUntilDepthChange, 3),
(Action::MoveUpUntilDepthChange, 2),
(Action::MoveUpUntilDepthChange, 1),
(Action::MoveUpUntilDepthChange, 0),
(Action::MoveUpUntilDepthChange, 0),
],
);
}
#[test]
fn test_move_up_down_until_depth_change_data_mode() {
let fj = parse_top_level_json(DATA_OBJECT.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Data);
assert_movements(
&mut viewer,
vec![
(Action::MoveDownUntilDepthChange, 1),
(Action::MoveDownUntilDepthChange, 2),
(Action::MoveDownUntilDepthChange, 3),
(Action::MoveDownUntilDepthChange, 6),
(Action::MoveDownUntilDepthChange, 7),
(Action::MoveDownUntilDepthChange, 11),
(Action::MoveDownUntilDepthChange, 11),
(Action::MoveUpUntilDepthChange, 7),
(Action::MoveUpUntilDepthChange, 6),
(Action::MoveUpUntilDepthChange, 3),
(Action::MoveUpUntilDepthChange, 2),
(Action::MoveUpUntilDepthChange, 1),
(Action::MoveUpUntilDepthChange, 0),
(Action::MoveUpUntilDepthChange, 0),
],
);
}
#[track_caller]
fn assert_movements(viewer: &mut JsonViewer, actions_and_focuses: Vec<(Action, Index)>) {
for (i, (action, expected_focused_row)) in actions_and_focuses.into_iter().enumerate() {
viewer.perform_action(action);
assert_eq!(
viewer.focused_row,
expected_focused_row,
"expected row {} to be focused after {} actions (last action: {:?})",
expected_focused_row,
i + 1,
action,
);
}
}
#[test]
fn test_ensure_focused_line_is_visible_in_line_mode() {
let fj = parse_top_level_json(OBJECT.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Line);
viewer.dimensions.height = 8;
viewer.scrolloff_setting = 2;
viewer.ensure_focused_row_is_visible();
assert_eq!(viewer.top_row, 0);
assert_window_tracking(
&mut viewer,
vec![
(Action::MoveDown(1), 0, 1),
(Action::MoveDown(5), 1, 6),
(Action::MoveDown(1), 2, 7),
],
);
assert_window_tracking(
&mut viewer,
vec![
(Action::MoveUp(1), 2, 6),
(Action::MoveUp(3), 1, 3),
(Action::MoveUp(1), 0, 2),
(Action::MoveUp(1), 0, 1),
],
);
viewer.scrolloff_setting = 0;
assert_window_tracking(
&mut viewer,
vec![
(Action::MoveDown(6), 0, 7),
(Action::MoveDown(1), 1, 8),
(Action::MoveDown(4), 5, 12),
(Action::MoveDown(1), 5, 12),
],
);
assert_window_tracking(
&mut viewer,
vec![
(Action::MoveUp(7), 5, 5),
(Action::MoveUp(1), 4, 4),
(Action::MoveUp(5), 0, 0),
],
);
viewer.top_row = 0;
viewer.focused_row = 1;
viewer.scrolloff_setting = 2;
assert_window_tracking(
&mut viewer,
vec![
(Action::MoveDown(9), 5, 10),
(Action::MoveDown(1), 5, 11),
(Action::MoveDown(1), 5, 12),
],
);
viewer.top_row = 8;
viewer.focused_row = 10;
assert_window_tracking(
&mut viewer,
vec![
(Action::MoveDown(1), 8, 11),
(Action::MoveDown(1), 8, 12),
(Action::MoveUp(2), 8, 10),
(Action::MoveUp(1), 7, 9),
],
);
viewer.top_row = 0;
viewer.focused_row = 0;
viewer.dimensions.height = 6;
viewer.flatjson.collapse(2);
assert_window_tracking(
&mut viewer,
vec![
(Action::MoveDown(3), 0, 6),
(Action::MoveDown(1), 1, 7),
(Action::MoveDown(1), 2, 8),
(Action::MoveDown(1), 6, 9),
(Action::MoveUp(2), 2, 7),
(Action::MoveUp(1), 1, 6),
(Action::MoveUp(1), 0, 2),
],
);
}
#[test]
fn test_ensure_focused_line_is_visible_in_data_mode() {
let fj = parse_top_level_json(DATA_OBJECT.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Data);
viewer.dimensions.height = 7;
viewer.scrolloff_setting = 2;
viewer.ensure_focused_row_is_visible();
assert_eq!(viewer.top_row, 0);
assert_window_tracking(
&mut viewer,
vec![
(Action::MoveDown(1), 0, 1),
(Action::MoveDown(4), 1, 6),
(Action::MoveDown(1), 2, 7),
],
);
assert_window_tracking(
&mut viewer,
vec![
(Action::MoveUp(1), 2, 6),
(Action::MoveUp(2), 1, 3),
(Action::MoveUp(1), 0, 2),
(Action::MoveUp(1), 0, 1),
],
);
assert_window_tracking(
&mut viewer,
vec![
(Action::MoveDown(6), 3, 8),
(Action::MoveDown(1), 3, 9),
(Action::MoveDown(1), 3, 11),
],
);
viewer.top_row = 6;
viewer.focused_row = 8;
assert_window_tracking(
&mut viewer,
vec![
(Action::MoveDown(1), 6, 9),
(Action::MoveDown(1), 6, 11),
(Action::MoveUp(2), 6, 8),
(Action::MoveUp(1), 4, 7),
],
);
viewer.top_row = 0;
viewer.focused_row = 0;
viewer.dimensions.height = 5;
viewer.flatjson.collapse(2);
assert_window_tracking(
&mut viewer,
vec![
(Action::MoveDown(2), 0, 2),
(Action::MoveDown(1), 1, 6),
(Action::MoveDown(1), 2, 7),
(Action::MoveDown(1), 6, 8),
(Action::MoveUp(1), 2, 7),
(Action::MoveUp(1), 1, 6),
(Action::MoveUp(1), 0, 2),
],
);
}
const TALL_OBJECT: &str = "[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]";
#[test]
fn test_ensure_focused_line_is_visible_centers_focus_line_after_big_jump() {
let fj = parse_top_level_json(TALL_OBJECT.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Line);
viewer.dimensions.height = 9;
viewer.scrolloff_setting = 2;
assert_window_tracking(
&mut viewer,
vec![
(Action::MoveDown(8), 2, 8),
(Action::FocusTop, 0, 0),
(Action::MoveDown(9), 3, 9),
(Action::FocusTop, 0, 0),
(Action::MoveDown(12), 6, 12),
(Action::FocusTop, 0, 0),
(Action::MoveDown(13), 11, 13),
],
);
viewer.scrolloff_setting = 3;
assert_window_tracking(
&mut viewer,
vec![(Action::FocusTop, 0, 0), (Action::MoveDown(13), 10, 13)],
);
}
#[test]
fn test_scroll() {
let fj = parse_top_level_json(OBJECT.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Line);
viewer.dimensions.height = 8;
viewer.scrolloff_setting = 2;
assert_window_tracking(
&mut viewer,
vec![
(Action::ScrollDown(1), 1, 3),
(Action::ScrollDown(1), 2, 4),
(Action::ScrollDown(3), 5, 7),
(Action::ScrollDown(1), 6, 8),
(Action::ScrollDown(4), 10, 12),
(Action::ScrollDown(1), 11, 12),
(Action::ScrollDown(1), 12, 12),
(Action::ScrollDown(1), 12, 12),
(Action::ScrollUp(1), 11, 12),
(Action::ScrollDown(1), 12, 12),
(Action::MoveUp(1), 9, 11),
],
);
viewer.top_row = 12;
viewer.focused_row = 12;
assert_window_tracking(
&mut viewer,
vec![
(Action::ScrollUp(1), 11, 12),
(Action::ScrollUp(1), 10, 12),
(Action::ScrollUp(4), 6, 11),
(Action::ScrollUp(1), 5, 10),
(Action::ScrollUp(6), 0, 5),
],
);
}
#[test]
fn test_jump() {
const TALL_OBJECT: &str = r#"{
"1": [2],
"4": [5],
"7": [8],
"10": [11],
"13": [14],
"16": [17],
}"#;
let fj = parse_top_level_json(TALL_OBJECT.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Line);
viewer.dimensions.height = 5;
viewer.scrolloff_setting = 0;
viewer.focused_row = 3;
assert_window_tracking(
&mut viewer,
vec![
(Action::JumpDown(None), 2, 5),
(Action::JumpDown(None), 4, 7),
(Action::JumpUp(None), 2, 5),
(Action::JumpDown(Some(4)), 6, 9),
(Action::JumpDown(None), 10, 13),
(Action::JumpUp(None), 6, 9),
],
);
viewer.dimensions.height = 8;
viewer.top_row = 3;
viewer.focused_row = 7;
viewer.scrolloff_setting = 3;
assert_window_tracking(
&mut viewer,
vec![
(Action::JumpUp(Some(2)), 1, 5),
(Action::JumpUp(None), 0, 4),
(Action::JumpUp(None), 0, 2),
(Action::JumpUp(None), 0, 0),
(Action::JumpDown(None), 2, 2),
],
);
viewer.dimensions.height = 8;
viewer.top_row = 9;
viewer.focused_row = 11;
assert_window_tracking(
&mut viewer,
vec![
(Action::JumpDown(Some(2)), 11, 13),
(Action::JumpDown(None), 12, 14),
(Action::JumpDown(None), 12, 16),
(Action::JumpDown(None), 12, 18),
(Action::JumpDown(None), 12, 19),
(Action::JumpUp(None), 10, 17),
],
);
viewer.top_row = 14;
viewer.focused_row = 15;
assert_window_tracking(
&mut viewer,
vec![
(Action::JumpDown(Some(2)), 14, 17),
(Action::JumpDown(None), 14, 19),
],
);
}
#[test]
fn test_move_focus() {
let fj = parse_top_level_json(OBJECT.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Line);
viewer.dimensions.height = 5;
viewer.scrolloff_setting = 1;
assert_window_tracking(
&mut viewer,
vec![
(Action::MoveFocusedLineToTop, 0, 0),
(Action::MoveFocusedLineToCenter, 0, 0),
(Action::MoveFocusedLineToBottom, 0, 0),
],
);
viewer.top_row = 10;
viewer.focused_row = 12;
assert_window_tracking(
&mut viewer,
vec![
(Action::MoveFocusedLineToTop, 11, 12),
(Action::MoveFocusedLineToCenter, 10, 12),
(Action::MoveFocusedLineToBottom, 9, 12),
],
);
viewer.top_row = 4;
viewer.focused_row = 6;
viewer.dimensions.height = 7;
assert_window_tracking(
&mut viewer,
vec![
(Action::MoveFocusedLineToTop, 5, 6),
(Action::MoveFocusedLineToCenter, 3, 6),
(Action::MoveFocusedLineToBottom, 1, 6),
],
);
viewer.dimensions.height = 8;
assert_window_tracking(
&mut viewer,
vec![
(Action::MoveFocusedLineToTop, 5, 6),
(Action::MoveFocusedLineToCenter, 2, 6),
(Action::MoveFocusedLineToBottom, 0, 6),
],
);
}
#[test]
fn test_click_row() {
let fj = parse_top_level_json(OBJECT.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Line);
viewer.dimensions.height = 7;
viewer.scrolloff_setting = 3;
assert_window_tracking(&mut viewer, vec![(Action::Click(6), 2, 5)]);
assert!(viewer.flatjson[5].is_expanded());
assert_window_tracking(&mut viewer, vec![(Action::Click(1), 0, 2)]);
assert!(viewer.flatjson[2].is_collapsed());
assert_window_tracking(&mut viewer, vec![(Action::Click(3), 0, 2)]);
assert!(viewer.flatjson[2].is_expanded());
assert_window_tracking(&mut viewer, vec![(Action::Click(5), 1, 4)]);
}
#[test]
fn test_focus_prev_next_sibling_line_mode() {
let fj = parse_top_level_json(OBJECT.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Line);
viewer.focused_row = 0;
viewer.desired_depth = 0;
assert_movements(
&mut viewer,
vec![
(Action::FocusNextSibling(1), 12),
(Action::FocusNextSibling(1), 12),
(Action::FocusPrevSibling(1), 0),
(Action::FocusPrevSibling(1), 0),
],
);
viewer.flatjson.collapse(0);
assert_movements(
&mut viewer,
vec![
(Action::FocusNextSibling(1), 0),
(Action::FocusPrevSibling(1), 0),
],
);
viewer.flatjson.expand(0);
viewer.focused_row = 0;
viewer.desired_depth = 1;
assert_movements(
&mut viewer,
vec![
(Action::FocusNextSibling(1), 1),
(Action::FocusNextSibling(2), 5),
(Action::FocusNextSibling(1), 6),
(Action::FocusNextSibling(1), 10),
(Action::FocusNextSibling(2), 12),
(Action::FocusNextSibling(1), 12),
(Action::FocusPrevSibling(3), 6),
(Action::FocusPrevSibling(4), 0),
(Action::FocusPrevSibling(1), 0),
],
);
viewer.focused_row = 2;
viewer.flatjson.collapse(2);
assert_movements(
&mut viewer,
vec![
(Action::FocusNextSibling(1), 6),
(Action::FocusNextSibling(1), 10),
(Action::FocusPrevSibling(1), 6),
(Action::FocusPrevSibling(1), 2),
],
);
}
#[test]
fn test_focus_prev_next_sibling_data_mode() {
let fj = parse_top_level_json(DATA_OBJECT.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Data);
viewer.focused_row = 0;
viewer.desired_depth = 0;
assert_movements(
&mut viewer,
vec![
(Action::FocusNextSibling(1), 0),
(Action::FocusPrevSibling(1), 0),
],
);
viewer.flatjson.collapse(0);
assert_movements(
&mut viewer,
vec![
(Action::FocusNextSibling(1), 0),
(Action::FocusPrevSibling(1), 0),
],
);
viewer.flatjson.expand(0);
viewer.focused_row = 0;
viewer.desired_depth = 1;
assert_movements(
&mut viewer,
vec![
(Action::FocusNextSibling(3), 6),
(Action::FocusNextSibling(1), 11),
(Action::FocusNextSibling(1), 11),
(Action::FocusPrevSibling(1), 6),
(Action::FocusPrevSibling(3), 0),
(Action::FocusPrevSibling(1), 0),
],
);
viewer.focused_row = 2;
viewer.flatjson.collapse(2);
assert_movements(
&mut viewer,
vec![
(Action::FocusNextSibling(1), 6),
(Action::FocusNextSibling(1), 11),
(Action::FocusPrevSibling(1), 6),
(Action::FocusPrevSibling(1), 2),
],
);
}
#[test]
fn test_focus_first_last_sibling() {
let fj = parse_top_level_json(OBJECT.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Line);
viewer.focused_row = 0;
assert_movements(
&mut viewer,
vec![
(Action::FocusFirstSibling, 0),
(Action::FocusLastSibling, 0),
(Action::FocusFirstSibling, 0),
],
);
viewer.focused_row = 2;
assert_movements(
&mut viewer,
vec![
(Action::FocusFirstSibling, 1),
(Action::FocusLastSibling, 11),
(Action::FocusFirstSibling, 1),
],
);
viewer.focused_row = 2;
assert_movements(&mut viewer, vec![(Action::FocusLastSibling, 11)]);
viewer.focused_row = 8;
assert_movements(
&mut viewer,
vec![
(Action::FocusLastSibling, 9),
(Action::FocusFirstSibling, 7),
],
);
}
#[test]
fn test_focus_top_and_bottom() {
let fj = parse_top_level_json(OBJECT.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Line);
viewer.dimensions.height = 8;
assert_window_tracking(
&mut viewer,
vec![(Action::FocusBottom, 5, 12), (Action::FocusTop, 0, 0)],
);
viewer.mode = Mode::Data;
assert_window_tracking(
&mut viewer,
vec![(Action::FocusBottom, 2, 11), (Action::FocusTop, 0, 0)],
);
}
#[test]
fn test_focus_bottom_newline_delimited_json() {
let nd_json = r#"
0
1
2
{
"a": 4
}
"#;
let fj = parse_top_level_json(nd_json.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Line);
assert_window_tracking(
&mut viewer,
vec![(Action::FocusBottom, 0, 5), (Action::FocusTop, 0, 0)],
);
viewer.flatjson.collapse(3);
assert_window_tracking(
&mut viewer,
vec![(Action::FocusBottom, 0, 3), (Action::FocusTop, 0, 0)],
);
viewer.mode = Mode::Data;
viewer.flatjson.expand(3);
assert_window_tracking(
&mut viewer,
vec![(Action::FocusBottom, 0, 4), (Action::FocusTop, 0, 0)],
);
viewer.flatjson.collapse(3);
assert_window_tracking(&mut viewer, vec![(Action::FocusBottom, 0, 3)]);
}
#[test]
fn test_focus_matching_pair() {
let fj = parse_top_level_json(OBJECT.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Line);
viewer.focused_row = 0;
assert_movements(
&mut viewer,
vec![
(Action::FocusMatchingPair, 12),
(Action::FocusMatchingPair, 0),
],
);
viewer.focused_row = 5;
assert_movements(
&mut viewer,
vec![
(Action::FocusMatchingPair, 2),
(Action::FocusMatchingPair, 5),
],
);
viewer.flatjson.collapse(6);
viewer.focused_row = 6;
assert_movements(&mut viewer, vec![(Action::FocusMatchingPair, 6)]);
}
const LOTS_OF_OBJECTS: &str = r#"{
"1": {
"2": 2
},
"4": [
{
"6": 6
},
{
"9": 9
}
],
"12": {
"13": 13
}
}"#;
#[test]
fn test_collapse_and_expand_node_and_siblings() {
let fj = parse_top_level_json(LOTS_OF_OBJECTS.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Line);
viewer.dimensions.height = 8;
viewer.scrolloff_setting = 1;
viewer.top_row = 6;
viewer.focused_row = 8;
viewer.perform_action(Action::ExpandNodeAndSiblings);
assert!(viewer.flatjson[5].is_expanded());
assert!(viewer.flatjson[8].is_expanded());
viewer.focused_row = 10;
viewer.perform_action(Action::CollapseNodeAndSiblings);
assert_eq!(5, viewer.top_row);
assert_eq!(8, viewer.focused_row);
assert!(viewer.flatjson[5].is_collapsed());
assert!(viewer.flatjson[8].is_collapsed());
viewer.flatjson.collapse(12);
viewer.top_row = 3;
viewer.focused_row = 12;
viewer.perform_action(Action::CollapseNodeAndSiblings);
assert_eq!(1, viewer.top_row);
assert!(viewer.flatjson[1].is_collapsed());
assert!(viewer.flatjson[4].is_collapsed());
assert!(viewer.flatjson[12].is_collapsed());
viewer.perform_action(Action::ExpandNodeAndSiblings);
assert!(viewer.flatjson[1].is_expanded());
assert!(viewer.flatjson[4].is_expanded());
assert!(viewer.flatjson[12].is_expanded());
}
#[test]
fn test_toggle_mode() {
let fj = parse_top_level_json(LOTS_OF_OBJECTS.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Line);
viewer.dimensions.height = 5;
viewer.scrolloff_setting = 1;
let tests = vec![
(Mode::Data, 0, 0, 0, 0),
(Mode::Line, 0, 0, 0, 0),
(Mode::Data, 2, 4, 3, 4), (Mode::Line, 2, 4, 1, 4), (Mode::Line, 7, 11, 5, 12),
(Mode::Line, 12, 15, 8, 13),
];
for (i, (mode, start_top, start_focused, end_top, end_focused)) in
tests.into_iter().enumerate()
{
viewer.mode = mode;
viewer.top_row = start_top;
viewer.focused_row = start_focused;
viewer.perform_action(Action::ToggleMode);
assert_eq!(
viewer.focused_row,
end_focused,
"Incorrect focused_row after test {}",
i + 1
);
assert_eq!(
viewer.top_row,
end_top,
"Incorrect top_row after test {}",
i + 1
);
}
}
#[track_caller]
fn assert_window_tracking(
viewer: &mut JsonViewer,
actions_and_rows: Vec<(Action, usize, usize)>,
) {
for (i, (action, top_row, focused_row)) in actions_and_rows.into_iter().enumerate() {
viewer.perform_action(action);
assert_eq!(
viewer.focused_row,
focused_row,
"Incorrect focused_row after {} actions",
i + 1
);
assert_eq!(
viewer.top_row,
top_row,
"Incorrect top_row after {} actions",
i + 1
);
}
}
}