use nom::{
IResult, Parser,
branch::alt,
bytes::complete::{tag, take},
character::complete::{anychar, char, digit1, multispace1, one_of, satisfy},
combinator::{opt, recognize},
multi::many0,
sequence::{pair, preceded},
};
use nom_locate::LocatedSpan;
type Span<'a> = LocatedSpan<&'a str>;
mod nav_tokens {
use super::*;
pub fn whitespace(input: Span<'_>) -> IResult<Span<'_>, ()> {
let (rest, _) = multispace1(input)?;
Ok((rest, ()))
}
pub fn string_literal(input: Span<'_>) -> IResult<Span<'_>, ()> {
let (rest, _) = recognize(pair(
char('"'),
pair(
many0(alt((
recognize(preceded(char('\\'), anychar)),
recognize(satisfy(|c| c != '"' && c != '\\')),
))),
char('"'),
),
))
.parse(input)?;
Ok((rest, ()))
}
pub fn identifier(input: Span<'_>) -> IResult<Span<'_>, Span<'_>> {
recognize(pair(
satisfy(|c| c.is_alphabetic() || c == '_'),
many0(satisfy(|c| c.is_alphanumeric() || c == '_')),
))
.parse(input)
}
pub fn number(input: Span<'_>) -> IResult<Span<'_>, ()> {
let (rest, _) = recognize(pair(
pair(opt(char('-')), digit1),
opt(alt((
recognize(pair(preceded(char('.'), digit1), opt(exponent))),
exponent,
))),
))
.parse(input)?;
Ok((rest, ()))
}
fn exponent(input: Span<'_>) -> IResult<Span<'_>, Span<'_>> {
recognize(pair(one_of("eE"), pair(opt(one_of("+-")), digit1))).parse(input)
}
pub fn two_char_op(input: Span<'_>) -> IResult<Span<'_>, ()> {
let (rest, _) =
alt((tag("=="), tag("!="), tag("<="), tag(">="), tag("//"))).parse(input)?;
Ok((rest, ()))
}
pub fn single_char_token(input: Span<'_>) -> IResult<Span<'_>, char> {
alt((
char('.'),
char('('),
char(')'),
char('['),
char(']'),
char('{'),
char('}'),
char('<'),
char('>'),
char('+'),
char('-'),
char('*'),
char('/'),
char(','),
char(':'),
char(';'),
char('?'),
))
.parse(input)
}
pub fn pipe(input: Span<'_>) -> IResult<Span<'_>, ()> {
let (rest, _) = char('|').parse(input)?;
Ok((rest, ()))
}
pub fn skip_one(input: Span<'_>) -> IResult<Span<'_>, ()> {
let (rest, _) = take(1usize).parse(input)?;
Ok((rest, ()))
}
}
#[derive(Debug, Clone)]
pub struct TextBuffer {
text: String,
cursor: usize, }
impl TextBuffer {
pub fn new(initial: &str) -> Self {
let cursor = initial.chars().count();
Self {
text: initial.to_string(),
cursor,
}
}
pub fn text(&self) -> &str {
&self.text
}
#[cfg(test)]
pub fn cursor(&self) -> usize {
self.cursor
}
#[cfg(test)]
pub fn set_cursor(&mut self, pos: usize) {
self.cursor = pos.min(self.len());
}
pub fn len(&self) -> usize {
self.text.chars().count()
}
pub fn insert(&mut self, c: char) {
let byte_pos = self.cursor_byte_pos();
self.text.insert(byte_pos, c);
self.cursor += 1;
}
pub fn backspace(&mut self) -> bool {
if self.cursor > 0 {
self.cursor -= 1;
let byte_pos = self.cursor_byte_pos();
self.text.remove(byte_pos);
true
} else {
false
}
}
pub fn delete(&mut self) -> bool {
if self.cursor < self.len() {
let byte_pos = self.cursor_byte_pos();
self.text.remove(byte_pos);
true
} else {
false
}
}
pub fn move_left(&mut self) {
self.cursor = self.cursor.saturating_sub(1);
}
pub fn move_right(&mut self) {
self.cursor = (self.cursor + 1).min(self.len());
}
pub fn move_to_start(&mut self) {
self.cursor = 0;
}
pub fn move_to_end(&mut self) {
self.cursor = self.len();
}
pub fn cursor_byte_pos(&self) -> usize {
self.char_to_byte_index(self.cursor)
}
fn char_to_byte_index(&self, char_index: usize) -> usize {
self.text
.char_indices()
.nth(char_index)
.map(|(i, _)| i)
.unwrap_or(self.text.len())
}
pub fn delete_word_back(&mut self) -> bool {
if self.cursor == 0 {
return false;
}
let chars: Vec<char> = self.text.chars().collect();
let mut new_cursor = self.cursor;
while new_cursor > 0 && (chars[new_cursor - 1] == '.' || chars[new_cursor - 1] == ' ') {
new_cursor -= 1;
}
while new_cursor > 0 && chars[new_cursor - 1] != '.' && chars[new_cursor - 1] != ' ' {
new_cursor -= 1;
}
if new_cursor < self.cursor {
let start_byte = self.char_to_byte_index(new_cursor);
let end_byte = self.char_to_byte_index(self.cursor);
self.text.replace_range(start_byte..end_byte, "");
self.cursor = new_cursor;
true
} else {
false
}
}
pub fn navigation_positions(&self) -> Vec<usize> {
Self::compute_navigation_positions(&self.text)
}
fn compute_navigation_positions(filter_text: &str) -> Vec<usize> {
use nav_tokens::*;
let mut byte_positions = vec![0usize];
let mut input = Span::new(filter_text);
let len = filter_text.len();
while !input.is_empty() {
if let Ok((rest, _)) = whitespace(input) {
input = rest;
continue;
}
if let Ok((rest, _)) = string_literal(input) {
byte_positions.push(rest.location_offset());
input = rest;
continue;
}
if let Ok((rest, _)) = pipe(input) {
byte_positions.push(input.location_offset()); byte_positions.push(rest.location_offset()); input = rest;
continue;
}
if let Ok((rest, _)) = two_char_op(input) {
byte_positions.push(rest.location_offset());
input = rest;
continue;
}
if let Ok((rest, _)) = identifier(input) {
byte_positions.push(rest.location_offset());
input = rest;
continue;
}
if let Ok((rest, _)) = number(input) {
byte_positions.push(rest.location_offset());
input = rest;
continue;
}
if let Ok((rest, _)) = single_char_token(input) {
byte_positions.push(rest.location_offset());
input = rest;
continue;
}
if let Ok((rest, _)) = skip_one(input) {
input = rest;
}
}
if len > 0 && !byte_positions.contains(&len) {
byte_positions.push(len);
}
let char_positions: Vec<usize> = byte_positions
.into_iter()
.map(|byte_pos| filter_text[..byte_pos].chars().count())
.collect();
let mut positions = char_positions;
positions.sort();
positions.dedup();
positions
}
pub fn jump_word_back(&mut self) {
if self.cursor == 0 {
return;
}
let positions = self.navigation_positions();
let mut target = 0;
for &pos in &positions {
if pos < self.cursor {
target = pos;
}
}
self.cursor = target;
}
pub fn jump_word_forward(&mut self) {
let len = self.len();
if self.cursor >= len {
return;
}
let positions = self.navigation_positions();
for &pos in &positions {
if pos > self.cursor {
self.cursor = pos;
return;
}
}
self.cursor = len;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let buf = TextBuffer::new("hello");
assert_eq!(buf.text(), "hello");
assert_eq!(buf.cursor(), 5);
assert_eq!(buf.len(), 5);
}
#[test]
fn test_insert() {
let mut buf = TextBuffer::new(".");
buf.insert('f');
assert_eq!(buf.text(), ".f");
buf.insert('o');
buf.insert('o');
assert_eq!(buf.text(), ".foo");
assert_eq!(buf.cursor(), 4);
}
#[test]
fn test_backspace() {
let mut buf = TextBuffer::new(".foo");
assert!(buf.backspace());
assert_eq!(buf.text(), ".fo");
assert_eq!(buf.cursor(), 3);
}
#[test]
fn test_backspace_at_start() {
let mut buf = TextBuffer::new(".");
buf.set_cursor(0);
assert!(!buf.backspace());
assert_eq!(buf.text(), ".");
}
#[test]
fn test_delete() {
let mut buf = TextBuffer::new(".foo");
buf.set_cursor(1);
assert!(buf.delete());
assert_eq!(buf.text(), ".oo");
assert_eq!(buf.cursor(), 1);
}
#[test]
fn test_delete_at_end() {
let mut buf = TextBuffer::new(".foo");
assert!(!buf.delete());
assert_eq!(buf.text(), ".foo");
}
#[test]
fn test_move_left_right() {
let mut buf = TextBuffer::new(".foo");
assert_eq!(buf.cursor(), 4);
buf.move_left();
assert_eq!(buf.cursor(), 3);
buf.move_right();
assert_eq!(buf.cursor(), 4);
buf.move_right(); assert_eq!(buf.cursor(), 4);
}
#[test]
fn test_move_to_start_end() {
let mut buf = TextBuffer::new(".foo");
buf.move_to_start();
assert_eq!(buf.cursor(), 0);
buf.move_to_end();
assert_eq!(buf.cursor(), 4);
}
#[test]
fn test_delete_word_back() {
let mut buf = TextBuffer::new(".foo.bar");
assert!(buf.delete_word_back());
assert_eq!(buf.text(), ".foo.");
assert_eq!(buf.cursor(), 5);
}
#[test]
fn test_delete_word_back_at_start() {
let mut buf = TextBuffer::new(".");
buf.set_cursor(0);
assert!(!buf.delete_word_back());
assert_eq!(buf.text(), ".");
}
#[test]
fn test_navigation_positions_identity() {
let buf = TextBuffer::new(".");
assert_eq!(buf.navigation_positions(), vec![0, 1]);
}
#[test]
fn test_navigation_positions_field() {
let buf = TextBuffer::new(".foo");
assert_eq!(buf.navigation_positions(), vec![0, 1, 4]);
}
#[test]
fn test_navigation_positions_iterate() {
let buf = TextBuffer::new(".[]");
assert_eq!(buf.navigation_positions(), vec![0, 1, 2, 3]);
}
#[test]
fn test_navigation_positions_multiple_iterate() {
let buf = TextBuffer::new(".[][][]");
assert_eq!(buf.navigation_positions(), vec![0, 1, 2, 3, 4, 5, 6, 7]);
}
#[test]
fn test_navigation_positions_index() {
let buf = TextBuffer::new(".[0]");
assert_eq!(buf.navigation_positions(), vec![0, 1, 2, 3, 4]);
}
#[test]
fn test_navigation_positions_field_chain() {
let buf = TextBuffer::new(".foo.bar");
assert_eq!(buf.navigation_positions(), vec![0, 1, 4, 5, 8]);
}
#[test]
fn test_navigation_positions_mixed() {
let buf = TextBuffer::new(".foo[0].bar");
assert_eq!(buf.navigation_positions(), vec![0, 1, 4, 5, 6, 7, 8, 11]);
}
#[test]
fn test_navigation_positions_quoted_field() {
let buf = TextBuffer::new(".[\"foo\"]");
assert_eq!(buf.navigation_positions(), vec![0, 1, 2, 7, 8]);
}
#[test]
fn test_jump_word_back() {
let mut buf = TextBuffer::new(".foo.bar");
buf.jump_word_back(); assert_eq!(buf.cursor(), 5);
buf.jump_word_back(); assert_eq!(buf.cursor(), 4);
buf.jump_word_back(); assert_eq!(buf.cursor(), 1);
buf.jump_word_back(); assert_eq!(buf.cursor(), 0);
}
#[test]
fn test_jump_word_forward() {
let mut buf = TextBuffer::new(".foo.bar");
buf.set_cursor(0);
buf.jump_word_forward(); assert_eq!(buf.cursor(), 1);
buf.jump_word_forward(); assert_eq!(buf.cursor(), 4);
buf.jump_word_forward(); assert_eq!(buf.cursor(), 5);
buf.jump_word_forward(); assert_eq!(buf.cursor(), 8);
}
#[test]
fn test_unicode_handling() {
let mut buf = TextBuffer::new(".héllo");
assert_eq!(buf.len(), 6);
assert_eq!(buf.cursor(), 6);
buf.move_left();
assert_eq!(buf.cursor(), 5);
buf.insert('!');
assert_eq!(buf.text(), ".héll!o");
}
#[test]
fn test_navigation_positions_with_pipes() {
let buf = TextBuffer::new(". | sort | reverse");
let positions = buf.navigation_positions();
assert!(positions.contains(&0)); assert!(positions.contains(&1)); assert!(positions.contains(&2)); assert!(positions.contains(&3)); assert!(positions.contains(&8)); assert!(positions.contains(&9)); assert!(positions.contains(&10)); assert!(positions.contains(&18)); }
#[test]
fn test_jump_word_back_with_pipes() {
let mut buf = TextBuffer::new(". | sort | reverse");
buf.jump_word_back(); assert_eq!(buf.cursor(), 10); buf.jump_word_back(); assert_eq!(buf.cursor(), 9); buf.jump_word_back(); assert_eq!(buf.cursor(), 8); buf.jump_word_back(); assert_eq!(buf.cursor(), 3); buf.jump_word_back(); assert_eq!(buf.cursor(), 2); buf.jump_word_back(); assert_eq!(buf.cursor(), 1); buf.jump_word_back(); assert_eq!(buf.cursor(), 0); }
#[test]
fn test_navigation_positions_escaped_backslash_in_string() {
let buf = TextBuffer::new(r#".["key\\"] | keys"#);
let positions = buf.navigation_positions();
assert!(positions.contains(&0)); assert!(positions.contains(&1)); assert!(positions.contains(&2)); assert!(positions.contains(&9)); assert!(positions.contains(&10)); assert!(positions.contains(&11)); assert!(positions.contains(&12)); assert!(positions.contains(&17)); }
#[test]
fn test_expression_nav_select_with_comparison() {
let buf = TextBuffer::new("[.items[] | select(.count < 20)]");
let positions = buf.navigation_positions();
assert!(positions.contains(&0)); assert!(positions.contains(&1)); assert!(positions.contains(&2)); assert!(positions.contains(&7)); assert!(positions.contains(&8)); assert!(positions.contains(&9)); assert!(positions.contains(&10)); assert!(positions.contains(&11)); assert!(positions.contains(&18)); assert!(positions.contains(&19)); assert!(positions.contains(&20)); assert!(positions.contains(&25)); assert!(positions.contains(&27)); assert!(positions.contains(&30)); assert!(positions.contains(&31)); assert!(positions.contains(&32)); }
#[test]
fn test_expression_nav_operators() {
let buf = TextBuffer::new(".x + .y - .z * 2 / 3 // 1");
let positions = buf.navigation_positions();
assert!(positions.contains(&0)); assert!(positions.contains(&1)); assert!(positions.contains(&2)); assert!(positions.contains(&4)); assert!(positions.contains(&6)); assert!(positions.contains(&7)); assert!(positions.contains(&9)); assert!(positions.contains(&11)); assert!(positions.contains(&12)); assert!(positions.contains(&14)); assert!(positions.contains(&16)); assert!(positions.contains(&18)); assert!(positions.contains(&20)); assert!(positions.contains(&23)); assert!(positions.contains(&25)); }
#[test]
fn test_expression_nav_nested_parens() {
let buf = TextBuffer::new("(((.x)))");
let positions = buf.navigation_positions();
assert!(positions.contains(&0)); assert!(positions.contains(&1)); assert!(positions.contains(&2)); assert!(positions.contains(&3)); assert!(positions.contains(&4)); assert!(positions.contains(&5)); assert!(positions.contains(&6)); assert!(positions.contains(&7)); assert!(positions.contains(&8)); }
#[test]
fn test_expression_nav_string_literals() {
let buf = TextBuffer::new(r#".foo | select(.bar == "test.value") | .baz"#);
let positions = buf.navigation_positions();
assert!(positions.contains(&0)); assert!(positions.contains(&1)); assert!(positions.contains(&4)); assert!(positions.contains(&5)); assert!(positions.contains(&6)); assert!(positions.contains(&13)); assert!(positions.contains(&14)); assert!(positions.contains(&15)); assert!(positions.contains(&18)); assert!(positions.contains(&21)); assert!(positions.contains(&34)); assert!(positions.contains(&35)); assert!(positions.contains(&36)); assert!(positions.contains(&37)); assert!(positions.contains(&39)); assert!(positions.contains(&42)); }
#[test]
fn test_expression_nav_keywords() {
let buf = TextBuffer::new("if .x > 0 then .y else .z end");
let positions = buf.navigation_positions();
assert!(positions.contains(&0)); assert!(positions.contains(&2)); assert!(positions.contains(&4)); assert!(positions.contains(&5)); assert!(positions.contains(&7)); assert!(positions.contains(&9)); assert!(positions.contains(&14)); assert!(positions.contains(&16)); assert!(positions.contains(&17)); assert!(positions.contains(&22)); assert!(positions.contains(&24)); assert!(positions.contains(&25)); assert!(positions.contains(&29)); }
#[test]
fn test_expression_nav_logical_operators() {
let buf = TextBuffer::new(".x > 0 and .y < 10 or not .z");
let positions = buf.navigation_positions();
assert!(positions.contains(&0)); assert!(positions.contains(&1)); assert!(positions.contains(&2)); assert!(positions.contains(&4)); assert!(positions.contains(&6)); assert!(positions.contains(&10)); assert!(positions.contains(&12)); assert!(positions.contains(&13)); assert!(positions.contains(&15)); assert!(positions.contains(&18)); assert!(positions.contains(&21)); assert!(positions.contains(&25)); assert!(positions.contains(&27)); assert!(positions.contains(&28)); }
#[test]
fn test_expression_nav_string_with_escaped_quote() {
let buf = TextBuffer::new(r#".x == "foo\"bar""#);
let positions = buf.navigation_positions();
assert!(positions.contains(&0)); assert!(positions.contains(&1)); assert!(positions.contains(&2)); assert!(positions.contains(&5)); assert!(positions.contains(&16)); assert!(!positions.contains(&7)); assert!(!positions.contains(&10)); }
#[test]
fn test_expression_nav_string_with_keywords() {
let buf = TextBuffer::new(r#".x == "and or if""#);
let positions = buf.navigation_positions();
assert!(positions.contains(&0)); assert!(positions.contains(&1)); assert!(positions.contains(&2)); assert!(positions.contains(&5)); assert!(positions.contains(&17)); assert!(!positions.contains(&7)); assert!(!positions.contains(&11)); assert!(!positions.contains(&14)); }
#[test]
fn test_expression_nav_string_with_operators() {
let buf = TextBuffer::new(r#".x == "> | .""#);
let positions = buf.navigation_positions();
assert!(positions.contains(&0)); assert!(positions.contains(&1)); assert!(positions.contains(&2)); assert!(positions.contains(&5)); assert!(positions.contains(&13)); assert!(!positions.contains(&7)); assert!(!positions.contains(&9)); assert!(!positions.contains(&11)); }
#[test]
fn test_expression_nav_complex_filter() {
let buf = TextBuffer::new(
"[.data[] | select(.val > 100 and .ok == true)] | sort_by(.val) | reverse",
);
let positions = buf.navigation_positions();
assert!(positions.contains(&0)); assert!(positions.contains(&1)); assert!(positions.contains(&2)); assert!(positions.contains(&6)); assert!(positions.contains(&7)); assert!(positions.contains(&8)); assert!(positions.contains(&9)); assert!(positions.contains(&10)); assert!(positions.contains(&17)); assert!(positions.contains(&18)); assert!(positions.contains(&19)); assert!(positions.contains(&22)); assert!(positions.contains(&24)); assert!(positions.contains(&28)); assert!(positions.contains(&32)); assert!(positions.contains(&34)); assert!(positions.contains(&36)); assert!(positions.contains(&39)); assert!(positions.contains(&44)); assert!(positions.contains(&45)); assert!(positions.contains(&46)); assert!(positions.contains(&47)); assert!(positions.contains(&48)); assert!(positions.contains(&56)); assert!(positions.contains(&57)); assert!(positions.contains(&58)); assert!(positions.contains(&61)); assert!(positions.contains(&62)); assert!(positions.contains(&63)); assert!(positions.contains(&64)); assert!(positions.contains(&72)); }
}