use hjkl_vim_types::{Mode, Operator, RangeKind};
use hjkl_engine::rope_util::rope_to_lines_vec;
use super::command::read_vim_range;
use super::*;
use crate::vim_state::vim_mut;
use hjkl_engine::Editor;
use hjkl_engine::buf_helpers::{buf_line_chars, buf_row_count, buf_set_cursor_rc};
pub fn text_object_inner_quote_bridge<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
quote: char,
) -> Option<((usize, usize), (usize, usize))> {
quote_text_object(ed, quote, true)
}
pub fn text_object_around_quote_bridge<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
quote: char,
) -> Option<((usize, usize), (usize, usize))> {
quote_text_object(ed, quote, false)
}
pub fn text_object_inner_bracket_bridge<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
open: char,
) -> Option<((usize, usize), (usize, usize))> {
bracket_text_object(ed, open, true, 1).map(|(s, e, _kind)| (s, e))
}
pub fn text_object_around_bracket_bridge<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
open: char,
) -> Option<((usize, usize), (usize, usize))> {
bracket_text_object(ed, open, false, 1).map(|(s, e, _kind)| (s, e))
}
pub fn text_object_inner_sentence_bridge<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
) -> Option<((usize, usize), (usize, usize))> {
sentence_text_object(ed, true, 1)
}
pub fn text_object_around_sentence_bridge<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
) -> Option<((usize, usize), (usize, usize))> {
sentence_text_object(ed, false, 1)
}
pub fn text_object_inner_paragraph_bridge<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
) -> Option<((usize, usize), (usize, usize))> {
paragraph_text_object(ed, true, 1)
}
pub fn text_object_around_paragraph_bridge<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
) -> Option<((usize, usize), (usize, usize))> {
paragraph_text_object(ed, false, 1)
}
pub fn text_object_inner_tag_bridge<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
) -> Option<((usize, usize), (usize, usize))> {
tag_text_object(ed, true)
}
pub fn text_object_around_tag_bridge<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
) -> Option<((usize, usize), (usize, usize))> {
tag_text_object(ed, false)
}
pub fn greedy_wrap(original: &[String], width: usize) -> Vec<String> {
let mut wrapped: Vec<String> = Vec::new();
let mut paragraph: Vec<String> = Vec::new();
let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
if para.is_empty() {
return;
}
let words = para.join(" ");
let mut current = String::new();
for word in words.split_whitespace() {
let extra = if current.is_empty() {
word.chars().count()
} else {
current.chars().count() + 1 + word.chars().count()
};
if extra > width && !current.is_empty() {
out.push(std::mem::take(&mut current));
current.push_str(word);
} else if current.is_empty() {
current.push_str(word);
} else {
current.push(' ');
current.push_str(word);
}
}
if !current.is_empty() {
out.push(current);
}
para.clear();
};
for line in original {
if line.trim().is_empty() {
flush(&mut paragraph, &mut wrapped, width);
wrapped.push(String::new());
} else {
paragraph.push(line.clone());
}
}
flush(&mut paragraph, &mut wrapped, width);
wrapped
}
pub fn reflow_rows<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
top: usize,
bot: usize,
) {
let width = ed.settings().textwidth.max(1);
let mut lines: Vec<String> = rope_to_lines_vec(&hjkl_engine::types::Query::rope(ed.buffer()));
let bot = bot.min(lines.len().saturating_sub(1));
if top > bot {
return;
}
let original = lines[top..=bot].to_vec();
let wrapped = greedy_wrap(&original, width);
let last_offset = wrapped
.iter()
.rposition(|l| !l.trim().is_empty())
.unwrap_or(0);
let last_row = top + last_offset;
let after: Vec<String> = lines.split_off(bot + 1);
lines.truncate(top);
lines.extend(wrapped);
lines.extend(after);
ed.restore(&lines, (last_row, 0));
move_first_non_whitespace(ed);
ed.mark_content_dirty();
}
pub fn reflow_rows_keep_cursor<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
top: usize,
bot: usize,
) -> (Vec<String>, Vec<String>) {
let width = ed.settings().textwidth.max(1);
let mut lines: Vec<String> = rope_to_lines_vec(&hjkl_engine::types::Query::rope(ed.buffer()));
let bot = bot.min(lines.len().saturating_sub(1));
if top > bot {
return (Vec::new(), Vec::new());
}
let original = lines[top..=bot].to_vec();
let wrapped = greedy_wrap(&original, width);
let after: Vec<String> = lines.split_off(bot + 1);
lines.truncate(top);
lines.extend(wrapped.clone());
lines.extend(after);
ed.restore(&lines, (top, 0));
ed.mark_content_dirty();
(original, wrapped)
}
pub fn reflow_keep_cursor(
top: usize,
cursor_row: usize,
cursor_col: usize,
before_lines: &[String],
after_lines: &[String],
) -> (usize, usize) {
let relative_row = cursor_row.saturating_sub(top);
let mut char_offset: usize = 0;
for (i, line) in before_lines.iter().enumerate() {
if i == relative_row {
let line_len = line.chars().count();
char_offset += cursor_col.min(line_len);
break;
}
char_offset += line.chars().count() + 1;
}
let mut remaining = char_offset;
for (i, line) in after_lines.iter().enumerate() {
let len = line.chars().count();
if remaining <= len {
let col = remaining.min(if len == 0 { 0 } else { len.saturating_sub(1) });
return (top + i, col);
}
remaining = remaining.saturating_sub(len + 1);
}
let last = after_lines.len().saturating_sub(1);
let last_len = after_lines.get(last).map_or(0, |l| l.chars().count());
let col = if last_len == 0 { 0 } else { last_len - 1 };
(top + last, col)
}
pub fn apply_case_op_to_selection<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
op: Operator,
top: (usize, usize),
bot: (usize, usize),
kind: RangeKind,
) {
use hjkl_buffer::{Edit, Position};
ed.push_undo();
let saved_yank = ed.yank();
let saved_yank_linewise = ed.yank_linewise();
let selection = read_vim_range(ed, top, bot, kind);
let n_rows_before = buf_row_count(ed.buffer());
cut_vim_range(ed, top, bot, kind);
let transformed = match op {
Operator::Uppercase => selection.to_uppercase(),
Operator::Lowercase => selection.to_lowercase(),
Operator::ToggleCase => toggle_case_str(&selection),
Operator::Rot13 => rot13_str(&selection),
_ => unreachable!(),
};
if !transformed.is_empty() {
let (ordered_top, ordered_bot) = order(top, bot);
let n_rows = buf_row_count(ed.buffer());
let (insert_at, final_text) = if kind == RangeKind::Linewise && ordered_top.0 >= n_rows {
let last_row = n_rows.saturating_sub(1);
let last_col = buf_line_chars(ed.buffer(), last_row);
let body = transformed.trim_end_matches('\n');
(Position::new(last_row, last_col), format!("\n{}", body))
} else {
let at = match kind {
RangeKind::Linewise => Position::new(ordered_top.0, 0),
_ => Position::new(ordered_top.0, ordered_top.1),
};
let text = if kind == RangeKind::Linewise
&& n_rows == 1
&& ordered_top.0 == 0
&& n_rows_before == ordered_bot.0 - ordered_top.0 + 1
{
transformed.trim_end_matches('\n').to_string()
} else {
transformed
};
(at, text)
};
ed.mutate_edit(Edit::InsertStr {
at: insert_at,
text: final_text,
});
}
buf_set_cursor_rc(ed.buffer_mut(), top.0, top.1);
ed.set_yank(saved_yank);
ed.set_yank_linewise(saved_yank_linewise);
vim_mut(ed).mode = Mode::Normal;
}
pub fn indent_rows<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
top: usize,
bot: usize,
count: usize,
) {
ed.sync_buffer_content_from_textarea();
let width = ed.settings().shiftwidth.saturating_mul(count.max(1));
let pad: String = if ed.settings().expandtab {
" ".repeat(width)
} else {
let tabstop = ed.settings().tabstop.max(1);
let tabs = width / tabstop;
let spaces = width % tabstop;
format!("{}{}", "\t".repeat(tabs), " ".repeat(spaces))
};
let mut lines: Vec<String> = rope_to_lines_vec(&hjkl_engine::types::Query::rope(ed.buffer()));
let bot = bot.min(lines.len().saturating_sub(1));
for line in lines.iter_mut().take(bot + 1).skip(top) {
if !line.is_empty() {
line.insert_str(0, &pad);
}
}
ed.restore(&lines, (top, 0));
move_first_non_whitespace(ed);
}
pub fn outdent_rows<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
top: usize,
bot: usize,
count: usize,
) {
ed.sync_buffer_content_from_textarea();
let width = ed.settings().shiftwidth.saturating_mul(count.max(1));
let tabstop = ed.settings().tabstop.max(1);
let mut lines: Vec<String> = rope_to_lines_vec(&hjkl_engine::types::Query::rope(ed.buffer()));
let bot = bot.min(lines.len().saturating_sub(1));
for line in lines.iter_mut().take(bot + 1).skip(top) {
let mut strip = 0;
let mut visual_width = 0;
for ch in line.chars() {
if !matches!(ch, ' ' | '\t') {
break;
}
let next_width = if ch == '\t' {
visual_width + tabstop - visual_width % tabstop
} else {
visual_width + 1
};
if next_width > width {
break;
}
strip += ch.len_utf8();
visual_width = next_width;
}
if strip > 0 {
line.drain(..strip);
}
}
ed.restore(&lines, (top, 0));
move_first_non_whitespace(ed);
}
pub fn bracket_net(line: &str) -> i32 {
let mut net: i32 = 0;
let mut chars = line.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'/' if chars.peek() == Some(&'/') => return net,
'"' => {
while let Some(c) = chars.next() {
match c {
'\\' => {
chars.next();
} '"' => break,
_ => {}
}
}
}
'\'' => {
let saved: Vec<char> = chars.clone().take(5).collect();
let close_idx = if saved.first() == Some(&'\\') {
saved.iter().skip(2).position(|&c| c == '\'').map(|p| p + 2)
} else {
saved.iter().skip(1).position(|&c| c == '\'').map(|p| p + 1)
};
if let Some(idx) = close_idx {
for _ in 0..=idx {
chars.next();
}
}
}
'{' | '(' | '[' => net += 1,
'}' | ')' | ']' => net -= 1,
_ => {}
}
}
net
}
pub fn auto_indent_rows<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
top: usize,
bot: usize,
) {
ed.sync_buffer_content_from_textarea();
let shiftwidth = ed.settings().shiftwidth;
let expandtab = ed.settings().expandtab;
let indent_unit: String = if expandtab {
" ".repeat(shiftwidth)
} else {
"\t".to_string()
};
let mut lines: Vec<String> = rope_to_lines_vec(&hjkl_engine::types::Query::rope(ed.buffer()));
let bot = bot.min(lines.len().saturating_sub(1));
let mut depth: i32 = 0;
for line in lines.iter().take(top) {
depth += bracket_net(line);
if depth < 0 {
depth = 0;
}
}
for line in lines.iter_mut().take(bot + 1).skip(top) {
let trimmed_owned = line.trim_start().to_owned();
if trimmed_owned.is_empty() {
*line = String::new();
continue;
}
let starts_with_close = trimmed_owned
.chars()
.next()
.is_some_and(|c| matches!(c, '}' | ')' | ']'));
let starts_with_dot = trimmed_owned.starts_with('.')
&& !trimmed_owned.starts_with("..")
&& !trimmed_owned.starts_with(".;");
let effective_depth = if starts_with_close {
depth.saturating_sub(1)
} else if starts_with_dot {
depth.saturating_add(1)
} else {
depth
}
.max(0) as usize;
let new_line = format!("{}{}", indent_unit.repeat(effective_depth), trimmed_owned);
depth += bracket_net(&trimmed_owned);
if depth < 0 {
depth = 0;
}
*line = new_line;
}
ed.restore(&lines, (top, 0));
move_first_non_whitespace(ed);
ed.set_last_indent_range(Some((top, bot)));
}
pub fn toggle_case_str(s: &str) -> String {
s.chars()
.map(|c| {
if c.is_lowercase() {
c.to_uppercase().collect::<String>()
} else if c.is_uppercase() {
c.to_lowercase().collect::<String>()
} else {
c.to_string()
}
})
.collect()
}
pub fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
if a <= b { (a, b) } else { (b, a) }
}
pub fn clamp_cursor_to_normal_mode<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) {
let (row, col) = ed.cursor();
let line_chars = buf_line_chars(ed.buffer(), row);
let max_col = line_chars.saturating_sub(1);
if col > max_col {
buf_set_cursor_rc(ed.buffer_mut(), row, max_col);
}
}
#[cfg(test)]
mod tests {
use super::{outdent_rows, toggle_case_str};
use hjkl_buffer::{View, rope_line_str};
use hjkl_engine::{DefaultHost, Editor, Options};
#[test]
fn toggle_case_preserves_multi_character_unicode_mappings() {
assert_eq!(toggle_case_str("Straße"), "sTRASSE");
assert_eq!(toggle_case_str("İ"), "i\u{307}");
}
#[test]
fn outdent_rows_consumes_visual_tab_width() {
let options = Options {
tabstop: 4,
shiftwidth: 4,
..Options::default()
};
let mut ed = Editor::new(View::from_str("\t\tfoo"), DefaultHost::new(), options);
outdent_rows(&mut ed, 0, 0, 1);
assert_eq!(rope_line_str(&ed.buffer().rope(), 0), "\tfoo");
}
}