Skip to main content

idet_core/
edits.rs

1//! Text insertion and indentation edits, all character-index based.
2
3use crate::completion::char_to_byte;
4
5const INDENT: &str = "    ";
6
7/// Inserts `insertion` at character index `cursor`, returning the new text and
8/// the cursor placed after the inserted text.
9#[must_use]
10pub fn insert(text: &str, cursor: usize, insertion: &str) -> (String, usize) {
11    let byte = char_to_byte(text, cursor);
12    let mut result = text.to_owned();
13    result.insert_str(byte, insertion);
14    (result, cursor + insertion.chars().count())
15}
16
17/// Inserts a newline at `cursor` followed by the leading whitespace of the
18/// current line, reproducing that line's indentation.
19#[must_use]
20pub fn newline_indent(text: &str, cursor: usize) -> (String, usize) {
21    let byte = char_to_byte(text, cursor);
22    let line_start = text[..byte].rfind('\n').map_or(0, |index| index + 1);
23    let indent: String = text[line_start..]
24        .chars()
25        .take_while(|c| *c == ' ' || *c == '\t')
26        .collect();
27    let mut insertion = String::with_capacity(indent.len() + 1);
28    insertion.push('\n');
29    insertion.push_str(&indent);
30    insert(text, cursor, &insertion)
31}
32
33/// Prepends four spaces to the current line, shifting the cursor accordingly.
34#[must_use]
35pub fn indent_line(text: &str, cursor: usize) -> (String, usize) {
36    let start = line_start_char(text, cursor);
37    let (result, _) = insert(text, start, INDENT);
38    (result, cursor + INDENT.chars().count())
39}
40
41/// Removes up to four leading spaces from the current line, clamping the cursor
42/// to the line start when it sat inside the removed indentation.
43#[must_use]
44pub fn dedent_line(text: &str, cursor: usize) -> (String, usize) {
45    let start = line_start_char(text, cursor);
46    let start_byte = char_to_byte(text, start);
47    let removable = text[start_byte..]
48        .chars()
49        .take(INDENT.len())
50        .take_while(|c| *c == ' ')
51        .count();
52    if removable == 0 {
53        return (text.to_owned(), cursor);
54    }
55    let end_byte = char_to_byte(text, start + removable);
56    let mut result = text.to_owned();
57    result.replace_range(start_byte..end_byte, "");
58    let new_cursor = if cursor >= start + removable {
59        cursor - removable
60    } else {
61        start
62    };
63    (result, new_cursor)
64}
65
66/// Returns the span of `after` that differs from `before`.
67///
68/// The common prefix and suffix are stripped, so what remains is the text a
69/// history step brought in: select it to show what an undo restored. An empty
70/// span means the step only removed text and marks where it went.
71#[must_use]
72pub fn changed_range(before: &str, after: &str) -> (usize, usize) {
73    let before_len = before.chars().count();
74    let after_len = after.chars().count();
75    let prefix = before
76        .chars()
77        .zip(after.chars())
78        .take_while(|(old, new)| old == new)
79        .count();
80    let suffix = before
81        .chars()
82        .rev()
83        .zip(after.chars().rev())
84        .take_while(|(old, new)| old == new)
85        .count()
86        .min(before_len.min(after_len) - prefix);
87    (prefix, after_len - suffix)
88}
89
90/// Returns the character index of the start of the line containing `cursor`.
91#[must_use]
92pub fn line_start_char(text: &str, cursor: usize) -> usize {
93    let byte = char_to_byte(text, cursor);
94    let start_byte = text[..byte].rfind('\n').map_or(0, |index| index + 1);
95    text[..start_byte].chars().count()
96}
97
98/// Returns the character index of the end of the line containing `cursor`.
99#[must_use]
100pub fn line_end_char(text: &str, cursor: usize) -> usize {
101    let byte = char_to_byte(text, cursor);
102    let end_byte = text[byte..]
103        .find('\n')
104        .map_or(text.len(), |index| byte + index);
105    text[..end_byte].chars().count()
106}