use ratatui::text::Line;
use crate::document::{Selection, VisualSelection};
use crate::editor::table_edit;
use crate::editor::EditorState;
pub(super) fn expand_selection_to_inline_markers(
buffer: &crate::document::Buffer,
sel: Selection,
) -> Selection {
let (start_char, end_char) = sel.range();
if end_char <= start_char {
return sel;
}
let rope = buffer.rope();
let start_byte = rope.char_to_byte(start_char);
let end_byte = rope.char_to_byte(end_char);
let source = buffer.contents();
if end_byte > source.len() {
return sel;
}
if source[start_byte..end_byte].contains('\n') {
return sel;
}
const DOUBLE_MARKERS: &[&str] = &["**", "__", "~~"];
const SINGLE_MARKERS: &[&str] = &["*", "_", "`"];
for m in DOUBLE_MARKERS.iter().chain(SINGLE_MARKERS.iter()) {
let len = m.len();
if start_byte < len || end_byte + len > source.len() {
continue;
}
let (Some(before), Some(after)) = (
source.get(start_byte - len..start_byte),
source.get(end_byte..end_byte + len),
) else {
continue;
};
if before == *m && after == *m {
if before.contains('\n') || after.contains('\n') {
continue;
}
let new_start_byte = start_byte - len;
let new_end_byte = end_byte + len;
let new_start = rope.byte_to_char(new_start_byte);
let new_end = rope.byte_to_char(new_end_byte);
let (anchor, active) = if sel.anchor <= sel.active {
(new_start, new_end)
} else {
(new_end, new_start)
};
return Selection { anchor, active };
}
}
sel
}
pub(super) fn word_range_around<F>(len: usize, at: usize, get_char: F) -> Option<(usize, usize)>
where
F: Fn(usize) -> char,
{
if len == 0 {
return None;
}
let at = at.min(len);
let is_word = |c: char| c.is_alphanumeric() || c == '_';
let mut start = at;
while start > 0 && is_word(get_char(start - 1)) {
start -= 1;
}
let mut end = at;
while end < len && is_word(get_char(end)) {
end += 1;
}
if start != end {
return Some((start, end));
}
let mut s2 = at;
while s2 > 0 {
let c = get_char(s2 - 1);
if c.is_whitespace() || is_word(c) {
break;
}
s2 -= 1;
}
let mut e2 = at;
while e2 < len {
let c = get_char(e2);
if c.is_whitespace() || is_word(c) {
break;
}
e2 += 1;
}
if s2 != e2 {
Some((s2, e2))
} else {
None
}
}
pub(super) fn select_word_at_cursor(state: &mut EditorState) {
let buf = &state.buffer;
let len = buf.len_chars();
let offset = state.cursor.offset.min(len);
if len == 0 {
state.selection = None;
return;
}
let rope = buf.rope();
match word_range_around(len, offset, |i| rope.char(i)) {
Some((start, end)) => {
state.selection = Some(Selection {
anchor: start,
active: end,
});
state.cursor.offset = end;
state.cursor.preferred_col = state.cursor.cell_col(&state.buffer);
}
None => {
state.selection = None;
}
}
}
pub(super) fn select_line_at_cursor(state: &mut EditorState) {
let source = state.buffer.contents();
let cursor_byte = state.buffer.rope().char_to_byte(state.cursor.offset);
if let Some(info) = table_edit::find_table_at(&source, cursor_byte) {
if let Some((row_idx, col_idx)) = table_edit::cursor_cell(&info, cursor_byte) {
if let Some(row) = info.rows.get(row_idx) {
if let Some(cell) = row.cells.get(col_idx) {
let raw_bytes = cell.raw.as_bytes();
let leading = raw_bytes
.iter()
.take_while(|b| matches!(**b, b' ' | b'\t'))
.count();
let trailing = raw_bytes
.iter()
.rev()
.take_while(|b| matches!(**b, b' ' | b'\t'))
.count();
let content_len = cell.raw.len().saturating_sub(leading + trailing);
let start_byte = row.start + cell.content_start + leading;
let end_byte = start_byte + content_len;
let rope = state.buffer.rope();
let anchor = rope.byte_to_char(start_byte);
let active = rope.byte_to_char(end_byte);
state.selection = Some(Selection { anchor, active });
state.cursor.offset = active;
state.cursor.preferred_col = state.cursor.cell_col(&state.buffer);
return;
}
}
}
}
let (line, _) = state.cursor.line_col(&state.buffer);
let start = state.buffer.line_to_char(line);
let end = if line + 1 < state.buffer.line_count() {
state.buffer.line_to_char(line + 1)
} else {
state.buffer.len_chars()
};
state.selection = Some(Selection {
anchor: start,
active: end,
});
state.cursor.offset = end;
state.cursor.preferred_col = state.cursor.cell_col(&state.buffer);
}
pub fn visual_selection_to_rendered_text(sel: VisualSelection, lines: &[Line<'_>]) -> String {
let (start, end) = sel.range();
let (start_line, start_col) = start;
let (end_line, end_col) = end;
if lines.is_empty() || start_line >= lines.len() {
return String::new();
}
let end_line = end_line.min(lines.len() - 1);
let mut out = String::new();
#[allow(clippy::needless_range_loop)]
for idx in start_line..=end_line {
let line = &lines[idx];
let chars: Vec<char> = line.spans.iter().flat_map(|s| s.content.chars()).collect();
let mut lo = if idx == start_line { start_col } else { 0 };
let mut hi = if idx == end_line {
end_col
} else {
chars.len()
};
if let Some(band) = sel.band {
let (band_lo, band_hi) = band.char_cols(line);
lo = lo.max(band_lo);
hi = hi.min(band_hi);
}
let lo = lo.min(chars.len());
let hi = hi.min(chars.len());
if lo < hi {
let slice: String = chars[lo..hi].iter().collect();
if sel.band.is_some() {
out.push_str(slice.trim_end());
} else {
out.push_str(&slice);
}
}
if idx < end_line {
out.push(if sel.band.is_some() { ' ' } else { '\n' });
}
}
out
}
#[cfg(test)]
mod marker_expansion_tests {
use super::*;
use crate::document::{Buffer, CellBand};
#[test]
fn banded_copy_takes_the_cell_from_every_sub_line() {
let lines = [Line::from("│ 日本 │ ab │"), Line::from("│ 語 │ cd │")];
let sel = VisualSelection {
anchor: (0, 7),
active: (1, 10),
band: Some(CellBand {
lines: (0, 1),
cols: (9, 11),
}),
};
assert_eq!(visual_selection_to_rendered_text(sel, &lines), "ab cd");
}
#[test]
fn no_panic_on_multibyte_char_adjacent_to_selection() {
let buffer = Buffer::from_str("—bc");
let sel = Selection {
anchor: 1,
active: 2,
};
let out = expand_selection_to_inline_markers(&buffer, sel);
assert_eq!((out.anchor, out.active), (1, 2));
}
#[test]
fn still_expands_real_markers() {
let buffer = Buffer::from_str("a *foo* b");
let sel = Selection {
anchor: 3,
active: 6,
};
let out = expand_selection_to_inline_markers(&buffer, sel);
assert_eq!((out.anchor, out.active), (2, 7));
}
}