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
5/// Inserts `insertion` at character index `cursor`, returning the new text and
6/// the cursor placed after the inserted text.
7#[must_use]
8pub fn insert(text: &str, cursor: usize, insertion: &str) -> (String, usize) {
9    let byte = char_to_byte(text, cursor);
10    let mut result = text.to_owned();
11    result.insert_str(byte, insertion);
12    (result, cursor + insertion.chars().count())
13}
14
15/// Inserts a newline at `cursor` followed by the leading whitespace of the
16/// current line, reproducing that line's indentation.
17#[must_use]
18pub fn newline_indent(text: &str, cursor: usize) -> (String, usize) {
19    let line_start = line_start_char(text, cursor);
20    let indent: String = text
21        .chars()
22        .skip(line_start)
23        .take_while(|c| *c == ' ' || *c == '\t')
24        .collect();
25    let mut insertion = String::with_capacity(indent.len() + 1);
26    insertion.push('\n');
27    insertion.push_str(&indent);
28    insert(text, cursor, &insertion)
29}
30
31/// Prepends `width` spaces to the current line, shifting the cursor
32/// accordingly.
33#[must_use]
34pub fn indent_line(text: &str, cursor: usize, width: usize) -> (String, usize) {
35    let start = line_start_char(text, cursor);
36    let (result, _) = insert(text, start, &" ".repeat(width));
37    (result, cursor + width)
38}
39
40/// Removes up to `width` leading spaces from the current line, clamping the
41/// cursor to the line start when it sat inside the removed indentation.
42#[must_use]
43pub fn dedent_line(text: &str, cursor: usize, width: usize) -> (String, usize) {
44    let start = line_start_char(text, cursor);
45    let removable = text
46        .chars()
47        .skip(start)
48        .take(width)
49        .take_while(|c| *c == ' ')
50        .count();
51    if removable == 0 {
52        return (text.to_owned(), cursor);
53    }
54    let start_byte = char_to_byte(text, start);
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    text.chars()
94        .take(cursor)
95        .enumerate()
96        .filter_map(|(index, character)| (character == '\n').then_some(index + 1))
97        .last()
98        .unwrap_or(0)
99}
100
101/// Returns the character index of the end of the line containing `cursor`.
102#[must_use]
103pub fn line_end_char(text: &str, cursor: usize) -> usize {
104    text.chars()
105        .skip(cursor)
106        .position(|character| character == '\n')
107        .map_or_else(|| text.chars().count(), |offset| cursor + offset)
108}