gchemol_parser/
view.rs

1// [[file:../parser.note::03bd258c][03bd258c]]
2use super::*;
3// 03bd258c ends here
4
5// [[file:../parser.note::6c729559][6c729559]]
6use ropey::str_utils::{byte_to_line_idx, char_to_byte_idx, line_to_byte_idx};
7
8/// A simple line-based text viewer for quick peeking part of text
9#[derive(Debug, Clone)]
10pub struct TextViewer {
11    text: String,
12    pos: usize,
13}
14
15impl TextViewer {
16    fn new(text: String) -> Self {
17        Self { text, pos: 0 }
18    }
19
20    /// Return byte index from line number in string.
21    fn line_pos(&self, line_num: usize) -> usize {
22        assert!(line_num >= 1, "invalid line number: {}", line_num);
23        line_to_byte_idx(&self.text, line_num - 1)
24    }
25
26    /// Return line number from byte index `pos`
27    fn pos_to_line_num(&self, pos: usize) -> usize {
28        byte_to_line_idx(&self.text, pos) + 1
29    }
30}
31// 6c729559 ends here
32
33// [[file:../parser.note::09977f99][09977f99]]
34use regex::RegexBuilder;
35
36/// Constructor a `TextViewer` like a text reader in read-only mode,
37/// suitable for small file that can be fully read into memory.
38impl TextViewer {
39    /// Create a view of text string.
40    pub fn from_str(txt: &str) -> Self {
41        Self::new(txt.to_owned())
42    }
43
44    /// Create a view of file context in path `p`
45    pub fn try_from_path(p: &Path) -> Result<Self> {
46        let text = gut::fs::read_file(p)?;
47        let view = Self::new(text);
48        Ok(view)
49    }
50}
51
52/// Core methods
53impl TextViewer {
54    /// Total number of lines
55    pub fn num_lines(&self) -> usize {
56        self.pos_to_line_num(self.text.len())
57    }
58
59    /// Get line number at cursor
60    pub fn current_line_num(&self) -> usize {
61        self.pos_to_line_num(self.pos)
62    }
63
64    /// Return str slice of inner text.
65    pub fn text(&self) -> &str {
66        self.text.as_str()
67    }
68
69    /// Peek the line at cursor
70    pub fn current_line(&self) -> &str {
71        self.peek_line(self.current_line_num())
72    }
73
74    /// Move the cursor to line `n`, counting from line 1 at beginning of the text.
75    pub fn goto_line(&mut self, n: usize) {
76        self.pos = self.line_pos(n);
77    }
78
79    /// Move the cursor to the beginning of the first line.
80    pub fn goto_first_line(&mut self) {
81        self.goto_line(1);
82    }
83
84    /// Move the cursor to the beginning of the last line.
85    pub fn goto_last_line(&mut self) {
86        self.goto_line(self.num_lines());
87    }
88
89    /// Move the cursor to the beginning of the next line.
90    pub fn goto_next_line(&mut self) {
91        self.goto_line(self.current_line_num() + 1);
92    }
93
94    /// Move the cursor to the beginning of the previous line.
95    pub fn goto_previous_line(&mut self) {
96        self.goto_line(self.current_line_num() - 1);
97    }
98
99    /// Move the cursor to the line matching `pattern`. Regex pattern
100    /// is allowed. Return current line number after search.
101    pub fn search_forward(&mut self, pattern: &str) -> Result<usize> {
102        let re = RegexBuilder::new(pattern).multi_line(true).build().context("invalid regex")?;
103        self.pos = re
104            .find_at(&self.text, self.pos)
105            .ok_or(format_err!("pattern not found: {}", pattern))?
106            .start();
107        Ok(self.current_line_num())
108    }
109
110    /// Search backward from current point for `pattern`. Return
111    /// current line number after search.
112    pub fn search_backward(&mut self, pattern: &str) -> Result<usize> {
113        let n = self.current_line_num();
114        let s = self.peek_lines(1, n);
115        let re = RegexBuilder::new(pattern).multi_line(true).build().context("invalid regex")?;
116        self.pos = re.find_iter(s).last().ok_or(format_err!("pattern not found: {}", pattern))?.start();
117        Ok(self.current_line_num())
118    }
119
120    /// Peek line `n` without moving cursor.
121    pub fn peek_line(&self, n: usize) -> &str {
122        let beg = self.line_pos(n);
123        let end = self.line_pos(n + 1);
124        &self.text[beg..end]
125    }
126
127    /// Peek the text between line `n` and `m` (including line `m`),
128    /// without moving cursor.
129    pub fn peek_lines(&self, n: usize, m: usize) -> &str {
130        let beg = self.line_pos(n);
131        // including the line `m`
132        let end = self.line_pos(m + 1);
133        &self.text[beg..end]
134    }
135
136    /// Select the next `n` lines from current point without moving
137    /// cursor, including current line.
138    pub fn selection(&self, n: usize) -> &str {
139        let m = self.current_line_num();
140        self.peek_lines(m, m + n - 1)
141    }
142
143    /// Select part of the string in next `n` lines (including
144    /// currrent line), in a rectangular area surrounded by columns in
145    /// `col_beg`--`col_end`.
146    pub fn column_selection(&self, n: usize, col_beg: usize, col_end: usize) -> String {
147        assert!(col_beg <= col_end, "invalid column data: {:?}", (col_beg, col_end));
148        let line_beg = self.current_line_num();
149        let line_end = line_beg + n - 1;
150
151        let lines = self.peek_lines(line_beg, line_end);
152        let mut selection = vec![];
153        for x in lines.lines() {
154            let p1 = char_to_byte_idx(x, col_beg);
155            let p2 = char_to_byte_idx(x, col_end);
156            selection.push(&x[p1..p2]);
157        }
158        selection.join("\n")
159    }
160}
161// 09977f99 ends here
162
163// [[file:../parser.note::c6e19a12][c6e19a12]]
164#[test]
165fn test_view() -> Result<()> {
166    let f = "./tests/files/lammps-test.dump";
167    let mut view = TextViewer::try_from_path(f.as_ref())?;
168
169    assert_eq!(view.num_lines(), 1639);
170    view.goto_line(2);
171    assert_eq!(view.current_line_num(), 2);
172
173    assert_eq!(view.peek_line(1), "ITEM: TIMESTEP\n");
174    assert_eq!(view.peek_lines(3, 5), "ITEM: NUMBER OF ATOMS\n537\nITEM: BOX BOUNDS pp pp pp\n");
175
176    view.search_forward(r"TIMESTEP$")?;
177    let n = view.current_line_num();
178    assert_eq!(n, 547);
179    view.search_forward(r"^ITEM: NU.*$")?;
180    let n = view.current_line_num();
181    assert_eq!(n, 549);
182
183    // search back
184    view.goto_last_line();
185    let l = view.current_line();
186    assert!(l.is_empty());
187    view.search_backward("^523 ");
188    let l = view.current_line();
189    assert!(l.starts_with("523 1 5.00268"));
190    assert_eq!(view.current_line_num(), 1624);
191
192    Ok(())
193}
194
195#[test]
196fn test_column_selection() -> Result<()> {
197    let f = "./tests/files/multi.xyz";
198    let mut view = TextViewer::try_from_path(f.as_ref())?;
199    view.goto_line(3);
200    let s = view.selection(2);
201    assert_eq!(s.lines().count(), 2);
202    let s = view.column_selection(3, 4, 100);
203    assert_eq!(s.lines().count(), 3);
204    let s = view.column_selection(3, 4, 24);
205    assert_eq!(s.lines().next().unwrap().split_whitespace().count(), 2);
206    println!("{}", s);
207
208    Ok(())
209}
210// c6e19a12 ends here