use crate::{
canvas::{modes::AppMode, state::SelectionState},
editor::features::history::EditKind,
textarea::{TextAreaDataProvider, TextAreaState},
};
#[derive(Clone, Copy)]
pub(crate) enum HelixCase {
Toggle,
Lower,
Upper,
}
#[derive(Clone, Copy)]
pub(crate) enum HelixPending {
Find { till: bool, forward: bool },
Replace,
SurroundAdd,
SurroundDelete,
SurroundReplaceFrom,
SurroundReplaceTo { from: char },
}
fn surround_pair(ch: char) -> (char, char) {
match ch {
'(' | ')' => ('(', ')'),
'[' | ']' => ('[', ']'),
'{' | '}' => ('{', '}'),
'<' | '>' => ('<', '>'),
other => (other, other),
}
}
#[derive(Clone, Copy)]
pub(crate) struct HelixFind {
pub ch: char,
pub till: bool,
pub forward: bool,
}
#[derive(Clone, Copy)]
enum HelixWordTarget {
NextWordStart,
NextWordEnd,
PrevWordStart,
PrevWordEnd,
NextLongWordStart,
NextLongWordEnd,
PrevLongWordStart,
PrevLongWordEnd,
}
impl<P: TextAreaDataProvider> TextAreaState<P> {
fn select_word_motion_helix(&mut self, count: usize, target: HelixWordTarget) {
let extend = self.mode() != AppMode::Nor;
for _ in 0..count.max(1) {
self.select_word_motion_step_helix(target, extend);
}
}
fn select_word_motion_step_helix(&mut self, target: HelixWordTarget, extend: bool) {
let field_count = self.editor.data_provider().field_count();
if field_count == 0 {
return;
}
let mut chars: Vec<char> = Vec::new();
let mut field_starts: Vec<usize> = Vec::with_capacity(field_count);
for f in 0..field_count {
field_starts.push(chars.len());
chars.extend(self.editor.data_provider().field_value(f).chars());
if f + 1 < field_count {
chars.push('\n');
}
}
let len = chars.len();
if len == 0 {
return;
}
let field = self.current_field();
let cursor = self.cursor_position();
let to_flat = |pos: (usize, usize)| field_starts[pos.0] + pos.1;
let flat_cursor = to_flat((field, cursor));
let pinned_anchor = match self.selection_state() {
SelectionState::Characterwise { anchor } => *anchor,
_ => (field, cursor),
};
let flat_anchor = to_flat(pinned_anchor);
let input = if flat_cursor >= flat_anchor {
HelixRange {
anchor: flat_anchor,
head: flat_cursor + 1,
}
} else {
HelixRange {
anchor: flat_anchor + 1,
head: flat_cursor,
}
};
let result = helix_word_move(&chars, input, target);
let (motion_anchor, new_cursor) = if result.anchor < result.head {
(result.anchor, result.head - 1)
} else if result.head < result.anchor {
(result.anchor - 1, result.head)
} else {
let c = result.head.min(len - 1);
(c, c)
};
if new_cursor == flat_cursor && (extend || motion_anchor == flat_anchor) {
return;
}
let (cur_field, cur_char) = flat_to_field(&chars, &field_starts, new_cursor);
let _ = self.transition_to_field(cur_field);
let field_len = self.current_text().chars().count();
self.editor.ui_state.set_cursor(cur_char, field_len, false);
let anchor = if extend {
pinned_anchor
} else {
flat_to_field(&chars, &field_starts, motion_anchor)
};
self.editor.ui_state.selection = SelectionState::Characterwise { anchor };
}
pub(crate) fn select_next_word_helix(&mut self, count: usize) {
self.select_word_motion_helix(count, HelixWordTarget::NextWordStart);
}
pub(crate) fn select_prev_word_helix(&mut self, count: usize) {
self.select_word_motion_helix(count, HelixWordTarget::PrevWordStart);
}
pub(crate) fn select_word_end_helix(&mut self, count: usize) {
self.select_word_motion_helix(count, HelixWordTarget::NextWordEnd);
}
pub(crate) fn select_word_end_prev_helix(&mut self, count: usize) {
self.select_word_motion_helix(count, HelixWordTarget::PrevWordEnd);
}
pub(crate) fn select_next_big_word_helix(&mut self, count: usize) {
self.select_word_motion_helix(count, HelixWordTarget::NextLongWordStart);
}
pub(crate) fn select_prev_big_word_helix(&mut self, count: usize) {
self.select_word_motion_helix(count, HelixWordTarget::PrevLongWordStart);
}
pub(crate) fn select_big_word_end_helix(&mut self, count: usize) {
self.select_word_motion_helix(count, HelixWordTarget::NextLongWordEnd);
}
pub(crate) fn select_big_word_end_prev_helix(&mut self, count: usize) {
self.select_word_motion_helix(count, HelixWordTarget::PrevLongWordEnd);
}
pub(crate) fn search_next_helix(&mut self, count: usize) {
self.navigate_search_helix(true, count);
}
pub(crate) fn search_prev_helix(&mut self, count: usize) {
self.navigate_search_helix(false, count);
}
fn navigate_search_helix(&mut self, forward: bool, count: usize) {
let matches = self.search_matches();
if matches.is_empty() {
return;
}
let reference = self
.active_search_match()
.map(|m| (m.line, m.start))
.unwrap_or_else(|| (self.current_field(), self.cursor_position()));
let mut idx = if forward {
matches
.iter()
.position(|m| (m.line, m.start) > reference)
.unwrap_or(0)
} else {
matches
.iter()
.rposition(|m| (m.line, m.start) < reference)
.unwrap_or(matches.len() - 1)
};
for _ in 1..count.max(1) {
idx = if forward {
(idx + 1) % matches.len()
} else {
(idx + matches.len() - 1) % matches.len()
};
}
self.active_search_match = Some(matches[idx]);
self.select_active_search_match_helix();
}
pub(crate) fn select_active_search_match_helix(&mut self) {
let Some(m) = self.active_search_match() else {
return;
};
if m.end <= m.start {
return;
}
let _ = self.transition_to_field(m.line);
let len = self.current_text().chars().count();
let cursor = m.end.saturating_sub(1).min(len.saturating_sub(1));
self.editor.ui_state.set_cursor(cursor, len, false);
self.editor.ui_state.selection = SelectionState::Characterwise {
anchor: (m.line, m.start),
};
}
pub(crate) fn select_all_helix(&mut self) {
let field_count = self.editor.data_provider().field_count();
if field_count == 0 {
return;
}
let last = field_count - 1;
let _ = self.transition_to_field(last);
let len = self.current_text().chars().count();
self.editor
.ui_state
.set_cursor(len.saturating_sub(1), len, false);
self.editor.ui_state.selection = SelectionState::Characterwise { anchor: (0, 0) };
}
pub(crate) fn flip_selection_helix(&mut self) {
match self.selection_state().clone() {
SelectionState::Characterwise { anchor } => {
let cursor = (self.current_field(), self.cursor_position());
if anchor == cursor {
return;
}
let _ = self.transition_to_field(anchor.0);
let len = self.current_text().chars().count();
self.editor.ui_state.set_cursor(anchor.1, len, false);
self.editor.ui_state.selection = SelectionState::Characterwise { anchor: cursor };
}
SelectionState::Linewise { anchor_field } => {
let current = self.current_field();
if anchor_field == current {
return;
}
let _ = self.transition_to_field(anchor_field);
self.editor.ui_state.selection = SelectionState::Linewise {
anchor_field: current,
};
}
SelectionState::None => {}
}
}
pub(crate) fn switch_case_selection_helix(&mut self, case: HelixCase) {
self.map_selection_chars_helix(|c| match case {
HelixCase::Lower => c.to_lowercase().next().unwrap_or(c),
HelixCase::Upper => c.to_uppercase().next().unwrap_or(c),
HelixCase::Toggle => {
if c.is_uppercase() {
c.to_lowercase().next().unwrap_or(c)
} else if c.is_lowercase() {
c.to_uppercase().next().unwrap_or(c)
} else {
c
}
}
});
}
fn map_selection_chars_helix(&mut self, map: impl Fn(char) -> char) {
let (start, end) = self.selection_endpoints();
let mut lines = self.editor.data_provider().capture_content();
if start.0 >= lines.len() || end.0 >= lines.len() {
return;
}
self.editor.record_checkpoint(EditKind::Other);
for field in start.0..=end.0 {
let count = lines[field].chars().count();
if count == 0 {
continue;
}
let (col_start, col_end) = if start.0 == end.0 {
(start.1, end.1)
} else if field == start.0 {
(start.1, count - 1)
} else if field == end.0 {
(0, end.1)
} else {
(0, count - 1)
};
let new_line: String = lines[field]
.chars()
.enumerate()
.map(|(i, c)| {
if i >= col_start && i <= col_end {
map(c)
} else {
c
}
})
.collect();
lines[field] = new_line;
}
self.editor.data_provider_mut().restore_content(&lines);
#[cfg(feature = "gui")]
{
self.edited_this_frame = true;
}
}
pub(crate) fn trim_selection_helix(&mut self) {
let SelectionState::Characterwise { anchor } = self.selection_state().clone() else {
return;
};
let cursor = (self.current_field(), self.cursor_position());
let forward = cursor >= anchor;
let (start, end) = (anchor.min(cursor), anchor.max(cursor));
if start.0 != end.0 {
return;
}
let line: Vec<char> = self.current_text_for_field(start.0);
let mut new_start = start.1;
let mut new_end = end.1.min(line.len().saturating_sub(1));
while new_start <= new_end && line[new_start].is_whitespace() {
new_start += 1;
}
while new_end > new_start && line[new_end].is_whitespace() {
new_end -= 1;
}
if new_start > new_end || line[new_start].is_whitespace() {
return; }
let _ = self.transition_to_field(start.0);
let len = self.current_text().chars().count();
let (anchor_pos, cursor_pos) = if forward {
((start.0, new_start), new_end)
} else {
((start.0, new_end), new_start)
};
self.editor.ui_state.set_cursor(cursor_pos, len, false);
self.editor.ui_state.selection = SelectionState::Characterwise { anchor: anchor_pos };
}
pub(crate) fn goto_first_nonwhitespace_helix(&mut self) {
let field = self.current_field();
let line: Vec<char> = self.current_text_for_field(field);
let target = line
.iter()
.position(|c| !c.is_whitespace())
.unwrap_or(0);
let extend = self.mode() != AppMode::Nor;
let len = line.len();
self.editor.ui_state.set_cursor(target, len, false);
if !extend {
self.editor.ui_state.selection = SelectionState::Characterwise {
anchor: (field, target),
};
}
}
fn current_text_for_field(&self, field: usize) -> Vec<char> {
self.editor
.data_provider()
.field_value(field)
.chars()
.collect()
}
pub(crate) fn search_selection_helix(&mut self) {
let SelectionState::Characterwise { anchor } = self.selection_state().clone() else {
return;
};
let cursor = (self.current_field(), self.cursor_position());
let (start, end) = (anchor.min(cursor), anchor.max(cursor));
if start.0 != end.0 {
return;
}
let line = self.current_text_for_field(start.0);
let pattern: String = line
.iter()
.skip(start.1)
.take(end.1 + 1 - start.1)
.collect();
if !pattern.trim().is_empty() {
self.set_search_query(pattern);
}
}
pub(crate) fn ensure_selection_forward_helix(&mut self) {
let SelectionState::Characterwise { anchor } = self.selection_state().clone() else {
return;
};
let cursor = (self.current_field(), self.cursor_position());
if cursor >= anchor {
return;
}
let _ = self.transition_to_field(anchor.0);
let len = self.current_text().chars().count();
self.editor.ui_state.set_cursor(anchor.1, len, false);
self.editor.ui_state.selection = SelectionState::Characterwise { anchor: cursor };
}
pub(crate) fn indent_selection_helix(&mut self, count: usize) {
const INDENT: usize = 4;
let width = INDENT * count.max(1);
let (start, end) = self.selection_endpoints();
let mut content = self.editor.data_provider().capture_content();
if end.0 >= content.len() {
return;
}
self.editor.record_checkpoint(EditKind::Other);
let indent: String = " ".repeat(width);
let mut deltas = vec![0isize; content.len()];
for f in start.0..=end.0 {
if content[f].is_empty() {
continue;
}
content[f] = format!("{indent}{}", content[f]);
deltas[f] = width as isize;
}
self.editor.data_provider_mut().restore_content(&content);
self.shift_selection_columns_helix(&deltas);
#[cfg(feature = "gui")]
{
self.edited_this_frame = true;
}
}
pub(crate) fn unindent_selection_helix(&mut self, count: usize) {
const INDENT: usize = 4;
let width = INDENT * count.max(1);
let (start, end) = self.selection_endpoints();
let mut content = self.editor.data_provider().capture_content();
if end.0 >= content.len() {
return;
}
self.editor.record_checkpoint(EditKind::Other);
let mut deltas = vec![0isize; content.len()];
for f in start.0..=end.0 {
let leading = content[f]
.chars()
.take_while(|c| *c == ' ')
.count()
.min(width);
if leading > 0 {
content[f] = content[f].chars().skip(leading).collect();
deltas[f] = -(leading as isize);
}
}
self.editor.data_provider_mut().restore_content(&content);
self.shift_selection_columns_helix(&deltas);
#[cfg(feature = "gui")]
{
self.edited_this_frame = true;
}
}
fn shift_selection_columns_helix(&mut self, deltas: &[isize]) {
let field = self.current_field();
let cursor = self.cursor_position();
let anchor = match self.selection_state() {
SelectionState::Characterwise { anchor } => Some(*anchor),
_ => None,
};
let shift = |f: usize, col: usize| -> usize {
let d = deltas.get(f).copied().unwrap_or(0);
(col as isize + d).max(0) as usize
};
let len = self.current_text().chars().count();
self.editor
.ui_state
.set_cursor(shift(field, cursor).min(len), len, false);
if let Some(a) = anchor {
self.editor.ui_state.selection = SelectionState::Characterwise {
anchor: (a.0, shift(a.0, a.1)),
};
}
}
pub(crate) fn change_number_helix(&mut self, delta: i64, count: usize) {
let field = self.current_field();
let line = self.current_text_for_field(field);
let n = line.len();
let cursor = self.cursor_position();
let mut i = 0;
while i < n {
if !line[i].is_ascii_digit() {
i += 1;
continue;
}
let mut end = i;
while end + 1 < n && line[end + 1].is_ascii_digit() {
end += 1;
}
if end >= cursor {
let neg = i > 0 && line[i - 1] == '-';
let num_start = if neg { i - 1 } else { i };
let text: String = line[num_start..=end].iter().collect();
if let Ok(val) = text.parse::<i64>() {
let new_val = val.saturating_add(delta.saturating_mul(count.max(1) as i64));
let new_str = new_val.to_string();
let mut new_line: Vec<char> = Vec::with_capacity(n);
new_line.extend_from_slice(&line[..num_start]);
new_line.extend(new_str.chars());
new_line.extend_from_slice(&line[end + 1..]);
self.editor.record_checkpoint(EditKind::Other);
self.editor
.data_provider_mut()
.set_field_value(field, new_line.iter().collect());
let new_count = new_str.chars().count();
let new_end = num_start + new_count - 1;
let len = self.current_text().chars().count();
self.editor.ui_state.set_cursor(new_end, len, false);
self.editor.ui_state.selection = SelectionState::Characterwise {
anchor: (field, num_start),
};
#[cfg(feature = "gui")]
{
self.edited_this_frame = true;
}
return;
}
}
i = end + 1;
}
}
pub(crate) fn delete_word_backward_helix(&mut self) {
use crate::canvas::actions::movement::word::find_prev_word_start;
let field = self.current_field();
let text = self.editor.data_provider().field_value(field).to_string();
let cursor = self.cursor_position();
if cursor == 0 {
return;
}
let start = find_prev_word_start(&text, cursor).min(cursor);
if start == cursor {
return;
}
self.editor.record_checkpoint(EditKind::Delete);
let new_line: String = text
.chars()
.take(start)
.chain(text.chars().skip(cursor))
.collect();
self.editor.data_provider_mut().set_field_value(field, new_line);
let len = self.current_text().chars().count();
self.editor.ui_state.set_cursor(start, len, false);
#[cfg(feature = "gui")]
{
self.edited_this_frame = true;
}
}
pub(crate) fn delete_to_line_start_helix(&mut self) {
let field = self.current_field();
let text = self.editor.data_provider().field_value(field).to_string();
let cursor = self.cursor_position();
if cursor == 0 {
return;
}
self.editor.record_checkpoint(EditKind::Delete);
let new_line: String = text.chars().skip(cursor).collect();
self.editor.data_provider_mut().set_field_value(field, new_line);
let len = self.current_text().chars().count();
self.editor.ui_state.set_cursor(0, len, false);
#[cfg(feature = "gui")]
{
self.edited_this_frame = true;
}
}
pub(crate) fn delete_word_forward_helix(&mut self) {
use crate::canvas::actions::movement::word::find_next_word_start;
let field = self.current_field();
let text = self.editor.data_provider().field_value(field).to_string();
let cursor = self.cursor_position();
let count = text.chars().count();
if cursor >= count {
return;
}
let end = find_next_word_start(&text, cursor).max(cursor + 1).min(count);
self.editor.record_checkpoint(EditKind::Delete);
let new_line: String = text
.chars()
.take(cursor)
.chain(text.chars().skip(end))
.collect();
self.editor.data_provider_mut().set_field_value(field, new_line);
let len = self.current_text().chars().count();
self.editor.ui_state.set_cursor(cursor.min(len), len, false);
#[cfg(feature = "gui")]
{
self.edited_this_frame = true;
}
}
pub(crate) fn set_helix_pending(&mut self, pending: HelixPending) {
self.helix_pending = Some(pending);
}
pub(crate) fn resolve_helix_pending(&mut self, pending: HelixPending, ch: char) {
match pending {
HelixPending::Find { till, forward } => {
self.helix_last_find = Some(HelixFind { ch, till, forward });
self.find_char_helix(ch, till, forward);
}
HelixPending::Replace => self.replace_selection_with_char_helix(ch),
HelixPending::SurroundAdd => self.surround_add_helix(ch),
HelixPending::SurroundDelete => self.surround_delete_helix(ch),
HelixPending::SurroundReplaceFrom => {
self.helix_pending = Some(HelixPending::SurroundReplaceTo { from: ch });
}
HelixPending::SurroundReplaceTo { from } => self.surround_replace_helix(from, ch),
}
}
pub(crate) fn repeat_last_find_helix(&mut self) {
if let Some(find) = self.helix_last_find {
self.find_char_helix(find.ch, find.till, find.forward);
}
}
pub(crate) fn find_char_helix(&mut self, ch: char, till: bool, forward: bool) {
let field = self.current_field();
let line = self.current_text_for_field(field);
let len = line.len();
let cursor = self.cursor_position();
let found = if forward {
((cursor + 1)..len).find(|&i| line[i] == ch)
} else {
(0..cursor).rev().find(|&i| line[i] == ch)
};
let Some(hit) = found else {
return;
};
let target = if till {
if forward {
hit.saturating_sub(1)
} else {
hit + 1
}
} else {
hit
};
if target == cursor {
return;
}
let extend = self.mode() != AppMode::Nor;
let anchor = match self.selection_state() {
SelectionState::Characterwise { anchor } if extend => *anchor,
_ => (field, cursor),
};
self.editor.ui_state.set_cursor(target, len, false);
self.editor.ui_state.selection = SelectionState::Characterwise { anchor };
}
pub(crate) fn replace_selection_with_char_helix(&mut self, ch: char) {
self.map_selection_chars_helix(|_| ch);
}
pub(crate) fn surround_add_helix(&mut self, ch: char) {
let (open, close) = surround_pair(ch);
let (start, end) = self.selection_endpoints();
let mut content = self.editor.data_provider().capture_content();
if start.0 >= content.len() || end.0 >= content.len() {
return;
}
self.editor.record_checkpoint(EditKind::Other);
let mut end_line: Vec<char> = content[end.0].chars().collect();
let close_at = (end.1 + 1).min(end_line.len());
end_line.insert(close_at, close);
content[end.0] = end_line.into_iter().collect();
let mut start_line: Vec<char> = content[start.0].chars().collect();
let open_at = start.1.min(start_line.len());
start_line.insert(open_at, open);
content[start.0] = start_line.into_iter().collect();
self.editor.data_provider_mut().restore_content(&content);
let bump = |pos: (usize, usize)| -> (usize, usize) {
if pos.0 == start.0 && pos.1 >= start.1 {
(pos.0, pos.1 + 1)
} else {
pos
}
};
let cursor = bump((self.current_field(), self.cursor_position()));
let anchor = match self.selection_state() {
SelectionState::Characterwise { anchor } => bump(*anchor),
_ => cursor,
};
let _ = self.transition_to_field(cursor.0);
let len = self.current_text().chars().count();
self.editor.ui_state.set_cursor(cursor.1, len, false);
self.editor.ui_state.selection = SelectionState::Characterwise { anchor };
#[cfg(feature = "gui")]
{
self.edited_this_frame = true;
}
}
pub(crate) fn surround_delete_helix(&mut self, ch: char) {
let (open, close) = surround_pair(ch);
let field = self.current_field();
let line = self.current_text_for_field(field);
let cursor = self.cursor_position().min(line.len().saturating_sub(1));
let Some((open_idx, close_idx)) = find_surround_pair(&line, cursor, open, close) else {
return;
};
self.editor.record_checkpoint(EditKind::Other);
let mut new_line = line.clone();
new_line.remove(close_idx);
new_line.remove(open_idx);
self.editor
.data_provider_mut()
.set_field_value(field, new_line.into_iter().collect());
let new_cursor = self.cursor_position().saturating_sub(1);
let len = self.current_text().chars().count();
self.editor.ui_state.set_cursor(new_cursor.min(len), len, false);
#[cfg(feature = "gui")]
{
self.edited_this_frame = true;
}
}
pub(crate) fn surround_replace_helix(&mut self, from: char, to: char) {
let (from_open, from_close) = surround_pair(from);
let (to_open, to_close) = surround_pair(to);
let field = self.current_field();
let line = self.current_text_for_field(field);
let cursor = self.cursor_position().min(line.len().saturating_sub(1));
let Some((open_idx, close_idx)) = find_surround_pair(&line, cursor, from_open, from_close)
else {
return;
};
self.editor.record_checkpoint(EditKind::Other);
let mut new_line = line.clone();
new_line[open_idx] = to_open;
new_line[close_idx] = to_close;
self.editor
.data_provider_mut()
.set_field_value(field, new_line.into_iter().collect());
#[cfg(feature = "gui")]
{
self.edited_this_frame = true;
}
}
pub(crate) fn match_brackets_helix(&mut self) {
const PAIRS: &[(char, char)] = &[('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
let field_count = self.editor.data_provider().field_count();
if field_count == 0 {
return;
}
let mut chars: Vec<char> = Vec::new();
let mut field_starts: Vec<usize> = Vec::with_capacity(field_count);
for f in 0..field_count {
field_starts.push(chars.len());
chars.extend(self.editor.data_provider().field_value(f).chars());
if f + 1 < field_count {
chars.push('\n');
}
}
let cursor_flat = field_starts[self.current_field()] + self.cursor_position();
let Some(&here) = chars.get(cursor_flat) else {
return;
};
let target = if let Some((_, close)) = PAIRS.iter().find(|(o, _)| *o == here) {
let mut depth = 0i32;
let mut found = None;
for (i, &c) in chars.iter().enumerate().skip(cursor_flat) {
if c == here {
depth += 1;
} else if c == *close {
depth -= 1;
if depth == 0 {
found = Some(i);
break;
}
}
}
found
} else if let Some((open, _)) = PAIRS.iter().find(|(_, c)| *c == here) {
let mut depth = 0i32;
let mut found = None;
for i in (0..=cursor_flat).rev() {
let c = chars[i];
if c == here {
depth += 1;
} else if c == *open {
depth -= 1;
if depth == 0 {
found = Some(i);
break;
}
}
}
found
} else {
None
};
let Some(target_flat) = target else {
return;
};
let (tf, tc) = flat_to_field(&chars, &field_starts, target_flat);
let extend = self.mode() != AppMode::Nor;
let _ = self.transition_to_field(tf);
let len = self.current_text().chars().count();
self.editor.ui_state.set_cursor(tc, len, false);
if !extend {
self.editor.ui_state.selection = SelectionState::Characterwise { anchor: (tf, tc) };
}
}
pub(crate) fn delete_selection_helix(&mut self, yank: bool, count: usize) {
for _ in 0..count.max(1) {
if !self.delete_selection_once(yank) {
break;
}
}
if self.mode() == AppMode::Nor {
self.ensure_helix_primary_selection();
}
}
pub(crate) fn change_selection_helix(&mut self, yank: bool, count: usize) {
for _ in 0..count.max(1) {
if !self.delete_selection_once(yank) {
break;
}
}
self.enter_edit_mode_helix();
#[cfg(feature = "gui")]
{
self.edited_this_frame = true;
}
}
pub(crate) fn yank_primary_selection_helix(&mut self) {
self.yank_selection();
if self.mode() == AppMode::Sel {
self.exit_highlight_mode_helix();
}
}
pub(crate) fn collapse_selection_helix(&mut self) {
self.collapse_helix_selection_to_cursor();
}
pub(crate) fn extend_line_below_helix(&mut self) {
let current = self.current_field();
let field_count = self.editor.data_provider().field_count();
self.ui_state.current_mode = AppMode::Nor;
match self.selection_state().clone() {
SelectionState::Linewise { anchor_field } => {
let next = (current + 1).min(field_count.saturating_sub(1));
if next != current {
let _ = self.transition_to_field(next);
}
self.ui_state.selection = SelectionState::Linewise { anchor_field };
}
SelectionState::Characterwise { anchor } => {
self.ui_state.selection = SelectionState::Linewise {
anchor_field: anchor.0,
};
}
SelectionState::None => {
self.ui_state.selection = SelectionState::Linewise {
anchor_field: current,
};
}
}
}
pub(crate) fn extend_to_line_bounds_helix(&mut self) {
let current = self.current_field();
self.ui_state.current_mode = AppMode::Nor;
let anchor_field = match self.selection_state() {
SelectionState::Linewise { anchor_field } => *anchor_field,
SelectionState::Characterwise { anchor } => anchor.0,
SelectionState::None => current,
};
self.ui_state.selection = SelectionState::Linewise { anchor_field };
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
struct HelixRange {
anchor: usize,
head: usize,
}
fn find_surround_pair(
line: &[char],
cursor: usize,
open: char,
close: char,
) -> Option<(usize, usize)> {
if line.is_empty() {
return None;
}
let cursor = cursor.min(line.len() - 1);
let open_idx = (0..=cursor).rev().find(|&i| line[i] == open)?;
let close_start = if open == close {
open_idx + 1
} else {
cursor.max(open_idx)
};
let close_idx = (close_start..line.len()).find(|&i| line[i] == close)?;
if open_idx < close_idx {
Some((open_idx, close_idx))
} else {
None
}
}
fn flat_to_field(chars: &[char], field_starts: &[usize], flat: usize) -> (usize, usize) {
let field_count = field_starts.len();
for f in 0..field_count {
let start = field_starts[f];
let field_end = if f + 1 < field_count {
field_starts[f + 1].saturating_sub(1)
} else {
chars.len()
};
if flat < field_end {
return (f, flat - start);
}
if flat == field_end {
return (f, (field_end - start).saturating_sub(1));
}
}
let last = field_count - 1;
let start = field_starts[last];
(last, chars.len().saturating_sub(start).saturating_sub(1))
}
impl HelixWordTarget {
fn is_prev(self) -> bool {
matches!(
self,
HelixWordTarget::PrevWordStart
| HelixWordTarget::PrevWordEnd
| HelixWordTarget::PrevLongWordStart
| HelixWordTarget::PrevLongWordEnd
)
}
fn is_long(self) -> bool {
matches!(
self,
HelixWordTarget::NextLongWordStart
| HelixWordTarget::NextLongWordEnd
| HelixWordTarget::PrevLongWordStart
| HelixWordTarget::PrevLongWordEnd
)
}
fn stops_at_word_start(self) -> bool {
matches!(
self,
HelixWordTarget::NextWordStart
| HelixWordTarget::PrevWordEnd
| HelixWordTarget::NextLongWordStart
| HelixWordTarget::PrevLongWordEnd
)
}
}
#[derive(PartialEq, Clone, Copy)]
enum CharClass {
Eol,
Whitespace,
Word,
Punctuation,
}
fn char_is_line_ending(c: char) -> bool {
c == '\n' || c == '\r'
}
fn classify(c: char) -> CharClass {
if char_is_line_ending(c) {
CharClass::Eol
} else if c.is_whitespace() {
CharClass::Whitespace
} else if c.is_alphanumeric() {
CharClass::Word
} else {
CharClass::Punctuation
}
}
fn is_word_boundary(a: char, b: char) -> bool {
classify(a) != classify(b)
}
fn is_long_word_boundary(a: char, b: char) -> bool {
match (classify(a), classify(b)) {
(CharClass::Word, CharClass::Punctuation) | (CharClass::Punctuation, CharClass::Word) => {
false
}
(x, y) => x != y,
}
}
fn reached_target(target: HelixWordTarget, prev: char, next: char) -> bool {
let boundary = if target.is_long() {
is_long_word_boundary(prev, next)
} else {
is_word_boundary(prev, next)
};
if !boundary {
return false;
}
if target.stops_at_word_start() {
classify(next) != CharClass::Whitespace
} else {
classify(prev) != CharClass::Whitespace
}
}
struct CharCursor<'a> {
chars: &'a [char],
pos: usize,
reversed: bool,
}
impl<'a> CharCursor<'a> {
fn new(chars: &'a [char], pos: usize) -> Self {
Self {
chars,
pos,
reversed: false,
}
}
fn reverse(&mut self) {
self.reversed = !self.reversed;
}
fn next(&mut self) -> Option<char> {
if self.reversed {
if self.pos == 0 {
return None;
}
self.pos -= 1;
self.chars.get(self.pos).copied()
} else {
let ch = self.chars.get(self.pos).copied();
if ch.is_some() {
self.pos += 1;
}
ch
}
}
fn prev(&mut self) -> Option<char> {
if self.reversed {
let ch = self.chars.get(self.pos).copied();
if ch.is_some() {
self.pos += 1;
}
ch
} else {
if self.pos == 0 {
return None;
}
self.pos -= 1;
self.chars.get(self.pos).copied()
}
}
}
fn range_to_target(chars: &[char], target: HelixWordTarget, origin: HelixRange) -> HelixRange {
let is_prev = target.is_prev();
let mut cursor = CharCursor::new(chars, origin.head);
if is_prev {
cursor.reverse();
}
let advance = |head: &mut usize| {
if is_prev {
*head = head.saturating_sub(1);
} else {
*head += 1;
}
};
let mut anchor = origin.anchor;
let mut head = origin.head;
let mut prev_ch = {
let ch = cursor.prev();
if ch.is_some() {
cursor.next();
}
ch
};
while let Some(ch) = cursor.next() {
if char_is_line_ending(ch) {
prev_ch = Some(ch);
advance(&mut head);
} else {
cursor.prev();
break;
}
}
if prev_ch.map(char_is_line_ending).unwrap_or(false) {
anchor = head;
}
let head_start = head;
while let Some(next_ch) = cursor.next() {
if prev_ch.is_none() || reached_target(target, prev_ch.unwrap(), next_ch) {
if head == head_start {
anchor = head;
} else {
break;
}
}
prev_ch = Some(next_ch);
advance(&mut head);
}
HelixRange { anchor, head }
}
fn helix_word_move(chars: &[char], range: HelixRange, target: HelixWordTarget) -> HelixRange {
let is_prev = target.is_prev();
let len = chars.len();
if (is_prev && range.head == 0) || (!is_prev && range.head == len) {
return range;
}
let start_range = if is_prev {
if range.anchor < range.head {
HelixRange {
anchor: range.head,
head: range.head.saturating_sub(1),
}
} else {
HelixRange {
anchor: (range.head + 1).min(len),
head: range.head,
}
}
} else if range.anchor < range.head {
HelixRange {
anchor: range.head.saturating_sub(1),
head: range.head,
}
} else {
HelixRange {
anchor: range.head,
head: (range.head + 1).min(len),
}
};
let next = range_to_target(chars, target, start_range);
if next == start_range {
range
} else {
next
}
}