Skip to content

Commit

Permalink
Add 'C' and 'E' commands to deeply collapse/expand nodes.
Browse files Browse the repository at this point in the history
Also update the focus behavior of c/e/C/E so that the focused node
stays in the same position on the screen. This is an improvement
over the previous behavior (and roughly matches the same behavior
that would occur when you pressed 'm' to toggle between data and
line mode), but there are still some wonky cases such as:

- Behavior when the top of the file is visible
- Behavior when you're focusing the closing of a container in
  line mode (maybe the opening of the container, if it's visible,
  should stay in the same place?)

It seems desirable that the viewport would be remain unchanged after
pressing 'c' and then 'e' (or vice versa), assuming the collapsed
state of everything is unchanged, but that might be impossible in
the general case.

Maybe there should be slightly different behavior for expanding vs
collapsing too?

I also left a TODO noting that it would be good to unify/classify
the desired viewport behavior after an operation, rather than using
a couple of very ill-defined boolean functions right now. Possibly
values include:

- ViewportUpdatedByAction (the code for the action manages updating
  the viewport by itself, like for zz commands)
- ScrollToEnsureFocusedRowIsVisible (the action changes the focused
  row, and the viewport should scroll to get it into view)
- KeepFocusedLineInSamePlaceOnScreen (the focused line should be
  in the same place on the screen after this)

The third one could be generalizable to keeping an arbitrary row
(not necessarily the focused one) in the same place, or put it
in the same spot +/- some delta.
  • Loading branch information
PaulJuliusMartinez committed Jul 17, 2023
1 parent 275f9cb commit 5d5a597
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 26 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ New features:
- You can jump to an exact line number using `<count>g` or `<count>G`.
When using `<count>g` (lowercase 'g'), if the desired line number is
hidden inside of a collapsed container, the last visible line number
before the desired one will be focused. When using `<coung>G`
before the desired one will be focused. When using `<count>G`
(uppercase 'G'), all the ancestors of the desired line will be
expanded to ensure it is visible.
- Add `C` and `E` commands, analagous to the existing `c` and `e`
commands, to deeply collapse/expand a node and all its siblings.

Improvements:
- In data mode, when a array element is focused, the highlighting on the
Expand All @@ -48,6 +50,10 @@ Improvements:
focused node is a primitive. Together these changes should make it
more clear which line is focused, especially when the terminal's
current style doesn't support dimming (`ESC [ 2 m`).
- When using the `c` and `e` commands (and the new `C` and `E`
commands), the focused row will stay at the same spot on the screen.
(Previously jless would try to keep the same row visible at the top of
the screen, which didn't make sense.)

Bug fixes:
- Scrolling with the mouse will now move the viewing window, rather than
Expand Down
2 changes: 2 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,9 @@ impl App {
Key::Right | Key::Char('l') => Some(Action::MoveRight),
Key::Char('H') => Some(Action::FocusParent),
Key::Char('c') => Some(Action::CollapseNodeAndSiblings),
Key::Char('C') => Some(Action::DeepCollapseNodeAndSiblings),
Key::Char('e') => Some(Action::ExpandNodeAndSiblings),
Key::Char('E') => Some(Action::DeepExpandNodeAndSiblings),
Key::Char(' ') => Some(Action::ToggleCollapsed),
Key::Char('^') => Some(Action::FocusFirstSibling),
Key::Char('$') => Some(Action::FocusLastSibling),
Expand Down
6 changes: 4 additions & 2 deletions src/jless.help
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,10 @@
count is given, focus that line number, expanding any of its
parent nodes if necessary.

c Collapse the focused node and all its siblings.
e Expand the focused node and all its siblings.
c Shallow collapse the focused node and all its siblings.
C Deeply collapse the focused node and all its siblings.
e Shallow expand the focused node and all its siblings.
E Deeply expand the focused node and all its siblings.

Space Toggle the collapsed state of the currently focused node.

Expand Down
201 changes: 178 additions & 23 deletions src/viewer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@ pub enum Action {

ToggleCollapsed,
CollapseNodeAndSiblings,
DeepCollapseNodeAndSiblings,
ExpandNodeAndSiblings,
DeepExpandNodeAndSiblings,

ToggleMode,

Expand All @@ -137,7 +139,15 @@ pub enum Action {

impl JsonViewer {
pub fn perform_action(&mut self, action: Action) {
// TODO: These two functions should really be refactored into a single function
// that returns something like:
// enum WindowTrackingBehavior {
// HandledByAction,
// EnsureFocusedRowIsVisible,
// KeepFocusedLineInSamePlaceOnScreen(u16)
// }
let track_window = JsonViewer::should_refocus_window(&action);
let prev_index_of_focused_row = self.should_keep_focused_row_at_same_screen_index(&action);
let reset_desired_depth = JsonViewer::should_reset_desired_depth(&action);

match action {
Expand Down Expand Up @@ -169,7 +179,9 @@ impl JsonViewer {
Action::Click(n) => self.click_row(n),
Action::ToggleCollapsed => self.toggle_collapsed(),
Action::CollapseNodeAndSiblings => self.collapse_node_and_siblings(),
Action::DeepCollapseNodeAndSiblings => self.deep_collapse_node_and_siblings(),
Action::ExpandNodeAndSiblings => self.expand_node_and_siblings(),
Action::DeepExpandNodeAndSiblings => self.deep_expand_node_and_siblings(),
Action::ToggleMode => self.toggle_mode(),
Action::ResizeViewerDimensions(dims) => self.dimensions = dims,
}
Expand All @@ -180,6 +192,10 @@ impl JsonViewer {

if track_window {
self.ensure_focused_row_is_visible();
} else if let Some(screen_index) = prev_index_of_focused_row {
// Keep focused line in same place on the screen.
self.top_row =
self.count_n_lines_before(self.focused_row, screen_index as usize, self.mode);
}
}

Expand Down Expand Up @@ -211,8 +227,10 @@ impl JsonViewer {
Action::MoveFocusedLineToCenter => false,
Action::MoveFocusedLineToBottom => false,
Action::Click(_) => true,
Action::CollapseNodeAndSiblings => true,
Action::ExpandNodeAndSiblings => true,
Action::CollapseNodeAndSiblings => false,
Action::DeepCollapseNodeAndSiblings => false,
Action::ExpandNodeAndSiblings => false,
Action::DeepExpandNodeAndSiblings => false,
Action::ToggleMode => false,
Action::ResizeViewerDimensions(_) => true,
_ => false,
Expand All @@ -235,6 +253,17 @@ impl JsonViewer {
)
}

fn should_keep_focused_row_at_same_screen_index(&self, action: &Action) -> Option<u16> {
match action {
Action::ToggleMode
| Action::CollapseNodeAndSiblings
| Action::DeepCollapseNodeAndSiblings
| Action::ExpandNodeAndSiblings
| Action::DeepExpandNodeAndSiblings => Some(self.index_of_focused_row_on_screen()),
_ => None,
}
}

fn move_up(&mut self, rows: usize) {
let mut row = self.focused_row;

Expand Down Expand Up @@ -684,6 +713,25 @@ impl JsonViewer {

fn collapse_node_and_siblings(&mut self) {
// If we're collapsing a node, make sure we're focused on the open.
self.switch_focus_to_opening_of_container_if_on_closing();
self.set_collapse_state_on_node_and_siblings(true);
}

fn deep_collapse_node_and_siblings(&mut self) {
// If we're collapsing a node, make sure we're focused on the open.
self.switch_focus_to_opening_of_container_if_on_closing();
self.set_deep_collapse_state_on_node_and_siblings(true);
}

fn expand_node_and_siblings(&mut self) {
self.set_collapse_state_on_node_and_siblings(false);
}

fn deep_expand_node_and_siblings(&mut self) {
self.set_deep_collapse_state_on_node_and_siblings(false);
}

fn switch_focus_to_opening_of_container_if_on_closing(&mut self) {
let focused_row = &mut self.flatjson[self.focused_row];
if focused_row.is_closing_of_container() {
debug_assert!(
Expand All @@ -692,12 +740,6 @@ impl JsonViewer {
);
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) {
Expand All @@ -722,9 +764,28 @@ impl JsonViewer {
}
}

fn toggle_mode(&mut self) {
let index_of_focused_row = self.index_of_focused_row_on_screen();
fn set_deep_collapse_state_on_node_and_siblings(&mut self, collapsed: bool) {
let (start, end) =
if let OptionIndex::Index(parent) = self.flatjson[self.focused_row].parent {
(parent + 1, self.flatjson[parent].pair_index().unwrap())
} else {
// If we don't have parent, that means we're at the top level, so the first
// sibling is the very first element.
(0, self.flatjson.0.len())
};

for i in start..end {
if self.flatjson[i].is_opening_of_container() {
if collapsed {
self.flatjson.collapse(i);
} else {
self.flatjson.expand(i);
}
}
}
}

fn toggle_mode(&mut self) {
// If we're transitioning from line mode to focused mode, and we're focused on
// the closing of a container, we need to move the focuse.
if self.mode == Mode::Line && self.flatjson[self.focused_row].is_closing_of_container() {
Expand All @@ -746,10 +807,6 @@ impl JsonViewer {
Mode::Line => Mode::Data,
Mode::Data => Mode::Line,
};

// Ensure focused line stays in same place on the screen.
self.top_row =
self.count_n_lines_before(self.focused_row, index_of_focused_row as usize, self.mode);
}

fn scrolloff(&self) -> u16 {
Expand Down Expand Up @@ -2036,39 +2093,137 @@ mod tests {
viewer.dimensions.height = 8;
viewer.scrolloff_setting = 1;

// This top_row will become invisible after collapsing.
viewer.top_row = 6;
viewer.focused_row = 8;
viewer.flatjson.collapse(8);

viewer.perform_action(Action::ExpandNodeAndSiblings);
assert!(viewer.flatjson[5].is_expanded());
assert!(viewer.flatjson[8].is_expanded());

viewer.focused_row = 10;
viewer.top_row = 8;
viewer.focused_row = 10; // Third line
viewer.perform_action(Action::CollapseNodeAndSiblings);
// Make sure top_row gets updated to a visible row.
assert_eq!(5, viewer.top_row);
// Make sure focused row is in same place on screen
// (Though this is awkward when we switch to the opening of the container.)
assert_eq!(4, 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);

// This top_row is the closing brace of a node that is
// about to be collapsed.
viewer.top_row = 3;
viewer.flatjson.expand(8);

viewer.focused_row = 12;
viewer.top_row = 10;
viewer.focused_row = 12; // Third line
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[8].is_expanded()); // Only shallow collapse
assert!(viewer.flatjson[12].is_collapsed());

viewer.flatjson.collapse(8);

viewer.perform_action(Action::ExpandNodeAndSiblings);
assert!(viewer.flatjson[1].is_expanded());
assert!(viewer.flatjson[4].is_expanded());
assert!(viewer.flatjson[8].is_collapsed()); // Only shallow expand
assert!(viewer.flatjson[12].is_expanded());

viewer.top_row = 0;
viewer.focused_row = 0;
viewer.perform_action(Action::ExpandNodeAndSiblings);
assert!(viewer.flatjson[0].is_expanded());
viewer.perform_action(Action::CollapseNodeAndSiblings);
assert!(viewer.flatjson[0].is_collapsed());
}

const LOTS_OF_TOP_LEVEL_OBJECTS: &str = r#"{
"1": {
"2": 2
},
"4": [
{
"6": 6
},
{
"9": 9
}
],
"12": {
"13": 13
}
}
{
"17": [
18,
],
}"#;

#[test]
fn test_deep_collapse_and_expand_node_and_siblings() {
let fj = parse_top_level_json(LOTS_OF_TOP_LEVEL_OBJECTS.to_owned()).unwrap();
let mut viewer = JsonViewer::new(fj, Mode::Line);

viewer.dimensions.height = 8;
viewer.scrolloff_setting = 1;

viewer.focused_row = 4;

viewer.flatjson.collapse(4);
viewer.flatjson.collapse(8);

viewer.perform_action(Action::DeepExpandNodeAndSiblings);
assert!(viewer.flatjson[1].is_expanded());
assert!(viewer.flatjson[4].is_expanded());
assert!(viewer.flatjson[5].is_expanded());
assert!(viewer.flatjson[8].is_expanded());
assert!(viewer.flatjson[12].is_expanded());

viewer.flatjson.expand(17);

viewer.top_row = 10;
viewer.focused_row = 11; // Second line
viewer.perform_action(Action::DeepCollapseNodeAndSiblings);
// Make sure top_row gets updated to a visible row.
assert_eq!(1, viewer.top_row);
assert_eq!(4, viewer.focused_row);
assert!(viewer.flatjson[1].is_collapsed());
assert!(viewer.flatjson[4].is_collapsed());
assert!(viewer.flatjson[5].is_collapsed());
assert!(viewer.flatjson[8].is_collapsed());
assert!(viewer.flatjson[12].is_collapsed());
// Cousin; not a sibling
assert!(viewer.flatjson[17].is_expanded());

viewer.flatjson.collapse(16);
viewer.flatjson.collapse(17);
viewer.top_row = 12;
viewer.focused_row = 15; // Second row
viewer.perform_action(Action::DeepExpandNodeAndSiblings);
assert_eq!(14, viewer.top_row);
assert_eq!(15, viewer.focused_row);
assert!(viewer.flatjson[0].is_expanded());
assert!(viewer.flatjson[1].is_expanded());
assert!(viewer.flatjson[4].is_expanded());
assert!(viewer.flatjson[5].is_expanded());
assert!(viewer.flatjson[8].is_expanded());
assert!(viewer.flatjson[12].is_expanded());
assert!(viewer.flatjson[16].is_expanded());
assert!(viewer.flatjson[17].is_expanded());

viewer.perform_action(Action::DeepCollapseNodeAndSiblings);
assert_eq!(0, viewer.top_row);
assert_eq!(0, viewer.focused_row);
assert!(viewer.flatjson[0].is_collapsed());
assert!(viewer.flatjson[1].is_collapsed());
assert!(viewer.flatjson[4].is_collapsed());
assert!(viewer.flatjson[5].is_collapsed());
assert!(viewer.flatjson[8].is_collapsed());
assert!(viewer.flatjson[12].is_collapsed());
assert!(viewer.flatjson[16].is_collapsed());
assert!(viewer.flatjson[17].is_collapsed());
}

#[test]
Expand Down

0 comments on commit 5d5a597

Please sign in to comment.