3226 lines
90 KiB
Rust
3226 lines
90 KiB
Rust
use std::{
|
|
collections::HashSet, fmt::Display, ops::{Index, IndexMut}, slice::SliceIndex
|
|
};
|
|
|
|
use smallvec::SmallVec;
|
|
use unicode_segmentation::UnicodeSegmentation;
|
|
use unicode_width::UnicodeWidthChar;
|
|
|
|
use super::vicmd::{
|
|
Anchor, Bound, Dest, Direction, Motion, MotionCmd, TextObj, To, Verb,
|
|
ViCmd, Word,
|
|
};
|
|
use crate::{
|
|
expand::expand_cmd_sub,
|
|
libsh::{error::ShResult, guards::{RawModeGuard, var_ctx_guard}},
|
|
parse::{
|
|
Redir, RedirType,
|
|
execute::exec_input,
|
|
lex::{LexFlags, LexStream, Tk, TkFlags},
|
|
},
|
|
prelude::*,
|
|
procio::{IoFrame, IoMode, IoStack},
|
|
readline::{
|
|
highlight::Highlighter, markers, register::RegisterContent, term::get_win_size, vicmd::{ReadSrc, VerbCmd, WriteDest}
|
|
},
|
|
state::{self, VarFlags, VarKind, read_shopts, read_vars, write_meta, write_vars},
|
|
};
|
|
|
|
const PUNCTUATION: [&str; 3] = ["?", "!", "."];
|
|
const DEFAULT_VIEWPORT_HEIGHT: usize = 40;
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub struct Grapheme(SmallVec<[char; 4]>);
|
|
|
|
impl Grapheme {
|
|
pub fn chars(&self) -> &[char] {
|
|
&self.0
|
|
}
|
|
/// Returns the display width of the Grapheme, treating unprintable chars as width 0
|
|
pub fn width(&self) -> usize {
|
|
self.0.iter().map(|c| c.width().unwrap_or(0)).sum()
|
|
}
|
|
/// Returns true if the Grapheme is wrapping a linefeed ('\n')
|
|
pub fn is_lf(&self) -> bool {
|
|
self.is_char('\n')
|
|
}
|
|
/// Returns true if the Grapheme consists of exactly one char and that char is `c`
|
|
pub fn is_char(&self, c: char) -> bool {
|
|
self.0.len() == 1 && self.0[0] == c
|
|
}
|
|
/// Returns the CharClass of the Grapheme, which is determined by the properties of its chars
|
|
pub fn class(&self) -> CharClass {
|
|
CharClass::from(self)
|
|
}
|
|
|
|
pub fn as_char(&self) -> Option<char> {
|
|
if self.0.len() == 1 {
|
|
Some(self.0[0])
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Returns true if the Grapheme is classified as whitespace (i.e. all chars are whitespace)
|
|
pub fn is_ws(&self) -> bool {
|
|
self.class() == CharClass::Whitespace
|
|
}
|
|
}
|
|
|
|
impl From<char> for Grapheme {
|
|
fn from(value: char) -> Self {
|
|
let mut new = SmallVec::<[char; 4]>::new();
|
|
new.push(value);
|
|
Self(new)
|
|
}
|
|
}
|
|
|
|
impl From<&str> for Grapheme {
|
|
fn from(value: &str) -> Self {
|
|
assert_eq!(value.graphemes(true).count(), 1);
|
|
let mut new = SmallVec::<[char; 4]>::new();
|
|
for char in value.chars() {
|
|
new.push(char);
|
|
}
|
|
Self(new)
|
|
}
|
|
}
|
|
|
|
impl From<String> for Grapheme {
|
|
fn from(value: String) -> Self {
|
|
Into::<Self>::into(value.as_str())
|
|
}
|
|
}
|
|
|
|
impl From<&String> for Grapheme {
|
|
fn from(value: &String) -> Self {
|
|
Into::<Self>::into(value.as_str())
|
|
}
|
|
}
|
|
|
|
impl Display for Grapheme {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
for ch in &self.0 {
|
|
write!(f, "{ch}")?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub fn to_graphemes(s: impl ToString) -> Vec<Grapheme> {
|
|
let s = s.to_string();
|
|
s.graphemes(true).map(Grapheme::from).collect()
|
|
}
|
|
|
|
pub fn to_lines(s: impl ToString) -> Vec<Line> {
|
|
let s = s.to_string();
|
|
s.split("\n").map(to_graphemes).map(Line::from).collect()
|
|
}
|
|
|
|
pub fn join_lines(lines: &[Line]) -> String {
|
|
lines
|
|
.iter()
|
|
.map(|line| line.to_string())
|
|
.collect::<Vec<String>>()
|
|
.join("\n")
|
|
}
|
|
|
|
pub fn trim_lines(lines: &mut Vec<Line>) {
|
|
while lines.last().is_some_and(|line| line.is_empty()) {
|
|
lines.pop();
|
|
}
|
|
}
|
|
|
|
pub fn split_lines_at(lines: &mut Vec<Line>, pos: Pos) -> Vec<Line> {
|
|
let tail = lines[pos.row].split_off(pos.col);
|
|
let mut rest: Vec<Line> = lines.drain(pos.row + 1..).collect();
|
|
rest.insert(0, tail);
|
|
rest
|
|
}
|
|
|
|
pub fn attach_lines(lines: &mut Vec<Line>, other: &mut Vec<Line>) {
|
|
if other.is_empty() {
|
|
return;
|
|
}
|
|
if lines.is_empty() {
|
|
lines.append(other);
|
|
return;
|
|
}
|
|
let mut head = other.remove(0);
|
|
let mut tail = lines.pop().unwrap();
|
|
tail.append(&mut head);
|
|
lines.push(tail);
|
|
lines.append(other);
|
|
}
|
|
|
|
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
|
pub struct Line(Vec<Grapheme>);
|
|
|
|
impl Line {
|
|
pub fn graphemes(&self) -> &[Grapheme] {
|
|
&self.0
|
|
}
|
|
pub fn len(&self) -> usize {
|
|
self.0.len()
|
|
}
|
|
pub fn is_empty(&self) -> bool {
|
|
self.len() == 0
|
|
}
|
|
pub fn push_str(&mut self, s: &str) {
|
|
for g in s.graphemes(true) {
|
|
self.0.push(Grapheme::from(g));
|
|
}
|
|
}
|
|
pub fn push_char(&mut self, c: char) {
|
|
self.0.push(Grapheme::from(c));
|
|
}
|
|
pub fn split_off(&mut self, at: usize) -> Line {
|
|
if at > self.0.len() {
|
|
return Line::default();
|
|
}
|
|
Line(self.0.split_off(at))
|
|
}
|
|
pub fn append(&mut self, other: &mut Line) {
|
|
self.0.append(&mut other.0);
|
|
}
|
|
pub fn insert_char(&mut self, at: usize, c: char) {
|
|
self.0.insert(at, Grapheme::from(c));
|
|
}
|
|
pub fn insert(&mut self, at: usize, g: Grapheme) {
|
|
self.0.insert(at, g);
|
|
}
|
|
pub fn width(&self) -> usize {
|
|
self.0.iter().map(|g| g.width()).sum()
|
|
}
|
|
pub fn trim_start(&mut self) -> Line {
|
|
let mut clone = self.clone();
|
|
while clone.0.first().is_some_and(|g| g.is_ws()) {
|
|
clone.0.remove(0);
|
|
}
|
|
clone
|
|
}
|
|
}
|
|
|
|
impl IndexMut<usize> for Line {
|
|
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
|
|
&mut self.0[index]
|
|
}
|
|
}
|
|
|
|
impl<T: SliceIndex<[Grapheme]>> Index<T> for Line {
|
|
type Output = T::Output;
|
|
fn index(&self, index: T) -> &Self::Output {
|
|
&self.0[index]
|
|
}
|
|
}
|
|
|
|
impl From<Vec<Grapheme>> for Line {
|
|
fn from(value: Vec<Grapheme>) -> Self {
|
|
Self(value)
|
|
}
|
|
}
|
|
|
|
impl Display for Line {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
for gr in &self.0 {
|
|
write!(f, "{gr}")?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
|
|
pub enum Delim {
|
|
Paren,
|
|
Brace,
|
|
Bracket,
|
|
Angle,
|
|
}
|
|
|
|
#[derive(Default, PartialEq, Eq, Debug, Clone, Copy)]
|
|
pub enum CharClass {
|
|
#[default]
|
|
Alphanum,
|
|
Symbol,
|
|
Whitespace,
|
|
Other,
|
|
}
|
|
|
|
impl CharClass {
|
|
pub fn is_other_class(&self, other: &CharClass) -> bool {
|
|
!self.eq(other)
|
|
}
|
|
pub fn is_other_class_not_ws(&self, other: &CharClass) -> bool {
|
|
if self.is_ws() || other.is_ws() {
|
|
false
|
|
} else {
|
|
self.is_other_class(other)
|
|
}
|
|
}
|
|
pub fn is_other_class_or_ws(&self, other: &CharClass) -> bool {
|
|
if self.is_ws() || other.is_ws() {
|
|
true
|
|
} else {
|
|
self.is_other_class(other)
|
|
}
|
|
}
|
|
pub fn is_ws(&self) -> bool {
|
|
*self == CharClass::Whitespace
|
|
}
|
|
}
|
|
|
|
impl From<&Grapheme> for CharClass {
|
|
fn from(g: &Grapheme) -> Self {
|
|
let Some(&first) = g.0.first() else {
|
|
return Self::Other;
|
|
};
|
|
|
|
if first.is_alphanumeric()
|
|
&& g.0[1..]
|
|
.iter()
|
|
.all(|&c| c.is_ascii_punctuation() || c == '\u{0301}' || c == '\u{0308}')
|
|
{
|
|
// Handles things like `ï`, `é`, etc., by manually allowing common diacritics
|
|
return CharClass::Alphanum;
|
|
}
|
|
|
|
if g.0.iter().all(|&c| c.is_alphanumeric() || c == '_') {
|
|
CharClass::Alphanum
|
|
} else if g.0.iter().all(|c| c.is_whitespace()) {
|
|
CharClass::Whitespace
|
|
} else if g.0.iter().all(|c| !c.is_alphanumeric()) {
|
|
CharClass::Symbol
|
|
} else {
|
|
CharClass::Other
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
pub enum SelectMode {
|
|
Char(Pos),
|
|
Line(Pos),
|
|
Block(Pos),
|
|
}
|
|
|
|
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub struct Pos {
|
|
pub row: usize,
|
|
pub col: usize,
|
|
}
|
|
|
|
impl Pos {
|
|
/// make sure you clamp this
|
|
pub const MAX: Self = Pos {
|
|
row: usize::MAX,
|
|
col: usize::MAX,
|
|
};
|
|
pub const MIN: Self = Pos {
|
|
row: usize::MIN, // just in case we discover something smaller than '0'
|
|
col: usize::MIN,
|
|
};
|
|
|
|
pub fn row_col_add(&self, row: isize, col: isize) -> Self {
|
|
Self {
|
|
row: self.row.saturating_add_signed(row),
|
|
col: self.col.saturating_add_signed(col),
|
|
}
|
|
}
|
|
|
|
pub fn col_add(&self, rhs: usize) -> Self {
|
|
self.row_col_add(0, rhs as isize)
|
|
}
|
|
|
|
pub fn col_add_signed(&self, rhs: isize) -> Self {
|
|
self.row_col_add(0, rhs)
|
|
}
|
|
|
|
pub fn col_sub(&self, rhs: usize) -> Self {
|
|
self.row_col_add(0, -(rhs as isize))
|
|
}
|
|
|
|
pub fn row_add(&self, rhs: usize) -> Self {
|
|
self.row_col_add(rhs as isize, 0)
|
|
}
|
|
|
|
pub fn row_sub(&self, rhs: usize) -> Self {
|
|
self.row_col_add(-(rhs as isize), 0)
|
|
}
|
|
|
|
pub fn clamp_row<T>(&mut self, other: &[T]) {
|
|
self.row = self.row.clamp(0, other.len().saturating_sub(1));
|
|
}
|
|
pub fn clamp_col<T>(&mut self, other: &[T], exclusive: bool) {
|
|
let mut max = other.len();
|
|
if exclusive && max > 0 {
|
|
max = max.saturating_sub(1);
|
|
}
|
|
self.col = self.col.clamp(0, max);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone)]
|
|
pub enum MotionKind {
|
|
/// A flat range from one grapheme position to another
|
|
/// `start` is not necessarily less than `end`. `start` in most cases
|
|
/// is the cursor's position.
|
|
Char {
|
|
start: Pos,
|
|
end: Pos,
|
|
inclusive: bool,
|
|
},
|
|
/// A range of whole lines.
|
|
Line {
|
|
start: usize,
|
|
end: usize,
|
|
inclusive: bool
|
|
},
|
|
Block {
|
|
start: Pos,
|
|
end: Pos,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
|
pub struct Cursor {
|
|
pub pos: Pos,
|
|
pub exclusive: bool,
|
|
}
|
|
|
|
#[derive(Default, Clone, Debug)]
|
|
pub struct Edit {
|
|
pub old_cursor: Pos,
|
|
pub new_cursor: Pos,
|
|
pub old: Vec<Line>,
|
|
pub new: Vec<Line>,
|
|
pub merging: bool,
|
|
}
|
|
|
|
impl Edit {
|
|
pub fn start_merge(&mut self) {
|
|
self.merging = true
|
|
}
|
|
pub fn stop_merge(&mut self) {
|
|
self.merging = false
|
|
}
|
|
pub fn is_empty(&self) -> bool {
|
|
self.old == self.new
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Clone, Debug)]
|
|
pub struct IndentCtx {
|
|
depth: usize,
|
|
ctx: Vec<Tk>,
|
|
}
|
|
|
|
impl IndentCtx {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
pub fn depth(&self) -> usize {
|
|
self.depth
|
|
}
|
|
|
|
pub fn ctx(&self) -> &[Tk] {
|
|
&self.ctx
|
|
}
|
|
|
|
pub fn descend(&mut self, tk: Tk) {
|
|
self.ctx.push(tk);
|
|
self.depth += 1;
|
|
}
|
|
|
|
pub fn ascend(&mut self) {
|
|
self.depth = self.depth.saturating_sub(1);
|
|
self.ctx.pop();
|
|
}
|
|
|
|
pub fn reset(&mut self) {
|
|
std::mem::take(self);
|
|
}
|
|
|
|
pub fn check_tk(&mut self, tk: Tk) {
|
|
if tk.is_opener() {
|
|
self.descend(tk);
|
|
} else if self.ctx.last().is_some_and(|t| tk.is_closer_for(t)) {
|
|
self.ascend();
|
|
}
|
|
}
|
|
|
|
pub fn calculate(&mut self, input: &str) -> usize {
|
|
self.depth = 0;
|
|
self.ctx.clear();
|
|
|
|
let input_arc = Arc::new(input.to_string());
|
|
let Ok(tokens) =
|
|
LexStream::new(input_arc, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()
|
|
else {
|
|
log::error!("Lexing failed during depth calculation: {:?}", input);
|
|
return 0;
|
|
};
|
|
|
|
for tk in tokens {
|
|
self.check_tk(tk);
|
|
}
|
|
|
|
self.depth
|
|
}
|
|
}
|
|
|
|
fn extract_range_contiguous(buf: &mut Vec<Line>, start: Pos, end: Pos) -> Vec<Line> {
|
|
let start_col = start.col.min(buf[start.row].len());
|
|
let end_col = end.col.min(buf[end.row].len());
|
|
|
|
if start.row == end.row {
|
|
// single line case
|
|
let line = &mut buf[start.row];
|
|
let removed: Vec<Grapheme> = line.0.drain(start_col..end_col).collect();
|
|
return vec![Line(removed)];
|
|
}
|
|
|
|
// multi line case
|
|
// tail of first line
|
|
let first_tail: Line = buf[start.row].split_off(start_col);
|
|
|
|
// all inbetween lines. extracts nothing if only two rows
|
|
let middle: Vec<Line> = buf.drain(start.row + 1..end.row).collect();
|
|
|
|
// head of last line
|
|
let last_col = end_col.min(buf[start.row + 1].len());
|
|
let last_head: Line = Line::from(buf[start.row + 1].0.drain(..last_col).collect::<Vec<_>>());
|
|
|
|
// tail of last line
|
|
let mut last_remainder = buf.remove(start.row + 1);
|
|
|
|
// attach tail of last line to head of first line
|
|
buf[start.row].append(&mut last_remainder);
|
|
|
|
// construct vector of extracted content
|
|
let mut extracts = vec![first_tail];
|
|
extracts.extend(middle);
|
|
extracts.push(last_head);
|
|
extracts
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct LineBuf {
|
|
pub lines: Vec<Line>,
|
|
pub hint: Option<Vec<Line>>,
|
|
pub cursor: Cursor,
|
|
|
|
pub select_mode: Option<SelectMode>,
|
|
pub last_selection: Option<(SelectMode, Pos)>,
|
|
|
|
pub insert_mode_start_pos: Option<Pos>,
|
|
pub saved_col: Option<usize>,
|
|
pub indent_ctx: IndentCtx,
|
|
|
|
pub scroll_offset: usize,
|
|
|
|
pub undo_stack: Vec<Edit>,
|
|
pub redo_stack: Vec<Edit>,
|
|
}
|
|
|
|
impl Default for LineBuf {
|
|
fn default() -> Self {
|
|
Self {
|
|
lines: vec![Line::from(vec![])],
|
|
hint: None,
|
|
cursor: Cursor {
|
|
pos: Pos { row: 0, col: 0 },
|
|
exclusive: false,
|
|
},
|
|
select_mode: None,
|
|
last_selection: None,
|
|
insert_mode_start_pos: None,
|
|
saved_col: None,
|
|
indent_ctx: IndentCtx::new(),
|
|
scroll_offset: 0,
|
|
undo_stack: vec![],
|
|
redo_stack: vec![],
|
|
}
|
|
}
|
|
}
|
|
|
|
impl LineBuf {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
pub fn get_viewport_height(&self) -> usize {
|
|
let raw = read_shopts(|o| {
|
|
let height = o.line.viewport_height.as_str();
|
|
if let Ok(num) = height.parse::<usize>() {
|
|
num
|
|
} else if let Some(pre) = height.strip_suffix('%')
|
|
&& let Ok(num) = pre.parse::<usize>() {
|
|
if !isatty(STDIN_FILENO).unwrap_or_default() { return DEFAULT_VIEWPORT_HEIGHT };
|
|
let (_,rows) = get_win_size(STDIN_FILENO);
|
|
(rows as f64 * (num as f64 / 100.0)).round() as usize
|
|
} else {
|
|
log::warn!("Invalid viewport height shopt value: '{}', using 50% of terminal height as default", height);
|
|
if !isatty(STDIN_FILENO).unwrap_or_default() { return DEFAULT_VIEWPORT_HEIGHT };
|
|
let (_,rows) = get_win_size(STDIN_FILENO);
|
|
(rows as f64 * 0.5).round() as usize
|
|
}
|
|
});
|
|
(raw.min(100)).min(self.lines.len())
|
|
}
|
|
pub fn update_scroll_offset(&mut self) {
|
|
let height = self.get_viewport_height();
|
|
let scrolloff = read_shopts(|o| o.line.scroll_offset);
|
|
if self.cursor.pos.row < self.scroll_offset + scrolloff {
|
|
self.scroll_offset = self.cursor.pos.row.saturating_sub(scrolloff);
|
|
}
|
|
if self.cursor.pos.row + scrolloff >= self.scroll_offset + height {
|
|
self.scroll_offset = self.cursor.pos.row + scrolloff + 1 - height;
|
|
}
|
|
|
|
let max_offset = self.lines.len().saturating_sub(height);
|
|
self.scroll_offset = self.scroll_offset.min(max_offset);
|
|
|
|
}
|
|
pub fn get_window(&self) -> Vec<Line> {
|
|
let height = self.get_viewport_height();
|
|
self.lines
|
|
.iter()
|
|
.skip(self.scroll_offset)
|
|
.take(height)
|
|
.cloned()
|
|
.collect()
|
|
}
|
|
pub fn window_joined(&self) -> String {
|
|
join_lines(&self.get_window())
|
|
}
|
|
pub fn display_window_joined(&self) -> String {
|
|
let display = self.to_string();
|
|
let do_hl = state::read_shopts(|s| s.prompt.highlight);
|
|
let mut highlighter = Highlighter::new();
|
|
highlighter.only_visual(!do_hl);
|
|
highlighter.load_input(&display, self.cursor_byte_pos());
|
|
highlighter.expand_control_chars();
|
|
highlighter.highlight();
|
|
let highlighted = highlighter.take();
|
|
let hint = self.get_hint_text();
|
|
let lines = to_lines(format!("{highlighted}{hint}"));
|
|
|
|
let offset = self.scroll_offset.min(lines.len());
|
|
let (_,mid) = lines.split_at(offset);
|
|
|
|
let height = self.get_viewport_height().min(mid.len());
|
|
let (mid,_) = mid.split_at(height);
|
|
|
|
join_lines(mid)
|
|
}
|
|
pub fn window_slice_to_cursor(&self) -> Option<String> {
|
|
let mut result = String::new();
|
|
let start_row = self.scroll_offset;
|
|
|
|
for i in start_row..self.cursor.pos.row {
|
|
result.push_str(&self.lines[i].to_string());
|
|
result.push('\n');
|
|
}
|
|
let line = &self.lines[self.cursor.pos.row];
|
|
let col = self.cursor.pos.col.min(line.len());
|
|
for g in &line.graphemes()[..col] {
|
|
result.push_str(&g.to_string());
|
|
}
|
|
Some(result)
|
|
}
|
|
pub fn is_empty(&self) -> bool {
|
|
self.lines.len() == 0 || (self.lines.len() == 1 && self.count_graphemes() == 0)
|
|
}
|
|
pub fn count_graphemes(&self) -> usize {
|
|
self.lines.iter().map(|line| line.len()).sum()
|
|
}
|
|
#[track_caller]
|
|
fn cur_line(&self) -> &Line {
|
|
let caller = std::panic::Location::caller();
|
|
log::trace!("cur_line called from {}:{}", caller.file(), caller.line());
|
|
&self.lines[self.cursor.pos.row]
|
|
}
|
|
fn cur_line_mut(&mut self) -> &mut Line {
|
|
&mut self.lines[self.cursor.pos.row]
|
|
}
|
|
fn line(&self, row: usize) -> &Line {
|
|
&self.lines[row]
|
|
}
|
|
fn line_mut(&mut self, row: usize) -> &mut Line {
|
|
&mut self.lines[row]
|
|
}
|
|
/// Takes an inclusive range of line numbers and returns an iterator over immutable borrows of those lines.
|
|
fn line_iter(&mut self, start: usize, end: usize) -> impl Iterator<Item = &Line> {
|
|
let (start,end) = ordered(start,end);
|
|
self.lines.iter().take(end + 1).skip(start)
|
|
}
|
|
fn line_iter_mut(&mut self, start: usize, end: usize) -> impl Iterator<Item = &mut Line> {
|
|
let (start,end) = ordered(start,end);
|
|
self.lines.iter_mut().take(end + 1).skip(start)
|
|
}
|
|
fn line_to_cursor(&self) -> &[Grapheme] {
|
|
let line = self.cur_line();
|
|
let col = self.cursor.pos.col.min(line.len());
|
|
&line[..col]
|
|
}
|
|
fn line_from_cursor(&self) -> &[Grapheme] {
|
|
let line = self.cur_line();
|
|
let col = self.cursor.pos.col.min(line.len());
|
|
&line[col..]
|
|
}
|
|
fn row_col(&self) -> (usize, usize) {
|
|
(self.row(), self.col())
|
|
}
|
|
fn row(&self) -> usize {
|
|
self.cursor.pos.row
|
|
}
|
|
fn offset_row(&self, offset: isize) -> usize {
|
|
let mut row = self.cursor.pos.row.saturating_add_signed(offset);
|
|
row = row.clamp(0, self.lines.len().saturating_sub(1));
|
|
row
|
|
}
|
|
fn col(&self) -> usize {
|
|
self.cursor.pos.col
|
|
}
|
|
fn offset_col(&self, row: usize, offset: isize) -> usize {
|
|
let mut col = self.cursor.pos.col.saturating_add_signed(offset);
|
|
let max = if self.cursor.exclusive {
|
|
self.lines[row].len().saturating_sub(1)
|
|
} else {
|
|
self.lines[row].len()
|
|
};
|
|
col = col.clamp(0, max);
|
|
col
|
|
}
|
|
fn offset_col_wrapping(&self, row: usize, offset: isize) -> (usize, usize) {
|
|
let mut row = row;
|
|
let mut col = self.cursor.pos.col as isize + offset;
|
|
|
|
while col < 0 {
|
|
if row == 0 {
|
|
col = 0;
|
|
break;
|
|
}
|
|
row -= 1;
|
|
col += self.lines[row].len() as isize + 1;
|
|
}
|
|
while col > self.lines[row].len() as isize {
|
|
if row >= self.lines.len() - 1 {
|
|
col = self.lines[row].len() as isize;
|
|
break;
|
|
}
|
|
col -= self.lines[row].len() as isize + 1;
|
|
row += 1;
|
|
}
|
|
|
|
(row, col as usize)
|
|
}
|
|
fn set_cursor(&mut self, mut pos: Pos) {
|
|
pos.clamp_row(&self.lines);
|
|
pos.clamp_col(&self.lines[pos.row].0, false);
|
|
self.cursor.pos = pos;
|
|
}
|
|
fn set_row(&mut self, row: usize) {
|
|
self.set_cursor(Pos {
|
|
row,
|
|
col: self.saved_col.unwrap_or(self.cursor.pos.col),
|
|
});
|
|
}
|
|
fn set_col(&mut self, col: usize) {
|
|
self.set_cursor(Pos {
|
|
row: self.cursor.pos.row,
|
|
col,
|
|
});
|
|
}
|
|
fn offset_cursor(&self, row_offset: isize, col_offset: isize) -> Pos {
|
|
let row = self.offset_row(row_offset);
|
|
let col = self.offset_col(row, col_offset);
|
|
Pos { row, col }
|
|
}
|
|
fn offset_cursor_wrapping(&self, row_offset: isize, col_offset: isize) -> Pos {
|
|
let row = self.offset_row(row_offset);
|
|
let (row, col) = self.offset_col_wrapping(row, col_offset);
|
|
Pos { row, col }
|
|
}
|
|
fn break_line(&mut self) {
|
|
let (row, col) = self.row_col();
|
|
let level = self.calc_indent_level();
|
|
let mut rest = self.lines[row].split_off(col);
|
|
let mut col = 0;
|
|
for tab in std::iter::repeat_n(Grapheme::from('\t'), level) {
|
|
rest.insert(0, tab);
|
|
col += 1;
|
|
}
|
|
|
|
self.lines.insert(row + 1, rest);
|
|
self.cursor.pos = Pos {
|
|
row: row + 1,
|
|
col,
|
|
};
|
|
}
|
|
fn verb_shell_cmd(&mut self, cmd: &str) -> ShResult<()> {
|
|
let mut vars = HashSet::new();
|
|
vars.insert("_BUFFER".into());
|
|
vars.insert("_CURSOR".into());
|
|
vars.insert("_ANCHOR".into());
|
|
let _guard = var_ctx_guard(vars);
|
|
|
|
let mut buf = self.joined();
|
|
let mut cursor = self.cursor_to_flat();
|
|
let mut anchor = self.select_mode.map(|r| {
|
|
match r {
|
|
SelectMode::Char(pos) |
|
|
SelectMode::Block(pos) |
|
|
SelectMode::Line(pos) => {
|
|
self.pos_to_flat(pos).to_string()
|
|
}
|
|
}
|
|
}).unwrap_or_default();
|
|
|
|
write_vars(|v| {
|
|
v.set_var("_BUFFER", VarKind::Str(buf.clone()), VarFlags::EXPORT)?;
|
|
v.set_var(
|
|
"_CURSOR",
|
|
VarKind::Str(cursor.to_string()),
|
|
VarFlags::EXPORT,
|
|
)?;
|
|
v.set_var(
|
|
"_ANCHOR",
|
|
VarKind::Str(anchor.clone()),
|
|
VarFlags::EXPORT,
|
|
)
|
|
})?;
|
|
|
|
RawModeGuard::with_cooked_mode(|| exec_input(cmd.to_string(), None, true, Some("<ex-mode-cmd>".into())))?;
|
|
|
|
let keys = write_vars(|v| {
|
|
buf = v.take_var("_BUFFER");
|
|
cursor = v.take_var("_CURSOR").parse().unwrap_or(cursor);
|
|
anchor = v.take_var("_ANCHOR");
|
|
v.take_var("_KEYS")
|
|
});
|
|
|
|
self.set_buffer(buf);
|
|
self.set_cursor_from_flat(cursor);
|
|
if let Ok(pos) = anchor.parse()
|
|
&& pos != cursor
|
|
&& self.select_mode.is_some() {
|
|
let new_pos = self.pos_from_flat(pos);
|
|
match self.select_mode.as_mut() {
|
|
Some(SelectMode::Line(pos)) |
|
|
Some(SelectMode::Block(pos)) |
|
|
Some(SelectMode::Char(pos)) => {
|
|
*pos = new_pos
|
|
}
|
|
None => unreachable!()
|
|
}
|
|
}
|
|
if !keys.is_empty() {
|
|
write_meta(|m| m.set_pending_widget_keys(&keys))
|
|
}
|
|
Ok(())
|
|
}
|
|
fn insert_at(&mut self, pos: Pos, gr: Grapheme) {
|
|
if gr.is_lf() {
|
|
self.set_cursor(pos);
|
|
self.break_line();
|
|
} else {
|
|
let row = pos.row;
|
|
let col = pos.col;
|
|
self.lines[row].insert(col, gr);
|
|
}
|
|
}
|
|
fn insert(&mut self, gr: Grapheme) {
|
|
self.insert_at(self.cursor.pos, gr);
|
|
}
|
|
fn insert_str(&mut self, s: &str) {
|
|
for gr in s.graphemes(true) {
|
|
let gr = Grapheme::from(gr);
|
|
if gr.is_lf() {
|
|
self.break_line();
|
|
} else {
|
|
self.insert(gr);
|
|
self.cursor.pos.col += 1;
|
|
}
|
|
}
|
|
}
|
|
fn push_str(&mut self, s: &str) {
|
|
let mut lines = to_lines(s);
|
|
attach_lines(&mut self.lines, &mut lines);
|
|
}
|
|
fn push(&mut self, gr: Grapheme) {
|
|
let last = self.lines.last_mut();
|
|
if let Some(last) = last {
|
|
last.0.push(gr);
|
|
} else {
|
|
self.lines.push(Line::from(vec![gr]));
|
|
}
|
|
}
|
|
fn scan_forward<F: FnMut(&Grapheme) -> bool>(&self, f: F) -> Option<Pos> {
|
|
self.scan_forward_from(self.cursor.pos, f)
|
|
}
|
|
fn scan_forward_from<F: FnMut(&Grapheme) -> bool>(&self, mut pos: Pos, mut f: F) -> Option<Pos> {
|
|
pos.clamp_row(&self.lines);
|
|
pos.clamp_col(&self.lines[pos.row].0, false);
|
|
let Pos { mut row, mut col } = pos;
|
|
|
|
loop {
|
|
let line = &self.lines[row];
|
|
if !line.is_empty() && f(&line[col]) {
|
|
return Some(Pos { row, col });
|
|
}
|
|
if col < self.lines[row].len().saturating_sub(1) {
|
|
col += 1;
|
|
} else if row < self.lines.len().saturating_sub(1) {
|
|
row += 1;
|
|
col = 0;
|
|
} else {
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
fn scan_backward<F: FnMut(&Grapheme) -> bool>(&self, f: F) -> Option<Pos> {
|
|
self.scan_backward_from(self.cursor.pos, f)
|
|
}
|
|
fn scan_backward_from<F: FnMut(&Grapheme) -> bool>(&self, mut pos: Pos, mut f: F) -> Option<Pos> {
|
|
pos.clamp_row(&self.lines);
|
|
pos.clamp_col(&self.lines[pos.row].0, false);
|
|
let Pos { mut row, mut col } = pos;
|
|
|
|
loop {
|
|
let line = &self.lines[row];
|
|
if !line.is_empty() && f(&line[col]) {
|
|
return Some(Pos { row, col });
|
|
}
|
|
if col > 0 {
|
|
col -= 1;
|
|
} else if row > 0 {
|
|
row -= 1;
|
|
col = self.lines[row].len().saturating_sub(1);
|
|
} else {
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
fn search_char(&self, dir: &Direction, dest: &Dest, char: &Grapheme) -> isize {
|
|
match dir {
|
|
Direction::Forward => {
|
|
let slice = self.line_from_cursor();
|
|
for (i, gr) in slice.iter().enumerate().skip(1) {
|
|
if gr == char {
|
|
match dest {
|
|
Dest::On => return i as isize,
|
|
Dest::Before => return (i as isize - 1).max(0),
|
|
Dest::After => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Direction::Backward => {
|
|
let slice = self.line_to_cursor();
|
|
for (i, gr) in slice.iter().rev().enumerate().skip(1) {
|
|
if gr == char {
|
|
match dest {
|
|
Dest::On => return -(i as isize) - 1,
|
|
Dest::Before => return -(i as isize),
|
|
Dest::After => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
0
|
|
}
|
|
fn eval_word_motion(
|
|
&self,
|
|
count: usize,
|
|
to: &To,
|
|
word: &Word,
|
|
dir: &Direction,
|
|
ignore_trailing_ws: bool,
|
|
mut inclusive: bool,
|
|
) -> Option<MotionKind> {
|
|
let mut target = self.cursor.pos;
|
|
|
|
for _ in 0..count {
|
|
match (to, dir) {
|
|
(To::Start, Direction::Forward) => {
|
|
target = self
|
|
// 'w' is a special snowflake motion so we need these two extra arguments
|
|
// if we hit the ignore_trailing_ws path in the function,
|
|
// inclusive is flipped to true.
|
|
.word_motion_w(word, target, ignore_trailing_ws, &mut inclusive)
|
|
.unwrap_or_else(|| {
|
|
// we set inclusive to true so that we catch the entire word
|
|
// instead of ignoring the last character
|
|
inclusive = true;
|
|
Pos::MAX
|
|
});
|
|
}
|
|
(To::End, Direction::Forward) => {
|
|
inclusive = true;
|
|
target = self.word_motion_e(word, target).unwrap_or(Pos::MAX);
|
|
}
|
|
(To::Start, Direction::Backward) => {
|
|
target = self.word_motion_b(word, target).unwrap_or(Pos::MIN);
|
|
}
|
|
(To::End, Direction::Backward) => {
|
|
inclusive = true;
|
|
target = self.word_motion_ge(word, target).unwrap_or(Pos::MIN);
|
|
}
|
|
}
|
|
}
|
|
|
|
target.clamp_row(&self.lines);
|
|
target.clamp_col(&self.lines[target.row].0, true);
|
|
|
|
Some(MotionKind::Char {
|
|
start: self.cursor.pos,
|
|
end: target,
|
|
inclusive,
|
|
})
|
|
}
|
|
fn word_motion_w(
|
|
&self,
|
|
word: &Word,
|
|
start: Pos,
|
|
ignore_trailing_ws: bool,
|
|
inclusive: &mut bool,
|
|
) -> Option<Pos> {
|
|
use CharClass as C;
|
|
|
|
// get our iterator of char classes
|
|
// we dont actually care what the chars are
|
|
// just what they look like.
|
|
// we are going to use .find() a lot to advance the iterator
|
|
let mut classes = self.char_classes_forward_from(start).peekable();
|
|
|
|
match word {
|
|
Word::Big => {
|
|
if let Some((_, C::Whitespace)) = classes.peek() {
|
|
// we are on whitespace. advance to the next non-ws char class
|
|
return classes.find(|(_, c)| !c.is_ws()).map(|(p, _)| p);
|
|
}
|
|
|
|
let last_non_ws = classes.find(|(_, c)| c.is_ws());
|
|
if ignore_trailing_ws {
|
|
return last_non_ws.map(|(p, _)| p);
|
|
}
|
|
classes.find(|(_, c)| !c.is_ws()).map(|(p, _)| p)
|
|
}
|
|
Word::Normal => {
|
|
if let Some((_, C::Whitespace)) = classes.peek() {
|
|
// we are on whitespace. advance to the next non-ws char class
|
|
return classes.find(|(_, c)| !c.is_ws()).map(|(p, _)| p);
|
|
}
|
|
|
|
// go forward until we find some char class that isnt this one
|
|
let mut last = classes.next()?;
|
|
let first_c = last.1;
|
|
while let Some((p,c)) = classes.next() {
|
|
match c {
|
|
C::Whitespace => {
|
|
if ignore_trailing_ws {
|
|
*inclusive = true;
|
|
return Some(last.0)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
c if !c.is_other_class_or_ws(&first_c) => {
|
|
last = (p,c);
|
|
}
|
|
_ => return Some(p)
|
|
}
|
|
}
|
|
|
|
// we found whitespace previously, look for the next non-whitespace char class
|
|
classes.find(|(_, c)| !c.is_ws()).map(|(p, _)| p)
|
|
}
|
|
}
|
|
}
|
|
fn word_motion_b(&self, word: &Word, start: Pos) -> Option<Pos> {
|
|
use CharClass as C;
|
|
// get our iterator again
|
|
let mut classes = self.char_classes_backward_from(start).peekable();
|
|
|
|
match word {
|
|
Word::Big => {
|
|
classes.next();
|
|
// for 'b', we handle starting on whitespace differently than 'w'
|
|
// we don't return immediately if find() returns Some() here.
|
|
let first_non_ws = if let Some((_, C::Whitespace)) = classes.peek() {
|
|
// we use find() to advance the iterator as usual
|
|
// but we can also be clever and use the question mark
|
|
// to return early if we don't find a word backwards
|
|
classes.find(|(_, c)| !c.is_ws())?
|
|
} else {
|
|
classes.next()?
|
|
};
|
|
|
|
// ok now we are off that whitespace
|
|
// now advance backwards until we find more whitespace, or next() is None
|
|
|
|
let mut last = first_non_ws;
|
|
while let Some((_, c)) = classes.peek() {
|
|
if c.is_ws() {
|
|
break;
|
|
}
|
|
last = classes.next()?;
|
|
}
|
|
Some(last.0)
|
|
}
|
|
Word::Normal => {
|
|
classes.next();
|
|
let first_non_ws = if let Some((_, C::Whitespace)) = classes.peek() {
|
|
classes.find(|(_, c)| !c.is_ws())?
|
|
} else {
|
|
classes.next()?
|
|
};
|
|
|
|
// ok, off the whitespace
|
|
// now advance until we find any different char class at all
|
|
let mut last = first_non_ws;
|
|
while let Some((_, c)) = classes.peek() {
|
|
if c.is_other_class(&last.1) {
|
|
break;
|
|
}
|
|
last = classes.next()?;
|
|
}
|
|
|
|
Some(last.0)
|
|
}
|
|
}
|
|
}
|
|
fn word_motion_e(&self, word: &Word, start: Pos) -> Option<Pos> {
|
|
use CharClass as C;
|
|
let mut classes = self.char_classes_forward_from(start).peekable();
|
|
|
|
match word {
|
|
Word::Big => {
|
|
classes.next(); // unconditionally skip first position for 'e'
|
|
let first_non_ws = if let Some((_, C::Whitespace)) = classes.peek() {
|
|
classes.find(|(_, c)| !c.is_ws())?
|
|
} else {
|
|
classes.next()?
|
|
};
|
|
|
|
let mut last = first_non_ws;
|
|
while let Some((_, c)) = classes.peek() {
|
|
if c.is_ws() {
|
|
return Some(last.0);
|
|
}
|
|
last = classes.next()?;
|
|
}
|
|
None
|
|
}
|
|
Word::Normal => {
|
|
classes.next();
|
|
let first_non_ws = if let Some((_, C::Whitespace)) = classes.peek() {
|
|
classes.find(|(_, c)| !c.is_ws())?
|
|
} else {
|
|
classes.next()?
|
|
};
|
|
|
|
let mut last = first_non_ws;
|
|
while let Some((_, c)) = classes.peek() {
|
|
if c.is_other_class_or_ws(&first_non_ws.1) {
|
|
return Some(last.0);
|
|
}
|
|
last = classes.next()?;
|
|
}
|
|
None
|
|
}
|
|
}
|
|
}
|
|
fn word_motion_ge(&self, word: &Word, start: Pos) -> Option<Pos> {
|
|
use CharClass as C;
|
|
let mut classes = self.char_classes_backward_from(start).peekable();
|
|
|
|
match word {
|
|
Word::Big => {
|
|
classes.next(); // unconditionally skip first position for 'ge'
|
|
if matches!(classes.peek(), Some((_, c)) if !c.is_ws()) {
|
|
classes.find(|(_, c)| c.is_ws());
|
|
}
|
|
|
|
classes.find(|(_, c)| !c.is_ws()).map(|(p, _)| p)
|
|
}
|
|
Word::Normal => {
|
|
classes.next();
|
|
if let Some((_, C::Whitespace)) = classes.peek() {
|
|
return classes.find(|(_, c)| !c.is_ws()).map(|(p, _)| p);
|
|
}
|
|
|
|
let cur_class = classes.peek()?.1;
|
|
let bound = classes.find(|(_, c)| c.is_other_class(&cur_class))?;
|
|
|
|
if bound.1.is_ws() {
|
|
classes.find(|(_, c)| !c.is_ws()).map(|(p, _)| p)
|
|
} else {
|
|
Some(bound.0)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
fn char_classes_forward_from(&self, pos: Pos) -> impl Iterator<Item = (Pos, CharClass)> {
|
|
CharClassIter::new(&self.lines, pos)
|
|
}
|
|
fn char_classes_forward(&self) -> impl Iterator<Item = (Pos, CharClass)> {
|
|
self.char_classes_forward_from(self.cursor.pos)
|
|
}
|
|
fn char_classes_backward_from(&self, pos: Pos) -> impl Iterator<Item = (Pos, CharClass)> {
|
|
CharClassIterRev::new(&self.lines, pos)
|
|
}
|
|
fn char_classes_backward(&self) -> impl Iterator<Item = (Pos, CharClass)> {
|
|
self.char_classes_backward_from(self.cursor.pos)
|
|
}
|
|
fn end_pos(&self) -> Pos {
|
|
let mut pos = Pos::MAX;
|
|
pos.clamp_row(&self.lines);
|
|
pos.clamp_col(&self.lines[pos.row].0, false);
|
|
pos
|
|
}
|
|
fn dispatch_text_obj(&mut self, count: u16, obj: TextObj) -> Option<MotionKind> {
|
|
match obj {
|
|
// text structures
|
|
TextObj::Word(word, bound) => self.text_obj_word(count, word, obj, bound),
|
|
TextObj::Sentence(_) |
|
|
TextObj::Paragraph(_) |
|
|
TextObj::WholeSentence(_) |
|
|
TextObj::Tag(_) |
|
|
TextObj::Custom(_) |
|
|
TextObj::WholeParagraph(_) => {
|
|
log::warn!("{:?} text objects are not implemented yet", obj);
|
|
None
|
|
}
|
|
|
|
// quote stuff
|
|
TextObj::DoubleQuote(bound) |
|
|
TextObj::SingleQuote(bound) |
|
|
TextObj::BacktickQuote(bound) => {
|
|
self.text_obj_quote(count, obj, bound)
|
|
}
|
|
|
|
// delimited blocks
|
|
TextObj::Paren(bound)
|
|
| TextObj::Bracket(bound)
|
|
| TextObj::Brace(bound)
|
|
| TextObj::Angle(bound) => self.text_obj_delim(count, obj, bound),
|
|
}
|
|
}
|
|
fn text_obj_word(
|
|
&mut self,
|
|
count: u16,
|
|
word: Word,
|
|
obj: TextObj,
|
|
bound: Bound,
|
|
) -> Option<MotionKind> {
|
|
use CharClass as C;
|
|
let mut fwd_classes = self.char_classes_forward();
|
|
let first_class = fwd_classes.next()?;
|
|
match first_class {
|
|
(pos,C::Whitespace) => {
|
|
match bound {
|
|
Bound::Inside => {
|
|
let mut fwd_classes = self.char_classes_forward_from(pos).peekable();
|
|
let mut bkwd_classes = self.char_classes_backward_from(pos).peekable();
|
|
let mut first = (pos,C::Whitespace);
|
|
let mut last = (pos,C::Whitespace);
|
|
while let Some((_,c)) = bkwd_classes.peek() {
|
|
if !c.is_ws() {
|
|
break;
|
|
}
|
|
first = bkwd_classes.next()?;
|
|
}
|
|
|
|
while let Some((_,c)) = fwd_classes.peek() {
|
|
if !c.is_ws() {
|
|
break;
|
|
}
|
|
last = fwd_classes.next()?;
|
|
}
|
|
|
|
Some(MotionKind::Char {
|
|
start: first.0,
|
|
end: last.0,
|
|
inclusive: true
|
|
})
|
|
}
|
|
Bound::Around => {
|
|
let mut fwd_classes = self.char_classes_forward_from(pos).peekable();
|
|
let mut bkwd_classes = self.char_classes_backward_from(pos).peekable();
|
|
let mut first = (pos,C::Whitespace);
|
|
let mut last = (pos,C::Whitespace);
|
|
while let Some((_,cl)) = bkwd_classes.peek() {
|
|
if !cl.is_ws() {
|
|
break;
|
|
}
|
|
first = bkwd_classes.next()?;
|
|
}
|
|
|
|
while let Some((_,cl)) = fwd_classes.peek() {
|
|
if !cl.is_ws() {
|
|
break;
|
|
}
|
|
last = fwd_classes.next()?;
|
|
}
|
|
let word_class = fwd_classes.next()?.1;
|
|
while let Some((_,cl)) = fwd_classes.peek() {
|
|
match word {
|
|
Word::Big => {
|
|
if cl.is_ws() {
|
|
break
|
|
}
|
|
}
|
|
Word::Normal => {
|
|
if cl.is_other_class_or_ws(&word_class) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
last = fwd_classes.next()?;
|
|
}
|
|
|
|
Some(MotionKind::Char {
|
|
start: first.0,
|
|
end: last.0,
|
|
inclusive: true
|
|
})
|
|
}
|
|
}
|
|
}
|
|
(pos, c) => {
|
|
let break_cond = |cl: &C, c: &C| -> bool {
|
|
match word {
|
|
Word::Big => cl.is_ws(),
|
|
Word::Normal => cl.is_other_class(c),
|
|
}
|
|
};
|
|
match bound {
|
|
Bound::Inside => {
|
|
let mut fwd_classes = self.char_classes_forward_from(pos).peekable();
|
|
let mut bkwd_classes = self.char_classes_backward_from(pos).peekable();
|
|
let mut first = (pos,c);
|
|
let mut last = (pos,c);
|
|
|
|
while let Some((_,cl)) = bkwd_classes.peek() {
|
|
if break_cond(cl, &c) {
|
|
break;
|
|
}
|
|
first = bkwd_classes.next()?;
|
|
}
|
|
|
|
while let Some((_,cl)) = fwd_classes.peek() {
|
|
if break_cond(cl, &c) {
|
|
break;
|
|
}
|
|
last = fwd_classes.next()?;
|
|
}
|
|
|
|
Some(MotionKind::Char {
|
|
start: first.0,
|
|
end: last.0,
|
|
inclusive: true
|
|
})
|
|
}
|
|
Bound::Around => {
|
|
let mut fwd_classes = self.char_classes_forward_from(pos).peekable();
|
|
let mut bkwd_classes = self.char_classes_backward_from(pos).peekable();
|
|
let mut first = (pos,c);
|
|
let mut last = (pos,c);
|
|
|
|
while let Some((_,cl)) = bkwd_classes.peek() {
|
|
if break_cond(cl, &c) {
|
|
break;
|
|
}
|
|
first = bkwd_classes.next()?;
|
|
}
|
|
|
|
while let Some((_,cl)) = fwd_classes.peek() {
|
|
if break_cond(cl, &c) {
|
|
break;
|
|
}
|
|
last = fwd_classes.next()?;
|
|
}
|
|
|
|
// Include trailing whitespace
|
|
while let Some((_,cl)) = fwd_classes.peek() {
|
|
if !cl.is_ws() {
|
|
break;
|
|
}
|
|
last = fwd_classes.next()?;
|
|
}
|
|
|
|
Some(MotionKind::Char {
|
|
start: first.0,
|
|
end: last.0,
|
|
inclusive: true
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
fn text_obj_quote(
|
|
&mut self,
|
|
count: u16,
|
|
obj: TextObj,
|
|
bound: Bound,
|
|
) -> Option<MotionKind> {
|
|
let q_ch = match obj {
|
|
TextObj::DoubleQuote(_) => '"',
|
|
TextObj::SingleQuote(_) => '\'',
|
|
TextObj::BacktickQuote(_) => '`',
|
|
_ => unreachable!(),
|
|
};
|
|
|
|
let start_pos = self
|
|
.scan_backward(|g| g.as_char() == Some(q_ch))
|
|
.or_else(|| self.scan_forward(|g| g.as_char() == Some(q_ch)))?;
|
|
|
|
let mut scan_start_pos = start_pos;
|
|
scan_start_pos.col += 1;
|
|
|
|
let mut end_pos = self.scan_forward_from(scan_start_pos, |g| g.as_char() == Some(q_ch))?;
|
|
|
|
match bound {
|
|
Bound::Around => {
|
|
// Around for quoted structures is weird. We have to include any trailing whitespace in the range.
|
|
end_pos.col += 1;
|
|
let mut classes = self.char_classes_forward_from(end_pos);
|
|
end_pos = classes
|
|
.find(|(_, c)| !c.is_ws())
|
|
.map(|(p, _)| p)
|
|
.unwrap_or(self.end_pos());
|
|
|
|
(start_pos <= end_pos).then_some(MotionKind::Char {
|
|
start: start_pos,
|
|
end: end_pos,
|
|
inclusive: false,
|
|
})
|
|
}
|
|
Bound::Inside => {
|
|
let mut start_pos = start_pos;
|
|
start_pos.col += 1;
|
|
(start_pos <= end_pos).then_some(MotionKind::Char {
|
|
start: start_pos,
|
|
end: end_pos,
|
|
inclusive: false,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
fn text_obj_delim(&mut self, count: u16, obj: TextObj, bound: Bound) -> Option<MotionKind> {
|
|
let (opener, closer) = match obj {
|
|
TextObj::Paren(_) => ('(', ')'),
|
|
TextObj::Bracket(_) => ('[', ']'),
|
|
TextObj::Brace(_) => ('{', '}'),
|
|
TextObj::Angle(_) => ('<', '>'),
|
|
_ => unreachable!(),
|
|
};
|
|
let mut depth = 0;
|
|
let start_pos = self
|
|
.scan_backward(|g| {
|
|
if g.as_char() == Some(closer) {
|
|
depth += 1;
|
|
}
|
|
if g.as_char() == Some(opener) {
|
|
if depth == 0 {
|
|
return true;
|
|
}
|
|
depth -= 1;
|
|
}
|
|
false
|
|
})
|
|
.or_else(|| self.scan_forward(|g| g.as_char() == Some(opener)))?;
|
|
|
|
depth = 0;
|
|
let end_pos = self.scan_forward_from(start_pos, |g| {
|
|
if g.as_char() == Some(opener) {
|
|
depth += 1;
|
|
}
|
|
if g.as_char() == Some(closer) {
|
|
depth -= 1;
|
|
}
|
|
depth == 0
|
|
})?;
|
|
|
|
match bound {
|
|
Bound::Around => Some(MotionKind::Char {
|
|
start: start_pos,
|
|
end: end_pos,
|
|
inclusive: true,
|
|
}),
|
|
Bound::Inside => {
|
|
let mut start_pos = start_pos;
|
|
start_pos.col += 1;
|
|
(start_pos <= end_pos).then_some(MotionKind::Char {
|
|
start: start_pos,
|
|
end: end_pos,
|
|
inclusive: false,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
fn gr_at(&self, pos: Pos) -> Option<&Grapheme> {
|
|
self.lines.get(pos.row)?.0.get(pos.col)
|
|
}
|
|
fn clamp_pos(&self, mut pos: Pos) -> Pos {
|
|
pos.clamp_row(&self.lines);
|
|
pos.clamp_col(&self.lines[pos.row].0, false);
|
|
pos
|
|
}
|
|
fn number_at_cursor(&self) -> Option<(Pos,Pos)> {
|
|
self.number_at(self.cursor.pos)
|
|
}
|
|
/// Returns the start/end span of a number at a given position, if any
|
|
fn number_at(&self, mut pos: Pos) -> Option<(Pos,Pos)> {
|
|
let is_number_char = |gr: &Grapheme| gr.as_char().is_some_and(|c| c == '.' || c == '-' || c.is_ascii_digit());
|
|
let is_digit = |gr: &Grapheme| gr.as_char().is_some_and(|c| c.is_ascii_digit());
|
|
|
|
pos = self.clamp_pos(pos);
|
|
if !is_number_char(self.gr_at(pos)?) {
|
|
return None;
|
|
}
|
|
|
|
// If cursor is on '-', advance to the first digit
|
|
if self.gr_at(pos)?.as_char() == Some('-') {
|
|
pos = pos.col_add(1);
|
|
}
|
|
|
|
let mut start = self.scan_backward_from(pos, |g| !is_digit(g))
|
|
.map(|pos| Pos { row: pos.row, col: pos.col + 1 })
|
|
.unwrap_or(Pos::MIN);
|
|
let end = self.scan_forward_from(pos, |g| !is_digit(g))
|
|
.map(|pos| Pos { row: pos.row, col: pos.col.saturating_sub(1) })
|
|
.unwrap_or(Pos { row: pos.row, col: self.lines[pos.row].len().saturating_sub(1) });
|
|
|
|
if start > Pos::MIN && self.lines[start.row][start.col.saturating_sub(1)].as_char() == Some('-') {
|
|
start.col -= 1;
|
|
}
|
|
|
|
Some((start, end))
|
|
}
|
|
fn adjust_number(&mut self, inc: i64) -> Option<()> {
|
|
let (s,e) = if let Some(range) = self.select_range() {
|
|
match range {
|
|
Motion::CharRange(s, e) => (s,e),
|
|
_ => return None,
|
|
}
|
|
} else if let Some((s,e)) = self.number_at_cursor() {
|
|
(s,e)
|
|
} else {
|
|
return None;
|
|
};
|
|
|
|
let word = self.pos_slice_str(s,e);
|
|
|
|
let num_fmt = if word.starts_with("0x") {
|
|
let body = word.strip_prefix("0x").unwrap();
|
|
let width = body.len();
|
|
let num = i64::from_str_radix(body, 16).ok()?;
|
|
let new_num = num + inc;
|
|
format!("0x{new_num:0>width$x}")
|
|
} else if word.starts_with("0b") {
|
|
let body = word.strip_prefix("0b").unwrap();
|
|
let width = body.len();
|
|
let num = i64::from_str_radix(body, 2).ok()?;
|
|
let new_num = num + inc;
|
|
format!("0b{new_num:0>width$b}")
|
|
} else if word.starts_with("0o") {
|
|
let body = word.strip_prefix("0o").unwrap();
|
|
let width = body.len();
|
|
let num = i64::from_str_radix(body, 8).ok()?;
|
|
let new_num = num + inc;
|
|
format!("0o{new_num:0>width$o}")
|
|
} else if let Ok(num) = word.parse::<i64>() {
|
|
let width = word.len();
|
|
let new_num = num + inc;
|
|
if new_num < 0 {
|
|
let abs = new_num.unsigned_abs();
|
|
let digit_width = if num < 0 { width - 1 } else { width };
|
|
format!("-{abs:0>digit_width$}")
|
|
} else if num < 0 {
|
|
let digit_width = width - 1;
|
|
format!("{new_num:0>digit_width$}")
|
|
} else {
|
|
format!("{new_num:0>width$}")
|
|
}
|
|
} else { return None };
|
|
|
|
self.replace_range(s, e, &num_fmt);
|
|
self.cursor.pos.col -= 1;
|
|
Some(())
|
|
}
|
|
fn replace_range(&mut self, s: Pos, e: Pos, new: &str) -> Vec<Line> {
|
|
let motion = MotionKind::Char { start: s, end: e, inclusive: true };
|
|
let content = self.extract_range(&motion);
|
|
self.set_cursor(s);
|
|
self.insert_str(new);
|
|
content
|
|
}
|
|
fn pos_slice_str(&self, s: Pos, e: Pos) -> String {
|
|
let (s,e) = ordered(s,e);
|
|
if s.row == e.row {
|
|
self.lines[s.row].0[s.col..=e.col]
|
|
.iter()
|
|
.map(|g| g.to_string())
|
|
.collect()
|
|
} else {
|
|
let mut result = String::new();
|
|
// First line from s.col to end
|
|
for g in &self.lines[s.row].0[s.col..] {
|
|
result.push_str(&g.to_string());
|
|
}
|
|
// Middle lines
|
|
for line in &self.lines[s.row + 1..e.row] {
|
|
result.push('\n');
|
|
result.push_str(&line.to_string());
|
|
}
|
|
// Last line from start to e.col
|
|
result.push('\n');
|
|
for g in &self.lines[e.row].0[..=e.col] {
|
|
result.push_str(&g.to_string());
|
|
}
|
|
result
|
|
}
|
|
}
|
|
fn find_delim_match(&mut self) -> Option<MotionKind> {
|
|
let is_opener = |g: &Grapheme| matches!(g.as_char(), Some(c) if "([{<".contains(c));
|
|
let is_closer = |g: &Grapheme| matches!(g.as_char(), Some(c) if ")]}>".contains(c));
|
|
let is_delim = |g: &Grapheme| is_opener(g) || is_closer(g);
|
|
let first = self.scan_forward(is_delim)?;
|
|
|
|
let delim_match = if is_closer(self.gr_at(first)?) {
|
|
let opener = match self.gr_at(first)?.as_char()? {
|
|
')' => '(',
|
|
']' => '[',
|
|
'}' => '{',
|
|
'>' => '<',
|
|
_ => unreachable!(),
|
|
};
|
|
self.scan_backward_from(first, |g| g.as_char() == Some(opener))?
|
|
} else if is_opener(self.gr_at(first)?) {
|
|
let closer = match self.gr_at(first)?.as_char()? {
|
|
'(' => ')',
|
|
'[' => ']',
|
|
'{' => '}',
|
|
'<' => '>',
|
|
_ => unreachable!(),
|
|
};
|
|
self.scan_forward_from(first, |g| g.as_char() == Some(closer))?
|
|
} else { unreachable!() };
|
|
|
|
Some(MotionKind::Char {
|
|
start: self.cursor.pos,
|
|
end: delim_match,
|
|
inclusive: true,
|
|
})
|
|
}
|
|
/// Wrapper for eval_motion_inner that calls it with `check_hint: false`
|
|
fn eval_motion(&mut self, cmd: &ViCmd) -> Option<MotionKind> {
|
|
self.eval_motion_inner(cmd, false)
|
|
}
|
|
fn eval_motion_inner(&mut self, cmd: &ViCmd, check_hint: bool) -> Option<MotionKind> {
|
|
let ViCmd { verb, motion, .. } = cmd;
|
|
let MotionCmd(count, motion) = motion.as_ref()?;
|
|
let buffer = self.lines.clone();
|
|
if let Some(mut hint) = self.hint.clone() {
|
|
attach_lines(&mut self.lines, &mut hint);
|
|
}
|
|
|
|
let kind = match motion {
|
|
Motion::WholeLine => {
|
|
let start = self.row();
|
|
let end = (self.row() + (count.saturating_sub(1))).min(self.lines.len().saturating_sub(1));
|
|
Some(MotionKind::Line {
|
|
start,
|
|
end,
|
|
inclusive: true
|
|
})
|
|
}
|
|
Motion::TextObj(text_obj) => self.dispatch_text_obj(*count as u16, text_obj.clone()),
|
|
Motion::EndOfLastWord => {
|
|
let row = self.row() + (count.saturating_sub(1));
|
|
let line = self.line_mut(row);
|
|
let mut target = Pos { row, col: 0 };
|
|
for (i, gr) in line.0.iter().enumerate() {
|
|
if !gr.is_ws() {
|
|
target.col = i;
|
|
}
|
|
}
|
|
|
|
(target != self.cursor.pos).then_some(MotionKind::Char {
|
|
start: self.cursor.pos,
|
|
end: target,
|
|
inclusive: true,
|
|
})
|
|
}
|
|
Motion::StartOfFirstWord => {
|
|
let mut target = Pos {
|
|
row: self.row(),
|
|
col: 0,
|
|
};
|
|
let line = self.cur_line();
|
|
for (i, gr) in line.0.iter().enumerate() {
|
|
target.col = i;
|
|
if !gr.is_ws() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
(target != self.cursor.pos).then_some(MotionKind::Char {
|
|
start: self.cursor.pos,
|
|
end: target,
|
|
inclusive: true,
|
|
})
|
|
}
|
|
dir @ (Motion::StartOfLine | Motion::EndOfLine) => {
|
|
let (inclusive,off) = match dir {
|
|
Motion::StartOfLine => (false,isize::MIN),
|
|
Motion::EndOfLine => (true,isize::MAX),
|
|
_ => unreachable!(),
|
|
};
|
|
let target = self.offset_cursor(0, off);
|
|
(target != self.cursor.pos).then_some(MotionKind::Char {
|
|
start: self.cursor.pos,
|
|
end: target,
|
|
inclusive,
|
|
})
|
|
}
|
|
Motion::WordMotion(to, word, dir) => {
|
|
// 'cw' is a weird case
|
|
// if you are on the word's left boundary, it will not delete whitespace after
|
|
// the end of the word
|
|
let ignore_trailing_ws = matches!(verb, Some(VerbCmd(_, Verb::Change)),)
|
|
&& matches!(
|
|
motion,
|
|
Motion::WordMotion(To::Start, _, Direction::Forward,)
|
|
);
|
|
let inclusive = verb.is_none();
|
|
|
|
self.eval_word_motion(*count, to, word, dir, ignore_trailing_ws, inclusive)
|
|
}
|
|
Motion::CharSearch(dir, dest, char) => {
|
|
let off = self.search_char(dir, dest, char);
|
|
let target = self.offset_cursor(0, off);
|
|
(target != self.cursor.pos).then_some(MotionKind::Char {
|
|
start: self.cursor.pos,
|
|
end: target,
|
|
inclusive: true,
|
|
})
|
|
}
|
|
dir @ (Motion::BackwardChar | Motion::ForwardChar)
|
|
| dir @ (Motion::BackwardCharForced | Motion::ForwardCharForced) => {
|
|
let (off, wrap) = match dir {
|
|
Motion::BackwardChar => (-(*count as isize), false),
|
|
Motion::ForwardChar => (*count as isize, false),
|
|
Motion::BackwardCharForced => (-(*count as isize), true),
|
|
Motion::ForwardCharForced => (*count as isize, true),
|
|
_ => unreachable!(),
|
|
};
|
|
let target = if wrap {
|
|
self.offset_cursor_wrapping(0, off)
|
|
} else {
|
|
self.offset_cursor(0, off)
|
|
};
|
|
|
|
(target != self.cursor.pos).then_some(MotionKind::Char {
|
|
start: self.cursor.pos,
|
|
end: target,
|
|
inclusive: false,
|
|
})
|
|
}
|
|
dir @ (Motion::LineDown | Motion::LineUp) => {
|
|
let off = match dir {
|
|
Motion::LineUp => -(*count as isize),
|
|
Motion::LineDown => *count as isize,
|
|
_ => unreachable!(),
|
|
};
|
|
if verb.is_some() {
|
|
let row = self.row();
|
|
let target_row = self.offset_row(off);
|
|
let (s, e) = ordered(row, target_row);
|
|
Some(MotionKind::Line { start: s, end: e, inclusive: true })
|
|
} else {
|
|
if self.saved_col.is_none() {
|
|
self.saved_col = Some(self.cursor.pos.col);
|
|
}
|
|
let row = self.offset_row(off);
|
|
let limit = if self.cursor.exclusive {
|
|
self.lines[row].len().saturating_sub(1)
|
|
} else {
|
|
self.lines[row].len()
|
|
};
|
|
let col = self.saved_col.unwrap().min(limit);
|
|
let target = Pos { row, col };
|
|
(target != self.cursor.pos).then_some(MotionKind::Char {
|
|
start: self.cursor.pos,
|
|
end: target,
|
|
inclusive: true,
|
|
})
|
|
}
|
|
}
|
|
dir @ (Motion::EndOfBuffer | Motion::StartOfBuffer) => {
|
|
let off = match dir {
|
|
Motion::StartOfBuffer => isize::MIN,
|
|
Motion::EndOfBuffer => isize::MAX,
|
|
_ => unreachable!(),
|
|
};
|
|
if verb.is_some() {
|
|
let row = self.row();
|
|
let target_row = self.offset_row(off);
|
|
let (s, e) = ordered(row, target_row);
|
|
Some(MotionKind::Line { start: s, end: e, inclusive: false })
|
|
} else {
|
|
let target = self.offset_cursor(off, 0);
|
|
(target != self.cursor.pos).then_some(MotionKind::Char {
|
|
start: self.cursor.pos,
|
|
end: target,
|
|
inclusive: true,
|
|
})
|
|
}
|
|
}
|
|
Motion::WholeBuffer => Some(MotionKind::Line {
|
|
start: 0,
|
|
end: self.lines.len().saturating_sub(1),
|
|
inclusive: false
|
|
}),
|
|
Motion::ToColumn => {
|
|
let row = self.row();
|
|
let end = Pos { row, col: count.saturating_sub(1) };
|
|
Some(MotionKind::Char { start: self.cursor.pos, end, inclusive: end > self.cursor.pos })
|
|
}
|
|
|
|
Motion::ToDelimMatch => self.find_delim_match(),
|
|
Motion::ToBracket(direction) |
|
|
Motion::ToParen(direction) |
|
|
Motion::ToBrace(direction) => {
|
|
let (opener,closer) = match motion {
|
|
Motion::ToBracket(_) => ('[', ']'),
|
|
Motion::ToParen(_) => ('(', ')'),
|
|
Motion::ToBrace(_) => ('{', '}'),
|
|
_ => unreachable!(),
|
|
};
|
|
match direction {
|
|
Direction::Forward => {
|
|
let mut depth = 0;
|
|
let target_pos = self.scan_forward(|g| {
|
|
if g.as_char() == Some(opener) { depth += 1; }
|
|
if g.as_char() == Some(closer) {
|
|
depth -= 1;
|
|
if depth <= 0 {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
})?;
|
|
return Some(MotionKind::Char {
|
|
start: self.cursor.pos,
|
|
end: target_pos,
|
|
inclusive: true,
|
|
})
|
|
}
|
|
Direction::Backward => {
|
|
let mut depth = 0;
|
|
let target_pos = self.scan_backward(|g| {
|
|
if g.as_char() == Some(closer) { depth += 1; }
|
|
if g.as_char() == Some(opener) {
|
|
depth -= 1;
|
|
if depth <= 0 {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
})?;
|
|
return Some(MotionKind::Char {
|
|
start: self.cursor.pos,
|
|
end: target_pos,
|
|
inclusive: true,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Motion::CharRange(s, e) => {
|
|
let (s, e) = ordered(*s, *e);
|
|
Some(MotionKind::Char {
|
|
start: s,
|
|
end: e,
|
|
inclusive: true,
|
|
})
|
|
}
|
|
Motion::LineRange(s, e) => {
|
|
let (s, e) = ordered(*s, *e);
|
|
Some(MotionKind::Line { start: s, end: e, inclusive: false })
|
|
}
|
|
Motion::BlockRange(s, e) => {
|
|
let (s, e) = ordered(*s, *e);
|
|
Some(MotionKind::Block { start: s, end: e })
|
|
}
|
|
Motion::RepeatMotion |
|
|
Motion::RepeatMotionRev => unreachable!("Repeat motions should have been resolved in readline/mod.rs"),
|
|
Motion::Global(val) |
|
|
Motion::NotGlobal(val) => {
|
|
log::warn!("Global motions are not implemented yet (val: {:?})", val);
|
|
None
|
|
}
|
|
Motion::Null => None,
|
|
};
|
|
|
|
self.lines = buffer;
|
|
kind
|
|
}
|
|
fn move_to_start(&mut self, motion: MotionKind) {
|
|
match motion {
|
|
MotionKind::Char { start, end, .. } => {
|
|
let (s,_) = ordered(start, end);
|
|
self.set_cursor(s);
|
|
}
|
|
MotionKind::Line { start, end, .. } => {
|
|
let (s,_) = ordered(start, end);
|
|
self.set_cursor(Pos { row: s, col: 0 });
|
|
}
|
|
MotionKind::Block { start, end } => todo!(),
|
|
}
|
|
}
|
|
/// Wrapper for apply_motion_inner that calls it with `accept_hint: false`
|
|
fn apply_motion(&mut self, motion: MotionKind) -> ShResult<()> {
|
|
self.apply_motion_inner(motion, false)
|
|
}
|
|
fn apply_motion_inner(&mut self, motion: MotionKind, accept_hint: bool) -> ShResult<()> {
|
|
match motion {
|
|
MotionKind::Char { end, .. } => {
|
|
if accept_hint && self.has_hint() && end >= self.end_pos() {
|
|
self.accept_hint_to(end);
|
|
} else {
|
|
self.set_cursor(end);
|
|
}
|
|
}
|
|
MotionKind::Line { start, .. } => {
|
|
self.set_row(start);
|
|
}
|
|
MotionKind::Block { start, end } => todo!(),
|
|
}
|
|
Ok(())
|
|
}
|
|
fn extract_range(&mut self, motion: &MotionKind) -> Vec<Line> {
|
|
log::debug!("Extracting range for motion: {:?}", motion);
|
|
let extracted = match motion {
|
|
MotionKind::Char {
|
|
start,
|
|
end,
|
|
inclusive,
|
|
} => {
|
|
let (s, e) = ordered(*start, *end);
|
|
let end = if *inclusive {
|
|
Pos {
|
|
row: e.row,
|
|
col: e.col + 1,
|
|
}
|
|
} else {
|
|
e
|
|
};
|
|
let mut buf = std::mem::take(&mut self.lines);
|
|
let extracted = extract_range_contiguous(&mut buf, s, end);
|
|
self.lines = buf;
|
|
extracted
|
|
}
|
|
MotionKind::Line { start, end, inclusive } => {
|
|
let end = if *inclusive {
|
|
*end
|
|
} else {
|
|
end.saturating_sub(1)
|
|
};
|
|
self.lines.drain(*start..=end).collect()
|
|
}
|
|
MotionKind::Block { start, end } => {
|
|
let (s, e) = ordered(*start, *end);
|
|
(s.row..=e.row)
|
|
.map(|row| {
|
|
let sc = s.col.min(self.lines[row].len());
|
|
let ec = (e.col + 1).min(self.lines[row].len());
|
|
Line(self.lines[row].0.drain(sc..ec).collect())
|
|
})
|
|
.collect()
|
|
}
|
|
};
|
|
if self.lines.is_empty() {
|
|
self.lines.push(Line::default());
|
|
}
|
|
extracted
|
|
}
|
|
fn yank_range(&self, motion: &MotionKind) -> Vec<Line> {
|
|
let mut tmp = Self {
|
|
lines: self.lines.clone(),
|
|
cursor: self.cursor,
|
|
..Default::default()
|
|
};
|
|
tmp.extract_range(motion)
|
|
}
|
|
fn delete_range(&mut self, motion: &MotionKind) -> Vec<Line> {
|
|
self.extract_range(motion)
|
|
}
|
|
pub fn calc_indent_level(&mut self) -> usize {
|
|
self.calc_indent_level_for_pos(self.cursor.pos)
|
|
}
|
|
pub fn calc_indent_level_for_pos(&mut self, pos: Pos) -> usize {
|
|
let mut lines = self.lines.clone();
|
|
split_lines_at(&mut lines, pos);
|
|
let raw = join_lines(&lines);
|
|
|
|
self.indent_ctx.calculate(&raw)
|
|
}
|
|
fn motion_mutation(&mut self, motion: MotionKind, f: impl Fn(&Grapheme) -> Grapheme) {
|
|
match motion {
|
|
MotionKind::Char {
|
|
start,
|
|
end,
|
|
inclusive,
|
|
} => {
|
|
let (s, e) = ordered(start, end);
|
|
if s.row == e.row {
|
|
let range = if inclusive {
|
|
s.col..e.col + 1
|
|
} else {
|
|
s.col..e.col
|
|
};
|
|
for col in range {
|
|
if col >= self.lines[s.row].len() {
|
|
break;
|
|
}
|
|
self.lines[s.row][col] = f(&self.lines[s.row][col]);
|
|
}
|
|
return;
|
|
}
|
|
let end = if inclusive { e.col + 1 } else { e.col };
|
|
|
|
for col in s.col..self.lines[s.row].len() {
|
|
self.lines[s.row][col] = f(&self.lines[s.row][col]);
|
|
}
|
|
for row in s.row + 1..e.row {
|
|
for col in 0..self.lines[row].len() {
|
|
self.lines[row][col] = f(&self.lines[row][col]);
|
|
}
|
|
}
|
|
for col in 0..end {
|
|
if col >= self.lines[e.row].len() {
|
|
break;
|
|
}
|
|
self.lines[e.row][col] = f(&self.lines[e.row][col]);
|
|
}
|
|
}
|
|
MotionKind::Line { start, end, inclusive } => {
|
|
let end = if inclusive { end } else { end.saturating_sub(1) };
|
|
let end = end.min(self.lines.len().saturating_sub(1));
|
|
for row in start..=end {
|
|
let line = self.line_mut(row);
|
|
for col in 0..line.len() {
|
|
line[col] = f(&line[col]);
|
|
}
|
|
}
|
|
}
|
|
MotionKind::Block { start, end } => todo!(),
|
|
}
|
|
}
|
|
fn inplace_mutation(&mut self, count: u16, f: impl Fn(&Grapheme) -> Grapheme) {
|
|
let mut first = true;
|
|
for i in 0..count {
|
|
if first {
|
|
first = false
|
|
} else {
|
|
self.cursor.pos = self.offset_cursor(0, 1);
|
|
}
|
|
let pos = self.cursor.pos;
|
|
let motion = MotionKind::Char {
|
|
start: pos,
|
|
end: pos,
|
|
inclusive: true,
|
|
};
|
|
self.motion_mutation(motion, &f);
|
|
}
|
|
}
|
|
fn exec_verb(&mut self, cmd: &ViCmd) -> ShResult<()> {
|
|
let ViCmd {
|
|
register,
|
|
verb,
|
|
motion,
|
|
..
|
|
} = cmd;
|
|
let Some(VerbCmd(_, verb)) = verb else {
|
|
// For verb-less motions in insert mode, merge hint before evaluating
|
|
// so motions like `w` can see into the hint text
|
|
let result = self.eval_motion_inner(cmd, true);
|
|
if let Some(motion_kind) = result {
|
|
self.apply_motion_inner(motion_kind, true)?;
|
|
}
|
|
return Ok(());
|
|
};
|
|
let count = motion.as_ref().map(|m| m.0).unwrap_or(1);
|
|
|
|
match verb {
|
|
Verb::Delete | Verb::Change | Verb::Yank => {
|
|
let Some(motion) = self.eval_motion(cmd) else {
|
|
return Ok(());
|
|
};
|
|
let content = if *verb == Verb::Yank {
|
|
self.yank_range(&motion)
|
|
} else if *verb == Verb::Change && matches!(motion, MotionKind::Line {..}) {
|
|
let n_lines = self.lines.len();
|
|
let content = self.delete_range(&motion);
|
|
let row = self.row();
|
|
if n_lines > 1 {
|
|
self.lines.insert(row, Line::default());
|
|
}
|
|
content
|
|
} else {
|
|
self.delete_range(&motion)
|
|
};
|
|
let reg_content = match &motion {
|
|
MotionKind::Char { .. } => RegisterContent::Span(content),
|
|
MotionKind::Line { .. } => RegisterContent::Line(content),
|
|
MotionKind::Block { .. } => RegisterContent::Block(content),
|
|
};
|
|
register.write_to_register(reg_content);
|
|
|
|
match motion {
|
|
MotionKind::Char { start, end, .. } => {
|
|
let (s, _) = ordered(start, end);
|
|
self.set_cursor(s);
|
|
}
|
|
MotionKind::Line { start, end, inclusive } => {
|
|
let end = if inclusive { end } else { end.saturating_sub(1) };
|
|
let (s, _) = ordered(start, end);
|
|
self.set_row(s);
|
|
if *verb == Verb::Change {
|
|
// we've gotta indent
|
|
let level = self.calc_indent_level();
|
|
let line = self.cur_line_mut();
|
|
let mut col = 0;
|
|
for tab in std::iter::repeat_n(Grapheme::from('\t'), level) {
|
|
line.0.insert(col, tab);
|
|
col += 1;
|
|
}
|
|
self.cursor.pos = self.offset_cursor(0, col as isize);
|
|
}
|
|
}
|
|
MotionKind::Block { start, .. } => {
|
|
let (s, _) = ordered(self.cursor.pos, start);
|
|
self.set_cursor(s);
|
|
}
|
|
}
|
|
}
|
|
Verb::Rot13 => {
|
|
let Some(motion) = self.eval_motion(cmd) else {
|
|
return Ok(());
|
|
};
|
|
self.motion_mutation(motion, |gr| {
|
|
gr.as_char()
|
|
.map(rot13_char)
|
|
.map(Grapheme::from)
|
|
.unwrap_or_else(|| gr.clone())
|
|
});
|
|
self.move_to_start(motion);
|
|
}
|
|
Verb::ReplaceChar(ch) => {
|
|
let Some(motion) = self.eval_motion(cmd) else {
|
|
return Ok(());
|
|
};
|
|
self.motion_mutation(motion, |_| Grapheme::from(*ch));
|
|
self.move_to_start(motion);
|
|
}
|
|
Verb::ReplaceCharInplace(ch, count) => self.inplace_mutation(*count, |_| Grapheme::from(*ch)),
|
|
Verb::ToggleCaseInplace(count) => {
|
|
self.inplace_mutation(*count, |gr| {
|
|
gr.as_char()
|
|
.map(toggle_case_char)
|
|
.map(Grapheme::from)
|
|
.unwrap_or_else(|| gr.clone())
|
|
});
|
|
self.cursor.pos = self.cursor.pos.col_add(1);
|
|
}
|
|
Verb::ToggleCaseRange => {
|
|
let Some(motion) = self.eval_motion(cmd) else {
|
|
return Ok(());
|
|
};
|
|
self.motion_mutation(motion, |gr| {
|
|
gr.as_char()
|
|
.map(toggle_case_char)
|
|
.map(Grapheme::from)
|
|
.unwrap_or_else(|| gr.clone())
|
|
});
|
|
self.move_to_start(motion);
|
|
}
|
|
Verb::IncrementNumber(n) => { self.adjust_number(*n as i64); },
|
|
Verb::DecrementNumber(n) => { self.adjust_number(-(*n as i64)); },
|
|
Verb::ToLower => {
|
|
let Some(motion) = self.eval_motion(cmd) else {
|
|
return Ok(());
|
|
};
|
|
self.motion_mutation(motion, |gr| {
|
|
gr.as_char()
|
|
.map(|c| c.to_ascii_lowercase())
|
|
.map(Grapheme::from)
|
|
.unwrap_or_else(|| gr.clone())
|
|
});
|
|
self.move_to_start(motion);
|
|
}
|
|
Verb::ToUpper => {
|
|
let Some(motion) = self.eval_motion(cmd) else {
|
|
return Ok(());
|
|
};
|
|
self.motion_mutation(motion, |gr| {
|
|
gr.as_char()
|
|
.map(|c| c.to_ascii_uppercase())
|
|
.map(Grapheme::from)
|
|
.unwrap_or_else(|| gr.clone())
|
|
});
|
|
self.move_to_start(motion);
|
|
}
|
|
Verb::Undo => {
|
|
if let Some(edit) = self.undo_stack.pop() {
|
|
self.lines = edit.old.clone();
|
|
self.cursor.pos = edit.old_cursor;
|
|
self.redo_stack.push(edit);
|
|
}
|
|
}
|
|
Verb::Redo => {
|
|
if let Some(edit) = self.redo_stack.pop() {
|
|
self.lines = edit.new.clone();
|
|
self.cursor.pos = edit.new_cursor;
|
|
self.undo_stack.push(edit);
|
|
}
|
|
}
|
|
Verb::Put(anchor) => {
|
|
let Some(content) = register.read_from_register() else {
|
|
return Ok(());
|
|
};
|
|
match content {
|
|
RegisterContent::Span(lines) => {
|
|
let move_cursor = lines.len() == 1 && lines[0].len() > 1;
|
|
let content_len: usize = lines.iter().map(|l| l.len()).sum();
|
|
let row = self.row();
|
|
let col = match anchor {
|
|
Anchor::After => (self.col() + 1).min(self.cur_line().len()),
|
|
Anchor::Before => self.col(),
|
|
};
|
|
let start_len = self.lines[row].len();
|
|
let mut right = self.lines[row].split_off(col);
|
|
|
|
let mut lines = lines.clone();
|
|
let last = lines.len() - 1;
|
|
|
|
// First line appends to current line
|
|
self.lines[row].append(&mut lines[0]);
|
|
|
|
// Middle + last lines get inserted after
|
|
for (i, line) in lines[1..].iter().cloned().enumerate() {
|
|
self.lines.insert(row + 1 + i, line);
|
|
}
|
|
|
|
// Reattach right half to the last inserted line
|
|
self.lines[row + last].append(&mut right);
|
|
|
|
let end_len = self.lines[row].len();
|
|
let mut delta = end_len.saturating_sub(start_len);
|
|
if let Anchor::Before = anchor { delta = delta.saturating_sub(1); }
|
|
if move_cursor {
|
|
self.cursor.pos = self.offset_cursor(0, delta as isize);
|
|
} else if content_len > 1 || *anchor == Anchor::After {
|
|
self.cursor.pos = self.offset_cursor(0, 1);
|
|
}
|
|
}
|
|
RegisterContent::Line(lines) => {
|
|
let row = match anchor {
|
|
Anchor::After => self.row() + 1,
|
|
Anchor::Before => self.row(),
|
|
};
|
|
for (i, line) in lines.iter().cloned().enumerate() {
|
|
self.lines.insert(row + i, line);
|
|
self.set_row(row + i);
|
|
}
|
|
}
|
|
RegisterContent::Block(lines) => todo!(),
|
|
RegisterContent::Empty => {}
|
|
}
|
|
}
|
|
Verb::InsertModeLineBreak(anchor) => match anchor {
|
|
Anchor::After => {
|
|
let row = self.row();
|
|
let target = (row + 1).min(self.lines.len());
|
|
self.lines.insert(target, Line::default());
|
|
|
|
let level = self.calc_indent_level_for_pos(Pos { row: target, col: 0 });
|
|
let line = self.line_mut(target);
|
|
let mut col = 0;
|
|
for tab in std::iter::repeat_n(Grapheme::from('\t'), level) {
|
|
line.insert(0, tab);
|
|
col += 1;
|
|
}
|
|
|
|
self.cursor.pos = Pos {
|
|
row: row + 1,
|
|
col,
|
|
};
|
|
}
|
|
Anchor::Before => {
|
|
let row = self.row();
|
|
self.lines.insert(row, Line::default());
|
|
self.cursor.pos = Pos { row, col: 0 };
|
|
}
|
|
},
|
|
Verb::SwapVisualAnchor => {
|
|
let cur_pos = self.cursor.pos;
|
|
let new_anchor;
|
|
{
|
|
let Some(select) = self.select_mode.as_mut() else {
|
|
return Ok(());
|
|
};
|
|
match select {
|
|
SelectMode::Block(select_anchor)
|
|
| SelectMode::Line(select_anchor)
|
|
| SelectMode::Char(select_anchor) => {
|
|
new_anchor = *select_anchor;
|
|
*select_anchor = cur_pos;
|
|
}
|
|
}
|
|
}
|
|
|
|
self.set_cursor(new_anchor);
|
|
}
|
|
Verb::JoinLines => {
|
|
let old_exclusive = self.cursor.exclusive;
|
|
self.cursor.exclusive = false;
|
|
for _ in 0..count {
|
|
let row = self.row();
|
|
let target_pos = Pos {
|
|
row,
|
|
col: self.offset_col(row, isize::MAX),
|
|
};
|
|
if row == self.lines.len() - 1 {
|
|
break;
|
|
}
|
|
|
|
let mut next_line = self.lines.remove(row + 1).trim_start();
|
|
let this_line = self.cur_line_mut();
|
|
let this_has_ws = this_line.0.last().is_some_and(|g| g.is_ws());
|
|
let join_with_space = !this_has_ws && !this_line.is_empty() && !next_line.is_empty();
|
|
|
|
if join_with_space {
|
|
next_line.insert_char(0, ' ');
|
|
}
|
|
|
|
this_line.append(&mut next_line);
|
|
self.set_cursor(target_pos);
|
|
}
|
|
|
|
self.cursor.exclusive = old_exclusive;
|
|
}
|
|
Verb::InsertChar(ch) => {
|
|
let level = self.calc_indent_level();
|
|
self.insert(Grapheme::from(*ch));
|
|
if let Some(motion) = self.eval_motion(cmd) {
|
|
self.apply_motion(motion)?;
|
|
}
|
|
let new_level = self.calc_indent_level();
|
|
if new_level < level {
|
|
let delta = level - new_level;
|
|
let line = self.cur_line_mut();
|
|
for _ in 0..delta {
|
|
if line.0.first().is_some_and(|c| c.as_char() == Some('\t')) {
|
|
line.0.remove(0);
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Verb::Insert(s) => self.insert_str(s),
|
|
Verb::Indent | Verb::Dedent => {
|
|
let Some(motion) = self.eval_motion(cmd) else {
|
|
return Ok(());
|
|
};
|
|
let (s, e) = match motion {
|
|
MotionKind::Char { start, end, .. } => ordered(start.row, end.row),
|
|
MotionKind::Line { start, end, .. } => ordered(start, end),
|
|
MotionKind::Block { .. } => todo!(),
|
|
};
|
|
let mut col_offset = 0;
|
|
for line in self.line_iter_mut(s, e) {
|
|
match verb {
|
|
Verb::Indent => {
|
|
line.insert(0, Grapheme::from('\t'));
|
|
col_offset += 1;
|
|
}
|
|
Verb::Dedent => {
|
|
if line.0.first().is_some_and(|c| c.as_char() == Some('\t')) {
|
|
line.0.remove(0);
|
|
col_offset -= 1;
|
|
}
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
self.cursor.pos = self.cursor.pos.col_add_signed(col_offset)
|
|
}
|
|
Verb::Equalize => {
|
|
let Some(motion) = self.eval_motion(cmd) else {
|
|
return Ok(())
|
|
};
|
|
let (s,e) = match motion {
|
|
MotionKind::Char { start, end, inclusive } => ordered(start.row, end.row),
|
|
MotionKind::Line { start, end, inclusive } => ordered(start, end),
|
|
MotionKind::Block { start, end } => todo!(),
|
|
};
|
|
for row in s..=e {
|
|
let line_len = self.line(row).len();
|
|
|
|
// we are going to calculate the level twice, once at column = 0 and once at column = line.len()
|
|
// "b-b-b-b-but the performance" i dont care
|
|
// the number of tabs we use for the line is the lesser of these two calculations
|
|
// if level_start > level_end, the line has an closer
|
|
// if level_end > level_start, the line has a opener
|
|
let level_start = self.calc_indent_level_for_pos(Pos { row, col: 0 });
|
|
let level_end = self.calc_indent_level_for_pos(Pos { row, col: line_len });
|
|
let num_tabs = level_start.min(level_end);
|
|
|
|
let line = self.line_mut(row);
|
|
while line.0.first().is_some_and(|c| c.as_char() == Some('\t')) {
|
|
line.0.remove(0);
|
|
}
|
|
for tab in std::iter::repeat_n(Grapheme::from('\t'), num_tabs) {
|
|
line.insert(0, tab);
|
|
}
|
|
}
|
|
}
|
|
Verb::AcceptLineOrNewline => {
|
|
// If we are here, we did not accept the line
|
|
// so we break to a new line
|
|
self.break_line();
|
|
}
|
|
Verb::ShellCmd(cmd) => self.verb_shell_cmd(cmd)?,
|
|
Verb::Read(src) => match src {
|
|
ReadSrc::File(path_buf) => {
|
|
if !path_buf.is_file() {
|
|
write_meta(|m| m.post_system_message(format!("{} is not a file", path_buf.display())));
|
|
return Ok(());
|
|
}
|
|
let Ok(contents) = std::fs::read_to_string(path_buf) else {
|
|
write_meta(|m| {
|
|
m.post_system_message(format!("Failed to read file {}", path_buf.display()))
|
|
});
|
|
return Ok(());
|
|
};
|
|
self.insert_str(&contents);
|
|
}
|
|
ReadSrc::Cmd(cmd) => {
|
|
let output = match expand_cmd_sub(&cmd) {
|
|
Ok(out) => out,
|
|
Err(e) => {
|
|
e.print_error();
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
self.insert_str(&output);
|
|
}
|
|
},
|
|
Verb::Write(dest) => match dest {
|
|
WriteDest::FileAppend(path_buf) | WriteDest::File(path_buf) => {
|
|
let Ok(mut file) = (if matches!(dest, WriteDest::File(_)) {
|
|
OpenOptions::new()
|
|
.create(true)
|
|
.truncate(true)
|
|
.write(true)
|
|
.open(path_buf)
|
|
} else {
|
|
OpenOptions::new().create(true).append(true).open(path_buf)
|
|
}) else {
|
|
write_meta(|m| {
|
|
m.post_system_message(format!("Failed to open file {}", path_buf.display()))
|
|
});
|
|
return Ok(());
|
|
};
|
|
|
|
if let Err(e) = file.write_all(self.joined().as_bytes()) {
|
|
write_meta(|m| {
|
|
m.post_system_message(format!(
|
|
"Failed to write to file {}: {e}",
|
|
path_buf.display()
|
|
))
|
|
});
|
|
}
|
|
return Ok(());
|
|
}
|
|
WriteDest::Cmd(cmd) => {
|
|
let buf = self.joined();
|
|
let io_mode = IoMode::Buffer {
|
|
tgt_fd: STDIN_FILENO,
|
|
buf,
|
|
flags: TkFlags::IS_HEREDOC | TkFlags::LIT_HEREDOC,
|
|
};
|
|
let redir = Redir::new(io_mode, RedirType::Input);
|
|
let mut frame = IoFrame::new();
|
|
|
|
frame.push(redir);
|
|
let mut stack = IoStack::new();
|
|
stack.push_frame(frame);
|
|
|
|
exec_input(cmd.to_string(), Some(stack), false, Some("ex write".into()))?;
|
|
}
|
|
},
|
|
Verb::Edit(path) => {
|
|
if read_vars(|v| v.try_get_var("EDITOR")).is_none() {
|
|
write_meta(|m| {
|
|
m.post_system_message(
|
|
"$EDITOR is unset. Aborting edit.".into(),
|
|
)
|
|
});
|
|
} else {
|
|
let input = format!("$EDITOR {}", path.display());
|
|
exec_input(input, None, true, Some("ex edit".into()))?;
|
|
}
|
|
}
|
|
|
|
Verb::Complete
|
|
| Verb::ExMode
|
|
| Verb::EndOfFile
|
|
| Verb::InsertMode
|
|
| Verb::NormalMode
|
|
| Verb::VisualMode
|
|
| Verb::VerbatimMode
|
|
| Verb::ReplaceMode
|
|
| Verb::VisualModeLine
|
|
| Verb::VisualModeBlock
|
|
| Verb::CompleteBackward
|
|
| Verb::VisualModeSelectLast => {
|
|
let Some(motion_kind) = self.eval_motion_inner(cmd, true) else {
|
|
return Ok(());
|
|
};
|
|
self.apply_motion_inner(motion_kind, true)?;
|
|
}
|
|
Verb::Normal(_)
|
|
| Verb::Substitute(..)
|
|
| Verb::RepeatSubstitute
|
|
| Verb::Quit
|
|
| Verb::RepeatGlobal => {
|
|
log::warn!("Verb {:?} is not implemented yet", verb);
|
|
}
|
|
Verb::RepeatLast => unreachable!("Verb::RepeatLast should be handled in readline/mod.rs"),
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> {
|
|
let is_char_insert = cmd.verb.as_ref().is_some_and(|v| v.1.is_char_insert());
|
|
let starts_merge = cmd.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Change));
|
|
let is_line_motion = cmd.is_line_motion()
|
|
|| cmd
|
|
.verb
|
|
.as_ref()
|
|
.is_some_and(|v| v.1 == Verb::AcceptLineOrNewline);
|
|
let is_undo_op = cmd.is_undo_op();
|
|
let is_vertical = matches!(
|
|
cmd.motion().map(|m| &m.1),
|
|
Some(Motion::LineUp | Motion::LineDown)
|
|
);
|
|
|
|
if !is_vertical {
|
|
self.saved_col = None;
|
|
}
|
|
|
|
let before = self.lines.clone();
|
|
let old_cursor = self.cursor.pos;
|
|
|
|
// Execute the command
|
|
let res = self.exec_verb(&cmd);
|
|
|
|
if self.is_empty() {
|
|
self.set_hint(None);
|
|
}
|
|
|
|
let new_cursor = self.cursor.pos;
|
|
|
|
// Stop merging on any non-char-insert command, even if buffer didn't change
|
|
if !is_char_insert && !is_undo_op {
|
|
if let Some(edit) = self.undo_stack.last_mut() {
|
|
edit.merging = false;
|
|
}
|
|
}
|
|
|
|
if self.lines != before && !is_undo_op {
|
|
self.redo_stack.clear();
|
|
if is_char_insert {
|
|
// Merge consecutive char inserts into one undo entry
|
|
if let Some(edit) = self.undo_stack.last_mut().filter(|e| e.merging) {
|
|
edit.new = self.lines.clone();
|
|
edit.new_cursor = new_cursor;
|
|
} else {
|
|
self.undo_stack.push(Edit {
|
|
old_cursor,
|
|
new_cursor,
|
|
old: before,
|
|
new: self.lines.clone(),
|
|
merging: true,
|
|
});
|
|
}
|
|
} else {
|
|
self.handle_edit(before, new_cursor, old_cursor);
|
|
// Change starts a new merge chain so subsequent InsertChars merge into it
|
|
if starts_merge {
|
|
if let Some(edit) = self.undo_stack.last_mut() {
|
|
edit.merging = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.fix_cursor();
|
|
res
|
|
}
|
|
|
|
pub fn handle_edit(&mut self, old: Vec<Line>, new_cursor: Pos, old_cursor: Pos) {
|
|
let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging);
|
|
if edit_is_merging {
|
|
// Update the `new` snapshot on the existing edit
|
|
if let Some(edit) = self.undo_stack.last_mut() {
|
|
edit.new = self.lines.clone();
|
|
}
|
|
} else {
|
|
self.undo_stack.push(Edit {
|
|
new_cursor,
|
|
old_cursor,
|
|
old,
|
|
new: self.lines.clone(),
|
|
merging: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
pub fn fix_cursor(&mut self) {
|
|
if self.cursor.pos.row >= self.lines.len() {
|
|
self.cursor.pos.row = self.lines.len().saturating_sub(1);
|
|
}
|
|
if self.cursor.exclusive {
|
|
let line = self.cur_line();
|
|
let col = self.col();
|
|
if col > 0 && col >= line.len() {
|
|
self.cursor.pos.col = line.len().saturating_sub(1);
|
|
}
|
|
} else {
|
|
let line = self.cur_line();
|
|
let col = self.col();
|
|
if col > 0 && col > line.len() {
|
|
self.cursor.pos.col = line.len();
|
|
}
|
|
}
|
|
self.update_scroll_offset();
|
|
}
|
|
|
|
pub fn joined(&self) -> String {
|
|
let mut lines = vec![];
|
|
for line in &self.lines {
|
|
lines.push(line.to_string());
|
|
}
|
|
lines.join("\n")
|
|
}
|
|
|
|
// ───── Compatibility shims for old flat-string interface ─────
|
|
|
|
/// Compat shim: replace buffer contents from a string, parsing into lines.
|
|
pub fn set_buffer(&mut self, s: String) {
|
|
self.lines = to_lines(&s);
|
|
if self.lines.is_empty() {
|
|
self.lines.push(Line::default());
|
|
}
|
|
// Clamp cursor to valid position
|
|
self.cursor.pos.row = self.cursor.pos.row.min(self.lines.len().saturating_sub(1));
|
|
let max_col = self.lines[self.cursor.pos.row].len();
|
|
self.cursor.pos.col = self.cursor.pos.col.min(max_col);
|
|
}
|
|
|
|
/// Compat shim: set hint text. None clears the hint.
|
|
pub fn set_hint(&mut self, hint: Option<String>) {
|
|
let joined = self.joined();
|
|
self.hint = hint
|
|
.and_then(|h| h.strip_prefix(&joined).map(|s| s.to_string()))
|
|
.and_then(|h| (!h.is_empty()).then_some(to_lines(h)));
|
|
}
|
|
|
|
/// Compat shim: returns true if there is a non-empty hint.
|
|
pub fn has_hint(&self) -> bool {
|
|
self
|
|
.hint
|
|
.as_ref()
|
|
.is_some_and(|h| !h.is_empty() && h.iter().any(|l| !l.is_empty()))
|
|
}
|
|
|
|
/// Compat shim: get hint text as a string.
|
|
pub fn get_hint_text(&self) -> String {
|
|
let text = self.get_hint_text_raw();
|
|
let text = format!("\x1b[90m{text}\x1b[0m");
|
|
|
|
text.replace("\n", "\n\x1b[90m")
|
|
}
|
|
|
|
pub fn get_hint_text_raw(&self) -> String {
|
|
let mut lines = vec![];
|
|
let mut hint = self.hint.clone().unwrap_or_default();
|
|
trim_lines(&mut hint);
|
|
for line in hint {
|
|
lines.push(line.to_string());
|
|
}
|
|
lines.join("\n")
|
|
}
|
|
|
|
/// Accept hint text up to a given target position.
|
|
/// Temporarily merges the hint into the buffer, moves the cursor to target,
|
|
/// then splits: everything from cursor onward becomes the new hint.
|
|
fn accept_hint_to(&mut self, target: Pos) {
|
|
let Some(mut hint) = self.hint.take() else {
|
|
self.set_cursor(target);
|
|
return;
|
|
};
|
|
attach_lines(&mut self.lines, &mut hint);
|
|
let split_col = if self.cursor.exclusive {
|
|
target.col + 1
|
|
} else {
|
|
target.col
|
|
};
|
|
|
|
// Split after the target position so the char at target
|
|
// becomes part of the buffer (w lands ON the next word start)
|
|
let split_pos = Pos {
|
|
row: target.row,
|
|
col: target.col + 1,
|
|
};
|
|
// Clamp to buffer bounds
|
|
let split_pos = Pos {
|
|
row: split_pos.row.min(self.lines.len().saturating_sub(1)),
|
|
col: split_pos
|
|
.col
|
|
.min(self.lines[split_pos.row.min(self.lines.len().saturating_sub(1))].len()),
|
|
};
|
|
|
|
let new_hint = split_lines_at(&mut self.lines, split_pos);
|
|
self.hint =
|
|
(!new_hint.is_empty() && new_hint.iter().any(|l| !l.is_empty())).then_some(new_hint);
|
|
self.set_cursor(target);
|
|
}
|
|
|
|
/// Compat shim: accept the current hint by appending it to the buffer.
|
|
pub fn accept_hint(&mut self) {
|
|
let hint_str = self.get_hint_text_raw();
|
|
if hint_str.is_empty() {
|
|
return;
|
|
}
|
|
self.push_str(&hint_str);
|
|
self.set_cursor(Pos::MAX);
|
|
self.fix_cursor();
|
|
self.hint = None;
|
|
}
|
|
|
|
/// Compat shim: return a constructor that sets initial buffer contents and cursor.
|
|
pub fn with_initial(mut self, s: &str, cursor_pos: usize) -> Self {
|
|
self.set_buffer(s.to_string());
|
|
// In the flat model, cursor_pos was a flat offset. Map to col on row .
|
|
self.cursor.pos = Pos {
|
|
row: 0,
|
|
col: cursor_pos.min(s.len()),
|
|
};
|
|
self
|
|
}
|
|
|
|
/// Compat shim: move cursor to end of buffer.
|
|
pub fn move_cursor_to_end(&mut self) {
|
|
let last_row = self.lines.len().saturating_sub(1);
|
|
let last_col = self.lines[last_row].len();
|
|
self.cursor.pos = Pos {
|
|
row: last_row,
|
|
col: last_col,
|
|
};
|
|
}
|
|
|
|
/// Compat shim: returns the maximum cursor position (flat grapheme count).
|
|
pub fn cursor_max(&self) -> usize {
|
|
// In single-line mode this is the length of the first line
|
|
// In multi-line mode this returns total grapheme count (for flat compat)
|
|
if self.lines.len() == 1 {
|
|
self.lines[0].len()
|
|
} else {
|
|
self.count_graphemes()
|
|
}
|
|
}
|
|
|
|
/// Compat shim: returns true if cursor is at the max position.
|
|
pub fn cursor_at_max(&self) -> bool {
|
|
let last_row = self.lines.len().saturating_sub(1);
|
|
let max = if self.cursor.exclusive {
|
|
self.lines[last_row].len().saturating_sub(1)
|
|
} else {
|
|
self.lines[last_row].len()
|
|
};
|
|
self.cursor.pos.row == last_row && self.cursor.pos.col >= max
|
|
}
|
|
|
|
/// Compat shim: set cursor with clamping.
|
|
pub fn set_cursor_clamp(&mut self, exclusive: bool) {
|
|
self.cursor.exclusive = exclusive;
|
|
}
|
|
|
|
/// Compat shim: returns the flat column of the start of the current line.
|
|
/// In the old flat model this returned 0 for single-line; for multi-line it's the
|
|
/// flat offset of the beginning of the current row.
|
|
pub fn start_of_line(&self) -> usize {
|
|
// Return 0-based flat offset of start of current row
|
|
let mut offset = 0;
|
|
for i in 0..self.cursor.pos.row {
|
|
offset += self.lines[i].len() + 1; // +1 for '\n'
|
|
}
|
|
offset
|
|
}
|
|
|
|
pub fn on_last_line(&self) -> bool {
|
|
self.cursor.pos.row == self.lines.len().saturating_sub(1)
|
|
}
|
|
|
|
/// Compat shim: returns slice of joined buffer from grapheme indices.
|
|
pub fn slice(&self, range: std::ops::Range<usize>) -> Option<String> {
|
|
let joined = self.joined();
|
|
let graphemes: Vec<&str> = joined.graphemes(true).collect();
|
|
if range.start > graphemes.len() || range.end > graphemes.len() {
|
|
return None;
|
|
}
|
|
Some(graphemes[range].join(""))
|
|
}
|
|
|
|
/// Compat shim: returns the string from buffer start to cursor position.
|
|
pub fn slice_to_cursor(&self) -> Option<String> {
|
|
let mut result = String::new();
|
|
for i in 0..self.cursor.pos.row {
|
|
result.push_str(&self.lines[i].to_string());
|
|
result.push('\n');
|
|
}
|
|
let line = &self.lines[self.cursor.pos.row];
|
|
let col = self.cursor.pos.col.min(line.len());
|
|
for g in &line.graphemes()[..col] {
|
|
result.push_str(&g.to_string());
|
|
}
|
|
Some(result)
|
|
}
|
|
|
|
/// Compat shim: returns cursor byte position in the joined string.
|
|
pub fn cursor_byte_pos(&self) -> usize {
|
|
let mut pos = 0;
|
|
for i in 0..self.cursor.pos.row {
|
|
pos += self.lines[i].to_string().len() + 1; // +1 for '\n'
|
|
}
|
|
let line_str = self.lines[self.cursor.pos.row].to_string();
|
|
let col = self
|
|
.cursor
|
|
.pos
|
|
.col
|
|
.min(self.lines[self.cursor.pos.row].len());
|
|
// Sum bytes of graphemes up to col
|
|
let mut byte_count = 0;
|
|
for (i, g) in line_str.graphemes(true).enumerate() {
|
|
if i >= col {
|
|
break;
|
|
}
|
|
byte_count += g.len();
|
|
}
|
|
pos + byte_count
|
|
}
|
|
|
|
pub fn start_char_select(&mut self) {
|
|
self.select_mode = Some(SelectMode::Char(self.cursor.pos));
|
|
}
|
|
|
|
pub fn start_line_select(&mut self) {
|
|
self.select_mode = Some(SelectMode::Line(self.cursor.pos));
|
|
}
|
|
|
|
pub fn start_block_select(&mut self) {
|
|
self.select_mode = Some(SelectMode::Block(self.cursor.pos));
|
|
}
|
|
|
|
/// Compat shim: stop visual selection.
|
|
pub fn stop_selecting(&mut self) {
|
|
if self.select_mode.is_some() {
|
|
self.last_selection = self.select_mode.map(|m| {
|
|
let anchor = match m {
|
|
SelectMode::Char(a) | SelectMode::Block(a) | SelectMode::Line(a) => a,
|
|
};
|
|
(m, anchor)
|
|
});
|
|
}
|
|
self.select_mode = None;
|
|
}
|
|
|
|
pub fn select_range(&self) -> Option<Motion> {
|
|
let mode = self.select_mode.as_ref()?;
|
|
match mode {
|
|
SelectMode::Char(pos) => {
|
|
let (s, e) = ordered(self.cursor.pos, *pos);
|
|
Some(Motion::CharRange(s, e))
|
|
}
|
|
SelectMode::Line(pos) => {
|
|
let (s, e) = ordered(self.row(), pos.row);
|
|
Some(Motion::LineRange(s, e))
|
|
}
|
|
SelectMode::Block(pos) => {
|
|
let (s, e) = ordered(self.cursor.pos, *pos);
|
|
Some(Motion::BlockRange(s, e))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Helper: convert a Pos to a flat grapheme offset.
|
|
fn pos_to_flat(&self, pos: Pos) -> usize {
|
|
let mut offset = 0;
|
|
let row = pos.row.min(self.lines.len().saturating_sub(1));
|
|
for i in 0..row {
|
|
offset += self.lines[i].len() + 1; // +1 for '\n'
|
|
}
|
|
offset + pos.col.min(self.lines[row].len())
|
|
}
|
|
|
|
fn pos_from_flat(&self, mut flat: usize) -> Pos {
|
|
for (i, line) in self.lines.iter().enumerate() {
|
|
if flat <= line.len() {
|
|
return Pos { row: i, col: flat };
|
|
}
|
|
flat = flat.saturating_sub(line.len() + 1); // +1 for '\n'
|
|
}
|
|
// If we exceed the total length, clamp to end
|
|
let last_row = self.lines.len().saturating_sub(1);
|
|
let last_col = self.lines[last_row].len();
|
|
Pos { row: last_row, col: last_col }
|
|
}
|
|
|
|
pub fn cursor_to_flat(&self) -> usize {
|
|
self.pos_to_flat(self.cursor.pos)
|
|
}
|
|
|
|
pub fn set_cursor_from_flat(&mut self, flat: usize) {
|
|
self.cursor.pos = self.pos_from_flat(flat);
|
|
self.fix_cursor();
|
|
}
|
|
|
|
/// Compat shim: attempt history expansion. Stub that returns false.
|
|
pub fn attempt_history_expansion(&mut self, _history: &super::history::History) -> bool {
|
|
// TODO: implement history expansion for 2D buffer
|
|
false
|
|
}
|
|
|
|
/// Compat shim: check if cursor is on an escaped char.
|
|
pub fn cursor_is_escaped(&self) -> bool {
|
|
if self.cursor.pos.col == 0 {
|
|
return false;
|
|
}
|
|
let line = &self.lines[self.cursor.pos.row];
|
|
if self.cursor.pos.col > line.len() {
|
|
return false;
|
|
}
|
|
line
|
|
.graphemes()
|
|
.get(self.cursor.pos.col.saturating_sub(1))
|
|
.is_some_and(|g| g.is_char('\\'))
|
|
}
|
|
|
|
/// Compat shim: take buffer contents and reset.
|
|
pub fn take_buf(&mut self) -> String {
|
|
let result = self.joined();
|
|
self.lines = vec![Line::default()];
|
|
self.cursor.pos = Pos { row: 0, col: 0 };
|
|
result
|
|
}
|
|
|
|
/// Compat shim: mark where insert mode started.
|
|
pub fn mark_insert_mode_start_pos(&mut self) {
|
|
self.insert_mode_start_pos = Some(self.cursor.pos);
|
|
}
|
|
|
|
/// Compat shim: clear insert mode start position.
|
|
pub fn clear_insert_mode_start_pos(&mut self) {
|
|
self.insert_mode_start_pos = None;
|
|
}
|
|
}
|
|
|
|
impl Display for LineBuf {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
if let Some(select) = self.select_mode.as_ref() {
|
|
let mut cloned = self.lines.clone();
|
|
|
|
match select {
|
|
SelectMode::Char(pos) => {
|
|
let (s, e) = ordered(self.cursor.pos, *pos);
|
|
if s.row == e.row {
|
|
// Same line: insert end first to avoid shifting start index
|
|
let line = &mut cloned[s.row];
|
|
if e.col + 1 >= line.len() {
|
|
line.push_char(markers::VISUAL_MODE_END);
|
|
} else {
|
|
line.insert(e.col + 1, markers::VISUAL_MODE_END.into());
|
|
}
|
|
line.insert(s.col, markers::VISUAL_MODE_START.into());
|
|
} else {
|
|
// Start line: highlight from s.col to end
|
|
cloned[s.row].insert(s.col, markers::VISUAL_MODE_START.into());
|
|
cloned[s.row].push_char(markers::VISUAL_MODE_END);
|
|
|
|
// Middle lines: fully highlighted
|
|
for row in cloned.iter_mut().skip(s.row + 1).take(e.row - s.row - 1) {
|
|
row.insert(0, markers::VISUAL_MODE_START.into());
|
|
row.push_char(markers::VISUAL_MODE_END);
|
|
}
|
|
|
|
// End line: highlight from start to e.col
|
|
let end_line = &mut cloned[e.row];
|
|
if e.col + 1 >= end_line.len() {
|
|
end_line.push_char(markers::VISUAL_MODE_END);
|
|
} else {
|
|
end_line.insert(e.col + 1, markers::VISUAL_MODE_END.into());
|
|
}
|
|
end_line.insert(0, markers::VISUAL_MODE_START.into());
|
|
}
|
|
}
|
|
SelectMode::Line(pos) => {
|
|
let (s, e) = ordered(self.row(), pos.row);
|
|
for row in cloned.iter_mut().take(e + 1).skip(s) {
|
|
row.insert(0, markers::VISUAL_MODE_START.into());
|
|
}
|
|
cloned[e].push_char(markers::VISUAL_MODE_END);
|
|
}
|
|
SelectMode::Block(pos) => todo!(),
|
|
}
|
|
let mut lines = vec![];
|
|
for line in &cloned {
|
|
lines.push(line.to_string());
|
|
}
|
|
let joined = lines.join("\n");
|
|
write!(f, "{joined}")
|
|
} else {
|
|
write!(f, "{}", self.joined())
|
|
}
|
|
}
|
|
}
|
|
|
|
struct CharClassIter<'a> {
|
|
lines: &'a [Line],
|
|
row: usize,
|
|
col: usize,
|
|
exhausted: bool,
|
|
at_boundary: bool,
|
|
}
|
|
|
|
impl<'a> CharClassIter<'a> {
|
|
pub fn new(lines: &'a [Line], start_pos: Pos) -> Self {
|
|
Self {
|
|
lines,
|
|
row: start_pos.row,
|
|
col: start_pos.col,
|
|
exhausted: false,
|
|
at_boundary: false,
|
|
}
|
|
}
|
|
fn get_pos(&self) -> Pos {
|
|
Pos {
|
|
row: self.row,
|
|
col: self.col,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> Iterator for CharClassIter<'a> {
|
|
type Item = (Pos, CharClass);
|
|
fn next(&mut self) -> Option<(Pos, CharClass)> {
|
|
if self.exhausted {
|
|
return None;
|
|
}
|
|
|
|
// Synthetic whitespace for line boundary
|
|
if self.at_boundary {
|
|
self.at_boundary = false;
|
|
let pos = self.get_pos();
|
|
return Some((pos, CharClass::Whitespace));
|
|
}
|
|
|
|
if self.row >= self.lines.len() {
|
|
self.exhausted = true;
|
|
return None;
|
|
}
|
|
|
|
if self.row >= self.lines.len() {
|
|
self.exhausted = true;
|
|
return None;
|
|
}
|
|
|
|
let line = &self.lines[self.row];
|
|
// Empty line = whitespace
|
|
if line.is_empty() {
|
|
let pos = Pos {
|
|
row: self.row,
|
|
col: 0,
|
|
};
|
|
self.row += 1;
|
|
self.col = 0;
|
|
return Some((pos, CharClass::Whitespace));
|
|
}
|
|
|
|
if self.col >= line.len() {
|
|
self.row += 1;
|
|
self.col = 0;
|
|
self.at_boundary = self.row < self.lines.len();
|
|
return self.next();
|
|
}
|
|
|
|
let pos = self.get_pos();
|
|
let class = line[self.col].class();
|
|
|
|
self.col += 1;
|
|
if self.col >= line.len() {
|
|
self.row += 1;
|
|
self.col = 0;
|
|
self.at_boundary = self.row < self.lines.len();
|
|
}
|
|
|
|
Some((pos, class))
|
|
}
|
|
}
|
|
|
|
struct CharClassIterRev<'a> {
|
|
lines: &'a [Line],
|
|
row: usize,
|
|
col: usize,
|
|
exhausted: bool,
|
|
at_boundary: bool,
|
|
}
|
|
|
|
impl<'a> CharClassIterRev<'a> {
|
|
pub fn new(lines: &'a [Line], start_pos: Pos) -> Self {
|
|
let row = start_pos.row.min(lines.len().saturating_sub(1));
|
|
let col = if lines.is_empty() || lines[row].is_empty() {
|
|
0
|
|
} else {
|
|
start_pos.col.min(lines[row].len().saturating_sub(1))
|
|
};
|
|
Self {
|
|
lines,
|
|
row,
|
|
col,
|
|
exhausted: false,
|
|
at_boundary: false,
|
|
}
|
|
}
|
|
fn get_pos(&self) -> Pos {
|
|
Pos {
|
|
row: self.row,
|
|
col: self.col,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> Iterator for CharClassIterRev<'a> {
|
|
type Item = (Pos, CharClass);
|
|
fn next(&mut self) -> Option<(Pos, CharClass)> {
|
|
if self.exhausted {
|
|
return None;
|
|
}
|
|
|
|
// Synthetic whitespace for line boundary
|
|
if self.at_boundary {
|
|
self.at_boundary = false;
|
|
let pos = self.get_pos();
|
|
return Some((pos, CharClass::Whitespace));
|
|
}
|
|
|
|
if self.row >= self.lines.len() {
|
|
self.exhausted = true;
|
|
return None;
|
|
}
|
|
|
|
let line = &self.lines[self.row];
|
|
// Empty line = whitespace
|
|
if line.is_empty() {
|
|
let pos = Pos {
|
|
row: self.row,
|
|
col: 0,
|
|
};
|
|
if self.row == 0 {
|
|
self.exhausted = true;
|
|
} else {
|
|
self.row -= 1;
|
|
self.col = self.lines[self.row].len().saturating_sub(1);
|
|
}
|
|
return Some((pos, CharClass::Whitespace));
|
|
}
|
|
|
|
let pos = self.get_pos();
|
|
let class = line[self.col].class();
|
|
|
|
if self.col == 0 {
|
|
if self.row == 0 {
|
|
self.exhausted = true;
|
|
} else {
|
|
self.row -= 1;
|
|
self.col = self.lines[self.row].len().saturating_sub(1);
|
|
self.at_boundary = true;
|
|
}
|
|
} else {
|
|
self.col -= 1;
|
|
}
|
|
|
|
Some((pos, class))
|
|
}
|
|
}
|
|
|
|
/// Rotate alphabetic characters by 13 alphabetic positions
|
|
pub fn rot13(input: &str) -> String {
|
|
input
|
|
.chars()
|
|
.map(|c| {
|
|
if c.is_ascii_lowercase() {
|
|
let offset = b'a';
|
|
(((c as u8 - offset + 13) % 26) + offset) as char
|
|
} else if c.is_ascii_uppercase() {
|
|
let offset = b'A';
|
|
(((c as u8 - offset + 13) % 26) + offset) as char
|
|
} else {
|
|
c
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub fn rot13_char(c: char) -> char {
|
|
let offset = if c.is_ascii_lowercase() {
|
|
b'a'
|
|
} else if c.is_ascii_uppercase() {
|
|
b'A'
|
|
} else {
|
|
return c;
|
|
};
|
|
(((c as u8 - offset + 13) % 26) + offset) as char
|
|
}
|
|
|
|
pub fn toggle_case_char(c: char) -> char {
|
|
if c.is_ascii_lowercase() {
|
|
c.to_ascii_uppercase()
|
|
} else if c.is_ascii_uppercase() {
|
|
c.to_ascii_lowercase()
|
|
} else {
|
|
c
|
|
}
|
|
}
|
|
|
|
pub fn ordered<T: Ord>(start: T, end: T) -> (T, T) {
|
|
if start > end {
|
|
(end, start)
|
|
} else {
|
|
(start, end)
|
|
}
|
|
}
|