Skip to main content

idet_core/
wordops.rs

1//! Swapping the word at the cursor with an adjacent word.
2
3use crate::completion::is_word_char;
4
5/// Direction in which [`swap`] exchanges text with an adjacent word.
6#[derive(Clone, Copy)]
7pub enum WordSwap {
8    /// Swap with the previous word.
9    Prev,
10    /// Swap with the next word.
11    Next,
12}
13
14/// Swaps a span with the adjacent word in `direction`.
15///
16/// `selection` is a pair of character indices; when its ends are equal the span
17/// is the word at that caret, otherwise it is the selected range itself (so a
18/// selection such as `don't` moves as a whole). Returns the new text and the
19/// sorted `(start, end)` range covering the moved span, or `None` when there is
20/// no span or no neighbouring word.
21#[must_use]
22pub fn swap(
23    text: &str,
24    selection: (usize, usize),
25    direction: WordSwap,
26) -> Option<(String, (usize, usize))> {
27    let chars: Vec<char> = text.chars().collect();
28    let length = chars.len();
29    let caret = selection.0.min(selection.1).min(length);
30    let high = selection.0.max(selection.1).min(length);
31    let is_caret = caret == high;
32    let (start, end) = if is_caret {
33        current_word(&chars, caret)?
34    } else {
35        (caret, high)
36    };
37    let mut out = Vec::with_capacity(length);
38    let moved = match direction {
39        WordSwap::Next => {
40            let (next_start, next_end) = next_word(&chars, end)?;
41            out.extend_from_slice(&chars[..start]);
42            out.extend_from_slice(&chars[next_start..next_end]);
43            out.extend_from_slice(&chars[end..next_start]);
44            out.extend_from_slice(&chars[start..end]);
45            out.extend_from_slice(&chars[next_end..]);
46            if is_caret {
47                let moved_caret = caret + (next_end - end);
48                (moved_caret, moved_caret)
49            } else {
50                let shifted = start + (next_end - end);
51                (shifted, shifted + (end - start))
52            }
53        }
54        WordSwap::Prev => {
55            let (prev_start, prev_end) = prev_word(&chars, start)?;
56            out.extend_from_slice(&chars[..prev_start]);
57            out.extend_from_slice(&chars[start..end]);
58            out.extend_from_slice(&chars[prev_end..start]);
59            out.extend_from_slice(&chars[prev_start..prev_end]);
60            out.extend_from_slice(&chars[end..]);
61            if is_caret {
62                let moved_caret = prev_start + (caret - start);
63                (moved_caret, moved_caret)
64            } else {
65                (prev_start, prev_start + (end - start))
66            }
67        }
68    };
69    Some((out.into_iter().collect(), moved))
70}
71
72fn current_word(chars: &[char], caret: usize) -> Option<(usize, usize)> {
73    let mut start = caret;
74    while start > 0 && is_word_char(chars[start - 1]) {
75        start -= 1;
76    }
77    let mut end = caret;
78    while end < chars.len() && is_word_char(chars[end]) {
79        end += 1;
80    }
81    (start != end).then_some((start, end))
82}
83
84fn next_word(chars: &[char], from: usize) -> Option<(usize, usize)> {
85    let mut start = from;
86    while start < chars.len() && !is_word_char(chars[start]) {
87        start += 1;
88    }
89    if start == chars.len() {
90        return None;
91    }
92    let mut end = start;
93    while end < chars.len() && is_word_char(chars[end]) {
94        end += 1;
95    }
96    Some((start, end))
97}
98
99fn prev_word(chars: &[char], before: usize) -> Option<(usize, usize)> {
100    let mut end = before;
101    while end > 0 && !is_word_char(chars[end - 1]) {
102        end -= 1;
103    }
104    if end == 0 {
105        return None;
106    }
107    let mut start = end;
108    while start > 0 && is_word_char(chars[start - 1]) {
109        start -= 1;
110    }
111    Some((start, end))
112}