idet-core 0.2.0

Shared text-editing library for a Micro-like terminal editor and a gedit-like egui GUI
Documentation
//! Swapping the word at the cursor with an adjacent word.

use crate::completion::is_word_char;

/// Direction in which [`swap`] exchanges text with an adjacent word.
#[derive(Clone, Copy)]
pub enum WordSwap {
    /// Swap with the previous word.
    Prev,
    /// Swap with the next word.
    Next,
}

/// Swaps a span with the adjacent word in `direction`.
///
/// `selection` is a pair of character indices; when its ends are equal the span
/// is the word at that caret, otherwise it is the selected range itself (so a
/// selection such as `don't` moves as a whole). Returns the new text and the
/// sorted `(start, end)` range covering the moved span, or `None` when there is
/// no span or no neighbouring word.
#[must_use]
pub fn swap(
    text: &str,
    selection: (usize, usize),
    direction: WordSwap,
) -> Option<(String, (usize, usize))> {
    let chars: Vec<char> = text.chars().collect();
    let length = chars.len();
    let caret = selection.0.min(selection.1).min(length);
    let high = selection.0.max(selection.1).min(length);
    let is_caret = caret == high;
    let (start, end) = if is_caret {
        current_word(&chars, caret)?
    } else {
        (caret, high)
    };
    let mut out = Vec::with_capacity(length);
    let moved = match direction {
        WordSwap::Next => {
            let (next_start, next_end) = next_word(&chars, end)?;
            out.extend_from_slice(&chars[..start]);
            out.extend_from_slice(&chars[next_start..next_end]);
            out.extend_from_slice(&chars[end..next_start]);
            out.extend_from_slice(&chars[start..end]);
            out.extend_from_slice(&chars[next_end..]);
            if is_caret {
                let moved_caret = caret + (next_end - end);
                (moved_caret, moved_caret)
            } else {
                let shifted = start + (next_end - end);
                (shifted, shifted + (end - start))
            }
        }
        WordSwap::Prev => {
            let (prev_start, prev_end) = prev_word(&chars, start)?;
            out.extend_from_slice(&chars[..prev_start]);
            out.extend_from_slice(&chars[start..end]);
            out.extend_from_slice(&chars[prev_end..start]);
            out.extend_from_slice(&chars[prev_start..prev_end]);
            out.extend_from_slice(&chars[end..]);
            if is_caret {
                let moved_caret = prev_start + (caret - start);
                (moved_caret, moved_caret)
            } else {
                (prev_start, prev_start + (end - start))
            }
        }
    };
    Some((out.into_iter().collect(), moved))
}

fn current_word(chars: &[char], caret: usize) -> Option<(usize, usize)> {
    let mut start = caret;
    while start > 0 && is_word_char(chars[start - 1]) {
        start -= 1;
    }
    let mut end = caret;
    while end < chars.len() && is_word_char(chars[end]) {
        end += 1;
    }
    (start != end).then_some((start, end))
}

fn next_word(chars: &[char], from: usize) -> Option<(usize, usize)> {
    let mut start = from;
    while start < chars.len() && !is_word_char(chars[start]) {
        start += 1;
    }
    if start == chars.len() {
        return None;
    }
    let mut end = start;
    while end < chars.len() && is_word_char(chars[end]) {
        end += 1;
    }
    Some((start, end))
}

fn prev_word(chars: &[char], before: usize) -> Option<(usize, usize)> {
    let mut end = before;
    while end > 0 && !is_word_char(chars[end - 1]) {
        end -= 1;
    }
    if end == 0 {
        return None;
    }
    let mut start = end;
    while start > 0 && is_word_char(chars[start - 1]) {
        start -= 1;
    }
    Some((start, end))
}