| 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)] |
| 6 | pub enum Kind { |
| 7 | Exclusive, |
| 8 | Inclusive, |
| 9 | Linewise, |
| 10 | } |
| 11 | |
| 12 | pub struct Buf { |
| 13 | pub c: Vec<char>, |
| 14 | } |
| 15 | |
| 16 | pub fn is_blank(ch: char) -> bool { |
| 17 | ch == ' ' || ch == '\t' |
| 18 | } |
| 19 | |
| 20 | fn is_space(ch: char) -> bool { |
| 21 | ch.is_whitespace() |
| 22 | } |
| 23 | |
| 24 | pub 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. |
| 30 | fn 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 | |
| 40 | impl 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). |
| 470 | pub 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 | } |