use super::*;
use ropey::str_utils::{byte_to_line_idx, char_to_byte_idx, line_to_byte_idx};
#[derive(Debug, Clone)]
pub struct TextViewer {
text: String,
pos: usize,
}
impl TextViewer {
fn new(text: String) -> Self {
Self { text, pos: 0 }
}
fn line_pos(&self, line_num: usize) -> usize {
assert!(line_num >= 1, "invalid line number: {}", line_num);
line_to_byte_idx(&self.text, line_num - 1)
}
fn pos_to_line_num(&self, pos: usize) -> usize {
byte_to_line_idx(&self.text, pos) + 1
}
}
use regex::RegexBuilder;
impl TextViewer {
pub fn from_str(txt: &str) -> Self {
Self::new(txt.to_owned())
}
pub fn try_from_path(p: &Path) -> Result<Self> {
let text = gut::fs::read_file(p)?;
let view = Self::new(text);
Ok(view)
}
}
impl TextViewer {
pub fn num_lines(&self) -> usize {
self.pos_to_line_num(self.text.len())
}
pub fn current_line_num(&self) -> usize {
self.pos_to_line_num(self.pos)
}
pub fn text(&self) -> &str {
self.text.as_str()
}
pub fn current_line(&self) -> &str {
self.peek_line(self.current_line_num())
}
pub fn goto_line(&mut self, n: usize) {
self.pos = self.line_pos(n);
}
pub fn goto_first_line(&mut self) {
self.goto_line(1);
}
pub fn goto_last_line(&mut self) {
self.goto_line(self.num_lines());
}
pub fn goto_next_line(&mut self) {
self.goto_line(self.current_line_num() + 1);
}
pub fn goto_previous_line(&mut self) {
self.goto_line(self.current_line_num() - 1);
}
pub fn search_forward(&mut self, pattern: &str) -> Result<usize> {
let re = RegexBuilder::new(pattern).multi_line(true).build().context("invalid regex")?;
self.pos = re
.find_at(&self.text, self.pos)
.ok_or(format_err!("pattern not found: {}", pattern))?
.start();
Ok(self.current_line_num())
}
pub fn search_backward(&mut self, pattern: &str) -> Result<usize> {
let n = self.current_line_num();
let s = self.peek_lines(1, n);
let re = RegexBuilder::new(pattern).multi_line(true).build().context("invalid regex")?;
self.pos = re.find_iter(s).last().ok_or(format_err!("pattern not found: {}", pattern))?.start();
Ok(self.current_line_num())
}
pub fn peek_line(&self, n: usize) -> &str {
let beg = self.line_pos(n);
let end = self.line_pos(n + 1);
&self.text[beg..end]
}
pub fn peek_lines(&self, n: usize, m: usize) -> &str {
let beg = self.line_pos(n);
let end = self.line_pos(m + 1);
&self.text[beg..end]
}
pub fn selection(&self, n: usize) -> &str {
let m = self.current_line_num();
self.peek_lines(m, m + n - 1)
}
pub fn column_selection(&self, n: usize, col_beg: usize, col_end: usize) -> String {
assert!(col_beg <= col_end, "invalid column data: {:?}", (col_beg, col_end));
let line_beg = self.current_line_num();
let line_end = line_beg + n - 1;
let lines = self.peek_lines(line_beg, line_end);
let mut selection = vec![];
for x in lines.lines() {
let p1 = char_to_byte_idx(x, col_beg);
let p2 = char_to_byte_idx(x, col_end);
selection.push(&x[p1..p2]);
}
selection.join("\n")
}
}
#[test]
fn test_view() -> Result<()> {
let f = "./tests/files/lammps-test.dump";
let mut view = TextViewer::try_from_path(f.as_ref())?;
assert_eq!(view.num_lines(), 1639);
view.goto_line(2);
assert_eq!(view.current_line_num(), 2);
assert_eq!(view.peek_line(1), "ITEM: TIMESTEP\n");
assert_eq!(view.peek_lines(3, 5), "ITEM: NUMBER OF ATOMS\n537\nITEM: BOX BOUNDS pp pp pp\n");
view.search_forward(r"TIMESTEP$")?;
let n = view.current_line_num();
assert_eq!(n, 547);
view.search_forward(r"^ITEM: NU.*$")?;
let n = view.current_line_num();
assert_eq!(n, 549);
view.goto_last_line();
let l = view.current_line();
assert!(l.is_empty());
view.search_backward("^523 ");
let l = view.current_line();
assert!(l.starts_with("523 1 5.00268"));
assert_eq!(view.current_line_num(), 1624);
Ok(())
}
#[test]
fn test_column_selection() -> Result<()> {
let f = "./tests/files/multi.xyz";
let mut view = TextViewer::try_from_path(f.as_ref())?;
view.goto_line(3);
let s = view.selection(2);
assert_eq!(s.lines().count(), 2);
let s = view.column_selection(3, 4, 100);
assert_eq!(s.lines().count(), 3);
let s = view.column_selection(3, 4, 24);
assert_eq!(s.lines().next().unwrap().split_whitespace().count(), 2);
println!("{}", s);
Ok(())
}