2082 lines
58 KiB
Rust
2082 lines
58 KiB
Rust
use std::{cmp::Ordering, fmt::Display, ops::{Range, RangeBounds}};
|
|
|
|
use unicode_segmentation::UnicodeSegmentation;
|
|
use unicode_width::UnicodeWidthStr;
|
|
|
|
use crate::libsh::{error::ShResult, sys::sh_quit, term::{Style, Styled}};
|
|
use crate::prelude::*;
|
|
|
|
use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, RegisterName, TextObj, To, Verb, ViCmd, Word};
|
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub enum CharClass {
|
|
Alphanum,
|
|
Symbol,
|
|
Whitespace,
|
|
Other
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum MotionKind {
|
|
Forward(usize),
|
|
To(usize), // Land just before
|
|
On(usize), // Land directly on
|
|
Before(usize), // Had to make a separate one for char searches, for some reason
|
|
Backward(usize),
|
|
Range((usize,usize)),
|
|
Line(isize), // positive = up line, negative = down line
|
|
ToLine(usize),
|
|
Null,
|
|
|
|
/// Absolute position based on display width of characters
|
|
/// Factors in the length of the prompt, and skips newlines
|
|
ScreenLine(isize)
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
|
pub enum SelectionAnchor {
|
|
Start,
|
|
#[default]
|
|
End
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum SelectionMode {
|
|
Char(SelectionAnchor),
|
|
Line(SelectionAnchor),
|
|
Block(SelectionAnchor)
|
|
}
|
|
|
|
impl Default for SelectionMode {
|
|
fn default() -> Self {
|
|
Self::Char(Default::default())
|
|
}
|
|
}
|
|
|
|
impl SelectionMode {
|
|
pub fn anchor(&self) -> &SelectionAnchor {
|
|
match self {
|
|
SelectionMode::Char(anchor) |
|
|
SelectionMode::Line(anchor) |
|
|
SelectionMode::Block(anchor) => anchor
|
|
}
|
|
}
|
|
pub fn invert_anchor(&mut self) {
|
|
match self {
|
|
SelectionMode::Char(anchor) |
|
|
SelectionMode::Line(anchor) |
|
|
SelectionMode::Block(anchor) => {
|
|
*anchor = match anchor {
|
|
SelectionAnchor::Start => SelectionAnchor::End,
|
|
SelectionAnchor::End => SelectionAnchor::Start
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl MotionKind {
|
|
pub fn range<R: RangeBounds<usize>>(range: R) -> Self {
|
|
let start = match range.start_bound() {
|
|
std::ops::Bound::Included(&start) => start,
|
|
std::ops::Bound::Excluded(&start) => start + 1,
|
|
std::ops::Bound::Unbounded => 0
|
|
};
|
|
let end = match range.end_bound() {
|
|
std::ops::Bound::Included(&end) => end,
|
|
std::ops::Bound::Excluded(&end) => end + 1,
|
|
std::ops::Bound::Unbounded => panic!("called range constructor with no upper bound")
|
|
};
|
|
if end > start {
|
|
Self::Range((start,end))
|
|
} else {
|
|
Self::Range((end,start))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<&str> for CharClass {
|
|
fn from(value: &str) -> Self {
|
|
if value.len() > 1 {
|
|
return Self::Symbol // Multi-byte grapheme
|
|
}
|
|
|
|
if value.chars().all(char::is_alphanumeric) {
|
|
CharClass::Alphanum
|
|
} else if value.chars().all(char::is_whitespace) {
|
|
CharClass::Whitespace
|
|
} else if !value.chars().all(char::is_alphanumeric) {
|
|
CharClass::Symbol
|
|
} else {
|
|
Self::Other
|
|
}
|
|
}
|
|
}
|
|
|
|
fn is_whitespace(a: &str) -> bool {
|
|
CharClass::from(a) == CharClass::Whitespace
|
|
}
|
|
|
|
fn is_other_class(a: &str, b: &str) -> bool {
|
|
let a = CharClass::from(a);
|
|
let b = CharClass::from(b);
|
|
a != b
|
|
}
|
|
|
|
fn is_other_class_or_ws(a: &str, b: &str) -> bool {
|
|
if is_whitespace(a) || is_whitespace(b) {
|
|
true
|
|
} else {
|
|
is_other_class(a, b)
|
|
}
|
|
}
|
|
|
|
#[derive(Default,Debug)]
|
|
pub struct Edit {
|
|
pub pos: usize,
|
|
pub cursor_pos: usize,
|
|
pub old: String,
|
|
pub new: String,
|
|
pub merging: bool,
|
|
}
|
|
|
|
impl Edit {
|
|
pub fn diff(a: &str, b: &str, old_cursor_pos: usize) -> Edit {
|
|
use std::cmp::min;
|
|
|
|
let mut start = 0;
|
|
let max_start = min(a.len(), b.len());
|
|
|
|
// Calculate the prefix of the edit
|
|
while start < max_start && a.as_bytes()[start] == b.as_bytes()[start] {
|
|
start += 1;
|
|
}
|
|
|
|
if start == a.len() && start == b.len() {
|
|
return Edit {
|
|
pos: start,
|
|
cursor_pos: old_cursor_pos,
|
|
old: String::new(),
|
|
new: String::new(),
|
|
merging: false,
|
|
};
|
|
}
|
|
|
|
let mut end_a = a.len();
|
|
let mut end_b = b.len();
|
|
|
|
// Calculate the suffix of the edit
|
|
while end_a > start && end_b > start && a.as_bytes()[end_a - 1] == b.as_bytes()[end_b - 1] {
|
|
end_a -= 1;
|
|
end_b -= 1;
|
|
}
|
|
|
|
// Slice off the prefix and suffix for both (safe because start/end are byte offsets)
|
|
let old = a[start..end_a].to_string();
|
|
let new = b[start..end_b].to_string();
|
|
|
|
Edit {
|
|
pos: start,
|
|
cursor_pos: old_cursor_pos,
|
|
old,
|
|
new,
|
|
merging: false
|
|
}
|
|
}
|
|
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.new.is_empty() &&
|
|
self.old.is_empty()
|
|
}
|
|
}
|
|
|
|
#[derive(Default,Debug)]
|
|
pub struct LineBuf {
|
|
buffer: String,
|
|
hint: Option<String>,
|
|
cursor: usize,
|
|
clamp_cursor: bool,
|
|
select_mode: Option<SelectionMode>,
|
|
selected_range: Option<Range<usize>>,
|
|
last_selected_range: Option<Range<usize>>,
|
|
first_line_offset: usize,
|
|
saved_col: Option<usize>,
|
|
term_dims: (usize,usize), // Height, width
|
|
move_cursor_on_undo: bool,
|
|
undo_stack: Vec<Edit>,
|
|
redo_stack: Vec<Edit>,
|
|
tab_stop: usize
|
|
}
|
|
|
|
impl LineBuf {
|
|
pub fn new() -> Self {
|
|
Self { tab_stop: 8, ..Default::default() }
|
|
}
|
|
pub fn with_initial(mut self, initial: &str) -> Self {
|
|
self.buffer = initial.to_string();
|
|
self
|
|
}
|
|
pub fn selected_range(&self) -> Option<&Range<usize>> {
|
|
self.selected_range.as_ref()
|
|
}
|
|
pub fn is_selecting(&self) -> bool {
|
|
self.select_mode.is_some()
|
|
}
|
|
pub fn stop_selecting(&mut self) {
|
|
self.select_mode = None;
|
|
if self.selected_range().is_some() {
|
|
self.last_selected_range = self.selected_range.take();
|
|
}
|
|
}
|
|
pub fn start_selecting(&mut self, mode: SelectionMode) {
|
|
self.select_mode = Some(mode);
|
|
self.selected_range = Some(self.cursor..(self.cursor + 1).min(self.byte_len().saturating_sub(1)))
|
|
}
|
|
pub fn has_hint(&self) -> bool {
|
|
self.hint.is_some()
|
|
}
|
|
pub fn set_hint(&mut self, hint: Option<String>) {
|
|
self.hint = hint
|
|
}
|
|
pub fn set_first_line_offset(&mut self, offset: usize) {
|
|
self.first_line_offset = offset
|
|
}
|
|
pub fn as_str(&self) -> &str {
|
|
&self.buffer
|
|
}
|
|
pub fn saved_col(&self) -> Option<usize> {
|
|
self.saved_col
|
|
}
|
|
pub fn update_term_dims(&mut self, dims: (usize,usize)) {
|
|
self.term_dims = dims
|
|
}
|
|
pub fn take(&mut self) -> String {
|
|
let line = std::mem::take(&mut self.buffer);
|
|
*self = Self::default();
|
|
line
|
|
}
|
|
pub fn byte_pos(&self) -> usize {
|
|
self.cursor
|
|
}
|
|
pub fn byte_len(&self) -> usize {
|
|
self.buffer.len()
|
|
}
|
|
pub fn at_end_of_buffer(&self) -> bool {
|
|
if self.clamp_cursor {
|
|
self.cursor == self.byte_len().saturating_sub(1)
|
|
} else {
|
|
self.cursor == self.byte_len()
|
|
}
|
|
}
|
|
pub fn undos(&self) -> usize {
|
|
self.undo_stack.len()
|
|
}
|
|
pub fn is_empty(&self) -> bool {
|
|
self.buffer.is_empty()
|
|
}
|
|
pub fn set_move_cursor_on_undo(&mut self, yn: bool) {
|
|
self.move_cursor_on_undo = yn;
|
|
}
|
|
pub fn clamp_cursor(&mut self) {
|
|
// Normal mode does not allow you to sit on the edge of the buffer, you must be hovering over a character
|
|
// Insert mode does let you set on the edge though, so that you can append new characters
|
|
// This method is used in Normal mode
|
|
if self.cursor == self.byte_len() || self.grapheme_at_cursor() == Some("\n") {
|
|
self.cursor_back(1);
|
|
}
|
|
}
|
|
pub fn clamp_range(&self, range: Range<usize>) -> Range<usize> {
|
|
let (mut start,mut end) = (range.start,range.end);
|
|
start = start.max(0);
|
|
end = end.min(self.byte_len());
|
|
start..end
|
|
}
|
|
pub fn grapheme_len(&self) -> usize {
|
|
self.buffer.grapheme_indices(true).count()
|
|
}
|
|
pub fn slice_from_cursor(&self) -> &str {
|
|
if let Some(slice) = &self.buffer.get(self.cursor..) {
|
|
slice
|
|
} else {
|
|
""
|
|
}
|
|
}
|
|
pub fn slice_to_cursor(&self) -> &str {
|
|
if let Some(slice) = self.buffer.get(..self.cursor) {
|
|
slice
|
|
} else {
|
|
&self.buffer
|
|
}
|
|
|
|
}
|
|
pub fn into_line(self) -> String {
|
|
self.buffer
|
|
}
|
|
pub fn slice_from_cursor_to_end_of_line(&self) -> &str {
|
|
let end = self.end_of_line();
|
|
&self.buffer[self.cursor..end]
|
|
}
|
|
pub fn slice_from_start_of_line_to_cursor(&self) -> &str {
|
|
let start = self.start_of_line();
|
|
&self.buffer[start..self.cursor]
|
|
}
|
|
pub fn slice_from(&self, pos: usize) -> &str {
|
|
&self.buffer[pos..]
|
|
}
|
|
pub fn slice_to(&self, pos: usize) -> &str {
|
|
&self.buffer[..pos]
|
|
}
|
|
pub fn set_cursor_clamp(&mut self, yn: bool) {
|
|
self.clamp_cursor = yn
|
|
}
|
|
pub fn g_idx_to_byte_pos(&self, pos: usize) -> Option<usize> {
|
|
if pos >= self.byte_len() {
|
|
None
|
|
} else {
|
|
self.buffer.grapheme_indices(true).map(|(i,_)| i).nth(pos)
|
|
}
|
|
}
|
|
pub fn grapheme_at_cursor(&self) -> Option<&str> {
|
|
if self.cursor == self.byte_len() {
|
|
None
|
|
} else {
|
|
self.slice_from_cursor().graphemes(true).next()
|
|
}
|
|
}
|
|
pub fn grapheme_at_cursor_offset(&self, offset: isize) -> Option<&str> {
|
|
match offset.cmp(&0) {
|
|
Ordering::Equal => {
|
|
self.grapheme_at(self.cursor)
|
|
}
|
|
Ordering::Less => {
|
|
// Walk backward from the start of the line or buffer up to the cursor
|
|
// and count graphemes in reverse.
|
|
let rev_graphemes: Vec<&str> = self.slice_to_cursor().graphemes(true).collect();
|
|
let idx = rev_graphemes.len().checked_sub((-offset) as usize)?;
|
|
rev_graphemes.get(idx).copied()
|
|
}
|
|
Ordering::Greater => {
|
|
self.slice_from_cursor()
|
|
.graphemes(true)
|
|
.nth(offset as usize)
|
|
}
|
|
}
|
|
}
|
|
pub fn grapheme_at(&self, pos: usize) -> Option<&str> {
|
|
if pos >= self.byte_len() {
|
|
None
|
|
} else {
|
|
self.buffer.graphemes(true).nth(pos)
|
|
}
|
|
}
|
|
pub fn is_whitespace(&self, pos: usize) -> bool {
|
|
let Some(g) = self.grapheme_at(pos) else {
|
|
return false
|
|
};
|
|
g.chars().all(char::is_whitespace)
|
|
}
|
|
pub fn on_whitespace(&self) -> bool {
|
|
self.is_whitespace(self.cursor)
|
|
}
|
|
pub fn next_pos(&self, n: usize) -> Option<usize> {
|
|
if self.cursor == self.byte_len() {
|
|
None
|
|
} else {
|
|
self.slice_from_cursor()
|
|
.grapheme_indices(true)
|
|
.take(n)
|
|
.last()
|
|
.map(|(i,s)| i + self.cursor + s.len())
|
|
}
|
|
}
|
|
pub fn prev_pos(&self, n: usize) -> Option<usize> {
|
|
if self.cursor == 0 {
|
|
None
|
|
} else {
|
|
self.slice_to_cursor()
|
|
.grapheme_indices(true)
|
|
.rev() // <- walk backward
|
|
.take(n)
|
|
.last()
|
|
.map(|(i, _)| i)
|
|
}
|
|
}
|
|
pub fn sync_cursor(&mut self) {
|
|
if !self.buffer.is_char_boundary(self.cursor) {
|
|
self.cursor = self.prev_pos(1).unwrap_or(0)
|
|
}
|
|
}
|
|
pub fn cursor_back(&mut self, dist: usize) -> bool {
|
|
let Some(pos) = self.prev_pos(dist) else {
|
|
return false
|
|
};
|
|
self.cursor = pos;
|
|
true
|
|
}
|
|
/// Constrain the cursor to the current line
|
|
pub fn cursor_back_confined(&mut self, dist: usize) -> bool {
|
|
for _ in 0..dist {
|
|
let Some(pos) = self.prev_pos(1) else {
|
|
return false
|
|
};
|
|
if let Some("\n") = self.grapheme_at(pos) {
|
|
return false
|
|
}
|
|
if !self.cursor_back(1) {
|
|
return false
|
|
}
|
|
}
|
|
true
|
|
}
|
|
pub fn cursor_fwd_confined(&mut self, dist: usize) -> bool {
|
|
for _ in 0..dist {
|
|
let Some(pos) = self.next_pos(1) else {
|
|
return false
|
|
};
|
|
if let Some("\n") = self.grapheme_at(pos) {
|
|
return false
|
|
}
|
|
if !self.cursor_fwd(1) {
|
|
return false
|
|
}
|
|
}
|
|
true
|
|
}
|
|
/// Up to but not including 'dist'
|
|
pub fn cursor_back_to(&mut self, dist: usize) -> bool {
|
|
let dist = dist.saturating_sub(1);
|
|
let Some(pos) = self.prev_pos(dist) else {
|
|
return false
|
|
};
|
|
self.cursor = pos;
|
|
true
|
|
}
|
|
pub fn cursor_fwd(&mut self, dist: usize) -> bool {
|
|
let Some(pos) = self.next_pos(dist) else {
|
|
return false
|
|
};
|
|
self.cursor = pos;
|
|
true
|
|
}
|
|
pub fn cursor_fwd_to(&mut self, dist: usize) -> bool {
|
|
let dist = dist.saturating_sub(1);
|
|
let Some(pos) = self.next_pos(dist) else {
|
|
return false
|
|
};
|
|
self.cursor = pos;
|
|
true
|
|
}
|
|
|
|
fn compute_display_positions<'a>(
|
|
text: impl Iterator<Item = &'a str>,
|
|
start_col: usize,
|
|
tab_stop: usize,
|
|
term_width: usize,
|
|
) -> (usize, usize) {
|
|
let mut lines = 0;
|
|
let mut col = start_col;
|
|
|
|
for grapheme in text {
|
|
match grapheme {
|
|
"\n" => {
|
|
lines += 1;
|
|
col = 1;
|
|
}
|
|
"\t" => {
|
|
let spaces_to_next_tab = tab_stop - (col % tab_stop);
|
|
if col + spaces_to_next_tab > term_width {
|
|
lines += 1;
|
|
col = 1;
|
|
} else {
|
|
col += spaces_to_next_tab;
|
|
}
|
|
|
|
// Don't ask why this is here
|
|
// I don't know either
|
|
// All I know is that it only finds the correct cursor position
|
|
// if i add one to the column here, for literally no reason
|
|
// Thank you linux terminal :)
|
|
col += 1;
|
|
}
|
|
_ => {
|
|
col += grapheme.width();
|
|
if col > term_width {
|
|
lines += 1;
|
|
col = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
(lines, col)
|
|
}
|
|
pub fn count_display_lines(&self, offset: usize, term_width: usize) -> usize {
|
|
let (lines, _) = Self::compute_display_positions(
|
|
self.buffer.graphemes(true),
|
|
offset.max(1),
|
|
self.tab_stop,
|
|
term_width,
|
|
);
|
|
lines
|
|
}
|
|
|
|
pub fn cursor_display_line_position(&self, offset: usize, term_width: usize) -> usize {
|
|
let (lines, _) = Self::compute_display_positions(
|
|
self.slice_to_cursor().graphemes(true),
|
|
offset.max(1),
|
|
self.tab_stop,
|
|
term_width,
|
|
);
|
|
lines
|
|
}
|
|
|
|
pub fn display_coords(&self, term_width: usize) -> (usize, usize) {
|
|
Self::compute_display_positions(
|
|
self.slice_to_cursor().graphemes(true),
|
|
0,
|
|
self.tab_stop,
|
|
term_width,
|
|
)
|
|
}
|
|
|
|
pub fn cursor_display_coords(&self, term_width: usize) -> (usize, usize) {
|
|
let (d_line, mut d_col) = self.display_coords(term_width);
|
|
let total_lines = self.count_display_lines(self.first_line_offset, term_width);
|
|
let logical_line = total_lines - d_line;
|
|
|
|
if logical_line == self.count_lines() {
|
|
d_col += self.first_line_offset;
|
|
}
|
|
|
|
(logical_line, d_col)
|
|
}
|
|
pub fn insert(&mut self, ch: char) {
|
|
if self.buffer.is_empty() {
|
|
self.buffer.push(ch)
|
|
} else {
|
|
self.buffer.insert(self.cursor, ch);
|
|
}
|
|
}
|
|
pub fn move_to(&mut self, pos: usize) -> bool {
|
|
if self.cursor == pos {
|
|
false
|
|
} else {
|
|
self.cursor = pos;
|
|
true
|
|
}
|
|
}
|
|
pub fn move_buf_start(&mut self) -> bool {
|
|
self.move_to(0)
|
|
}
|
|
pub fn move_buf_end(&mut self) -> bool {
|
|
if self.clamp_cursor {
|
|
self.move_to(self.byte_len().saturating_sub(1))
|
|
} else {
|
|
self.move_to(self.byte_len())
|
|
}
|
|
}
|
|
pub fn move_home(&mut self) -> bool {
|
|
let start = self.start_of_line();
|
|
self.move_to(start)
|
|
}
|
|
pub fn move_end(&mut self) -> bool {
|
|
let end = self.end_of_line();
|
|
self.move_to(end)
|
|
}
|
|
/// Consume the LineBuf and return the buffer
|
|
pub fn pack_line(self) -> String {
|
|
self.buffer
|
|
}
|
|
pub fn accept_hint(&mut self) {
|
|
if let Some(hint) = self.hint.take() {
|
|
flog!(DEBUG, "accepting hint");
|
|
let old_buf = self.buffer.clone();
|
|
self.buffer.push_str(&hint);
|
|
let new_buf = self.buffer.clone();
|
|
self.handle_edit(old_buf, new_buf, self.cursor);
|
|
self.move_buf_end();
|
|
}
|
|
}
|
|
pub fn accept_hint_partial(&mut self, accept_to: usize) {
|
|
if let Some(hint) = self.hint.take() {
|
|
let accepted = &hint[..accept_to];
|
|
let remainder = &hint[accept_to..];
|
|
self.buffer.push_str(accepted);
|
|
self.hint = Some(remainder.to_string());
|
|
}
|
|
}
|
|
/// If we have a hint, then motions are able to extend into it
|
|
/// and partially accept pieces of it, instead of the whole thing
|
|
pub fn apply_motion_with_hint(&mut self, motion: MotionKind) {
|
|
let buffer_end = self.byte_len().saturating_sub(1);
|
|
flog!(DEBUG,self.hint);
|
|
if let Some(hint) = self.hint.take() {
|
|
self.buffer.push_str(&hint);
|
|
flog!(DEBUG,motion);
|
|
self.apply_motion(/*forced*/ true, motion);
|
|
flog!(DEBUG, self.cursor);
|
|
flog!(DEBUG, self.grapheme_at_cursor());
|
|
if self.cursor > buffer_end {
|
|
let remainder = if self.clamp_cursor {
|
|
self.slice_from((self.cursor + 1).min(self.byte_len()))
|
|
} else {
|
|
self.slice_from_cursor()
|
|
};
|
|
flog!(DEBUG,remainder);
|
|
if !remainder.is_empty() {
|
|
self.hint = Some(remainder.to_string());
|
|
}
|
|
let buffer = if self.clamp_cursor {
|
|
self.slice_to((self.cursor + 1).min(self.byte_len()))
|
|
} else {
|
|
self.slice_to_cursor()
|
|
};
|
|
flog!(DEBUG,buffer);
|
|
self.buffer = buffer.to_string();
|
|
flog!(DEBUG,self.hint);
|
|
} else {
|
|
let old_hint = self.slice_from(buffer_end + 1);
|
|
flog!(DEBUG,old_hint);
|
|
self.hint = Some(old_hint.to_string());
|
|
let buffer = self.slice_to(buffer_end + 1);
|
|
flog!(DEBUG,buffer);
|
|
self.buffer = buffer.to_string();
|
|
}
|
|
}
|
|
}
|
|
pub fn find_prev_line_pos(&mut self) -> Option<usize> {
|
|
if self.start_of_line() == 0 {
|
|
return None
|
|
};
|
|
let col = self.saved_col.unwrap_or(self.cursor_column());
|
|
let line = self.line_no();
|
|
if self.saved_col.is_none() {
|
|
self.saved_col = Some(col);
|
|
}
|
|
let (start,end) = self.select_line(line - 1).unwrap();
|
|
Some((start + col).min(end.saturating_sub(1)))
|
|
}
|
|
pub fn find_next_line_pos(&mut self) -> Option<usize> {
|
|
if self.end_of_line() == self.byte_len() {
|
|
return None
|
|
};
|
|
let col = self.saved_col.unwrap_or(self.cursor_column());
|
|
let line = self.line_no();
|
|
if self.saved_col.is_none() {
|
|
self.saved_col = Some(col);
|
|
}
|
|
let (start,end) = self.select_line(line + 1).unwrap();
|
|
Some((start + col).min(end.saturating_sub(1)))
|
|
}
|
|
pub fn cursor_column(&self) -> usize {
|
|
let line_start = self.start_of_line();
|
|
self.buffer[line_start..self.cursor].graphemes(true).count()
|
|
}
|
|
pub fn start_of_line(&self) -> usize {
|
|
if let Some(i) = self.slice_to_cursor().rfind('\n') {
|
|
i + 1 // Land on start of this line, instead of the end of the last one
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
pub fn end_of_line(&self) -> usize {
|
|
if let Some(i) = self.slice_from_cursor().find('\n') {
|
|
i + self.cursor
|
|
} else {
|
|
self.byte_len()
|
|
}
|
|
}
|
|
pub fn this_line(&self) -> (usize,usize) {
|
|
(
|
|
self.start_of_line(),
|
|
self.end_of_line()
|
|
)
|
|
}
|
|
pub fn prev_line(&self, offset: usize) -> (usize,usize) {
|
|
let (start,_) = self.select_lines_up(offset);
|
|
let end = self.slice_from_cursor().find('\n').unwrap_or(self.byte_len());
|
|
(start,end)
|
|
}
|
|
pub fn next_line(&self, offset: usize) -> Option<(usize,usize)> {
|
|
if self.this_line().1 == self.byte_len() {
|
|
return None
|
|
}
|
|
let (_,mut end) = self.select_lines_down(offset);
|
|
end = end.min(self.byte_len().saturating_sub(1));
|
|
let start = self.slice_to(end + 1).rfind('\n').unwrap_or(0);
|
|
Some((start,end))
|
|
}
|
|
pub fn count_lines(&self) -> usize {
|
|
self.buffer
|
|
.chars()
|
|
.filter(|&c| c == '\n')
|
|
.count()
|
|
}
|
|
pub fn line_no(&self) -> usize {
|
|
self.slice_to_cursor()
|
|
.chars()
|
|
.filter(|&c| c == '\n')
|
|
.count()
|
|
}
|
|
/// Returns the (start, end) byte range for the given line number.
|
|
///
|
|
/// - Line 0 starts at the beginning of the buffer and ends at the first newline (or end of buffer).
|
|
/// - Line 1 starts just after the first newline, ends at the second, etc.
|
|
///
|
|
/// Returns `None` if the line number is beyond the last line in the buffer.
|
|
pub fn select_line(&self, n: usize) -> Option<(usize, usize)> {
|
|
let mut start = 0;
|
|
|
|
let bytes = self.as_str(); // or whatever gives the full buffer as &str
|
|
let mut line_iter = bytes.match_indices('\n').map(|(i, _)| i + 1);
|
|
|
|
// Advance to the nth newline (start of line n)
|
|
for _ in 0..n {
|
|
start = line_iter.next()?;
|
|
}
|
|
|
|
// Find the next newline (end of line n), or end of buffer
|
|
let end = line_iter.next().unwrap_or(bytes.len());
|
|
|
|
Some((start, end))
|
|
}
|
|
/// Find the span from the start of the nth line above the cursor, to the end of the current line.
|
|
///
|
|
/// Returns (start,end)
|
|
/// 'start' is the first character after the previous newline, or the start of the buffer
|
|
/// 'end' is the index of the newline after the nth line
|
|
///
|
|
/// The caller can choose whether to include the newline itself in the selection by using either
|
|
/// * `(start..end)` to exclude it
|
|
/// * `(start..=end)` to include it
|
|
pub fn select_lines_up(&self, n: usize) -> (usize,usize) {
|
|
let end = self.end_of_line();
|
|
let mut start = self.start_of_line();
|
|
if start == 0 {
|
|
return (start,end)
|
|
}
|
|
|
|
for _ in 0..n {
|
|
let slice = self.slice_to(start - 1);
|
|
if let Some(prev_newline) = slice.rfind('\n') {
|
|
start = prev_newline;
|
|
} else {
|
|
start = 0;
|
|
break
|
|
}
|
|
}
|
|
|
|
(start,end)
|
|
}
|
|
/// Find the range from the start of this line, to the end of the nth line after the cursor
|
|
///
|
|
/// Returns (start,end)
|
|
/// 'start' is the first character after the previous newline, or the start of the buffer
|
|
/// 'end' is the index of the newline after the nth line
|
|
///
|
|
/// The caller can choose whether to include the newline itself in the selection by using either
|
|
/// * `(start..end)` to exclude it
|
|
/// * `(start..=end)` to include it
|
|
pub fn select_lines_down(&self, n: usize) -> (usize,usize) {
|
|
let mut end = self.end_of_line();
|
|
let start = self.start_of_line();
|
|
if end == self.byte_len() {
|
|
return (start,end)
|
|
}
|
|
|
|
for _ in 0..=n {
|
|
let next_ln_start = end + 1;
|
|
if next_ln_start >= self.byte_len() {
|
|
end = self.byte_len();
|
|
break
|
|
}
|
|
if let Some(next_newline) = self.slice_from(next_ln_start).find('\n') {
|
|
end += next_newline;
|
|
} else {
|
|
end = self.byte_len();
|
|
break
|
|
}
|
|
}
|
|
|
|
(start,end)
|
|
}
|
|
pub fn select_lines_to(&self, line_no: usize) -> (usize,usize) {
|
|
let cursor_line_no = self.line_no();
|
|
let offset = (cursor_line_no as isize) - (line_no as isize);
|
|
match offset.cmp(&0) {
|
|
Ordering::Less => self.select_lines_down(offset.unsigned_abs()),
|
|
Ordering::Equal => self.this_line(),
|
|
Ordering::Greater => self.select_lines_up(offset as usize)
|
|
}
|
|
}
|
|
fn on_start_of_word(&self, size: Word) -> bool {
|
|
self.is_start_of_word(size, self.cursor)
|
|
}
|
|
fn on_end_of_word(&self, size: Word) -> bool {
|
|
self.is_end_of_word(size, self.cursor)
|
|
}
|
|
fn is_start_of_word(&self, size: Word, pos: usize) -> bool {
|
|
if self.grapheme_at(pos).is_some_and(|g| g.chars().all(char::is_whitespace)) {
|
|
return false
|
|
}
|
|
match size {
|
|
Word::Big => {
|
|
let Some(prev_g) = self.grapheme_at(pos.saturating_sub(1)) else {
|
|
return true // We are on the very first grapheme, so it is the start of a word
|
|
};
|
|
prev_g.chars().all(char::is_whitespace)
|
|
}
|
|
Word::Normal => {
|
|
let Some(cur_g) = self.grapheme_at(pos) else {
|
|
return false // We aren't on a character to begin with
|
|
};
|
|
let Some(prev_g) = self.grapheme_at(pos.saturating_sub(1)) else {
|
|
return true
|
|
};
|
|
is_other_class_or_ws(cur_g, prev_g)
|
|
}
|
|
}
|
|
}
|
|
fn is_end_of_word(&self, size: Word, pos: usize) -> bool {
|
|
if self.grapheme_at(pos).is_some_and(|g| g.chars().all(char::is_whitespace)) {
|
|
return false
|
|
}
|
|
match size {
|
|
Word::Big => {
|
|
let Some(next_g) = self.grapheme_at(pos + 1) else {
|
|
return false
|
|
};
|
|
next_g.chars().all(char::is_whitespace)
|
|
}
|
|
Word::Normal => {
|
|
let Some(cur_g) = self.grapheme_at(pos) else {
|
|
return false
|
|
};
|
|
let Some(next_g) = self.grapheme_at(pos + 1) else {
|
|
return false
|
|
};
|
|
is_other_class_or_ws(cur_g, next_g)
|
|
}
|
|
}
|
|
}
|
|
pub fn eval_text_object(&self, obj: TextObj, bound: Bound) -> Option<Range<usize>> {
|
|
flog!(DEBUG, obj);
|
|
flog!(DEBUG, bound);
|
|
match obj {
|
|
TextObj::Word(word) => {
|
|
match word {
|
|
Word::Big => match bound {
|
|
Bound::Inside => {
|
|
let start = self.rfind(is_whitespace)
|
|
.map(|pos| pos+1)
|
|
.unwrap_or(0);
|
|
let end = self.find(is_whitespace)
|
|
.map(|pos| pos-1)
|
|
.unwrap_or(self.byte_len());
|
|
Some(start..end)
|
|
}
|
|
Bound::Around => {
|
|
let start = self.rfind(is_whitespace)
|
|
.map(|pos| pos+1)
|
|
.unwrap_or(0);
|
|
let mut end = self.find(is_whitespace)
|
|
.unwrap_or(self.byte_len());
|
|
if end != self.byte_len() {
|
|
end = self.find_from(end,|c| !is_whitespace(c))
|
|
.map(|pos| pos-1)
|
|
.unwrap_or(self.byte_len())
|
|
}
|
|
Some(start..end)
|
|
}
|
|
}
|
|
Word::Normal => match bound {
|
|
Bound::Inside => {
|
|
let cur_graph = self.grapheme_at_cursor()?;
|
|
let start = self.rfind(|c| is_other_class(c, cur_graph))
|
|
.map(|pos| pos+1)
|
|
.unwrap_or(0);
|
|
let end = self.find(|c| is_other_class(c, cur_graph))
|
|
.map(|pos| pos-1)
|
|
.unwrap_or(self.byte_len());
|
|
Some(start..end)
|
|
}
|
|
Bound::Around => {
|
|
let cur_graph = self.grapheme_at_cursor()?;
|
|
let start = self.rfind(|c| is_other_class(c, cur_graph))
|
|
.map(|pos| pos+1)
|
|
.unwrap_or(0);
|
|
let mut end = self.find(|c| is_other_class(c, cur_graph))
|
|
.unwrap_or(self.byte_len());
|
|
if end != self.byte_len() && self.is_whitespace(end) {
|
|
end = self.find_from(end,|c| !is_whitespace(c))
|
|
.map(|pos| pos-1)
|
|
.unwrap_or(self.byte_len())
|
|
} else {
|
|
end -= 1;
|
|
}
|
|
Some(start..end)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
TextObj::Line => todo!(),
|
|
TextObj::Sentence => todo!(),
|
|
TextObj::Paragraph => todo!(),
|
|
TextObj::DoubleQuote => todo!(),
|
|
TextObj::SingleQuote => todo!(),
|
|
TextObj::BacktickQuote => todo!(),
|
|
TextObj::Paren => todo!(),
|
|
TextObj::Bracket => todo!(),
|
|
TextObj::Brace => todo!(),
|
|
TextObj::Angle => todo!(),
|
|
TextObj::Tag => todo!(),
|
|
TextObj::Custom(_) => todo!(),
|
|
}
|
|
}
|
|
pub fn find_word_pos(&self, word: Word, to: To, dir: Direction) -> Option<usize> {
|
|
// FIXME: This uses a lot of hardcoded +1/-1 offsets, but they need to account for grapheme boundaries
|
|
let mut pos = self.cursor;
|
|
match word {
|
|
Word::Big => {
|
|
match dir {
|
|
Direction::Forward => {
|
|
match to {
|
|
To::Start => {
|
|
if self.on_whitespace() {
|
|
return self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)
|
|
}
|
|
if self.on_start_of_word(word) {
|
|
pos += 1;
|
|
if pos >= self.byte_len() {
|
|
return Some(self.byte_len())
|
|
}
|
|
}
|
|
let Some(ws_pos) = self.find_from(pos, |c| CharClass::from(c) == CharClass::Whitespace) else {
|
|
return Some(self.byte_len())
|
|
};
|
|
let word_start = self.find_from(ws_pos, |c| CharClass::from(c) != CharClass::Whitespace)?;
|
|
Some(word_start)
|
|
}
|
|
To::End => {
|
|
if self.on_whitespace() {
|
|
let Some(non_ws_pos) = self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace) else {
|
|
return Some(self.byte_len())
|
|
};
|
|
pos = non_ws_pos
|
|
}
|
|
match self.on_end_of_word(word) {
|
|
true => {
|
|
pos += 1;
|
|
if pos >= self.byte_len() {
|
|
return Some(self.byte_len())
|
|
}
|
|
let word_start = self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)?;
|
|
match self.find_from(word_start, |c| CharClass::from(c) == CharClass::Whitespace) {
|
|
Some(n) => Some(n.saturating_sub(1)), // Land on char before whitespace
|
|
None => Some(self.byte_len()) // End of buffer
|
|
}
|
|
}
|
|
false => {
|
|
match self.find_from(pos, |c| CharClass::from(c) == CharClass::Whitespace) {
|
|
Some(n) => Some(n.saturating_sub(1)), // Land on char before whitespace
|
|
None => Some(self.byte_len()) // End of buffer
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Direction::Backward => {
|
|
match to {
|
|
To::Start => {
|
|
if self.on_whitespace() {
|
|
let Some(non_ws_pos) = self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace) else {
|
|
return Some(0)
|
|
};
|
|
pos = non_ws_pos
|
|
}
|
|
match self.on_start_of_word(word) {
|
|
true => {
|
|
pos = pos.checked_sub(1)?;
|
|
let Some(prev_word_end) = self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace) else {
|
|
return Some(0)
|
|
};
|
|
match self.rfind_from(prev_word_end, |c| CharClass::from(c) == CharClass::Whitespace) {
|
|
Some(n) => Some(n + 1), // Land on char after whitespace
|
|
None => Some(0) // Start of buffer
|
|
}
|
|
}
|
|
false => {
|
|
match self.rfind_from(pos, |c| CharClass::from(c) == CharClass::Whitespace) {
|
|
Some(n) => Some(n + 1), // Land on char after whitespace
|
|
None => Some(0) // Start of buffer
|
|
}
|
|
}
|
|
}
|
|
}
|
|
To::End => {
|
|
if self.on_whitespace() {
|
|
return Some(self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace).unwrap_or(0))
|
|
}
|
|
if self.on_end_of_word(word) {
|
|
pos = pos.checked_sub(1)?;
|
|
}
|
|
let Some(last_ws) = self.rfind_from(pos, |c| CharClass::from(c) == CharClass::Whitespace) else {
|
|
return Some(0)
|
|
};
|
|
let Some(prev_word_end) = self.rfind_from(last_ws, |c| CharClass::from(c) != CharClass::Whitespace) else {
|
|
return Some(0)
|
|
};
|
|
Some(prev_word_end)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Word::Normal => {
|
|
match dir {
|
|
Direction::Forward => {
|
|
match to {
|
|
To::Start => {
|
|
if self.on_whitespace() {
|
|
return Some(self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace).unwrap_or(self.byte_len()))
|
|
}
|
|
if self.on_start_of_word(word) {
|
|
let cur_char_class = CharClass::from(self.grapheme_at_cursor()?);
|
|
pos += 1;
|
|
if pos >= self.byte_len() {
|
|
return Some(self.byte_len())
|
|
}
|
|
let next_char = self.grapheme_at(self.next_pos(1)?)?;
|
|
let next_char_class = CharClass::from(next_char);
|
|
if cur_char_class != next_char_class && next_char_class != CharClass::Whitespace {
|
|
return Some(pos)
|
|
}
|
|
}
|
|
let cur_graph = self.grapheme_at(pos)?;
|
|
let Some(diff_class_pos) = self.find_from(pos, |c| is_other_class_or_ws(c, cur_graph)) else {
|
|
return Some(self.byte_len())
|
|
};
|
|
if let CharClass::Whitespace = CharClass::from(self.grapheme_at(diff_class_pos)?) {
|
|
let non_ws_pos = self.find_from(diff_class_pos, |c| CharClass::from(c) != CharClass::Whitespace)?;
|
|
Some(non_ws_pos)
|
|
} else {
|
|
Some(diff_class_pos)
|
|
}
|
|
}
|
|
To::End => {
|
|
flog!(DEBUG,self.buffer);
|
|
if self.on_whitespace() {
|
|
let Some(non_ws_pos) = self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace) else {
|
|
return Some(self.byte_len())
|
|
};
|
|
pos = non_ws_pos
|
|
}
|
|
match self.on_end_of_word(word) {
|
|
true => {
|
|
flog!(DEBUG, "on end of word");
|
|
let cur_char_class = CharClass::from(self.grapheme_at_cursor()?);
|
|
pos += 1;
|
|
if pos >= self.byte_len() {
|
|
return Some(self.byte_len())
|
|
}
|
|
let next_char = self.grapheme_at(self.next_pos(1)?)?;
|
|
let next_char_class = CharClass::from(next_char);
|
|
if cur_char_class != next_char_class && next_char_class != CharClass::Whitespace {
|
|
let Some(end_pos) = self.find_from(pos, |c| is_other_class_or_ws(c, next_char)) else {
|
|
return Some(self.byte_len())
|
|
};
|
|
pos = end_pos.saturating_sub(1);
|
|
return Some(pos)
|
|
}
|
|
|
|
let cur_graph = self.grapheme_at(pos)?;
|
|
match self.find_from(pos, |c| is_other_class_or_ws(c, cur_graph)) {
|
|
Some(n) => {
|
|
let cur_graph = self.grapheme_at(n)?;
|
|
if CharClass::from(cur_graph) == CharClass::Whitespace {
|
|
let Some(non_ws_pos) = self.find_from(n, |c| CharClass::from(c) != CharClass::Whitespace) else {
|
|
return Some(self.byte_len())
|
|
};
|
|
let cur_graph = self.grapheme_at(non_ws_pos)?;
|
|
let Some(word_end_pos) = self.find_from(non_ws_pos, |c| is_other_class_or_ws(c, cur_graph)) else {
|
|
return Some(self.byte_len())
|
|
};
|
|
Some(word_end_pos.saturating_sub(1))
|
|
} else {
|
|
Some(pos.saturating_sub(1))
|
|
}
|
|
}
|
|
None => Some(self.byte_len()) // End of buffer
|
|
}
|
|
}
|
|
false => {
|
|
flog!(DEBUG, "not on end of word");
|
|
let cur_graph = self.grapheme_at(pos)?;
|
|
flog!(DEBUG,cur_graph);
|
|
match self.find_from(pos, |c| is_other_class_or_ws(c, cur_graph)) {
|
|
Some(n) => Some(n.saturating_sub(1)), // Land on char before other char class
|
|
None => Some(self.byte_len()) // End of buffer
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Direction::Backward => {
|
|
match to {
|
|
To::Start => {
|
|
if self.on_whitespace() {
|
|
pos = self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)?;
|
|
}
|
|
match self.on_start_of_word(word) {
|
|
true => {
|
|
pos = pos.checked_sub(1)?;
|
|
let cur_char_class = CharClass::from(self.grapheme_at_cursor()?);
|
|
let prev_char = self.grapheme_at(self.prev_pos(1)?)?;
|
|
let prev_char_class = CharClass::from(prev_char);
|
|
let is_diff_class = cur_char_class != prev_char_class && prev_char_class != CharClass::Whitespace;
|
|
if is_diff_class && self.is_start_of_word(Word::Normal, self.prev_pos(1)?) {
|
|
return Some(pos)
|
|
}
|
|
let prev_word_end = self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)?;
|
|
let cur_graph = self.grapheme_at(prev_word_end)?;
|
|
match self.rfind_from(prev_word_end, |c| is_other_class_or_ws(c, cur_graph)) {
|
|
Some(n) => Some(n + 1), // Land on char after whitespace
|
|
None => Some(0) // Start of buffer
|
|
}
|
|
}
|
|
false => {
|
|
let cur_graph = self.grapheme_at(pos)?;
|
|
match self.rfind_from(pos, |c| is_other_class_or_ws(c, cur_graph)) {
|
|
Some(n) => Some(n + 1), // Land on char after whitespace
|
|
None => Some(0) // Start of buffer
|
|
}
|
|
}
|
|
}
|
|
}
|
|
To::End => {
|
|
if self.on_whitespace() {
|
|
return Some(self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace).unwrap_or(0))
|
|
}
|
|
if self.on_end_of_word(word) {
|
|
pos = pos.checked_sub(1)?;
|
|
let cur_char_class = CharClass::from(self.grapheme_at_cursor()?);
|
|
let prev_char = self.grapheme_at(self.prev_pos(1)?)?;
|
|
let prev_char_class = CharClass::from(prev_char);
|
|
if cur_char_class != prev_char_class && prev_char_class != CharClass::Whitespace {
|
|
return Some(pos)
|
|
}
|
|
}
|
|
let cur_graph = self.grapheme_at(pos)?;
|
|
let Some(diff_class_pos) = self.rfind_from(pos, |c|is_other_class_or_ws(c, cur_graph)) else {
|
|
return Some(0)
|
|
};
|
|
if let CharClass::Whitespace = self.grapheme_at(diff_class_pos)?.into() {
|
|
let prev_word_end = self.rfind_from(diff_class_pos, |c| CharClass::from(c) != CharClass::Whitespace).unwrap_or(0);
|
|
Some(prev_word_end)
|
|
} else {
|
|
Some(diff_class_pos)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
pub fn find<F: Fn(&str) -> bool>(&self, op: F) -> Option<usize> {
|
|
self.find_from(self.cursor, op)
|
|
}
|
|
pub fn rfind<F: Fn(&str) -> bool>(&self, op: F) -> Option<usize> {
|
|
self.rfind_from(self.cursor, op)
|
|
}
|
|
|
|
/// Find the first grapheme at or after `pos` for which `op` returns true.
|
|
/// Returns the byte index of that grapheme in the buffer.
|
|
pub fn find_from<F: Fn(&str) -> bool>(&self, pos: usize, op: F) -> Option<usize> {
|
|
|
|
// Iterate over grapheme indices starting at `pos`
|
|
let slice = &self.slice_from(pos);
|
|
for (offset, grapheme) in slice.grapheme_indices(true) {
|
|
if op(grapheme) {
|
|
return Some(pos + offset);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
/// Find the last grapheme at or before `pos` for which `op` returns true.
|
|
/// Returns the byte index of that grapheme in the buffer.
|
|
pub fn rfind_from<F: Fn(&str) -> bool>(&self, pos: usize, op: F) -> Option<usize> {
|
|
|
|
// Iterate grapheme boundaries backward up to pos
|
|
let slice = &self.slice_to(pos);
|
|
let graphemes = slice.grapheme_indices(true).rev();
|
|
|
|
for (offset, grapheme) in graphemes {
|
|
if op(grapheme) {
|
|
return Some(offset);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
pub fn eval_motion_with_hint(&mut self, motion: Motion) -> MotionKind {
|
|
let Some(hint) = self.hint.as_ref() else {
|
|
return MotionKind::Null
|
|
};
|
|
let buffer = self.buffer.clone();
|
|
self.buffer.push_str(hint);
|
|
let motion_eval = self.eval_motion(motion);
|
|
self.buffer = buffer;
|
|
motion_eval
|
|
}
|
|
pub fn eval_motion(&mut self, motion: Motion) -> MotionKind {
|
|
flog!(DEBUG,self.buffer);
|
|
flog!(DEBUG,motion);
|
|
match motion {
|
|
Motion::WholeLine => MotionKind::Line(0),
|
|
Motion::TextObj(text_obj, bound) => {
|
|
let Some(range) = self.eval_text_object(text_obj, bound) else {
|
|
return MotionKind::Null
|
|
};
|
|
MotionKind::range(range)
|
|
}
|
|
Motion::BeginningOfFirstWord => {
|
|
let (start,_) = self.this_line();
|
|
let first_graph_pos = self.find_from(start, |c| CharClass::from(c) != CharClass::Whitespace).unwrap_or(start);
|
|
MotionKind::To(first_graph_pos)
|
|
}
|
|
Motion::BeginningOfLine => MotionKind::To(self.this_line().0),
|
|
Motion::EndOfLine => MotionKind::To(self.this_line().1),
|
|
Motion::BackwardWord(to, word) => {
|
|
let Some(pos) = self.find_word_pos(word, to, Direction::Backward) else {
|
|
return MotionKind::Null
|
|
};
|
|
MotionKind::To(pos)
|
|
}
|
|
Motion::ForwardWord(to, word) => {
|
|
let Some(pos) = self.find_word_pos(word, to, Direction::Forward) else {
|
|
return MotionKind::Null
|
|
};
|
|
match to {
|
|
To::Start => MotionKind::To(pos),
|
|
To::End => MotionKind::On(pos),
|
|
}
|
|
}
|
|
Motion::CharSearch(direction, dest, ch) => {
|
|
let ch = format!("{ch}");
|
|
let saved_cursor = self.cursor;
|
|
match direction {
|
|
Direction::Forward => {
|
|
if self.grapheme_at_cursor().is_some_and(|c| c == ch) {
|
|
self.cursor_fwd(1);
|
|
}
|
|
let Some(pos) = self.find(|c| c == ch) else {
|
|
self.cursor = saved_cursor;
|
|
return MotionKind::Null
|
|
};
|
|
self.cursor = saved_cursor;
|
|
match dest {
|
|
Dest::On => MotionKind::On(pos),
|
|
Dest::Before => MotionKind::Before(pos),
|
|
Dest::After => todo!(),
|
|
}
|
|
}
|
|
Direction::Backward => {
|
|
if self.grapheme_at_cursor().is_some_and(|c| c == ch) {
|
|
self.cursor_back(1);
|
|
}
|
|
let Some(pos) = self.rfind(|c| c == ch) else {
|
|
self.cursor = saved_cursor;
|
|
return MotionKind::Null
|
|
};
|
|
self.cursor = saved_cursor;
|
|
match dest {
|
|
Dest::On => MotionKind::On(pos),
|
|
Dest::Before => MotionKind::Before(pos),
|
|
Dest::After => todo!(),
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
Motion::BackwardChar => MotionKind::Backward(1),
|
|
Motion::ForwardChar => MotionKind::Forward(1),
|
|
Motion::LineUp => MotionKind::Line(-1),
|
|
Motion::LineDown => MotionKind::Line(1),
|
|
Motion::ScreenLineUp => MotionKind::ScreenLine(-1),
|
|
Motion::ScreenLineDown => MotionKind::ScreenLine(1),
|
|
Motion::WholeBuffer => todo!(),
|
|
Motion::BeginningOfBuffer => MotionKind::To(0),
|
|
Motion::EndOfBuffer => MotionKind::To(self.byte_len()),
|
|
Motion::ToColumn(n) => {
|
|
let (start,end) = self.this_line();
|
|
let pos = start + n;
|
|
if pos > end {
|
|
MotionKind::To(end)
|
|
} else {
|
|
MotionKind::To(pos)
|
|
}
|
|
}
|
|
Motion::Range(start, end) => {
|
|
let start = start.clamp(0, self.byte_len().saturating_sub(1));
|
|
let end = end.clamp(0, self.byte_len().saturating_sub(1));
|
|
MotionKind::range(mk_range(start, end))
|
|
}
|
|
Motion::Builder(_) => todo!(),
|
|
Motion::RepeatMotion => todo!(),
|
|
Motion::RepeatMotionRev => todo!(),
|
|
Motion::Null => MotionKind::Null
|
|
}
|
|
}
|
|
pub fn calculate_display_offset(&self, n_lines: isize) -> Option<usize> {
|
|
let (start,end) = self.this_line();
|
|
let graphemes: Vec<(usize, usize, &str)> = self.buffer[start..end]
|
|
.graphemes(true)
|
|
.scan(start, |idx, g| {
|
|
let current = *idx;
|
|
*idx += g.len(); // Advance by number of bytes
|
|
Some((g.width(), current, g))
|
|
}).collect();
|
|
|
|
let mut cursor_line_index = 0;
|
|
let mut cursor_visual_col = 0;
|
|
let mut screen_lines = vec![];
|
|
let mut cur_line = vec![];
|
|
let mut line_width = 0;
|
|
|
|
for (width, byte_idx, grapheme) in graphemes {
|
|
if byte_idx == self.cursor {
|
|
// Save this to later find column
|
|
cursor_line_index = screen_lines.len();
|
|
cursor_visual_col = line_width;
|
|
}
|
|
|
|
let new_line_width = line_width + width;
|
|
if new_line_width > self.term_dims.1 {
|
|
screen_lines.push(std::mem::take(&mut cur_line));
|
|
cur_line.push((width, byte_idx, grapheme));
|
|
line_width = width;
|
|
} else {
|
|
cur_line.push((width, byte_idx, grapheme));
|
|
line_width = new_line_width;
|
|
}
|
|
}
|
|
|
|
if !cur_line.is_empty() {
|
|
screen_lines.push(cur_line);
|
|
}
|
|
|
|
if screen_lines.len() == 1 {
|
|
return None
|
|
}
|
|
|
|
let target_line_index = (cursor_line_index as isize + n_lines)
|
|
.clamp(0, (screen_lines.len() - 1) as isize) as usize;
|
|
|
|
let mut col = 0;
|
|
for (width, byte_idx, _) in &screen_lines[target_line_index] {
|
|
if col + width > cursor_visual_col {
|
|
return Some(*byte_idx);
|
|
}
|
|
col += width;
|
|
}
|
|
|
|
// If you went past the end of the line
|
|
screen_lines[target_line_index]
|
|
.last()
|
|
.map(|(_, byte_idx, _)| *byte_idx)
|
|
}
|
|
pub fn get_range_from_motion(&self, verb: &Verb, motion: &MotionKind) -> Option<Range<usize>> {
|
|
let range = match motion {
|
|
MotionKind::Forward(n) => {
|
|
let pos = self.next_pos(*n)?;
|
|
let range = self.cursor..pos;
|
|
assert!(range.end <= self.byte_len());
|
|
Some(range)
|
|
}
|
|
MotionKind::To(n) => {
|
|
let range = mk_range(self.cursor, *n);
|
|
assert!(range.end <= self.byte_len());
|
|
Some(range)
|
|
}
|
|
MotionKind::On(n) => {
|
|
let range = mk_range_inclusive(self.cursor, *n);
|
|
Some(range)
|
|
}
|
|
MotionKind::Before(n) => {
|
|
let n = match n.cmp(&self.cursor) {
|
|
Ordering::Less => (n + 1).min(self.byte_len()),
|
|
Ordering::Equal => n.saturating_sub(1),
|
|
Ordering::Greater => *n
|
|
};
|
|
let range = mk_range_inclusive(n, self.cursor);
|
|
Some(range)
|
|
}
|
|
MotionKind::Backward(n) => {
|
|
let pos = self.prev_pos(*n)?;
|
|
let range = pos..self.cursor;
|
|
Some(range)
|
|
}
|
|
MotionKind::Range(range) => {
|
|
Some(range.0..range.1)
|
|
}
|
|
MotionKind::Line(n) => {
|
|
match n.cmp(&0) {
|
|
Ordering::Less => {
|
|
let (start,end) = self.select_lines_up(n.unsigned_abs());
|
|
let mut range = match verb {
|
|
Verb::Delete => mk_range_inclusive(start,end),
|
|
_ => mk_range(start,end),
|
|
};
|
|
range = self.clamp_range(range);
|
|
Some(range)
|
|
}
|
|
Ordering::Equal => {
|
|
let (start,end) = self.this_line();
|
|
let mut range = match verb {
|
|
Verb::Delete => mk_range_inclusive(start,end),
|
|
_ => mk_range(start,end),
|
|
};
|
|
range = self.clamp_range(range);
|
|
Some(range)
|
|
}
|
|
Ordering::Greater => {
|
|
let (start, mut end) = self.select_lines_down(*n as usize);
|
|
end = (end + 1).min(self.byte_len() - 1);
|
|
let mut range = match verb {
|
|
Verb::Delete => mk_range_inclusive(start,end),
|
|
_ => mk_range(start,end),
|
|
};
|
|
range = self.clamp_range(range);
|
|
Some(range)
|
|
}
|
|
}
|
|
}
|
|
MotionKind::ToLine(n) => {
|
|
let (start,end) = self.select_lines_to(*n);
|
|
let range = match verb {
|
|
Verb::Change => start..end,
|
|
Verb::Delete => start..end.saturating_add(1),
|
|
_ => unreachable!()
|
|
};
|
|
Some(range)
|
|
}
|
|
MotionKind::Null => None,
|
|
MotionKind::ScreenLine(n) => {
|
|
let pos = self.calculate_display_offset(*n)?;
|
|
Some(mk_range(pos, self.cursor))
|
|
}
|
|
};
|
|
range.map(|rng| self.clamp_range(rng))
|
|
}
|
|
pub fn indent_lines(&mut self, range: Range<usize>) {
|
|
let (start,end) = (range.start,range.end);
|
|
|
|
self.buffer.insert(start, '\t');
|
|
|
|
let graphemes = self.buffer[start + 1..end].grapheme_indices(true);
|
|
let mut tab_insert_indices = vec![];
|
|
let mut next_is_tab_pos = false;
|
|
for (i,g) in graphemes {
|
|
if g == "\n" {
|
|
next_is_tab_pos = true;
|
|
} else if next_is_tab_pos {
|
|
tab_insert_indices.push(start + i + 1);
|
|
next_is_tab_pos = false;
|
|
}
|
|
}
|
|
|
|
for i in tab_insert_indices {
|
|
if i < self.byte_len() {
|
|
self.buffer.insert(i, '\t');
|
|
}
|
|
}
|
|
}
|
|
pub fn dedent_lines(&mut self, range: Range<usize>) {
|
|
|
|
todo!()
|
|
}
|
|
pub fn exec_verb(&mut self, verb: Verb, motion: MotionKind, register: RegisterName) -> ShResult<()> {
|
|
match verb {
|
|
Verb::Change |
|
|
Verb::Delete => {
|
|
let Some(mut range) = self.get_range_from_motion(&verb, &motion) else {
|
|
return Ok(())
|
|
};
|
|
let restore_col = matches!(motion, MotionKind::Line(_)) && matches!(verb, Verb::Delete);
|
|
if restore_col {
|
|
self.saved_col = Some(self.cursor_column())
|
|
}
|
|
let deleted = self.buffer.drain(range.clone());
|
|
register.write_to_register(deleted.collect());
|
|
|
|
self.cursor = range.start;
|
|
if restore_col {
|
|
let saved = self.saved_col.unwrap();
|
|
let line_start = self.this_line().0;
|
|
|
|
self.cursor = line_start + saved;
|
|
}
|
|
}
|
|
Verb::DeleteChar(anchor) => {
|
|
match anchor {
|
|
Anchor::After => {
|
|
if self.grapheme_at(self.cursor).is_some() {
|
|
self.buffer.remove(self.cursor);
|
|
}
|
|
}
|
|
Anchor::Before => {
|
|
if self.grapheme_at(self.cursor.saturating_sub(1)).is_some() {
|
|
self.buffer.remove(self.cursor.saturating_sub(1));
|
|
self.cursor_back(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Verb::VisualModeSelectLast => {
|
|
if let Some(range) = self.last_selected_range.as_ref() {
|
|
self.selected_range = Some(range.clone());
|
|
let mode = self.select_mode.unwrap_or_default();
|
|
self.cursor = match mode.anchor() {
|
|
SelectionAnchor::Start => range.start,
|
|
SelectionAnchor::End => range.end
|
|
}
|
|
}
|
|
}
|
|
Verb::SwapVisualAnchor => {
|
|
if let Some(range) = self.selected_range() {
|
|
if let Some(mut mode) = self.select_mode {
|
|
mode.invert_anchor();
|
|
self.cursor = match mode.anchor() {
|
|
SelectionAnchor::Start => range.start,
|
|
SelectionAnchor::End => range.end,
|
|
};
|
|
self.select_mode = Some(mode);
|
|
}
|
|
}
|
|
}
|
|
Verb::Yank => {
|
|
let Some(range) = self.get_range_from_motion(&verb, &motion) else {
|
|
return Ok(())
|
|
};
|
|
let yanked = &self.buffer[range.clone()];
|
|
register.write_to_register(yanked.to_string());
|
|
self.cursor = range.start;
|
|
}
|
|
Verb::ReplaceChar(c) => {
|
|
let Some(range) = self.get_range_from_motion(&verb, &motion) else {
|
|
return Ok(())
|
|
};
|
|
let delta = range.end - range.start;
|
|
let new_range = format!("{c}").repeat(delta);
|
|
let cursor_pos = range.end;
|
|
self.buffer.replace_range(range, &new_range);
|
|
self.cursor = cursor_pos
|
|
}
|
|
Verb::Substitute => todo!(),
|
|
Verb::ToLower => {
|
|
let Some(range) = self.get_range_from_motion(&verb, &motion) else {
|
|
return Ok(())
|
|
};
|
|
let mut new_range = String::new();
|
|
let slice = &self.buffer[range.clone()];
|
|
for ch in slice.chars() {
|
|
if ch.is_ascii_uppercase() {
|
|
new_range.push(ch.to_ascii_lowercase())
|
|
} else {
|
|
new_range.push(ch)
|
|
}
|
|
}
|
|
self.buffer.replace_range(range.clone(), &new_range);
|
|
self.cursor = range.end;
|
|
}
|
|
Verb::ToUpper => {
|
|
let Some(range) = self.get_range_from_motion(&verb, &motion) else {
|
|
return Ok(())
|
|
};
|
|
let mut new_range = String::new();
|
|
let slice = &self.buffer[range.clone()];
|
|
for ch in slice.chars() {
|
|
if ch.is_ascii_lowercase() {
|
|
new_range.push(ch.to_ascii_uppercase())
|
|
} else {
|
|
new_range.push(ch)
|
|
}
|
|
}
|
|
self.buffer.replace_range(range.clone(), &new_range);
|
|
self.cursor = range.end;
|
|
}
|
|
Verb::ToggleCase => {
|
|
let Some(range) = self.get_range_from_motion(&verb, &motion) else {
|
|
return Ok(())
|
|
};
|
|
let mut new_range = String::new();
|
|
let slice = &self.buffer[range.clone()];
|
|
for ch in slice.chars() {
|
|
if ch.is_ascii_lowercase() {
|
|
new_range.push(ch.to_ascii_uppercase())
|
|
} else if ch.is_ascii_uppercase() {
|
|
new_range.push(ch.to_ascii_lowercase())
|
|
} else {
|
|
new_range.push(ch)
|
|
}
|
|
}
|
|
self.buffer.replace_range(range.clone(), &new_range);
|
|
self.cursor = range.end;
|
|
}
|
|
Verb::Complete => todo!(),
|
|
Verb::CompleteBackward => todo!(),
|
|
Verb::Undo => {
|
|
let Some(undo) = self.undo_stack.pop() else {
|
|
return Ok(())
|
|
};
|
|
let Edit { pos, cursor_pos, old, new, .. } = undo;
|
|
let range = pos..pos + new.len();
|
|
self.buffer.replace_range(range, &old);
|
|
let redo_cursor_pos = self.cursor;
|
|
if self.move_cursor_on_undo {
|
|
self.cursor = cursor_pos;
|
|
}
|
|
let redo = Edit { pos, cursor_pos: redo_cursor_pos, old: new, new: old, merging: false };
|
|
self.redo_stack.push(redo);
|
|
}
|
|
Verb::Redo => {
|
|
let Some(redo) = self.redo_stack.pop() else {
|
|
return Ok(())
|
|
};
|
|
let Edit { pos, cursor_pos, old, new, .. } = redo;
|
|
let range = pos..pos + new.len();
|
|
self.buffer.replace_range(range, &old);
|
|
let undo_cursor_pos = self.cursor;
|
|
if self.move_cursor_on_undo {
|
|
self.cursor = cursor_pos;
|
|
}
|
|
let undo = Edit { pos, cursor_pos: undo_cursor_pos, old: new, new: old, merging: false };
|
|
self.undo_stack.push(undo);
|
|
}
|
|
Verb::RepeatLast => todo!(),
|
|
Verb::Put(anchor) => {
|
|
let Some(register_content) = register.read_from_register() else {
|
|
return Ok(())
|
|
};
|
|
match anchor {
|
|
Anchor::After => {
|
|
for ch in register_content.chars() {
|
|
self.cursor_fwd(1); // Only difference is which one you start with
|
|
self.insert(ch);
|
|
}
|
|
}
|
|
Anchor::Before => {
|
|
for ch in register_content.chars() {
|
|
self.insert(ch);
|
|
self.cursor_fwd(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Verb::InsertModeLineBreak(anchor) => {
|
|
match anchor {
|
|
Anchor::After => {
|
|
let (_,end) = self.this_line();
|
|
self.cursor = end;
|
|
self.insert('\n');
|
|
self.cursor_fwd(1);
|
|
}
|
|
Anchor::Before => {
|
|
let (start,_) = self.this_line();
|
|
self.cursor = start;
|
|
self.insert('\n');
|
|
}
|
|
}
|
|
}
|
|
Verb::JoinLines => {
|
|
let (start,end) = self.this_line();
|
|
let Some((nstart,nend)) = self.next_line(1) else {
|
|
return Ok(())
|
|
};
|
|
let line = &self.buffer[start..end];
|
|
let next_line = &self.buffer[nstart..nend].trim_start().to_string(); // strip leading whitespace
|
|
flog!(DEBUG,next_line);
|
|
let replace_newline_with_space = !line.ends_with([' ', '\t']);
|
|
self.cursor = end;
|
|
if replace_newline_with_space {
|
|
self.buffer.replace_range(end..end+1, " ");
|
|
self.buffer.replace_range(end+1..nend, next_line);
|
|
} else {
|
|
self.buffer.replace_range(end..end+1, "");
|
|
self.buffer.replace_range(end..nend, next_line);
|
|
}
|
|
}
|
|
Verb::InsertChar(ch) => {
|
|
self.insert(ch);
|
|
self.apply_motion(/*forced*/ true, motion);
|
|
}
|
|
Verb::Insert(str) => {
|
|
for ch in str.chars() {
|
|
self.insert(ch);
|
|
self.cursor_fwd(1);
|
|
}
|
|
}
|
|
Verb::Breakline(anchor) => todo!(),
|
|
Verb::Indent => {
|
|
let Some(range) = self.get_range_from_motion(&verb, &motion) else {
|
|
return Ok(())
|
|
};
|
|
self.indent_lines(range)
|
|
}
|
|
Verb::Dedent => {
|
|
let Some(range) = self.get_range_from_motion(&verb, &motion) else {
|
|
return Ok(())
|
|
};
|
|
self.dedent_lines(range)
|
|
}
|
|
Verb::Rot13 => {
|
|
let Some(range) = self.get_range_from_motion(&verb, &motion) else {
|
|
return Ok(())
|
|
};
|
|
let slice = &self.buffer[range.clone()];
|
|
let rot13 = rot13(slice);
|
|
self.buffer.replace_range(range, &rot13);
|
|
}
|
|
Verb::Equalize => todo!(), // I fear this one
|
|
Verb::Builder(verb_builder) => todo!(),
|
|
Verb::EndOfFile => {
|
|
if !self.buffer.is_empty() {
|
|
self.cursor = 0;
|
|
self.buffer.clear();
|
|
} else {
|
|
sh_quit(0)
|
|
}
|
|
}
|
|
|
|
Verb::AcceptLine |
|
|
Verb::ReplaceMode |
|
|
Verb::InsertMode |
|
|
Verb::NormalMode |
|
|
Verb::VisualModeLine |
|
|
Verb::VisualModeBlock |
|
|
Verb::VisualMode => {
|
|
/* Already handled */
|
|
self.apply_motion(/*forced*/ true,motion);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
pub fn apply_motion(&mut self, forced: bool, motion: MotionKind) {
|
|
|
|
match motion {
|
|
MotionKind::Forward(n) => {
|
|
for _ in 0..n {
|
|
if forced {
|
|
if !self.cursor_fwd(1) {
|
|
break
|
|
}
|
|
} else if !self.cursor_fwd_confined(1) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
MotionKind::Backward(n) => {
|
|
for _ in 0..n {
|
|
if forced {
|
|
if !self.cursor_back(1) {
|
|
break
|
|
}
|
|
} else if !self.cursor_back_confined(1) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
MotionKind::To(n) |
|
|
MotionKind::On(n) => {
|
|
if n > self.byte_len() {
|
|
self.cursor = self.byte_len();
|
|
} else {
|
|
self.cursor = n
|
|
}
|
|
}
|
|
MotionKind::Before(n) => {
|
|
if n > self.byte_len() {
|
|
self.cursor = self.byte_len();
|
|
} else {
|
|
match n.cmp(&self.cursor) {
|
|
Ordering::Less => {
|
|
let n = (n + 1).min(self.byte_len());
|
|
self.cursor = n
|
|
}
|
|
Ordering::Equal => {
|
|
self.cursor = n
|
|
}
|
|
Ordering::Greater => {
|
|
let n = n.saturating_sub(1);
|
|
self.cursor = n
|
|
}
|
|
}
|
|
}
|
|
}
|
|
MotionKind::Range(range) => {
|
|
assert!((0..self.byte_len()).contains(&range.0));
|
|
if self.cursor != range.0 {
|
|
self.cursor = range.0
|
|
}
|
|
}
|
|
MotionKind::Line(n) => {
|
|
match n.cmp(&0) {
|
|
Ordering::Equal => (),
|
|
Ordering::Less => {
|
|
for _ in 0..n.unsigned_abs() {
|
|
let Some(pos) = self.find_prev_line_pos() else {
|
|
return
|
|
};
|
|
self.cursor = pos;
|
|
}
|
|
}
|
|
Ordering::Greater => {
|
|
for _ in 0..n.unsigned_abs() {
|
|
let Some(pos) = self.find_next_line_pos() else {
|
|
return
|
|
};
|
|
self.cursor = pos;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
MotionKind::ToLine(n) => {
|
|
let Some((start,_)) = self.select_line(n) else {
|
|
return
|
|
};
|
|
self.cursor = start;
|
|
}
|
|
MotionKind::Null => { /* Pass */ }
|
|
MotionKind::ScreenLine(n) => {
|
|
let Some(pos) = self.calculate_display_offset(n) else {
|
|
return
|
|
};
|
|
self.cursor = pos;
|
|
}
|
|
}
|
|
if let Some(mut mode) = self.select_mode {
|
|
let Some(range) = self.selected_range.clone() else {
|
|
return
|
|
};
|
|
let (mut start,mut end) = (range.start,range.end);
|
|
match mode {
|
|
SelectionMode::Char(anchor) => {
|
|
match anchor {
|
|
SelectionAnchor::Start => {
|
|
start = self.cursor;
|
|
}
|
|
SelectionAnchor::End => {
|
|
end = self.cursor;
|
|
}
|
|
}
|
|
}
|
|
SelectionMode::Line(anchor) => todo!(),
|
|
SelectionMode::Block(anchor) => todo!(),
|
|
}
|
|
if start >= end {
|
|
flog!(DEBUG, "inverting anchor");
|
|
mode.invert_anchor();
|
|
flog!(DEBUG,start,end);
|
|
std::mem::swap(&mut start, &mut end);
|
|
|
|
self.select_mode = Some(mode);
|
|
flog!(DEBUG,start,end);
|
|
flog!(DEBUG,mode);
|
|
}
|
|
self.selected_range = Some(start..end);
|
|
}
|
|
}
|
|
pub fn edit_is_merging(&self) -> bool {
|
|
self.undo_stack.last().is_some_and(|edit| edit.merging)
|
|
}
|
|
pub fn handle_edit(&mut self, old: String, new: String, curs_pos: usize) {
|
|
if self.edit_is_merging() {
|
|
let diff = Edit::diff(&old, &new, curs_pos);
|
|
if diff.is_empty() {
|
|
return
|
|
}
|
|
let Some(mut edit) = self.undo_stack.pop() else {
|
|
self.undo_stack.push(diff);
|
|
return
|
|
};
|
|
|
|
edit.new.push_str(&diff.new);
|
|
edit.old.push_str(&diff.old);
|
|
|
|
self.undo_stack.push(edit);
|
|
} else {
|
|
let diff = Edit::diff(&old, &new, curs_pos);
|
|
flog!(DEBUG, diff);
|
|
if !diff.is_empty() {
|
|
self.undo_stack.push(diff);
|
|
}
|
|
}
|
|
}
|
|
pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> {
|
|
let clear_redos = !cmd.is_undo_op() || cmd.verb.as_ref().is_some_and(|v| v.1.is_edit());
|
|
let is_char_insert = cmd.verb.as_ref().is_some_and(|v| v.1.is_char_insert());
|
|
let is_line_motion = cmd.is_line_motion();
|
|
let is_undo_op = cmd.is_undo_op();
|
|
|
|
// Merge character inserts into one edit
|
|
if self.edit_is_merging() && cmd.verb.as_ref().is_none_or(|v| !v.1.is_char_insert()) {
|
|
if let Some(edit) = self.undo_stack.last_mut() {
|
|
edit.stop_merge();
|
|
}
|
|
}
|
|
|
|
let ViCmd { register, verb, motion, .. } = cmd;
|
|
|
|
let verb_count = verb.as_ref().map(|v| v.0);
|
|
let motion_count = motion.as_ref().map(|m| m.0);
|
|
|
|
let before = self.buffer.clone();
|
|
let cursor_pos = self.cursor;
|
|
|
|
for _ in 0..verb_count.unwrap_or(1) {
|
|
for _ in 0..motion_count.unwrap_or(1) {
|
|
let motion_eval = motion
|
|
.clone()
|
|
.map(|m| self.eval_motion(m.1))
|
|
.unwrap_or({
|
|
self.selected_range
|
|
.clone()
|
|
.map(MotionKind::range)
|
|
.unwrap_or(MotionKind::Null)
|
|
});
|
|
|
|
flog!(DEBUG,self.hint);
|
|
if let Some(verb) = verb.clone() {
|
|
self.exec_verb(verb.1, motion_eval, register)?;
|
|
} else if self.has_hint() {
|
|
let motion_eval = motion
|
|
.clone()
|
|
.map(|m| self.eval_motion_with_hint(m.1))
|
|
.unwrap_or(MotionKind::Null);
|
|
flog!(DEBUG, "applying motion with hint");
|
|
self.apply_motion_with_hint(motion_eval);
|
|
} else {
|
|
self.apply_motion(/*forced*/ false,motion_eval);
|
|
}
|
|
}
|
|
}
|
|
|
|
let after = self.buffer.clone();
|
|
if clear_redos {
|
|
self.redo_stack.clear();
|
|
}
|
|
|
|
if before != after && !is_undo_op {
|
|
self.handle_edit(before, after, cursor_pos);
|
|
}
|
|
|
|
if !is_line_motion {
|
|
self.saved_col = None;
|
|
}
|
|
|
|
if is_char_insert {
|
|
if let Some(edit) = self.undo_stack.last_mut() {
|
|
edit.start_merge();
|
|
}
|
|
}
|
|
|
|
flog!(DEBUG, self.select_mode);
|
|
flog!(DEBUG, self.selected_range);
|
|
|
|
if self.clamp_cursor {
|
|
self.clamp_cursor();
|
|
}
|
|
self.sync_cursor();
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Display for LineBuf {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
let mut full_buf = self.buffer.clone();
|
|
if let Some(range) = self.selected_range.clone() {
|
|
let mode = self.select_mode.unwrap_or_default();
|
|
match mode.anchor() {
|
|
SelectionAnchor::Start => {
|
|
let mut inclusive = range.start..=range.end;
|
|
if *inclusive.end() == self.byte_len() {
|
|
inclusive = range.start..=range.end.saturating_sub(1);
|
|
}
|
|
let selected = full_buf[inclusive.clone()].styled(Style::BgWhite | Style::Black);
|
|
full_buf.replace_range(inclusive, &selected);
|
|
}
|
|
SelectionAnchor::End => {
|
|
let selected = full_buf[range.clone()].styled(Style::BgWhite | Style::Black);
|
|
full_buf.replace_range(range, &selected);
|
|
}
|
|
}
|
|
}
|
|
if let Some(hint) = self.hint.as_ref() {
|
|
full_buf.push_str(&hint.styled(Style::BrightBlack));
|
|
}
|
|
write!(f,"{}",full_buf)
|
|
}
|
|
}
|
|
|
|
pub fn strip_ansi_codes_and_escapes(s: &str) -> String {
|
|
let mut out = String::with_capacity(s.len());
|
|
let mut chars = s.chars().peekable();
|
|
|
|
while let Some(c) = chars.next() {
|
|
if c == '\x1b' && chars.peek() == Some(&'[') {
|
|
// Skip over the escape sequence
|
|
chars.next(); // consume '['
|
|
while let Some(&ch) = chars.peek() {
|
|
if ch.is_ascii_lowercase() || ch.is_ascii_uppercase() {
|
|
chars.next(); // consume final letter
|
|
break;
|
|
}
|
|
chars.next(); // consume intermediate characters
|
|
}
|
|
} else {
|
|
match c {
|
|
'\n' |
|
|
'\r' => { /* Continue */ }
|
|
_ => out.push(c)
|
|
}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
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 is_grapheme_boundary(s: &str, pos: usize) -> bool {
|
|
s.is_char_boundary(pos) && s.grapheme_indices(true).any(|(i,_)| i == pos)
|
|
}
|
|
|
|
fn mk_range_inclusive(a: usize, b: usize) -> Range<usize> {
|
|
let b = b + 1;
|
|
std::cmp::min(a, b)..std::cmp::max(a, b)
|
|
}
|
|
|
|
fn mk_range(a: usize, b: usize) -> Range<usize> {
|
|
std::cmp::min(a, b)..std::cmp::max(a, b)
|
|
}
|