use strop_core::Range;
use strop_grammar::{self as grammar, Command, Op, Parse};
use super::{Editor, Key, Mode};
pub(crate) const EX_COMMANDS: &[(&str, &str)] = &[
("w", "write"),
("w!", "write, force (file changed on disk)"),
("wq!", "write forced + quit"),
("q", "quit"),
("q!", "quit, force"),
("wq", "write + quit"),
("e", "edit file"),
("e!", "edit file, force"),
("vs", "split vertical"),
("vsplit", "split vertical"),
("sp", "split horizontal"),
("split", "split horizontal"),
("help", "help buffer"),
("h", "help buffer"),
("!", "run shell command"),
("view", "open readonly"),
];
impl Editor {
pub(crate) fn feed_normal(&mut self, key: Key) {
if self.buf().readonly {
return self.feed_readonly(key);
}
if key == Key::CtrlR {
return self.redo();
}
if key == Key::CtrlW {
self.walker.prefix = "\x17"; return;
}
if !self.pending.is_empty() {
return self.feed_pending(key);
}
if key == Key::Enter {
return self.dive_from_blame();
}
if key == Key::CtrlO {
return self.jump_back();
}
if key == Key::Tab {
return self.jump_forward();
}
if key == Key::Esc {
self.collapse_cursors();
self.walker.clear();
return;
}
match key {
Key::Up => return self.run_motion("k"),
Key::Down => return self.run_motion("j"),
Key::Left => return self.run_motion("h"),
Key::Right => return self.run_motion("l"),
_ => {}
}
let Key::Char(c) = key else {
return;
};
match self.walker.feed(c) {
super::input::Walk::Pending => {}
super::input::Walk::Complete(keys) => self.dispatch_keys(&keys),
super::input::Walk::EnterText(c) => {
self.pending = c.to_string();
self.pending_normal = false;
self.pending_cursor = self.pending.len();
}
}
}
fn dispatch_keys(&mut self, keys: &str) {
let digits: String = keys.chars().take_while(|c| c.is_ascii_digit()).collect();
if !digits.is_empty() {
let count: usize = digits.parse().unwrap_or(1);
let rest = &keys[digits.len()..];
let toks: Vec<String> = seq_tokens(rest);
if let Some(row) = crate::keymap::find_dispatch(&toks) {
use crate::keymap::Handler;
match row.handler {
Handler::Leaf(f) => {
if row.id == "insert-entries" {
self.insert_count = count;
f(self, rest.chars().last().unwrap_or('\0'));
} else if row.id == "paste" {
self.paste_n(count, rest == "P");
} else {
for _ in 0..count {
f(self, rest.chars().last().unwrap_or('\0'));
}
}
return;
}
Handler::Alias(_) => {}
Handler::AbsorbChar(crate::keymap::AbsorbKind::Replace) => {
let c = rest.chars().nth(1).unwrap_or('\0');
self.replace_char_n(c, count);
return;
}
_ => {}
}
}
}
if let Some(row) = crate::keymap::find_dispatch(&seq_tokens(keys)) {
if let crate::keymap::Handler::AbsorbChar(kind) = row.handler {
let c = keys.chars().nth(1).unwrap_or('\0');
use crate::keymap::AbsorbKind;
match kind {
AbsorbKind::Replace => self.replace_char_n(c, 1),
AbsorbKind::MarkSet => self.set_mark(c),
AbsorbKind::MarkJump => self.jump_mark(c),
AbsorbKind::Find => {} }
if kind != AbsorbKind::Find {
return;
}
}
}
if let Some(rest) = keys.strip_prefix('"').filter(|r| r.len() > 1) {
let reg = rest.chars().next().unwrap();
let rest = &rest[reg.len_utf8()..];
if let Some(row) = crate::keymap::find_dispatch(&seq_tokens(rest)) {
if let crate::keymap::Handler::Leaf(f) = row.handler {
if row.id == "paste" {
self.paste_named(Some(reg), rest == "P");
return;
}
let _ = f; }
}
}
let toks: Vec<String> = seq_tokens(keys);
if let Some(row) = crate::keymap::find_dispatch(&toks) {
use crate::keymap::Handler;
match row.handler {
Handler::Leaf(f) => return f(self, keys.chars().last().unwrap_or('\0')),
Handler::Alias(expansion) => {
for c in expansion.chars() {
self.feed_normal(crate::editor::Key::Char(c));
}
return;
}
Handler::Prefix => {
self.walker.prefix = if keys == " g" { " g" } else { "" };
if keys == " g" {
return;
}
self.message = format!("prefix: {keys}");
return;
}
Handler::TextLine => {
self.pending = if keys == " |" {
"|".into()
} else {
keys.into()
};
self.pending_normal = false;
self.pending_cursor = self.pending.len();
return;
}
_ => {}
}
}
match grammar::parse(keys) {
Parse::Complete(cmd) => match cmd.op {
None => self.move_cursor(&cmd),
Some(_) => self.execute(&cmd),
},
Parse::Incomplete => {
self.pending = keys.into();
self.pending_normal = false;
self.pending_cursor = self.pending.len();
}
Parse::Invalid => self.message = format!("not an editor command: {keys}"),
}
}
pub(crate) fn delete_char(&mut self) {
let end = self
.buf()
.ceil_boundary(self.head() + 1)
.min(self.buf().line_end(self.buf().line_of(self.head())));
if end > self.head() {
self.tx_begin();
let range = Range::charwise(self.head(), end);
let text = self.buf_mut().delete(range);
self.tx_commit();
self.set_register(None, text, false);
self.flash(range);
self.last_cmd_keys = "x".into();
self.last_insert = None;
}
}
pub(crate) fn paste_named(&mut self, name: Option<char>, before: bool) {
self.paste(name, before);
self.last_cmd_keys = if before { "P".into() } else { "p".into() };
self.last_insert = None;
}
pub(crate) fn append(&mut self) {
self.set_head(
self.buf()
.ceil_boundary(self.head() + 1)
.min(self.buf().line_end(self.buf().line_of(self.head()))),
);
self.enter_insert_from("a");
}
pub(crate) fn append_eol(&mut self) {
self.set_head(self.buf().line_end(self.buf().line_of(self.head())));
self.enter_insert_from("A");
}
pub(crate) fn open_below(&mut self) {
let indent = self.auto_indent_full_line();
let end = self.buf().line_end(self.buf().line_of(self.head()));
let text = format!("\n{indent}");
self.insert_open = Some(text.clone());
self.buf_mut().insert(end, &text);
self.set_head(end + text.len());
self.enter_insert_from("o");
}
pub(crate) fn open_above(&mut self) {
let indent = self.auto_indent_full_line();
let start = self.buf().line_start(self.buf().line_of(self.head()));
let text = format!("{indent}\n");
self.insert_open = Some(text.clone());
self.buf_mut().insert(start, &text);
self.set_head(start + indent.len());
self.enter_insert_from("O");
}
pub(crate) fn enter_visual(&mut self, linewise: bool) {
self.sels_mut().collapse_extras();
self.mode = if linewise {
Mode::VisualLine
} else {
Mode::Visual
};
let h = self.head();
self.sels_mut().stretch_primary(h, h);
}
pub(crate) fn dot_repeat_pub(&mut self) {
self.dot_repeat();
}
pub(crate) fn toggle_case_pub(&mut self) {
self.toggle_case();
}
pub(crate) fn join_lines_pub(&mut self) {
self.join_lines();
}
fn execute_surround(&mut self, cmd: &Command) -> Option<()> {
let r = grammar::resolve(self.buf(), self.head(), cmd)?;
let pair = |ch: char| match ch {
'b' | '(' | ')' => ('(', ')'),
'B' | '{' | '}' => ('{', '}'),
'r' | '[' | ']' => ('[', ']'),
'a' | '<' | '>' => ('<', '>'),
q => (q, q),
};
self.tx_begin();
match &cmd.target {
grammar::Target::SurroundDelete(_) => {
self.buf_mut()
.delete(Range::charwise(r.range.end - 1, r.range.end));
self.buf_mut()
.delete(Range::charwise(r.range.start, r.range.start + 1));
self.set_head(r.range.start);
}
grammar::Target::SurroundChange { to, .. } => {
let (o, c) = pair(*to);
self.buf_mut()
.delete(Range::charwise(r.range.end - 1, r.range.end));
self.buf_mut().insert(r.range.end - 1, &c.to_string());
self.buf_mut()
.delete(Range::charwise(r.range.start, r.range.start + 1));
self.buf_mut().insert(r.range.start, &o.to_string());
self.set_head(r.range.start);
}
grammar::Target::SurroundAdd { ch, .. } => {
let (o, c) = pair(*ch);
self.buf_mut().insert(r.range.end, &c.to_string());
self.buf_mut().insert(r.range.start, &o.to_string());
self.set_head(r.range.start + 1);
}
_ => {
self.tx_commit();
return None;
}
}
self.tx_commit();
self.clamp_cursor();
self.flash(Range::charwise(self.head(), self.head()));
self.last_cmd_keys = cmd.keys.clone();
self.last_insert = None;
Some(())
}
pub(crate) fn alias(&mut self, alias_key: &str, expansion: &str) {
self.feed_text(expansion);
self.last_cmd_keys = alias_key.into();
}
fn feed_pending(&mut self, key: Key) {
let is_ex = self.pending.starts_with(':');
let is_pipe = self.pending.starts_with('|');
let is_search = !is_ex && (self.pending.contains('/') || self.pending.contains('?'));
match key {
Key::Esc => {
if self.pending_normal {
self.pending.clear();
self.pending_normal = false;
} else {
self.pending_normal = true;
self.pending_cursor = self.pending.len();
}
}
Key::Backspace => {
if self.pending_normal {
self.pending_normal_key('h'); } else {
self.pending.pop();
self.pending_cursor = self.pending.len();
}
}
Key::Enter if is_ex => self.run_ex(),
Key::Enter if is_search => {
let pat = &self.pending[1..];
if pat.is_empty() {
let reversed = self.pending.starts_with('?');
self.pending.clear();
return self.repeat_search(reversed);
}
self.pending.push('\r');
self.resolve_pending();
}
Key::Enter if is_pipe => self.pipe_current_line(),
Key::Tab if is_ex => self.ex_tab_complete(),
Key::Enter => self.pending.clear(),
Key::CtrlR | Key::CtrlW | Key::CtrlX | Key::CtrlD | Key::CtrlO => {} Key::Up | Key::Down | Key::Left | Key::Right | Key::Tab | Key::Backtab => {}
Key::Char(c) => {
if self.pending_normal {
self.pending_normal_key(c);
return;
}
self.pending.push(c);
if !is_ex && !is_pipe {
self.resolve_pending();
}
}
}
}
fn pending_normal_key(&mut self, c: char) {
let mut le = strop_picker::LineEdit::new(std::mem::take(&mut self.pending));
le.cursor = self.pending_cursor;
le.normal = true;
le.normal_key(c);
self.pending_normal = le.normal;
self.pending_cursor = le.cursor;
self.pending = le.text;
}
fn resolve_pending(&mut self) {
match grammar::parse(&self.pending) {
Parse::Incomplete => {}
Parse::Invalid => {
self.message = format!("not an editor command: {}", self.pending);
self.pending.clear();
}
Parse::Complete(cmd) => {
self.pending.clear();
match cmd.op {
None => self.move_cursor(&cmd),
Some(_) => self.execute(&cmd),
}
}
}
}
pub(crate) fn run_motion(&mut self, keys: &str) {
if let Parse::Complete(cmd) = grammar::parse(keys) {
self.move_cursor(&cmd);
}
}
fn pipe_current_line(&mut self) {
let cmd = self.pending[1..].to_string();
self.pending.clear();
let line = self.buf().line_of(self.head());
let start = self.buf().line_start(line);
let end = self.buf().line_start(line + 1).min(self.buf().len_bytes());
self.pipe_run(start, end, &cmd);
}
fn note_search(&mut self, cmd: &Command) {
match &cmd.target {
strop_grammar::Target::Motion(strop_grammar::Motion::Search(p)) => {
self.last_search = Some(super::LastSearch {
pattern: p.clone(),
backward: false,
whole_word: false,
});
}
strop_grammar::Target::Motion(strop_grammar::Motion::SearchBackward(p)) => {
self.last_search = Some(super::LastSearch {
pattern: p.clone(),
backward: true,
whole_word: false,
});
}
strop_grammar::Target::Motion(strop_grammar::Motion::FindChar {
ch,
till,
backward,
}) => {
self.last_find = Some((*ch, *backward, *till));
}
_ => {}
}
}
pub(crate) fn move_cursor(&mut self, cmd: &Command) {
self.note_search(cmd);
if matches!(
cmd.target,
strop_grammar::Target::Motion(
strop_grammar::Motion::FirstLine
| strop_grammar::Motion::LastLine
| strop_grammar::Motion::MatchPair
| strop_grammar::Motion::Search(_)
| strop_grammar::Motion::SearchBackward(_)
)
) {
self.push_jump();
}
let primary_hit = grammar::resolve(self.buf(), self.head(), cmd);
if let Some(r) = &primary_hit {
self.set_head(grammar::cursor_after(self.buf(), self.head(), cmd, r));
}
let extras: Vec<usize> = self
.extra_selections()
.iter()
.map(|s| {
let c = match grammar::resolve(self.buf(), s.head, cmd) {
Some(r) => grammar::cursor_after(self.buf(), s.head, cmd, &r),
None => s.head,
};
self.clamp_pos(c)
})
.collect();
self.sels_mut().set_extras(extras);
self.clamp_cursor();
self.normalize_cursors();
if matches!(
cmd.target,
strop_grammar::Target::Motion(
strop_grammar::Motion::Search(_) | strop_grammar::Motion::SearchBackward(_)
)
) && primary_hit.is_none()
{
self.message = "pattern not found".into();
}
}
pub(crate) fn repeat_find(&mut self, reverse: bool) {
let Some((ch, backward, till)) = self.last_find else {
self.message = "no previous find".into();
return;
};
let backward = backward ^ reverse;
let seek = |buf: &strop_core::Buffer, c: usize| -> Option<usize> {
let line = buf.line_of(c);
let (ls, le) = (buf.line_start(line), buf.line_end(line));
let text = buf.line_text(line);
if backward {
for (off, t) in text.char_indices().rev() {
let pos = ls + off;
if pos >= c.min(le) {
continue;
}
if t == ch {
return Some(if till { (pos + 1).min(le) } else { pos });
}
}
return None;
}
for (off, t) in text.char_indices() {
let pos = ls + off;
if pos <= c {
continue;
}
if pos >= le {
break;
}
if t == ch {
return Some(if till {
pos.saturating_sub(1).max(ls)
} else {
pos
});
}
}
None
};
let extras: Vec<usize> = self
.extra_selections()
.iter()
.map(|s| seek(self.buf(), s.head).unwrap_or(s.head))
.collect();
self.sels_mut().set_extras(extras);
match seek(self.buf(), self.head()) {
Some(h) => {
self.set_head(h);
self.flash(Range::charwise(self.head(), self.head()));
}
None => self.message = "find: no more matches".into(),
}
self.normalize_cursors();
}
pub(crate) fn repeat_search(&mut self, invert: bool) {
let Some(ls) = self.last_search.clone() else {
self.message = "no previous search".into();
return;
};
let backward = ls.backward ^ invert;
self.push_jump(); let word_char = |b: u8| b.is_ascii_alphanumeric() || b == b'_';
let boundary_ok = |buf: &strop_core::Buffer, at: usize, len: usize| {
let before_ok = at == 0 || !word_char(buf.byte(at - 1));
let after_ok = at + len >= buf.len_bytes() || !word_char(buf.byte(at + len));
!ls.whole_word || (before_ok && after_ok)
};
let seek = |buf: &strop_core::Buffer, from: usize| {
let len = ls.pattern.len();
let mut hit = if backward {
grammar::search_backward(buf, from, &ls.pattern)
} else {
grammar::search_forward(buf, from + 1, &ls.pattern)
};
let mut guard = 0;
while hit.is_some_and(|h| !boundary_ok(buf, h, len)) && guard < 64 {
let h = hit;
hit = if backward {
grammar::search_backward(buf, h.unwrap_or(0), &ls.pattern)
} else {
grammar::search_forward(buf, h.map(|x| x + 1).unwrap_or(0), &ls.pattern)
};
guard += 1;
}
hit.or_else(|| {
let mut h = if backward {
grammar::search_backward(buf, buf.len_bytes(), &ls.pattern)
} else {
grammar::search_forward(buf, 0, &ls.pattern)
};
let mut guard = 0;
while h.is_some_and(|x| !boundary_ok(buf, x, len)) && guard < 64 {
let cur = h;
h = if backward {
grammar::search_backward(buf, cur.unwrap_or(0), &ls.pattern)
} else {
grammar::search_forward(buf, cur.map(|x| x + 1).unwrap_or(0), &ls.pattern)
};
guard += 1;
}
h
})
};
let extras: Vec<usize> = self
.extra_selections()
.iter()
.map(|s| {
seek(self.buf(), s.head)
.map(|h| self.buf().clamp_boundary(h))
.unwrap_or(s.head)
})
.collect();
self.sels_mut().set_extras(extras);
match seek(self.buf(), self.head()) {
Some(h) => {
self.set_head(self.buf().clamp_boundary(h));
self.clamp_cursor();
self.flash(Range::charwise(self.head(), self.head()));
}
None => self.message = format!("pattern not found: {}", ls.pattern),
}
self.normalize_cursors();
}
pub(crate) fn search_word_under_cursor(&mut self, backward: bool) {
let word_char = |b: u8| b.is_ascii_alphanumeric() || b == b'_';
let buf_len = self.buf().len_bytes();
if self.head() >= buf_len || !word_char(self.buf().byte(self.head())) {
self.message = "no word under cursor".into();
return;
}
let mut start = self.head();
while start > 0 && word_char(self.buf().byte(start - 1)) {
start -= 1;
}
let mut end = self.head();
while end < buf_len && word_char(self.buf().byte(end)) {
end += 1;
}
let pattern = self.buf().rope.byte_slice(start..end).to_string();
self.last_search = Some(super::LastSearch {
pattern,
backward,
whole_word: true,
});
if backward {
self.set_head(start);
}
self.repeat_search(false);
}
pub fn preview(&self) -> Option<(Vec<Range>, String)> {
if self.pending.is_empty() {
return None;
}
match grammar::parse(&self.pending) {
Parse::Complete(cmd) if cmd.op.is_some() => {
let spec = grammar::resolve(self.buf(), self.head(), &cmd)?.spec;
let plan = grammar::plan(self.buf(), &self.all_cursors(), &cmd)?;
Some((plan.targets.iter().map(|t| t.range).collect(), spec))
}
_ => {
if let Some(idx) = self.pending.find('?') {
let pat = &self.pending[idx + 1..];
if !pat.is_empty() && !pat.contains('\r') {
if let Some(hit) = grammar::search_backward(self.buf(), self.head(), pat) {
return Some((
vec![Range::charwise(hit, self.head())],
format!("search ?{pat}"),
));
}
}
}
if let Some(idx) = self.pending.find('/') {
let pat = &self.pending[idx + 1..];
if !pat.is_empty() {
if let Some(hit) = grammar::search_forward(self.buf(), self.head() + 1, pat)
{
return Some((
vec![Range::charwise(self.head(), hit)],
format!("search /{pat}"),
));
}
}
}
None
}
}
}
pub(crate) fn ex_candidates(&self) -> Vec<(&'static str, &'static str)> {
let Some(prefix) = self.pending.strip_prefix(':') else {
return Vec::new();
};
if prefix.contains(' ') {
return Vec::new();
}
EX_COMMANDS
.iter()
.filter(|(name, _)| name.starts_with(prefix))
.copied()
.collect()
}
fn ex_tab_complete(&mut self) {
let cands = self.ex_candidates();
if cands.is_empty() {
return;
}
let prefix = self.pending.strip_prefix(':').unwrap_or("");
let next = cands
.iter()
.position(|(name, _)| *name == prefix)
.map_or(cands[0].0, |i| cands[(i + 1) % cands.len()].0);
self.pending = format!(":{next}");
}
pub fn find_candidates(&self) -> Option<(u8, bool)> {
let b = self.pending.as_bytes();
let (&pfx, _) = b.split_last()?;
let backward = matches!(pfx, b'F' | b'T');
if !matches!(pfx, b'f' | b'F' | b't' | b'T') {
return None;
}
Some((pfx, backward))
}
pub fn search_pattern(&self) -> Option<&str> {
self.pending
.find('/')
.map(|i| &self.pending[i + 1..])
.filter(|p| !p.is_empty())
}
fn execute(&mut self, cmd: &Command) {
self.last_change = Some(cmd.clone());
self.last_cmd_keys.clear();
self.note_search(cmd);
if let Some(()) = self.execute_surround(cmd) {
return;
}
let Some(plan) = grammar::plan(self.buf(), &self.all_cursors(), cmd) else {
self.message = "no target".into();
return;
};
let kept: Vec<(usize, Range, bool)> = plan
.targets
.iter()
.map(|t| (t.cursor, t.range, t.range.is_linewise()))
.collect();
match cmd.op.unwrap() {
Op::Yank => {
let texts: Vec<String> = kept
.iter()
.map(|(_, r, _)| self.buf().slice_string(*r))
.collect();
let linewise = kept.first().is_some_and(|t| t.2);
self.set_register(cmd.register, texts.join("\n"), linewise);
self.flash(kept[0].1);
}
Op::Indent | Op::Dedent => {
self.tx_begin();
for (_, r, _) in kept.iter().rev() {
self.apply_indent(*r, cmd.op.unwrap() == Op::Indent);
}
self.tx_commit();
self.normalize_cursors();
self.flash(Range::charwise(self.head(), self.head()));
}
Op::Delete | Op::Change => {
if cmd.op.unwrap() == Op::Change && kept.first().is_some_and(|t| t.2) {
self.change_lines(cmd, &kept);
self.last_cmd_keys = cmd.keys.clone();
self.last_insert = None;
return;
}
let texts: Vec<String> = kept
.iter()
.map(|(_, r, _)| self.buf().slice_string(*r))
.collect();
let linewise = kept.first().is_some_and(|t| t.2);
self.tx_begin();
for (_, r, _) in kept.iter().rev() {
self.buf_mut().delete(*r);
}
self.set_register(cmd.register, texts.join("\n"), linewise);
let mut shift = 0usize;
let mut landings: Vec<(bool, usize)> = Vec::with_capacity(kept.len());
for (c, r, _) in &kept {
landings.push((*c == self.head(), r.start - shift));
shift += r.end - r.start;
}
self.set_head(
landings
.iter()
.find(|(p, _)| *p)
.map(|(_, s)| *s)
.unwrap_or(self.head()),
);
let mut starts: Vec<usize> = landings.iter().map(|(_, s)| *s).collect();
starts.sort_unstable();
self.sels_mut().set_extras(starts);
if cmd.op.unwrap() == Op::Change {
self.enter_insert_from(&cmd.keys);
self.clamp_cursor();
} else {
self.clamp_cursor();
self.tx_commit();
}
self.flash(Range::charwise(self.head(), self.head()));
}
}
self.last_cmd_keys = cmd.keys.clone();
self.last_insert = None;
}
fn change_lines(&mut self, cmd: &Command, kept: &[(usize, strop_core::Range, bool)]) {
let texts: Vec<String> = kept
.iter()
.map(|(_, r, _)| {
let first = self.buf().line_of(r.start);
let last = self.buf().line_of(r.end.saturating_sub(1));
let s = self.buf().line_start(first);
let e = self.buf().line_start(last + 1).min(self.buf().len_bytes());
self.buf().rope.byte_slice(s..e).to_string()
})
.collect();
let indents: Vec<String> = kept
.iter()
.map(|(_, r, _)| {
let line = self.buf().line_of(r.start);
self.buf()
.line_text(line)
.chars()
.take_while(|c| *c == ' ' || *c == '\t')
.collect()
})
.collect();
self.tx_begin();
let mut entries: Vec<(bool, usize, isize)> = Vec::with_capacity(kept.len());
for ((c, r, _), indent) in kept.iter().zip(indents.iter()).rev() {
let first = self.buf().line_of(r.start);
let last = self.buf().line_of(r.end.saturating_sub(1));
let start = self.buf().line_start(first);
let before = self.buf().len_bytes() as isize;
if first == last {
let end = self.buf().line_end(first);
self.buf_mut().delete(Range::charwise(start, end));
self.buf_mut().insert(start, indent);
} else {
let end = self.buf().line_start(last + 1).min(self.buf().len_bytes());
self.buf_mut().delete(Range::charwise(start, end));
self.buf_mut().insert(start, &format!("{indent}\n"));
}
let net = self.buf().len_bytes() as isize - before;
entries.push((*c == self.head(), start + indent.len(), net));
}
let landings: Vec<(bool, usize)> = entries
.iter()
.enumerate()
.map(|(i, (p, at, _))| {
let shift: isize = entries[i + 1..].iter().map(|(_, _, n)| n).sum();
(*p, (*at as isize + shift).max(0) as usize)
})
.collect();
self.set_register(cmd.register, texts.join(""), true);
self.set_head(
landings
.iter()
.find(|(p, _)| *p)
.map(|(_, s)| *s)
.unwrap_or(self.head()),
);
let mut starts: Vec<usize> = landings.iter().map(|(_, s)| *s).collect();
starts.sort_unstable();
self.sels_mut().set_extras(starts);
self.enter_insert_from(&cmd.keys);
self.clamp_cursor();
self.flash(Range::charwise(self.head(), self.head()));
}
fn dot_repeat(&mut self) {
if self.last_change.is_none() && self.last_cmd_keys.is_empty() && self.last_insert.is_none()
{
return;
}
let insert = self.last_insert.clone();
if let Some(cmd) = self.last_change.clone() {
self.execute(&cmd);
} else {
let keys = self.last_cmd_keys.clone();
if !keys.is_empty() {
self.feed_text(&keys);
}
}
if let Some(text) = insert {
let was_insert = self.mode == Mode::Insert;
if !was_insert {
self.enter_insert_from("i");
}
for c in text.chars() {
self.feed(Key::Char(c));
}
self.feed(Key::Esc);
self.message = "repeated".into();
}
}
fn replace_char_n(&mut self, c: char, count: usize) {
let line_end = self.buf().line_end(self.buf().line_of(self.head()));
let end = (self.head() + count).min(line_end);
if end <= self.head() || c == '\n' {
return;
}
let cursor = self.head();
self.tx_begin();
self.buf_mut().delete(Range::charwise(cursor, end));
let text: String = std::iter::repeat_n(c, end - cursor).collect();
self.buf_mut().insert(cursor, &text);
self.tx_commit();
self.set_head(cursor + text.len() - 1); self.flash(Range::charwise(self.head(), self.head()));
self.last_cmd_keys = format!("r{c}");
self.last_insert = None;
}
fn join_lines(&mut self) {
self.tx_begin();
let line = self.buf().line_of(self.head());
if line + 1 >= self.buf().len_lines() {
self.tx_commit();
return;
}
let eol = self.buf().line_end(line);
let next_start = self.buf().line_start(line + 1);
let next_end = self.buf().line_end(line + 1);
let mut join_at = next_start;
while join_at < next_end
&& self.buf().byte(join_at).is_ascii_whitespace()
&& self.buf().byte(join_at) != b'\n'
{
join_at += 1;
}
self.buf_mut().delete(Range::charwise(eol, join_at));
if join_at < next_end {
self.buf_mut().insert(eol, " ");
}
self.set_head(eol);
self.tx_commit();
self.clamp_cursor();
self.flash(Range::charwise(eol, (eol + 1).min(self.buf().len_bytes())));
self.last_cmd_keys = "J".into();
self.last_insert = None;
}
fn toggle_case(&mut self) {
if self.buf().readonly {
self.message = "readonly buffer".into();
return;
}
let line_end = self.buf().line_end(self.buf().line_of(self.head()));
if self.head() >= line_end {
return;
}
let b = self.buf().byte(self.head());
let c = b as char;
if c.is_ascii_alphabetic() {
let flipped = if c.is_ascii_lowercase() {
c.to_ascii_uppercase()
} else {
c.to_ascii_lowercase()
};
let cursor = self.head();
self.tx_begin();
self.buf_mut().delete(Range::charwise(cursor, cursor + 1));
self.buf_mut().insert(cursor, &flipped.to_string());
self.tx_commit();
}
self.set_head(self.buf().ceil_boundary(self.head() + 1));
self.clamp_cursor();
self.last_cmd_keys = "~".into();
self.last_insert = None;
}
pub(crate) fn apply_indent(&mut self, range: Range, right: bool) {
let line = self.buf().line_of(range.start);
let last = self.buf().line_of(range.end.saturating_sub(1)) + 1;
for l in line..last {
let start = self.buf().line_start(l);
if right {
let indent = self.config.indent();
self.buf_mut().insert(start, &indent);
} else {
let end = self.buf().line_end(l);
let width = self.config.tab_size;
let mut strip = 0;
while strip < width && start + strip < end && self.buf().byte(start + strip) == b' '
{
strip += 1;
}
if strip == 0 && self.buf().byte_at(start) == Some(b'\t') {
strip = 1;
}
if strip > 0 {
self.buf_mut().delete(Range::charwise(start, start + strip));
}
}
}
self.set_head(self.buf().line_start(line));
self.clamp_cursor();
}
pub(crate) fn run_ex(&mut self) {
let cmdline = self
.pending
.trim_start_matches(':')
.trim_end_matches('\r')
.to_string();
self.pending.clear();
let (cmd, arg) = cmdline.split_once(' ').unwrap_or((cmdline.as_str(), ""));
match cmd {
_ if cmdline.starts_with('!') => self.shell_run(&cmdline[1..]),
"w" | "w!" => match self.buf_mut().save(cmd == "w!") {
Ok(()) => {
crate::session::save(self);
self.message = "written".into();
}
Err(e) => self.message = format!("write failed: {e}"),
},
"wq" | "wq!" => {
match self.buf_mut().save(cmd == "wq!") {
Ok(()) => {
crate::session::save(self);
self.close_buffer(true);
}
Err(e) => self.message = format!("write failed: {e}"),
}
}
"view" => {
if arg.is_empty() {
self.buf_mut().readonly = true;
self.message = "readonly".into();
} else if let Err(e) = self.open_buffer(arg) {
self.message = format!("view {arg}: {e}");
} else {
self.buf_mut().readonly = true;
}
}
"q" => {
self.close_pane_or_buffer(false);
}
"q!" => {
self.close_pane_or_buffer(true);
}
"noh" => {
self.last_search = None;
}
_ if cmdline.bytes().all(|b| b.is_ascii_digit()) && !cmdline.is_empty() => {
let n: usize = cmdline.parse().unwrap_or(1);
let mut last = self.buf().len_lines().saturating_sub(1);
if self.buf().line_start(last) >= self.buf().len_bytes() {
last = last.saturating_sub(1);
}
self.push_jump(); self.set_head(self.buf().line_start(n.saturating_sub(1).min(last)));
self.clamp_cursor();
}
"vs" | "vsplit" => self.split(true, if arg.is_empty() { None } else { Some(arg) }),
"sp" | "split" => self.split(false, if arg.is_empty() { None } else { Some(arg) }),
"help" | "h" => self.open_help(),
"e" | "e!" => {
if arg.is_empty() {
self.message = ":e needs a path".into();
} else if self.buf().dirty && cmd == "e" {
self.message = "unsaved changes — :e! to force".into();
} else if let Err(e) = self.open_buffer(arg) {
self.message = format!("open {arg}: {e}");
}
}
other => self.message = format!("unknown ex: :{other}"),
}
}
pub(crate) fn repeat_search_pub(&mut self, invert: bool) {
self.repeat_search(invert);
}
pub(crate) fn jump_hunk_pub(&mut self, forward: bool) {
self.jump_hunk(forward);
}
pub(crate) fn search_word_under_cursor_pub(&mut self, backward: bool) {
self.search_word_under_cursor(backward);
}
pub(crate) fn repeat_find_pub(&mut self, reverse: bool) {
self.repeat_find(reverse);
}
pub(crate) fn paste_named_pub(&mut self, name: Option<char>, before: bool) {
self.paste_named(name, before);
}
pub(crate) fn join_or_repeat(&mut self, key: char) {
if key == 'J' {
self.join_lines_pub();
} else {
self.dot_repeat_pub();
}
}
pub(crate) fn insert_entry_pub(&mut self, key: char) {
match key {
'i' => self.enter_insert_from("i"),
'a' => self.append(),
'A' => self.append_eol(),
'o' => self.open_below(),
'O' => self.open_above(),
'I' => self.alias("I", "^i"),
_ => {}
}
}
pub(crate) fn enter_visual_pub(&mut self, key: char) {
self.enter_visual(key == 'V');
}
}
pub(crate) fn seq_tokens(keys: &str) -> Vec<String> {
let mut out = Vec::new();
for c in keys.chars() {
match c {
' ' => out.push("space".to_string()),
'\x17' => out.push("ctrl-w".to_string()),
c => out.push(c.to_string()),
}
}
out
}