rat_text/text_core/
core_op.rs

1use crate::core::{TextCore, TextStore};
2use crate::{Cursor, TextError, TextPosition, TextRange};
3
4/// Auto-quote the selected text.
5#[allow(clippy::needless_bool)]
6pub fn insert_quotes<Store: TextStore + Default>(
7    core: &mut TextCore<Store>,
8    mut sel: TextRange,
9    c: char,
10) -> Result<bool, TextError> {
11    core.begin_undo_seq();
12
13    // remove matching quotes/brackets
14    if sel.end.x > 0 {
15        let first = TextRange::new(sel.start, (sel.start.x + 1, sel.start.y));
16        let last = TextRange::new((sel.end.x - 1, sel.end.y), sel.end);
17        let c0 = core.str_slice(first).expect("valid_slice");
18        let c1 = core.str_slice(last).expect("valid_slice");
19        let remove_quote = if c == '\'' || c == '`' || c == '"' {
20            if c0 == "'" && c1 == "'" {
21                true
22            } else if c0 == "\"" && c1 == "\"" {
23                true
24            } else if c0 == "`" && c1 == "`" {
25                true
26            } else {
27                false
28            }
29        } else {
30            if c0 == "<" && c1 == ">" {
31                true
32            } else if c0 == "(" && c1 == ")" {
33                true
34            } else if c0 == "[" && c1 == "]" {
35                true
36            } else if c0 == "{" && c1 == "}" {
37                true
38            } else {
39                false
40            }
41        };
42        if remove_quote {
43            core.remove_char_range(last)?;
44            core.remove_char_range(first)?;
45            if sel.start.y == sel.end.y {
46                sel = TextRange::new(sel.start, TextPosition::new(sel.end.x - 2, sel.end.y));
47            } else {
48                sel = TextRange::new(sel.start, TextPosition::new(sel.end.x - 1, sel.end.y));
49            }
50        }
51    }
52
53    let cc = match c {
54        '\'' => '\'',
55        '`' => '`',
56        '"' => '"',
57        '<' => '>',
58        '(' => ')',
59        '[' => ']',
60        '{' => '}',
61        _ => unreachable!("invalid quotes"),
62    };
63    core.insert_char(sel.end, cc)?;
64    core.insert_char(sel.start, c)?;
65    if sel.start.y == sel.end.y {
66        sel = TextRange::new(sel.start, TextPosition::new(sel.end.x + 2, sel.end.y));
67    } else {
68        sel = TextRange::new(sel.start, TextPosition::new(sel.end.x + 1, sel.end.y));
69    }
70    core.set_selection(sel.start, sel.end);
71    core.end_undo_seq();
72
73    Ok(true)
74}
75
76/// Insert a tab, either expanded or literally.
77pub fn insert_tab<Store: TextStore + Default>(
78    core: &mut TextCore<Store>,
79    mut pos: TextPosition,
80    expand_tabs: bool,
81    tab_width: u32,
82) -> Result<bool, TextError> {
83    if expand_tabs {
84        let n = tab_width - (pos.x % tab_width);
85        for _ in 0..n {
86            core.insert_char(pos, ' ')?;
87            pos.x += 1;
88        }
89    } else {
90        core.insert_char(pos, '\t')?;
91    }
92
93    Ok(true)
94}
95
96/// Remove the previous character
97pub fn remove_prev_char<Store: TextStore + Default>(
98    core: &mut TextCore<Store>,
99    pos: TextPosition,
100) -> Result<bool, TextError> {
101    let (sx, sy) = if pos.y == 0 && pos.x == 0 {
102        (0, 0)
103    } else if pos.y > 0 && pos.x == 0 {
104        let prev_line_width = core.line_width(pos.y - 1).expect("line_width");
105        (prev_line_width, pos.y - 1)
106    } else {
107        (pos.x - 1, pos.y)
108    };
109    let range = TextRange::new((sx, sy), (pos.x, pos.y));
110
111    core.remove_char_range(range)
112}
113
114/// Remove the next characters.
115pub fn remove_next_char<Store: TextStore + Default>(
116    core: &mut TextCore<Store>,
117    pos: TextPosition,
118) -> Result<bool, TextError> {
119    let c_line_width = core.line_width(pos.y)?;
120    let c_last_line = core.len_lines() - 1;
121
122    let (ex, ey) = if pos.y == c_last_line && pos.x == c_line_width {
123        (pos.x, pos.y)
124    } else if pos.y != c_last_line && pos.x == c_line_width {
125        (0, pos.y + 1)
126    } else {
127        (pos.x + 1, pos.y)
128    };
129    let range = TextRange::new((pos.x, pos.y), (ex, ey));
130
131    core.remove_char_range(range)
132}
133
134/// Find the start of the next word. If the position is at the start
135/// or inside a word, the same position is returned.
136pub fn next_word_start<Store: TextStore + Default>(
137    core: &TextCore<Store>,
138    pos: TextPosition,
139) -> Result<TextPosition, TextError> {
140    let mut it = core.text_graphemes(pos)?;
141    let mut last_pos = it.text_offset();
142    loop {
143        let Some(c) = it.next() else {
144            break;
145        };
146        last_pos = c.text_bytes().start;
147        if !c.is_whitespace() {
148            break;
149        }
150    }
151
152    Ok(core.byte_pos(last_pos).expect("valid_pos"))
153}
154
155/// Find the end of the next word. Skips whitespace first, then goes on
156/// until it finds the next whitespace.
157pub fn next_word_end<Store: TextStore + Default>(
158    core: &TextCore<Store>,
159    pos: TextPosition,
160) -> Result<TextPosition, TextError> {
161    let mut it = core.text_graphemes(pos)?;
162    let mut last_pos = it.text_offset();
163    let mut init = true;
164    loop {
165        let Some(c) = it.next() else {
166            break;
167        };
168        last_pos = c.text_bytes().start;
169        if init {
170            if !c.is_whitespace() {
171                init = false;
172            }
173        } else {
174            if c.is_whitespace() {
175                break;
176            }
177        }
178        last_pos = c.text_bytes().end;
179    }
180
181    Ok(core.byte_pos(last_pos).expect("valid_pos"))
182}
183
184/// Find the start of the prev word. Skips whitespace first, then goes on
185/// until it finds the next whitespace.
186///
187/// Attention: start/end are mirrored here compared to next_word_start/next_word_end,
188/// both return start<=end!
189pub fn prev_word_start<Store: TextStore + Default>(
190    core: &TextCore<Store>,
191    pos: TextPosition,
192) -> Result<TextPosition, TextError> {
193    let mut it = core.text_graphemes(pos)?;
194    let mut last_pos = it.text_offset();
195    let mut init = true;
196    loop {
197        let Some(c) = it.prev() else {
198            break;
199        };
200        if init {
201            if !c.is_whitespace() {
202                init = false;
203            }
204        } else {
205            if c.is_whitespace() {
206                break;
207            }
208        }
209        last_pos = c.text_bytes().start;
210    }
211
212    Ok(core.byte_pos(last_pos).expect("valid_pos"))
213}
214
215/// Find the end of the previous word. Word is everything that is not whitespace.
216/// Attention: start/end are mirrored here compared to next_word_start/next_word_end,
217/// both return start<=end!
218pub fn prev_word_end<Store: TextStore + Default>(
219    core: &TextCore<Store>,
220    pos: TextPosition,
221) -> Result<TextPosition, TextError> {
222    let mut it = core.text_graphemes(pos)?;
223    let mut last_pos = it.text_offset();
224    loop {
225        let Some(c) = it.prev() else {
226            break;
227        };
228        if !c.is_whitespace() {
229            break;
230        }
231        last_pos = c.text_bytes().start;
232    }
233
234    Ok(core.byte_pos(last_pos).expect("valid_pos"))
235}
236
237/// Is the position at a word boundary?
238pub fn is_word_boundary<Store: TextStore + Default>(
239    core: &TextCore<Store>,
240    pos: TextPosition,
241) -> Result<bool, TextError> {
242    let mut it = core.text_graphemes(pos)?;
243    if let Some(c0) = it.prev() {
244        it.next();
245        if let Some(c1) = it.next() {
246            Ok(c0.is_whitespace() && !c1.is_whitespace()
247                || !c0.is_whitespace() && c1.is_whitespace())
248        } else {
249            Ok(false)
250        }
251    } else {
252        Ok(false)
253    }
254}
255
256/// Find the start of the word at pos.
257/// Returns pos if the position is not inside a word.
258pub fn word_start<Store: TextStore + Default>(
259    core: &TextCore<Store>,
260    pos: TextPosition,
261) -> Result<TextPosition, TextError> {
262    let mut it = core.text_graphemes(pos)?;
263    let mut last_pos = it.text_offset();
264    loop {
265        let Some(c) = it.prev() else {
266            break;
267        };
268        if c.is_whitespace() {
269            break;
270        }
271        last_pos = c.text_bytes().start;
272    }
273
274    Ok(core.byte_pos(last_pos).expect("valid_pos"))
275}
276
277/// Find the end of the word at pos.
278/// Returns pos if the position is not inside a word.
279pub fn word_end<Store: TextStore + Default>(
280    core: &TextCore<Store>,
281    pos: TextPosition,
282) -> Result<TextPosition, TextError> {
283    let mut it = core.text_graphemes(pos)?;
284    let mut last_pos = it.text_offset();
285    loop {
286        let Some(c) = it.next() else {
287            break;
288        };
289        last_pos = c.text_bytes().start;
290        if c.is_whitespace() {
291            break;
292        }
293        last_pos = c.text_bytes().end;
294    }
295
296    Ok(core.byte_pos(last_pos).expect("valid_pos"))
297}