1use super::*;
3use ropey::str_utils::{byte_to_line_idx, char_to_byte_idx, line_to_byte_idx};
7
8#[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 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 fn pos_to_line_num(&self, pos: usize) -> usize {
28 byte_to_line_idx(&self.text, pos) + 1
29 }
30}
31use regex::RegexBuilder;
35
36impl TextViewer {
39 pub fn from_str(txt: &str) -> Self {
41 Self::new(txt.to_owned())
42 }
43
44 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
52impl TextViewer {
54 pub fn num_lines(&self) -> usize {
56 self.pos_to_line_num(self.text.len())
57 }
58
59 pub fn current_line_num(&self) -> usize {
61 self.pos_to_line_num(self.pos)
62 }
63
64 pub fn text(&self) -> &str {
66 self.text.as_str()
67 }
68
69 pub fn current_line(&self) -> &str {
71 self.peek_line(self.current_line_num())
72 }
73
74 pub fn goto_line(&mut self, n: usize) {
76 self.pos = self.line_pos(n);
77 }
78
79 pub fn goto_first_line(&mut self) {
81 self.goto_line(1);
82 }
83
84 pub fn goto_last_line(&mut self) {
86 self.goto_line(self.num_lines());
87 }
88
89 pub fn goto_next_line(&mut self) {
91 self.goto_line(self.current_line_num() + 1);
92 }
93
94 pub fn goto_previous_line(&mut self) {
96 self.goto_line(self.current_line_num() - 1);
97 }
98
99 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 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 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 pub fn peek_lines(&self, n: usize, m: usize) -> &str {
130 let beg = self.line_pos(n);
131 let end = self.line_pos(m + 1);
133 &self.text[beg..end]
134 }
135
136 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 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#[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 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