mirror of
https://github.com/PaulJuliusMartinez/jless
synced 2024-07-01 07:56:01 +00:00
Functional Search!
This commit is contained in:
parent
5d10274949
commit
b9d309a4b5
116
SEARCH.md
Normal file
116
SEARCH.md
Normal file
|
@ -0,0 +1,116 @@
|
|||
# Search
|
||||
|
||||
## vim search
|
||||
|
||||
- Type /term, hit enter, jumps to it
|
||||
- Bottom of screen says [M/N] indicating matches, then
|
||||
specifies "W [1/N]" when wrapping
|
||||
|
||||
|
||||
- With default settings:
|
||||
- Cursor disappears when you hit '/'
|
||||
- Matches get highlighted as you type
|
||||
- Next match is white
|
||||
- Other matches are yellow
|
||||
- Matches stay highlighted
|
||||
|
||||
- There is a setting (`redrawtime`) that specifies max time
|
||||
spent finding matches
|
||||
|
||||
- `hlsearch` indicates whether results are highlighted when done searching
|
||||
- `incsearch` indicates whether results are highlighted while typing term
|
||||
|
||||
## Our initial implementation
|
||||
|
||||
- No highlighting as you type or after searching
|
||||
- Need to store last search term
|
||||
- Also computed regex
|
||||
- Need to store list of matches
|
||||
- We will find ALL matches at first
|
||||
- Store them in a single vector?
|
||||
|
||||
- Do we need to store a index of our last jump anywhere?
|
||||
- Multiple matches in a single line
|
||||
- Store last match jumped to
|
||||
|
||||
## Desired behavior
|
||||
|
||||
- Incremental + highlight search until a non-search key is pressed
|
||||
(either `n`/`N` or `*`/`#`)
|
||||
- Need to store whether actively searching still
|
||||
- Match is highlighted in string values
|
||||
- How are multiline searches highlighted?
|
||||
- How are keys highlighted?
|
||||
- Need to handle true search vs. key search slightly differently
|
||||
|
||||
|
||||
### Collapsed Containers
|
||||
|
||||
```
|
||||
{
|
||||
a: "apple",
|
||||
b: [
|
||||
"cherry",
|
||||
"date",
|
||||
],
|
||||
c: "cherry",
|
||||
}
|
||||
|
||||
{
|
||||
a: "apple",
|
||||
b: ["cherry", "date"],
|
||||
c: "cherry",
|
||||
}
|
||||
```
|
||||
|
||||
When a match is found in a collapsed container (e.g., searching for
|
||||
`cherry` above while `b` is highlighted), we will jump to that key/row,
|
||||
and display a message "There are N matches inside this container", then
|
||||
next search will continue after the collapsed container.
|
||||
|
||||
- Maybe there's a setting to automatically expand containers while
|
||||
searching.
|
||||
|
||||
## Search State
|
||||
|
||||
```
|
||||
struct SearchState {
|
||||
mode: enum { Key, Free },
|
||||
direction: enum { Down, Up },
|
||||
search_term: String,
|
||||
compiled_regex: Regex,
|
||||
matches: Vec<Range<usize>>,
|
||||
last_jump: usize,
|
||||
actively_searching: bool,
|
||||
}
|
||||
```
|
||||
|
||||
## Search Inputs
|
||||
|
||||
`/` Start freeform search
|
||||
`?` Start reverse freeform search
|
||||
`*` Start object key search
|
||||
`#` Start reverse object key search
|
||||
`n` Go to next match
|
||||
`N` Go to previous match
|
||||
|
||||
|
||||
When starting search =>
|
||||
- Create SearchState with actively searching `false`
|
||||
- Need mode & direction naturally
|
||||
- Need search term
|
||||
- Need json text
|
||||
- Then basically do a "go to next match"
|
||||
|
||||
## Messaging:
|
||||
|
||||
- Show search term in bottom while "actively searching"
|
||||
- After each jump show `W? [M/N]`
|
||||
- On collapsed containers display how many matches are inside
|
||||
- "No matches found"
|
||||
- Bad regex
|
||||
|
||||
|
||||
## Tricky stuff
|
||||
|
||||
- Updating immediate search state appropriately
|
|
@ -47,7 +47,7 @@ impl From<usize> for OptionIndex {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct FlatJson(
|
||||
Vec<Row>,
|
||||
pub Vec<Row>,
|
||||
// Single-line pretty printed version of the JSON.
|
||||
// Rows will contain references into this.
|
||||
pub String,
|
||||
|
@ -215,6 +215,13 @@ impl Row {
|
|||
pub fn pair_index(&self) -> OptionIndex {
|
||||
self.value.pair_index()
|
||||
}
|
||||
|
||||
pub fn full_range(&self) -> Range<usize> {
|
||||
match &self.key_range {
|
||||
Some(key_range) => key_range.start..self.range.end,
|
||||
None => self.range.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
|
|
58
src/jless.rs
58
src/jless.rs
|
@ -10,6 +10,7 @@ use crate::flatjson;
|
|||
use crate::input::TuiEvent;
|
||||
use crate::input::TuiEvent::{KeyEvent, MouseEvent, WinChEvent};
|
||||
use crate::screenwriter::{AnsiTTYWriter, ScreenWriter};
|
||||
use crate::search::{JumpDirection, SearchDirection, SearchMode, SearchState};
|
||||
use crate::types::TTYDimensions;
|
||||
use crate::viewer::{Action, JsonViewer};
|
||||
use crate::Opt;
|
||||
|
@ -19,6 +20,7 @@ pub struct JLess {
|
|||
screen_writer: ScreenWriter,
|
||||
input_buffer: Vec<u8>,
|
||||
input_filename: String,
|
||||
search_state: SearchState,
|
||||
}
|
||||
|
||||
pub const MAX_BUFFER_SIZE: usize = 9;
|
||||
|
@ -54,6 +56,7 @@ pub fn new(
|
|||
screen_writer,
|
||||
input_buffer: vec![],
|
||||
input_filename,
|
||||
search_state: SearchState::empty(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -120,6 +123,14 @@ impl JLess {
|
|||
let lines = self.parse_input_buffer_as_number();
|
||||
Some(Action::FocusNextSibling(lines))
|
||||
}
|
||||
Key::Char('n') => {
|
||||
let count = self.parse_input_buffer_as_number();
|
||||
Some(self.jump_to_next_search_match(count))
|
||||
}
|
||||
Key::Char('N') => {
|
||||
let count = self.parse_input_buffer_as_number();
|
||||
Some(self.jump_to_prev_search_match(count))
|
||||
}
|
||||
// These may interpret the input buffer some other way
|
||||
Key::Char('t') => {
|
||||
if self.input_buffer == "z".as_bytes() {
|
||||
|
@ -163,6 +174,24 @@ impl JLess {
|
|||
// Some(Action::Command(parse_command(_readline))
|
||||
None
|
||||
}
|
||||
Key::Char('/') => {
|
||||
let search_term = self.screen_writer.get_command().unwrap();
|
||||
self.initialize_freeform_search(SearchDirection::Forward, search_term);
|
||||
Some(self.jump_to_next_search_match(1))
|
||||
}
|
||||
Key::Char('?') => {
|
||||
let search_term = self.screen_writer.get_command().unwrap();
|
||||
self.initialize_freeform_search(SearchDirection::Reverse, search_term);
|
||||
Some(self.jump_to_next_search_match(1))
|
||||
}
|
||||
Key::Char('*') => {
|
||||
self.initialize_object_key_search(SearchDirection::Forward);
|
||||
Some(self.jump_to_next_search_match(1))
|
||||
}
|
||||
Key::Char('#') => {
|
||||
self.initialize_object_key_search(SearchDirection::Reverse);
|
||||
Some(self.jump_to_next_search_match(1))
|
||||
}
|
||||
_ => {
|
||||
print!("{}Got: {:?}\r", BELL, event);
|
||||
None
|
||||
|
@ -238,4 +267,33 @@ impl JLess {
|
|||
self.input_buffer.clear();
|
||||
n.unwrap_or(1)
|
||||
}
|
||||
|
||||
fn initialize_freeform_search(&mut self, direction: SearchDirection, search_term: String) {
|
||||
self.search_state = SearchState::initialize_search(
|
||||
&search_term,
|
||||
&self.viewer.flatjson.1,
|
||||
SearchMode::Freeform,
|
||||
direction,
|
||||
);
|
||||
}
|
||||
|
||||
fn initialize_object_key_search(&mut self, direction: SearchDirection) {}
|
||||
|
||||
fn jump_to_next_search_match(&mut self, jumps: usize) -> Action {
|
||||
let destination = self.search_state.jump_to_match(
|
||||
self.viewer.focused_row,
|
||||
&self.viewer.flatjson,
|
||||
JumpDirection::Next,
|
||||
);
|
||||
Action::MoveTo(destination)
|
||||
}
|
||||
|
||||
fn jump_to_prev_search_match(&mut self, jumps: usize) -> Action {
|
||||
let destination = self.search_state.jump_to_match(
|
||||
self.viewer.focused_row,
|
||||
&self.viewer.flatjson,
|
||||
JumpDirection::Prev,
|
||||
);
|
||||
Action::MoveTo(destination)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ mod jsonparser;
|
|||
mod jsontokenizer;
|
||||
mod lineprinter;
|
||||
mod screenwriter;
|
||||
mod search;
|
||||
mod truncate;
|
||||
mod tuicontrol;
|
||||
mod types;
|
||||
|
|
184
src/search.rs
Normal file
184
src/search.rs
Normal file
|
@ -0,0 +1,184 @@
|
|||
use regex::Regex;
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::flatjson::{FlatJson, Index};
|
||||
use crate::viewer::Action;
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum SearchMode {
|
||||
// Searching for an Object key, initiated by '*' or '#'.
|
||||
ObjectKey,
|
||||
// Searching for freeform text, initiated by / or ?
|
||||
Freeform,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum SearchDirection {
|
||||
Forward,
|
||||
Reverse,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum JumpDirection {
|
||||
Next,
|
||||
Prev,
|
||||
}
|
||||
|
||||
pub struct SearchState {
|
||||
mode: SearchMode,
|
||||
direction: SearchDirection,
|
||||
|
||||
search_term: String,
|
||||
compiled_regex: Regex,
|
||||
|
||||
matches: Vec<Range<usize>>,
|
||||
|
||||
immediate_state: ImmediateSearchState,
|
||||
}
|
||||
|
||||
pub enum ImmediateSearchState {
|
||||
NotSearching,
|
||||
ActivelySearching {
|
||||
last_match_jumped_to: usize,
|
||||
last_search_into_collapsed_container: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl SearchState {
|
||||
pub fn empty() -> SearchState {
|
||||
SearchState {
|
||||
mode: SearchMode::Freeform,
|
||||
direction: SearchDirection::Forward,
|
||||
search_term: "".to_owned(),
|
||||
compiled_regex: Regex::new("").unwrap(),
|
||||
matches: vec![],
|
||||
immediate_state: ImmediateSearchState::NotSearching,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize_search(
|
||||
needle: &str,
|
||||
haystack: &str,
|
||||
mode: SearchMode,
|
||||
direction: SearchDirection,
|
||||
) -> SearchState {
|
||||
let regex = Regex::new(needle).unwrap();
|
||||
let matches: Vec<Range<usize>> = regex.find_iter(haystack).map(|m| m.range()).collect();
|
||||
|
||||
SearchState {
|
||||
mode,
|
||||
direction,
|
||||
search_term: needle.to_owned(),
|
||||
compiled_regex: regex,
|
||||
matches,
|
||||
immediate_state: ImmediateSearchState::NotSearching,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn jump_to_match(
|
||||
&mut self,
|
||||
focused_row: Index,
|
||||
flatjson: &FlatJson,
|
||||
jump_direction: JumpDirection,
|
||||
) -> usize {
|
||||
if self.matches.is_empty() {
|
||||
eprintln!("NEED TO HANDLE NO MATCHES");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let true_direction = self.true_direction(jump_direction);
|
||||
|
||||
let next_match_index = self.get_next_match(focused_row, flatjson, true_direction);
|
||||
let destination_row = self.compute_destination_row(flatjson, next_match_index);
|
||||
|
||||
// TODO: Need to make sure that destination_row is not in a collapsed container.
|
||||
|
||||
destination_row
|
||||
}
|
||||
|
||||
fn true_direction(&self, jump_direction: JumpDirection) -> SearchDirection {
|
||||
match (self.direction, jump_direction) {
|
||||
(SearchDirection::Forward, JumpDirection::Next) => SearchDirection::Forward,
|
||||
(SearchDirection::Forward, JumpDirection::Prev) => SearchDirection::Reverse,
|
||||
(SearchDirection::Reverse, JumpDirection::Next) => SearchDirection::Reverse,
|
||||
(SearchDirection::Reverse, JumpDirection::Prev) => SearchDirection::Forward,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_next_match(
|
||||
&mut self,
|
||||
focused_row: Index,
|
||||
flatjson: &FlatJson,
|
||||
true_direction: SearchDirection,
|
||||
) -> usize {
|
||||
match self.immediate_state {
|
||||
ImmediateSearchState::NotSearching => {
|
||||
let focused_row_range = flatjson[focused_row].full_range();
|
||||
|
||||
match true_direction {
|
||||
SearchDirection::Forward => {
|
||||
// When searching forwards, we want the first match that
|
||||
// starts _after_ (or equal) the end of focused row.
|
||||
let next_match = self.matches.partition_point(|match_range| {
|
||||
match_range.start <= focused_row_range.end
|
||||
});
|
||||
|
||||
// If NONE of the matches start after the end of the focused row,
|
||||
// parition_point returns the length of the array, but then we
|
||||
// want to jump back to the start in that case.
|
||||
if next_match == self.matches.len() {
|
||||
0
|
||||
} else {
|
||||
next_match
|
||||
}
|
||||
}
|
||||
SearchDirection::Reverse => {
|
||||
// When searching backwards, we want the last match that
|
||||
// ends before the start of focused row.
|
||||
let next_match = self.matches.partition_point(|match_range| {
|
||||
match_range.end < focused_row_range.start
|
||||
});
|
||||
|
||||
// If the very first match ends the start of the focused row,
|
||||
// then partition_point will return 0, and we need to wrap
|
||||
// around to the end of the file.
|
||||
//
|
||||
// But otherwise, partition_point will return the first match
|
||||
// that didn't end before the start of the focused row, so we
|
||||
// need to subtract 1.
|
||||
if next_match == 0 {
|
||||
self.matches.len() - 1
|
||||
} else {
|
||||
next_match - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImmediateSearchState::ActivelySearching {
|
||||
last_match_jumped_to,
|
||||
last_search_into_collapsed_container,
|
||||
} => {
|
||||
let delta: isize = match true_direction {
|
||||
SearchDirection::Forward => 1,
|
||||
SearchDirection::Reverse => -1,
|
||||
};
|
||||
|
||||
let next_match = ((last_match_jumped_to + self.matches.len()) as isize + delta)
|
||||
as usize
|
||||
% self.matches.len();
|
||||
|
||||
next_match
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_destination_row(&self, flatjson: &FlatJson, match_index: usize) -> Index {
|
||||
let match_range = &self.matches[match_index]; // [a, b)
|
||||
|
||||
// We want to jump to the last row that starts before (or at) the start of the match.
|
||||
flatjson
|
||||
.0
|
||||
.partition_point(|row| row.full_range().start <= match_range.start)
|
||||
- 1
|
||||
}
|
||||
}
|
|
@ -54,10 +54,14 @@ impl JsonViewer {
|
|||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Action {
|
||||
// Does nothing, for debugging, shouldn't modify any state.
|
||||
NoOp,
|
||||
|
||||
MoveUp(usize),
|
||||
MoveDown(usize),
|
||||
MoveLeft,
|
||||
MoveRight,
|
||||
MoveTo(Index),
|
||||
|
||||
FocusParent,
|
||||
|
||||
|
@ -100,10 +104,12 @@ impl JsonViewer {
|
|||
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::FocusParent => self.focus_parent(),
|
||||
Action::FocusPrevSibling(n) => self.focus_prev_sibling(n),
|
||||
Action::FocusNextSibling(n) => self.focus_next_sibling(n),
|
||||
|
@ -141,10 +147,12 @@ impl JsonViewer {
|
|||
|
||||
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::FocusParent => true,
|
||||
Action::FocusPrevSibling(_) => true,
|
||||
Action::FocusNextSibling(_) => true,
|
||||
|
@ -171,6 +179,7 @@ impl JsonViewer {
|
|||
|
||||
fn should_reset_desired_depth(action: &Action) -> bool {
|
||||
match action {
|
||||
Action::NoOp => false,
|
||||
Action::FocusPrevSibling(_) => false,
|
||||
Action::FocusNextSibling(_) => false,
|
||||
Action::ScrollUp(_) => false,
|
||||
|
|
Loading…
Reference in New Issue
Block a user