Krolyxon
Commit 6efefdd

Branches

Tags

Refs →

bench51cratesbench51-guisrcvimtext.rs

488 lines · 14.0 KB · Rust

1//! Char-indexed text helpers for the Vim engine: lines, columns, word
2//! classes, motions and text objects. Positions are char indices.
3
4/// How a motion's range is taken by an operator (`:help exclusive`).
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum Kind {
7 Exclusive,
8 Inclusive,
9 Linewise,
10}
11
12pub struct Buf {
13 pub c: Vec<char>,
14}
15
16pub fn is_blank(ch: char) -> bool {
17 ch == ' ' || ch == '\t'
18}
19
20fn is_space(ch: char) -> bool {
21 ch.is_whitespace()
22}
23
24pub fn is_keyword(ch: char) -> bool {
25 ch.is_alphanumeric() || ch == '_'
26}
27
28/// 0 whitespace, 1 keyword, 2 punctuation; `big` words only tell
29/// whitespace from the rest.
30fn class(ch: char, big: bool) -> u8 {
31 if is_space(ch) {
32 0
33 } else if big || is_keyword(ch) {
34 1
35 } else {
36 2
37 }
38}
39
40impl Buf {
41 pub fn new(text: &str) -> Self {
42 Self {
43 c: text.chars().collect(),
44 }
45 }
46
47 pub fn text(&self) -> String {
48 self.c.iter().collect()
49 }
50
51 pub fn len(&self) -> usize {
52 self.c.len()
53 }
54
55 pub fn line_start(&self, pos: usize) -> usize {
56 let mut i = pos.min(self.len());
57 while i > 0 && self.c[i - 1] != '\n' {
58 i -= 1;
59 }
60 i
61 }
62
63 /// Index of the line's `\n`, or the text length on the last line.
64 pub fn line_end(&self, pos: usize) -> usize {
65 let mut i = pos.min(self.len());
66 while i < self.len() && self.c[i] != '\n' {
67 i += 1;
68 }
69 i
70 }
71
72 pub fn line_of(&self, pos: usize) -> usize {
73 self.c[..pos.min(self.len())]
74 .iter()
75 .filter(|&&ch| ch == '\n')
76 .count()
77 }
78
79 pub fn lines(&self) -> usize {
80 self.c.iter().filter(|&&ch| ch == '\n').count() + 1
81 }
82
83 /// Start of `line`, clamped to the last line.
84 pub fn start_of_line(&self, line: usize) -> usize {
85 if line == 0 {
86 return 0;
87 }
88 let mut seen = 0;
89 let mut last_start = 0;
90 for (i, &ch) in self.c.iter().enumerate() {
91 if ch == '\n' {
92 seen += 1;
93 last_start = i + 1;
94 if seen == line {
95 return i + 1;
96 }
97 }
98 }
99 last_start
100 }
101
102 pub fn col(&self, pos: usize) -> usize {
103 pos - self.line_start(pos)
104 }
105
106 pub fn line_is_blank(&self, line: usize) -> bool {
107 let s = self.start_of_line(line);
108 let e = self.line_end(s);
109 self.c[s..e].iter().all(|&ch| is_blank(ch))
110 }
111
112 pub fn line_text(&self, pos: usize) -> String {
113 let s = self.line_start(pos);
114 self.c[s..self.line_end(s)].iter().collect()
115 }
116
117 /// Position on `line` at `col`, clamped to the line's last char.
118 pub fn at_col(&self, line: usize, col: usize) -> usize {
119 let s = self.start_of_line(line);
120 let e = self.line_end(s);
121 if e == s { s } else { (s + col).min(e - 1) }
122 }
123
124 pub fn first_non_blank(&self, pos: usize) -> usize {
125 let s = self.line_start(pos);
126 let e = self.line_end(s);
127 let mut i = s;
128 while i < e && is_blank(self.c[i]) {
129 i += 1;
130 }
131 if i == e && e > s { e - 1 } else { i }
132 }
133
134 /// In Normal mode the cursor sits on a character, never on the `\n`
135 /// (except on an empty line).
136 pub fn clamp_normal(&self, pos: usize) -> usize {
137 let pos = pos.min(self.len());
138 let s = self.line_start(pos);
139 let e = self.line_end(pos);
140 if e > s && pos >= e { e - 1 } else { pos }
141 }
142
143 pub fn insert(&mut self, at: usize, s: &str) -> usize {
144 let new: Vec<char> = s.chars().collect();
145 let n = new.len();
146 self.c.splice(at..at, new);
147 n
148 }
149
150 pub fn remove(&mut self, start: usize, end: usize) -> String {
151 self.c.drain(start..end).collect()
152 }
153
154 // ---- Motions ----
155
156 pub fn word_fwd(&self, pos: usize, big: bool) -> usize {
157 let n = self.len();
158 if pos >= n {
159 return n;
160 }
161 let mut i = pos;
162 let cls = class(self.c[i], big);
163 if cls != 0 {
164 while i < n && class(self.c[i], big) == cls {
165 i += 1;
166 }
167 }
168 while i < n && is_space(self.c[i]) {
169 // An empty line counts as a word.
170 if self.c[i] == '\n' && i + 1 < n && self.c[i + 1] == '\n' {
171 return i + 1;
172 }
173 i += 1;
174 }
175 i
176 }
177
178 pub fn word_back(&self, pos: usize, big: bool) -> usize {
179 if pos == 0 {
180 return 0;
181 }
182 let mut i = pos - 1;
183 while is_space(self.c[i]) {
184 let empty_line = self.c[i] == '\n' && (i == 0 || self.c[i - 1] == '\n');
185 if empty_line && i < pos {
186 return i;
187 }
188 if i == 0 {
189 return 0;
190 }
191 i -= 1;
192 }
193 let cls = class(self.c[i], big);
194 while i > 0 && class(self.c[i - 1], big) == cls {
195 i -= 1;
196 }
197 i
198 }
199
200 pub fn word_end(&self, pos: usize, big: bool) -> usize {
201 let n = self.len();
202 if n == 0 {
203 return 0;
204 }
205 let mut i = pos + 1;
206 while i < n && is_space(self.c[i]) {
207 i += 1;
208 }
209 if i >= n {
210 return n - 1;
211 }
212 let cls = class(self.c[i], big);
213 while i + 1 < n && class(self.c[i + 1], big) == cls {
214 i += 1;
215 }
216 i
217 }
218
219 /// End (inclusive) of the run of same-class chars starting at `pos`.
220 pub fn run_end(&self, pos: usize, big: bool) -> usize {
221 let cls = class(self.c[pos], big);
222 let mut i = pos;
223 while i + 1 < self.len() && self.c[i + 1] != '\n' && class(self.c[i + 1], big) == cls {
224 i += 1;
225 }
226 i
227 }
228
229 /// `f`/`t` (forward) or `F`/`T` (backward) to `ch` on the current line.
230 /// `repeat` is for `;` and `,`, where `t` mustn't stay stuck right
231 /// before the char it stopped at last time.
232 pub fn find_char(
233 &self,
234 pos: usize,
235 kind: char,
236 ch: char,
237 count: usize,
238 repeat: bool,
239 ) -> Option<usize> {
240 let s = self.line_start(pos);
241 let e = self.line_end(pos);
242 let mut i = pos;
243 for k in 0..count {
244 let skip = repeat && matches!(kind, 't' | 'T') && k == 0;
245 i = match kind {
246 'f' | 't' => {
247 let from = i + 1 + usize::from(skip && i + 1 < e && self.c[i + 1] == ch);
248 (from..e).find(|&j| self.c[j] == ch)?
249 }
250 _ => {
251 let to = if skip && i > s && self.c[i - 1] == ch {
252 i - 1
253 } else {
254 i
255 };
256 (s..to).rev().find(|&j| self.c[j] == ch)?
257 }
258 };
259 }
260 Some(match kind {
261 't' => i - 1,
262 'T' => i + 1,
263 _ => i,
264 })
265 }
266
267 /// `%`: the bracket matching the first one at or after `pos` on its line.
268 pub fn match_pair(&self, pos: usize) -> Option<usize> {
269 let e = self.line_end(pos);
270 let start = (pos..e).find(|&i| "()[]{}".contains(self.c[i]))?;
271 let open = self.c[start];
272 let (close, fwd) = match open {
273 '(' => (')', true),
274 '[' => (']', true),
275 '{' => ('}', true),
276 ')' => ('(', false),
277 ']' => ('[', false),
278 _ => ('{', false),
279 };
280 let mut depth = 0usize;
281 if fwd {
282 for i in start..self.len() {
283 if self.c[i] == open {
284 depth += 1;
285 } else if self.c[i] == close {
286 depth -= 1;
287 if depth == 0 {
288 return Some(i);
289 }
290 }
291 }
292 } else {
293 for i in (0..=start).rev() {
294 if self.c[i] == open {
295 depth += 1;
296 } else if self.c[i] == close {
297 depth -= 1;
298 if depth == 0 {
299 return Some(i);
300 }
301 }
302 }
303 }
304 None
305 }
306
307 /// `}` (forward) or `{`: the next blank line past the current paragraph.
308 pub fn paragraph(&self, pos: usize, fwd: bool) -> usize {
309 let last = self.lines() - 1;
310 let mut l = self.line_of(pos);
311 if fwd {
312 while l < last && self.line_is_blank(l) {
313 l += 1;
314 }
315 while l < last && !self.line_is_blank(l) {
316 l += 1;
317 }
318 if l == last && !self.line_is_blank(l) {
319 return self.len();
320 }
321 } else {
322 while l > 0 && self.line_is_blank(l) {
323 l -= 1;
324 }
325 while l > 0 && !self.line_is_blank(l) {
326 l -= 1;
327 }
328 }
329 self.start_of_line(l)
330 }
331
332 /// Text object `c` (`w W " ' ( b [ { B < p`) around `pos`.
333 pub fn object(&self, pos: usize, c: char, around: bool) -> Option<(usize, usize, bool)> {
334 match c {
335 'w' => self.obj_word(pos, false, around),
336 'W' => self.obj_word(pos, true, around),
337 '"' | '\'' | '`' => self.obj_quote(pos, c, around),
338 '(' | ')' | 'b' => self.obj_paren(pos, '(', ')', around),
339 '[' | ']' => self.obj_paren(pos, '[', ']', around),
340 '{' | '}' | 'B' => self.obj_paren(pos, '{', '}', around),
341 '<' | '>' => self.obj_paren(pos, '<', '>', around),
342 'p' => self.obj_paragraph(pos, around),
343 _ => None,
344 }
345 }
346
347 // ---- Text objects ----
348 //
349 // Charwise objects return (start, end exclusive, false); linewise ones
350 // return (a position on the first line, one on the last line, true).
351
352 pub fn obj_word(&self, pos: usize, big: bool, around: bool) -> Option<(usize, usize, bool)> {
353 if pos >= self.len() || self.c[pos] == '\n' {
354 return None;
355 }
356 let s = self.line_start(pos);
357 let e = self.line_end(pos);
358 let cls = class(self.c[pos], big);
359 let mut a = pos;
360 while a > s && class(self.c[a - 1], big) == cls {
361 a -= 1;
362 }
363 let mut b = pos + 1;
364 while b < e && class(self.c[b], big) == cls {
365 b += 1;
366 }
367 if around {
368 let b2 = (b..e).find(|&i| !is_blank(self.c[i])).unwrap_or(e);
369 if b2 > b {
370 b = b2;
371 } else {
372 while a > s && is_blank(self.c[a - 1]) {
373 a -= 1;
374 }
375 }
376 }
377 Some((a, b, false))
378 }
379
380 pub fn obj_quote(&self, pos: usize, q: char, around: bool) -> Option<(usize, usize, bool)> {
381 let s = self.line_start(pos);
382 let e = self.line_end(pos);
383 let quotes: Vec<usize> = (s..e).filter(|&i| self.c[i] == q).collect();
384 let pair = quotes
385 .as_chunks::<2>()
386 .0
387 .iter()
388 .find(|[_, close]| pos <= *close)
389 .map(|&[open, close]| (open, close))?;
390 let (open, close) = pair;
391 if around {
392 let mut b = close + 1;
393 while b < e && is_blank(self.c[b]) {
394 b += 1;
395 }
396 Some((open, b, false))
397 } else {
398 Some((open + 1, close, false))
399 }
400 }
401
402 pub fn obj_paren(
403 &self,
404 pos: usize,
405 open: char,
406 close: char,
407 around: bool,
408 ) -> Option<(usize, usize, bool)> {
409 let mut depth = 0usize;
410 let mut o = None;
411 let start = if pos < self.len() && self.c[pos] == close {
412 pos.checked_sub(1)?
413 } else {
414 pos
415 };
416 for i in (0..=start.min(self.len().saturating_sub(1))).rev() {
417 if self.c[i] == close && i != pos {
418 depth += 1;
419 } else if self.c[i] == open {
420 if depth == 0 {
421 o = Some(i);
422 break;
423 }
424 depth -= 1;
425 }
426 }
427 let o = o?;
428 let mut depth = 0usize;
429 let c = (o + 1..self.len()).find(|&i| {
430 if self.c[i] == open {
431 depth += 1;
432 } else if self.c[i] == close {
433 if depth == 0 {
434 return true;
435 }
436 depth -= 1;
437 }
438 false
439 })?;
440 Some(if around {
441 (o, c + 1, false)
442 } else {
443 (o + 1, c, false)
444 })
445 }
446
447 pub fn obj_paragraph(&self, pos: usize, around: bool) -> Option<(usize, usize, bool)> {
448 let last = self.lines() - 1;
449 let l = self.line_of(pos);
450 let blank = self.line_is_blank(l);
451 let mut a = l;
452 while a > 0 && self.line_is_blank(a - 1) == blank {
453 a -= 1;
454 }
455 let mut b = l;
456 while b < last && self.line_is_blank(b + 1) == blank {
457 b += 1;
458 }
459 if around {
460 while b < last && self.line_is_blank(b + 1) != blank {
461 b += 1;
462 }
463 }
464 Some((self.start_of_line(a), self.start_of_line(b), true))
465 }
466}
467
468/// Indentation for a new line after `line`: its leading whitespace, or for
469/// a `label:` line, the column where its instruction starts (8 if bare).
470pub fn auto_indent(line: &str) -> String {
471 let lead: String = line.chars().take_while(|&c| is_blank(c)).collect();
472 if !lead.is_empty() || line.trim().is_empty() {
473 return lead;
474 }
475 let label_end = line
476 .char_indices()
477 .find(|&(_, c)| !(is_keyword(c) || c == '?'))
478 .map_or(line.len(), |(i, _)| i);
479 if label_end > 0 && line[label_end..].starts_with(':') {
480 let rest = &line[label_end + 1..];
481 if rest.trim().is_empty() || rest.trim_start().starts_with(';') {
482 return " ".repeat(8);
483 }
484 let pad = rest.chars().take_while(|&c| is_blank(c)).count();
485 return " ".repeat(line[..label_end].chars().count() + 1 + pad);
486 }
487 String::new()
488}