Added rustfmt.toml, formatted codebase
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
pub mod readline;
|
||||
pub mod highlight;
|
||||
pub mod readline;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use readline::{FernVi, Readline};
|
||||
|
||||
use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*, shopt::FernEditMode, state::read_shopts};
|
||||
use crate::{
|
||||
expand::expand_prompt, libsh::error::ShResult, prelude::*, shopt::FernEditMode,
|
||||
state::read_shopts,
|
||||
};
|
||||
|
||||
/// Initialize the line editor
|
||||
fn get_prompt() -> ShResult<String> {
|
||||
let Ok(prompt) = env::var("PS1") else {
|
||||
// prompt expands to:
|
||||
//
|
||||
// username@hostname
|
||||
// short/path/to/pwd/
|
||||
// $ _
|
||||
let default = "\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ";
|
||||
return expand_prompt(default)
|
||||
};
|
||||
let Ok(prompt) = env::var("PS1") else {
|
||||
// prompt expands to:
|
||||
//
|
||||
// username@hostname
|
||||
// short/path/to/pwd/
|
||||
// $ _
|
||||
let default =
|
||||
"\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ";
|
||||
return expand_prompt(default);
|
||||
};
|
||||
|
||||
expand_prompt(&prompt)
|
||||
expand_prompt(&prompt)
|
||||
}
|
||||
|
||||
pub fn readline(edit_mode: FernEditMode) -> ShResult<String> {
|
||||
let prompt = get_prompt()?;
|
||||
let mut reader: Box<dyn Readline> = match edit_mode {
|
||||
FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))?),
|
||||
FernEditMode::Emacs => todo!()
|
||||
};
|
||||
reader.readline()
|
||||
let prompt = get_prompt()?;
|
||||
let mut reader: Box<dyn Readline> = match edit_mode {
|
||||
FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))?),
|
||||
FernEditMode::Emacs => todo!(),
|
||||
};
|
||||
reader.readline()
|
||||
}
|
||||
|
||||
@@ -1,350 +1,393 @@
|
||||
use std::{env, fmt::{Write,Display}, fs::{self, OpenOptions}, io::Write as IoWrite, path::{Path, PathBuf}, str::FromStr, time::{Duration, SystemTime, UNIX_EPOCH}};
|
||||
use std::{
|
||||
env,
|
||||
fmt::{Display, Write},
|
||||
fs::{self, OpenOptions},
|
||||
io::Write as IoWrite,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
||||
use crate::prelude::*;
|
||||
|
||||
use super::vicmd::Direction; // surprisingly useful
|
||||
|
||||
#[derive(Default,Clone,Copy,Debug)]
|
||||
#[derive(Default, Clone, Copy, Debug)]
|
||||
pub enum SearchKind {
|
||||
Fuzzy,
|
||||
#[default]
|
||||
Prefix
|
||||
Fuzzy,
|
||||
#[default]
|
||||
Prefix,
|
||||
}
|
||||
|
||||
#[derive(Default,Clone,Debug)]
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct SearchConstraint {
|
||||
kind: SearchKind,
|
||||
term: String,
|
||||
kind: SearchKind,
|
||||
term: String,
|
||||
}
|
||||
|
||||
impl SearchConstraint {
|
||||
pub fn new(kind: SearchKind, term: String) -> Self {
|
||||
Self { kind, term }
|
||||
}
|
||||
pub fn new(kind: SearchKind, term: String) -> Self {
|
||||
Self { kind, term }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HistEntry {
|
||||
id: u32,
|
||||
timestamp: SystemTime,
|
||||
command: String,
|
||||
new: bool
|
||||
id: u32,
|
||||
timestamp: SystemTime,
|
||||
command: String,
|
||||
new: bool,
|
||||
}
|
||||
|
||||
impl HistEntry {
|
||||
pub fn id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
pub fn timestamp(&self) -> &SystemTime {
|
||||
&self.timestamp
|
||||
}
|
||||
pub fn command(&self) -> &str {
|
||||
&self.command
|
||||
}
|
||||
fn with_escaped_newlines(&self) -> String {
|
||||
let mut escaped = String::new();
|
||||
let mut chars = self.command.chars();
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'\\' => {
|
||||
escaped.push(ch);
|
||||
if let Some(ch) = chars.next() {
|
||||
escaped.push(ch)
|
||||
}
|
||||
}
|
||||
'\n' => {
|
||||
escaped.push_str("\\\n");
|
||||
}
|
||||
_ => escaped.push(ch),
|
||||
}
|
||||
}
|
||||
escaped
|
||||
}
|
||||
pub fn is_new(&self) -> bool {
|
||||
self.new
|
||||
}
|
||||
pub fn id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
pub fn timestamp(&self) -> &SystemTime {
|
||||
&self.timestamp
|
||||
}
|
||||
pub fn command(&self) -> &str {
|
||||
&self.command
|
||||
}
|
||||
fn with_escaped_newlines(&self) -> String {
|
||||
let mut escaped = String::new();
|
||||
let mut chars = self.command.chars();
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'\\' => {
|
||||
escaped.push(ch);
|
||||
if let Some(ch) = chars.next() {
|
||||
escaped.push(ch)
|
||||
}
|
||||
}
|
||||
'\n' => {
|
||||
escaped.push_str("\\\n");
|
||||
}
|
||||
_ => escaped.push(ch),
|
||||
}
|
||||
}
|
||||
escaped
|
||||
}
|
||||
pub fn is_new(&self) -> bool {
|
||||
self.new
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for HistEntry {
|
||||
type Err = ShErr;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let err = Err(
|
||||
ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on history entry '{s}'"), notes: vec![] }
|
||||
);
|
||||
type Err = ShErr;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let err = Err(ShErr::Simple {
|
||||
kind: ShErrKind::HistoryReadErr,
|
||||
msg: format!("Bad formatting on history entry '{s}'"),
|
||||
notes: vec![],
|
||||
});
|
||||
|
||||
//: 248972349;148;echo foo; echo bar
|
||||
let Some(cleaned) = s.strip_prefix(": ") else { return err };
|
||||
//248972349;148;echo foo; echo bar
|
||||
let Some((timestamp,id_and_command)) = cleaned.split_once(';') else { return err };
|
||||
//("248972349","148;echo foo; echo bar")
|
||||
let Some((id,command)) = id_and_command.split_once(';') else { return err };
|
||||
//("148","echo foo; echo bar")
|
||||
let Ok(ts_seconds) = timestamp.parse::<u64>() else { return err };
|
||||
let Ok(id) = id.parse::<u32>() else { return err };
|
||||
let timestamp = UNIX_EPOCH + Duration::from_secs(ts_seconds);
|
||||
let command = command.to_string();
|
||||
Ok(Self { id, timestamp, command, new: false })
|
||||
}
|
||||
//: 248972349;148;echo foo; echo bar
|
||||
let Some(cleaned) = s.strip_prefix(": ") else {
|
||||
return err;
|
||||
};
|
||||
//248972349;148;echo foo; echo bar
|
||||
let Some((timestamp, id_and_command)) = cleaned.split_once(';') else {
|
||||
return err;
|
||||
};
|
||||
//("248972349","148;echo foo; echo bar")
|
||||
let Some((id, command)) = id_and_command.split_once(';') else {
|
||||
return err;
|
||||
};
|
||||
//("148","echo foo; echo bar")
|
||||
let Ok(ts_seconds) = timestamp.parse::<u64>() else {
|
||||
return err;
|
||||
};
|
||||
let Ok(id) = id.parse::<u32>() else {
|
||||
return err;
|
||||
};
|
||||
let timestamp = UNIX_EPOCH + Duration::from_secs(ts_seconds);
|
||||
let command = command.to_string();
|
||||
Ok(Self {
|
||||
id,
|
||||
timestamp,
|
||||
command,
|
||||
new: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for HistEntry {
|
||||
/// Similar to zsh's history format, but not entirely
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let command = self.with_escaped_newlines();
|
||||
let HistEntry { id, timestamp, command: _, new: _ } = self;
|
||||
let timestamp = timestamp.duration_since(UNIX_EPOCH).unwrap().as_secs();
|
||||
writeln!(f, ": {timestamp};{id};{command}")
|
||||
}
|
||||
/// Similar to zsh's history format, but not entirely
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let command = self.with_escaped_newlines();
|
||||
let HistEntry {
|
||||
id,
|
||||
timestamp,
|
||||
command: _,
|
||||
new: _,
|
||||
} = self;
|
||||
let timestamp = timestamp.duration_since(UNIX_EPOCH).unwrap().as_secs();
|
||||
writeln!(f, ": {timestamp};{id};{command}")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HistEntries(Vec<HistEntry>);
|
||||
|
||||
|
||||
impl FromStr for HistEntries {
|
||||
type Err = ShErr;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut entries = vec![];
|
||||
type Err = ShErr;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut entries = vec![];
|
||||
|
||||
let mut lines = s.lines().enumerate().peekable();
|
||||
let mut cur_line = String::new();
|
||||
let mut lines = s.lines().enumerate().peekable();
|
||||
let mut cur_line = String::new();
|
||||
|
||||
while let Some((i,line)) = lines.next() {
|
||||
if !line.starts_with(": ") {
|
||||
return Err(
|
||||
ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on line {i}"), notes: vec![] }
|
||||
)
|
||||
}
|
||||
let mut chars = line.chars().peekable();
|
||||
let mut feeding_lines = true;
|
||||
while feeding_lines {
|
||||
feeding_lines = false;
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'\\' => {
|
||||
if let Some(esc_ch) = chars.next() {
|
||||
cur_line.push(esc_ch);
|
||||
} else {
|
||||
cur_line.push('\n');
|
||||
feeding_lines = true;
|
||||
}
|
||||
}
|
||||
'\n' => {
|
||||
break
|
||||
}
|
||||
_ => {
|
||||
cur_line.push(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
if feeding_lines {
|
||||
let Some((_,line)) = lines.next() else {
|
||||
return Err(
|
||||
ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on line {i}"), notes: vec![] }
|
||||
)
|
||||
};
|
||||
chars = line.chars().peekable();
|
||||
}
|
||||
}
|
||||
let entry = cur_line.parse::<HistEntry>()?;
|
||||
entries.push(entry);
|
||||
cur_line.clear();
|
||||
}
|
||||
while let Some((i, line)) = lines.next() {
|
||||
if !line.starts_with(": ") {
|
||||
return Err(ShErr::Simple {
|
||||
kind: ShErrKind::HistoryReadErr,
|
||||
msg: format!("Bad formatting on line {i}"),
|
||||
notes: vec![],
|
||||
});
|
||||
}
|
||||
let mut chars = line.chars().peekable();
|
||||
let mut feeding_lines = true;
|
||||
while feeding_lines {
|
||||
feeding_lines = false;
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'\\' => {
|
||||
if let Some(esc_ch) = chars.next() {
|
||||
cur_line.push(esc_ch);
|
||||
} else {
|
||||
cur_line.push('\n');
|
||||
feeding_lines = true;
|
||||
}
|
||||
}
|
||||
'\n' => break,
|
||||
_ => {
|
||||
cur_line.push(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
if feeding_lines {
|
||||
let Some((_, line)) = lines.next() else {
|
||||
return Err(ShErr::Simple {
|
||||
kind: ShErrKind::HistoryReadErr,
|
||||
msg: format!("Bad formatting on line {i}"),
|
||||
notes: vec![],
|
||||
});
|
||||
};
|
||||
chars = line.chars().peekable();
|
||||
}
|
||||
}
|
||||
let entry = cur_line.parse::<HistEntry>()?;
|
||||
entries.push(entry);
|
||||
cur_line.clear();
|
||||
}
|
||||
|
||||
|
||||
Ok(Self(entries))
|
||||
}
|
||||
Ok(Self(entries))
|
||||
}
|
||||
}
|
||||
|
||||
fn read_hist_file(path: &Path) -> ShResult<Vec<HistEntry>> {
|
||||
if !path.exists() {
|
||||
fs::File::create(path)?;
|
||||
}
|
||||
let raw = fs::read_to_string(path)?;
|
||||
Ok(raw.parse::<HistEntries>()?.0)
|
||||
if !path.exists() {
|
||||
fs::File::create(path)?;
|
||||
}
|
||||
let raw = fs::read_to_string(path)?;
|
||||
Ok(raw.parse::<HistEntries>()?.0)
|
||||
}
|
||||
|
||||
pub struct History {
|
||||
path: PathBuf,
|
||||
entries: Vec<HistEntry>,
|
||||
search_mask: Vec<HistEntry>,
|
||||
cursor: usize,
|
||||
search_direction: Direction,
|
||||
ignore_dups: bool,
|
||||
max_size: Option<u32>,
|
||||
path: PathBuf,
|
||||
entries: Vec<HistEntry>,
|
||||
search_mask: Vec<HistEntry>,
|
||||
cursor: usize,
|
||||
search_direction: Direction,
|
||||
ignore_dups: bool,
|
||||
max_size: Option<u32>,
|
||||
}
|
||||
|
||||
impl History {
|
||||
pub fn new() -> ShResult<Self> {
|
||||
let path = PathBuf::from(env::var("FERNHIST").unwrap_or({
|
||||
let home = env::var("HOME").unwrap();
|
||||
format!("{home}/.fern_history")
|
||||
}));
|
||||
let mut entries = read_hist_file(&path)?;
|
||||
{
|
||||
let id = entries.last().map(|ent| ent.id + 1).unwrap_or(0);
|
||||
let timestamp = SystemTime::now();
|
||||
let command = "".into();
|
||||
entries.push(HistEntry { id, timestamp, command, new: true })
|
||||
}
|
||||
let search_mask = entries.clone();
|
||||
let cursor = entries.len() - 1;
|
||||
let mut new = Self {
|
||||
path,
|
||||
entries,
|
||||
search_mask,
|
||||
cursor,
|
||||
search_direction: Direction::Backward,
|
||||
ignore_dups: true,
|
||||
max_size: None,
|
||||
};
|
||||
new.push_empty_entry(); // Current pending command
|
||||
Ok(new)
|
||||
}
|
||||
pub fn new() -> ShResult<Self> {
|
||||
let path = PathBuf::from(env::var("FERNHIST").unwrap_or({
|
||||
let home = env::var("HOME").unwrap();
|
||||
format!("{home}/.fern_history")
|
||||
}));
|
||||
let mut entries = read_hist_file(&path)?;
|
||||
{
|
||||
let id = entries.last().map(|ent| ent.id + 1).unwrap_or(0);
|
||||
let timestamp = SystemTime::now();
|
||||
let command = "".into();
|
||||
entries.push(HistEntry {
|
||||
id,
|
||||
timestamp,
|
||||
command,
|
||||
new: true,
|
||||
})
|
||||
}
|
||||
let search_mask = entries.clone();
|
||||
let cursor = entries.len() - 1;
|
||||
let mut new = Self {
|
||||
path,
|
||||
entries,
|
||||
search_mask,
|
||||
cursor,
|
||||
search_direction: Direction::Backward,
|
||||
ignore_dups: true,
|
||||
max_size: None,
|
||||
};
|
||||
new.push_empty_entry(); // Current pending command
|
||||
Ok(new)
|
||||
}
|
||||
|
||||
pub fn entries(&self) -> &[HistEntry] {
|
||||
&self.entries
|
||||
}
|
||||
pub fn entries(&self) -> &[HistEntry] {
|
||||
&self.entries
|
||||
}
|
||||
|
||||
pub fn masked_entries(&self) -> &[HistEntry] {
|
||||
&self.search_mask
|
||||
}
|
||||
pub fn masked_entries(&self) -> &[HistEntry] {
|
||||
&self.search_mask
|
||||
}
|
||||
|
||||
pub fn push_empty_entry(&mut self) {
|
||||
}
|
||||
pub fn push_empty_entry(&mut self) {}
|
||||
|
||||
pub fn cursor_entry(&self) -> Option<&HistEntry> {
|
||||
self.search_mask.get(self.cursor)
|
||||
}
|
||||
pub fn cursor_entry(&self) -> Option<&HistEntry> {
|
||||
self.search_mask.get(self.cursor)
|
||||
}
|
||||
|
||||
pub fn update_pending_cmd(&mut self, command: &str) {
|
||||
let Some(ent) = self.last_mut() else {
|
||||
return
|
||||
};
|
||||
let cmd = command.to_string();
|
||||
let constraint = SearchConstraint { kind: SearchKind::Prefix, term: cmd.clone() };
|
||||
pub fn update_pending_cmd(&mut self, command: &str) {
|
||||
let Some(ent) = self.last_mut() else { return };
|
||||
let cmd = command.to_string();
|
||||
let constraint = SearchConstraint {
|
||||
kind: SearchKind::Prefix,
|
||||
term: cmd.clone(),
|
||||
};
|
||||
|
||||
ent.command = cmd;
|
||||
self.constrain_entries(constraint);
|
||||
}
|
||||
|
||||
ent.command = cmd;
|
||||
self.constrain_entries(constraint);
|
||||
}
|
||||
pub fn last_mut(&mut self) -> Option<&mut HistEntry> {
|
||||
self.entries.last_mut()
|
||||
}
|
||||
|
||||
pub fn last_mut(&mut self) -> Option<&mut HistEntry> {
|
||||
self.entries.last_mut()
|
||||
}
|
||||
pub fn get_new_id(&self) -> u32 {
|
||||
let Some(ent) = self.entries.last() else {
|
||||
return 0;
|
||||
};
|
||||
ent.id + 1
|
||||
}
|
||||
|
||||
pub fn get_new_id(&self) -> u32 {
|
||||
let Some(ent) = self.entries.last() else {
|
||||
return 0
|
||||
};
|
||||
ent.id + 1
|
||||
}
|
||||
pub fn ignore_dups(&mut self, yn: bool) {
|
||||
self.ignore_dups = yn
|
||||
}
|
||||
|
||||
pub fn ignore_dups(&mut self, yn: bool) {
|
||||
self.ignore_dups = yn
|
||||
}
|
||||
pub fn max_hist_size(&mut self, size: Option<u32>) {
|
||||
self.max_size = size
|
||||
}
|
||||
|
||||
pub fn max_hist_size(&mut self, size: Option<u32>) {
|
||||
self.max_size = size
|
||||
}
|
||||
pub fn constrain_entries(&mut self, constraint: SearchConstraint) {
|
||||
flog!(DEBUG, constraint);
|
||||
let SearchConstraint { kind, term } = constraint;
|
||||
match kind {
|
||||
SearchKind::Prefix => {
|
||||
if term.is_empty() {
|
||||
self.search_mask = self.entries.clone();
|
||||
} else {
|
||||
let filtered = self
|
||||
.entries
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|ent| ent.command().starts_with(&term));
|
||||
|
||||
pub fn constrain_entries(&mut self, constraint: SearchConstraint) {
|
||||
flog!(DEBUG,constraint);
|
||||
let SearchConstraint { kind, term } = constraint;
|
||||
match kind {
|
||||
SearchKind::Prefix => {
|
||||
if term.is_empty() {
|
||||
self.search_mask = self.entries.clone();
|
||||
} else {
|
||||
let filtered = self.entries
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|ent| ent.command().starts_with(&term));
|
||||
self.search_mask = filtered.collect();
|
||||
}
|
||||
self.cursor = self.search_mask.len().saturating_sub(1);
|
||||
}
|
||||
SearchKind::Fuzzy => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
self.search_mask = filtered.collect();
|
||||
}
|
||||
self.cursor = self.search_mask.len().saturating_sub(1);
|
||||
}
|
||||
SearchKind::Fuzzy => todo!(),
|
||||
}
|
||||
}
|
||||
pub fn hint_entry(&self) -> Option<&HistEntry> {
|
||||
let second_to_last = self.search_mask.len().checked_sub(2)?;
|
||||
self.search_mask.get(second_to_last)
|
||||
}
|
||||
|
||||
pub fn hint_entry(&self) -> Option<&HistEntry> {
|
||||
let second_to_last = self.search_mask.len().checked_sub(2)?;
|
||||
self.search_mask.get(second_to_last)
|
||||
}
|
||||
pub fn get_hint(&self) -> Option<String> {
|
||||
if self
|
||||
.cursor_entry()
|
||||
.is_some_and(|ent| ent.is_new() && !ent.command().is_empty())
|
||||
{
|
||||
let entry = self.hint_entry()?;
|
||||
let prefix = self.cursor_entry()?.command();
|
||||
Some(entry.command().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_hint(&self) -> Option<String> {
|
||||
if self.cursor_entry().is_some_and(|ent| ent.is_new() && !ent.command().is_empty()) {
|
||||
let entry = self.hint_entry()?;
|
||||
let prefix = self.cursor_entry()?.command();
|
||||
Some(entry.command().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> {
|
||||
let new_idx = self
|
||||
.cursor
|
||||
.saturating_add_signed(offset)
|
||||
.clamp(0, self.search_mask.len().saturating_sub(1));
|
||||
let ent = self.search_mask.get(new_idx)?;
|
||||
|
||||
pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> {
|
||||
let new_idx = self.cursor
|
||||
.saturating_add_signed(offset)
|
||||
.clamp(0, self.search_mask.len().saturating_sub(1));
|
||||
let ent = self.search_mask.get(new_idx)?;
|
||||
self.cursor = new_idx;
|
||||
|
||||
self.cursor = new_idx;
|
||||
Some(ent)
|
||||
}
|
||||
|
||||
Some(ent)
|
||||
}
|
||||
pub fn push(&mut self, command: String) {
|
||||
let timestamp = SystemTime::now();
|
||||
let id = self.get_new_id();
|
||||
if self.ignore_dups && self.is_dup(&command) {
|
||||
return;
|
||||
}
|
||||
self.entries.push(HistEntry {
|
||||
id,
|
||||
timestamp,
|
||||
command,
|
||||
new: true,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn push(&mut self, command: String) {
|
||||
let timestamp = SystemTime::now();
|
||||
let id = self.get_new_id();
|
||||
if self.ignore_dups && self.is_dup(&command) {
|
||||
return
|
||||
}
|
||||
self.entries.push(HistEntry { id, timestamp, command, new: true });
|
||||
}
|
||||
pub fn is_dup(&self, other: &str) -> bool {
|
||||
let Some(ent) = self.entries.last() else {
|
||||
return false;
|
||||
};
|
||||
let ent_cmd = &ent.command;
|
||||
ent_cmd == other
|
||||
}
|
||||
|
||||
pub fn is_dup(&self, other: &str) -> bool {
|
||||
let Some(ent) = self.entries.last() else {
|
||||
return false
|
||||
};
|
||||
let ent_cmd = &ent.command;
|
||||
ent_cmd == other
|
||||
}
|
||||
pub fn save(&mut self) -> ShResult<()> {
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&self.path)?;
|
||||
|
||||
pub fn save(&mut self) -> ShResult<()> {
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&self.path)?;
|
||||
let last_file_entry = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|ent| !ent.new)
|
||||
.next_back()
|
||||
.map(|ent| ent.command.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let last_file_entry = self.entries
|
||||
.iter()
|
||||
.filter(|ent| !ent.new)
|
||||
.next_back()
|
||||
.map(|ent| ent.command.clone())
|
||||
.unwrap_or_default();
|
||||
let entries = self.entries.iter_mut().filter(|ent| {
|
||||
ent.new
|
||||
&& !ent.command.is_empty()
|
||||
&& if self.ignore_dups {
|
||||
ent.command() != last_file_entry
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
let entries = self.entries
|
||||
.iter_mut()
|
||||
.filter(|ent| {
|
||||
ent.new &&
|
||||
!ent.command.is_empty() &&
|
||||
if self.ignore_dups {
|
||||
ent.command() != last_file_entry
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
let mut data = String::new();
|
||||
for ent in entries {
|
||||
ent.new = false;
|
||||
write!(data, "{ent}").unwrap();
|
||||
}
|
||||
|
||||
let mut data = String::new();
|
||||
for ent in entries {
|
||||
ent.new = false;
|
||||
write!(data, "{ent}").unwrap();
|
||||
}
|
||||
file.write_all(data.as_bytes())?;
|
||||
|
||||
file.write_all(data.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,140 +3,137 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
// Credit to Rustyline for the design ideas in this module
|
||||
// https://github.com/kkawakam/rustyline
|
||||
#[derive(Clone,PartialEq,Eq,Debug)]
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct KeyEvent(pub KeyCode, pub ModKeys);
|
||||
|
||||
|
||||
impl KeyEvent {
|
||||
pub fn new(ch: &str, mut mods: ModKeys) -> Self {
|
||||
use {KeyCode as K, KeyEvent as E, ModKeys as M};
|
||||
pub fn new(ch: &str, mut mods: ModKeys) -> Self {
|
||||
use {KeyCode as K, KeyEvent as E, ModKeys as M};
|
||||
|
||||
let mut graphemes = ch.graphemes(true);
|
||||
let mut graphemes = ch.graphemes(true);
|
||||
|
||||
let first = match graphemes.next() {
|
||||
Some(g) => g,
|
||||
None => return E(K::Null, mods),
|
||||
};
|
||||
let first = match graphemes.next() {
|
||||
Some(g) => g,
|
||||
None => return E(K::Null, mods),
|
||||
};
|
||||
|
||||
// If more than one grapheme, it's not a single key event
|
||||
if graphemes.next().is_some() {
|
||||
return E(K::Null, mods); // Or panic, or wrap in Grapheme if desired
|
||||
}
|
||||
// If more than one grapheme, it's not a single key event
|
||||
if graphemes.next().is_some() {
|
||||
return E(K::Null, mods); // Or panic, or wrap in Grapheme if desired
|
||||
}
|
||||
|
||||
let mut chars = first.chars();
|
||||
let mut chars = first.chars();
|
||||
|
||||
let single_char = chars.next();
|
||||
let is_single_char = chars.next().is_none();
|
||||
let single_char = chars.next();
|
||||
let is_single_char = chars.next().is_none();
|
||||
|
||||
match single_char {
|
||||
Some(c) if is_single_char && c.is_control() => {
|
||||
match c {
|
||||
'\x00' => E(K::Char('@'), mods | M::CTRL),
|
||||
'\x01' => E(K::Char('A'), mods | M::CTRL),
|
||||
'\x02' => E(K::Char('B'), mods | M::CTRL),
|
||||
'\x03' => E(K::Char('C'), mods | M::CTRL),
|
||||
'\x04' => E(K::Char('D'), mods | M::CTRL),
|
||||
'\x05' => E(K::Char('E'), mods | M::CTRL),
|
||||
'\x06' => E(K::Char('F'), mods | M::CTRL),
|
||||
'\x07' => E(K::Char('G'), mods | M::CTRL),
|
||||
'\x08' => E(K::Backspace, mods),
|
||||
'\x09' => {
|
||||
if mods.contains(M::SHIFT) {
|
||||
mods.remove(M::SHIFT);
|
||||
E(K::BackTab, mods)
|
||||
} else {
|
||||
E(K::Tab, mods)
|
||||
}
|
||||
}
|
||||
'\x0a' => E(K::Char('J'), mods | M::CTRL),
|
||||
'\x0b' => E(K::Char('K'), mods | M::CTRL),
|
||||
'\x0c' => E(K::Char('L'), mods | M::CTRL),
|
||||
'\x0d' => E(K::Enter, mods),
|
||||
'\x0e' => E(K::Char('N'), mods | M::CTRL),
|
||||
'\x0f' => E(K::Char('O'), mods | M::CTRL),
|
||||
'\x10' => E(K::Char('P'), mods | M::CTRL),
|
||||
'\x11' => E(K::Char('Q'), mods | M::CTRL),
|
||||
'\x12' => E(K::Char('R'), mods | M::CTRL),
|
||||
'\x13' => E(K::Char('S'), mods | M::CTRL),
|
||||
'\x14' => E(K::Char('T'), mods | M::CTRL),
|
||||
'\x15' => E(K::Char('U'), mods | M::CTRL),
|
||||
'\x16' => E(K::Char('V'), mods | M::CTRL),
|
||||
'\x17' => E(K::Char('W'), mods | M::CTRL),
|
||||
'\x18' => E(K::Char('X'), mods | M::CTRL),
|
||||
'\x19' => E(K::Char('Y'), mods | M::CTRL),
|
||||
'\x1a' => E(K::Char('Z'), mods | M::CTRL),
|
||||
'\x1b' => E(K::Esc, mods),
|
||||
'\x1c' => E(K::Char('\\'), mods | M::CTRL),
|
||||
'\x1d' => E(K::Char(']'), mods | M::CTRL),
|
||||
'\x1e' => E(K::Char('^'), mods | M::CTRL),
|
||||
'\x1f' => E(K::Char('_'), mods | M::CTRL),
|
||||
'\x7f' => E(K::Backspace, mods),
|
||||
'\u{9b}' => E(K::Esc, mods | M::SHIFT),
|
||||
_ => E(K::Null, mods),
|
||||
}
|
||||
}
|
||||
Some(c) if is_single_char => {
|
||||
if !mods.is_empty() {
|
||||
mods.remove(M::SHIFT);
|
||||
}
|
||||
E(K::Char(c), mods)
|
||||
}
|
||||
_ => {
|
||||
// multi-char grapheme (emoji, accented, etc)
|
||||
if !mods.is_empty() {
|
||||
mods.remove(M::SHIFT);
|
||||
}
|
||||
E(K::Grapheme(Arc::from(first)), mods)
|
||||
}
|
||||
}
|
||||
}
|
||||
match single_char {
|
||||
Some(c) if is_single_char && c.is_control() => match c {
|
||||
'\x00' => E(K::Char('@'), mods | M::CTRL),
|
||||
'\x01' => E(K::Char('A'), mods | M::CTRL),
|
||||
'\x02' => E(K::Char('B'), mods | M::CTRL),
|
||||
'\x03' => E(K::Char('C'), mods | M::CTRL),
|
||||
'\x04' => E(K::Char('D'), mods | M::CTRL),
|
||||
'\x05' => E(K::Char('E'), mods | M::CTRL),
|
||||
'\x06' => E(K::Char('F'), mods | M::CTRL),
|
||||
'\x07' => E(K::Char('G'), mods | M::CTRL),
|
||||
'\x08' => E(K::Backspace, mods),
|
||||
'\x09' => {
|
||||
if mods.contains(M::SHIFT) {
|
||||
mods.remove(M::SHIFT);
|
||||
E(K::BackTab, mods)
|
||||
} else {
|
||||
E(K::Tab, mods)
|
||||
}
|
||||
}
|
||||
'\x0a' => E(K::Char('J'), mods | M::CTRL),
|
||||
'\x0b' => E(K::Char('K'), mods | M::CTRL),
|
||||
'\x0c' => E(K::Char('L'), mods | M::CTRL),
|
||||
'\x0d' => E(K::Enter, mods),
|
||||
'\x0e' => E(K::Char('N'), mods | M::CTRL),
|
||||
'\x0f' => E(K::Char('O'), mods | M::CTRL),
|
||||
'\x10' => E(K::Char('P'), mods | M::CTRL),
|
||||
'\x11' => E(K::Char('Q'), mods | M::CTRL),
|
||||
'\x12' => E(K::Char('R'), mods | M::CTRL),
|
||||
'\x13' => E(K::Char('S'), mods | M::CTRL),
|
||||
'\x14' => E(K::Char('T'), mods | M::CTRL),
|
||||
'\x15' => E(K::Char('U'), mods | M::CTRL),
|
||||
'\x16' => E(K::Char('V'), mods | M::CTRL),
|
||||
'\x17' => E(K::Char('W'), mods | M::CTRL),
|
||||
'\x18' => E(K::Char('X'), mods | M::CTRL),
|
||||
'\x19' => E(K::Char('Y'), mods | M::CTRL),
|
||||
'\x1a' => E(K::Char('Z'), mods | M::CTRL),
|
||||
'\x1b' => E(K::Esc, mods),
|
||||
'\x1c' => E(K::Char('\\'), mods | M::CTRL),
|
||||
'\x1d' => E(K::Char(']'), mods | M::CTRL),
|
||||
'\x1e' => E(K::Char('^'), mods | M::CTRL),
|
||||
'\x1f' => E(K::Char('_'), mods | M::CTRL),
|
||||
'\x7f' => E(K::Backspace, mods),
|
||||
'\u{9b}' => E(K::Esc, mods | M::SHIFT),
|
||||
_ => E(K::Null, mods),
|
||||
},
|
||||
Some(c) if is_single_char => {
|
||||
if !mods.is_empty() {
|
||||
mods.remove(M::SHIFT);
|
||||
}
|
||||
E(K::Char(c), mods)
|
||||
}
|
||||
_ => {
|
||||
// multi-char grapheme (emoji, accented, etc)
|
||||
if !mods.is_empty() {
|
||||
mods.remove(M::SHIFT);
|
||||
}
|
||||
E(K::Grapheme(Arc::from(first)), mods)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,PartialEq,Eq,Debug)]
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub enum KeyCode {
|
||||
UnknownEscSeq,
|
||||
Backspace,
|
||||
BackTab,
|
||||
BracketedPasteStart,
|
||||
BracketedPasteEnd,
|
||||
Char(char),
|
||||
Grapheme(Arc<str>),
|
||||
Delete,
|
||||
Down,
|
||||
End,
|
||||
Enter,
|
||||
Esc,
|
||||
F(u8),
|
||||
Home,
|
||||
Insert,
|
||||
Left,
|
||||
Null,
|
||||
PageDown,
|
||||
PageUp,
|
||||
Right,
|
||||
Tab,
|
||||
Up,
|
||||
UnknownEscSeq,
|
||||
Backspace,
|
||||
BackTab,
|
||||
BracketedPasteStart,
|
||||
BracketedPasteEnd,
|
||||
Char(char),
|
||||
Grapheme(Arc<str>),
|
||||
Delete,
|
||||
Down,
|
||||
End,
|
||||
Enter,
|
||||
Esc,
|
||||
F(u8),
|
||||
Home,
|
||||
Insert,
|
||||
Left,
|
||||
Null,
|
||||
PageDown,
|
||||
PageUp,
|
||||
Right,
|
||||
Tab,
|
||||
Up,
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct ModKeys: u8 {
|
||||
/// Control modifier
|
||||
const CTRL = 1<<3;
|
||||
/// Escape or Alt modifier
|
||||
const ALT = 1<<2;
|
||||
/// Shift modifier
|
||||
const SHIFT = 1<<1;
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct ModKeys: u8 {
|
||||
/// Control modifier
|
||||
const CTRL = 1<<3;
|
||||
/// Escape or Alt modifier
|
||||
const ALT = 1<<2;
|
||||
/// Shift modifier
|
||||
const SHIFT = 1<<1;
|
||||
|
||||
/// No modifier
|
||||
const NONE = 0;
|
||||
/// Ctrl + Shift
|
||||
const CTRL_SHIFT = Self::CTRL.bits() | Self::SHIFT.bits();
|
||||
/// Alt + Shift
|
||||
const ALT_SHIFT = Self::ALT.bits() | Self::SHIFT.bits();
|
||||
/// Ctrl + Alt
|
||||
const CTRL_ALT = Self::CTRL.bits() | Self::ALT.bits();
|
||||
/// Ctrl + Alt + Shift
|
||||
const CTRL_ALT_SHIFT = Self::CTRL.bits() | Self::ALT.bits() | Self::SHIFT.bits();
|
||||
}
|
||||
/// No modifier
|
||||
const NONE = 0;
|
||||
/// Ctrl + Shift
|
||||
const CTRL_SHIFT = Self::CTRL.bits() | Self::SHIFT.bits();
|
||||
/// Alt + Shift
|
||||
const ALT_SHIFT = Self::ALT.bits() | Self::SHIFT.bits();
|
||||
/// Ctrl + Alt
|
||||
const CTRL_ALT = Self::CTRL.bits() | Self::ALT.bits();
|
||||
/// Ctrl + Alt + Shift
|
||||
const CTRL_ALT_SHIFT = Self::CTRL.bits() | Self::ALT.bits() | Self::SHIFT.bits();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,396 +6,377 @@ use term::{get_win_size, raw_mode, KeyReader, Layout, LineWriter, TermReader, Te
|
||||
use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd};
|
||||
use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
|
||||
|
||||
use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, sys::sh_quit, term::{Style, Styled}};
|
||||
use crate::libsh::{
|
||||
error::{ShErr, ShErrKind, ShResult},
|
||||
sys::sh_quit,
|
||||
term::{Style, Styled},
|
||||
};
|
||||
use crate::prelude::*;
|
||||
|
||||
pub mod term;
|
||||
pub mod linebuf;
|
||||
pub mod layout;
|
||||
pub mod keys;
|
||||
pub mod vicmd;
|
||||
pub mod register;
|
||||
pub mod vimode;
|
||||
pub mod history;
|
||||
pub mod keys;
|
||||
pub mod layout;
|
||||
pub mod linebuf;
|
||||
pub mod register;
|
||||
pub mod term;
|
||||
pub mod vicmd;
|
||||
pub mod vimode;
|
||||
|
||||
// Very useful for testing
|
||||
const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.";
|
||||
const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.";
|
||||
|
||||
pub trait Readline {
|
||||
fn readline(&mut self) -> ShResult<String>;
|
||||
fn readline(&mut self) -> ShResult<String>;
|
||||
}
|
||||
|
||||
pub struct FernVi {
|
||||
pub reader: Box<dyn KeyReader>,
|
||||
pub writer: Box<dyn LineWriter>,
|
||||
pub prompt: String,
|
||||
pub mode: Box<dyn ViMode>,
|
||||
pub old_layout: Option<Layout>,
|
||||
pub repeat_action: Option<CmdReplay>,
|
||||
pub repeat_motion: Option<MotionCmd>,
|
||||
pub editor: LineBuf,
|
||||
pub history: History
|
||||
pub reader: Box<dyn KeyReader>,
|
||||
pub writer: Box<dyn LineWriter>,
|
||||
pub prompt: String,
|
||||
pub mode: Box<dyn ViMode>,
|
||||
pub old_layout: Option<Layout>,
|
||||
pub repeat_action: Option<CmdReplay>,
|
||||
pub repeat_motion: Option<MotionCmd>,
|
||||
pub editor: LineBuf,
|
||||
pub history: History,
|
||||
}
|
||||
|
||||
impl Readline for FernVi {
|
||||
fn readline(&mut self) -> ShResult<String> {
|
||||
let raw_mode_guard = raw_mode(); // Restores termios state on drop
|
||||
fn readline(&mut self) -> ShResult<String> {
|
||||
let raw_mode_guard = raw_mode(); // Restores termios state on drop
|
||||
|
||||
loop {
|
||||
raw_mode_guard.disable_for(|| self.print_line())?;
|
||||
loop {
|
||||
raw_mode_guard.disable_for(|| self.print_line())?;
|
||||
|
||||
let Some(key) = self.reader.read_key() else {
|
||||
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
|
||||
std::mem::drop(raw_mode_guard);
|
||||
return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF"))
|
||||
};
|
||||
flog!(DEBUG, key);
|
||||
let Some(key) = self.reader.read_key() else {
|
||||
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
|
||||
std::mem::drop(raw_mode_guard);
|
||||
return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF"));
|
||||
};
|
||||
flog!(DEBUG, key);
|
||||
|
||||
if self.should_accept_hint(&key) {
|
||||
self.editor.accept_hint();
|
||||
self.history.update_pending_cmd(self.editor.as_str());
|
||||
self.print_line()?;
|
||||
continue
|
||||
}
|
||||
if self.should_accept_hint(&key) {
|
||||
self.editor.accept_hint();
|
||||
self.history.update_pending_cmd(self.editor.as_str());
|
||||
self.print_line()?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(mut cmd) = self.mode.handle_key(key) else {
|
||||
flog!(DEBUG, "got none??");
|
||||
continue
|
||||
};
|
||||
flog!(DEBUG,cmd);
|
||||
cmd.alter_line_motion_if_no_verb();
|
||||
let Some(mut cmd) = self.mode.handle_key(key) else {
|
||||
flog!(DEBUG, "got none??");
|
||||
continue;
|
||||
};
|
||||
flog!(DEBUG, cmd);
|
||||
cmd.alter_line_motion_if_no_verb();
|
||||
|
||||
if self.should_grab_history(&cmd) {
|
||||
self.scroll_history(cmd);
|
||||
self.print_line()?;
|
||||
continue
|
||||
}
|
||||
if self.should_grab_history(&cmd) {
|
||||
self.scroll_history(cmd);
|
||||
self.print_line()?;
|
||||
continue;
|
||||
}
|
||||
|
||||
if cmd.should_submit() {
|
||||
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
|
||||
std::mem::drop(raw_mode_guard);
|
||||
return Ok(self.editor.take_buf())
|
||||
}
|
||||
if cmd.should_submit() {
|
||||
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
|
||||
std::mem::drop(raw_mode_guard);
|
||||
return Ok(self.editor.take_buf());
|
||||
}
|
||||
|
||||
if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) {
|
||||
if self.editor.buffer.is_empty() {
|
||||
std::mem::drop(raw_mode_guard);
|
||||
sh_quit(0);
|
||||
} else {
|
||||
self.editor.buffer.clear();
|
||||
continue
|
||||
}
|
||||
}
|
||||
flog!(DEBUG,cmd);
|
||||
if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) {
|
||||
if self.editor.buffer.is_empty() {
|
||||
std::mem::drop(raw_mode_guard);
|
||||
sh_quit(0);
|
||||
} else {
|
||||
self.editor.buffer.clear();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
flog!(DEBUG, cmd);
|
||||
|
||||
let before = self.editor.buffer.clone();
|
||||
self.exec_cmd(cmd)?;
|
||||
let after = self.editor.as_str();
|
||||
let before = self.editor.buffer.clone();
|
||||
self.exec_cmd(cmd)?;
|
||||
let after = self.editor.as_str();
|
||||
|
||||
if before != after {
|
||||
self.history.update_pending_cmd(self.editor.as_str());
|
||||
}
|
||||
if before != after {
|
||||
self.history.update_pending_cmd(self.editor.as_str());
|
||||
}
|
||||
|
||||
let hint = self.history.get_hint();
|
||||
self.editor.set_hint(hint);
|
||||
}
|
||||
}
|
||||
let hint = self.history.get_hint();
|
||||
self.editor.set_hint(hint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FernVi {
|
||||
pub fn new(prompt: Option<String>) -> ShResult<Self> {
|
||||
Ok(Self {
|
||||
reader: Box::new(TermReader::new()),
|
||||
writer: Box::new(TermWriter::new(STDOUT_FILENO)),
|
||||
prompt: prompt.unwrap_or("$ ".styled(Style::Green)),
|
||||
mode: Box::new(ViInsert::new()),
|
||||
old_layout: None,
|
||||
repeat_action: None,
|
||||
repeat_motion: None,
|
||||
editor: LineBuf::new().with_initial(LOREM_IPSUM, 0),
|
||||
history: History::new()?
|
||||
})
|
||||
}
|
||||
pub fn new(prompt: Option<String>) -> ShResult<Self> {
|
||||
Ok(Self {
|
||||
reader: Box::new(TermReader::new()),
|
||||
writer: Box::new(TermWriter::new(STDOUT_FILENO)),
|
||||
prompt: prompt.unwrap_or("$ ".styled(Style::Green)),
|
||||
mode: Box::new(ViInsert::new()),
|
||||
old_layout: None,
|
||||
repeat_action: None,
|
||||
repeat_motion: None,
|
||||
editor: LineBuf::new().with_initial(LOREM_IPSUM, 0),
|
||||
history: History::new()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_layout(&mut self) -> Layout {
|
||||
let line = self.editor.to_string();
|
||||
flog!(DEBUG,line);
|
||||
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
|
||||
let (cols,_) = get_win_size(STDIN_FILENO);
|
||||
Layout::from_parts(
|
||||
/*tab_stop:*/ 8,
|
||||
cols,
|
||||
&self.prompt,
|
||||
to_cursor,
|
||||
&line
|
||||
)
|
||||
}
|
||||
pub fn scroll_history(&mut self, cmd: ViCmd) {
|
||||
flog!(DEBUG,"scrolling");
|
||||
/*
|
||||
if self.history.cursor_entry().is_some_and(|ent| ent.is_new()) {
|
||||
let constraint = SearchConstraint::new(SearchKind::Prefix, self.editor.to_string());
|
||||
self.history.constrain_entries(constraint);
|
||||
}
|
||||
*/
|
||||
let count = &cmd.motion().unwrap().0;
|
||||
let motion = &cmd.motion().unwrap().1;
|
||||
flog!(DEBUG,count,motion);
|
||||
flog!(DEBUG,self.history.masked_entries());
|
||||
let entry = match motion {
|
||||
Motion::LineUpCharwise => {
|
||||
let Some(hist_entry) = self.history.scroll(-(*count as isize)) else {
|
||||
return
|
||||
};
|
||||
flog!(DEBUG,"found entry");
|
||||
flog!(DEBUG,hist_entry.command());
|
||||
hist_entry
|
||||
}
|
||||
Motion::LineDownCharwise => {
|
||||
let Some(hist_entry) = self.history.scroll(*count as isize) else {
|
||||
return
|
||||
};
|
||||
flog!(DEBUG,"found entry");
|
||||
flog!(DEBUG,hist_entry.command());
|
||||
hist_entry
|
||||
}
|
||||
_ => unreachable!()
|
||||
};
|
||||
let col = self.editor.saved_col.unwrap_or(self.editor.cursor_col());
|
||||
let mut buf = LineBuf::new().with_initial(entry.command(),0);
|
||||
let line_end = buf.end_of_line();
|
||||
if let Some(dest) = self.mode.hist_scroll_start_pos() {
|
||||
match dest {
|
||||
To::Start => {
|
||||
/* Already at 0 */
|
||||
}
|
||||
To::End => {
|
||||
// History entries cannot be empty
|
||||
// So this subtraction is safe (maybe)
|
||||
buf.cursor.add(line_end);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let target = (col).min(line_end);
|
||||
buf.cursor.add(target);
|
||||
}
|
||||
pub fn get_layout(&mut self) -> Layout {
|
||||
let line = self.editor.to_string();
|
||||
flog!(DEBUG, line);
|
||||
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
|
||||
let (cols, _) = get_win_size(STDIN_FILENO);
|
||||
Layout::from_parts(/* tab_stop: */ 8, cols, &self.prompt, to_cursor, &line)
|
||||
}
|
||||
pub fn scroll_history(&mut self, cmd: ViCmd) {
|
||||
flog!(DEBUG, "scrolling");
|
||||
/*
|
||||
if self.history.cursor_entry().is_some_and(|ent| ent.is_new()) {
|
||||
let constraint = SearchConstraint::new(SearchKind::Prefix, self.editor.to_string());
|
||||
self.history.constrain_entries(constraint);
|
||||
}
|
||||
*/
|
||||
let count = &cmd.motion().unwrap().0;
|
||||
let motion = &cmd.motion().unwrap().1;
|
||||
flog!(DEBUG, count, motion);
|
||||
flog!(DEBUG, self.history.masked_entries());
|
||||
let entry = match motion {
|
||||
Motion::LineUpCharwise => {
|
||||
let Some(hist_entry) = self.history.scroll(-(*count as isize)) else {
|
||||
return;
|
||||
};
|
||||
flog!(DEBUG, "found entry");
|
||||
flog!(DEBUG, hist_entry.command());
|
||||
hist_entry
|
||||
}
|
||||
Motion::LineDownCharwise => {
|
||||
let Some(hist_entry) = self.history.scroll(*count as isize) else {
|
||||
return;
|
||||
};
|
||||
flog!(DEBUG, "found entry");
|
||||
flog!(DEBUG, hist_entry.command());
|
||||
hist_entry
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let col = self.editor.saved_col.unwrap_or(self.editor.cursor_col());
|
||||
let mut buf = LineBuf::new().with_initial(entry.command(), 0);
|
||||
let line_end = buf.end_of_line();
|
||||
if let Some(dest) = self.mode.hist_scroll_start_pos() {
|
||||
match dest {
|
||||
To::Start => { /* Already at 0 */ }
|
||||
To::End => {
|
||||
// History entries cannot be empty
|
||||
// So this subtraction is safe (maybe)
|
||||
buf.cursor.add(line_end);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let target = (col).min(line_end);
|
||||
buf.cursor.add(target);
|
||||
}
|
||||
|
||||
self.editor = buf
|
||||
}
|
||||
pub fn should_accept_hint(&self, event: &KeyEvent) -> bool {
|
||||
flog!(DEBUG,self.editor.cursor_at_max());
|
||||
flog!(DEBUG,self.editor.cursor);
|
||||
if self.editor.cursor_at_max() && self.editor.has_hint() {
|
||||
match self.mode.report_mode() {
|
||||
ModeReport::Replace |
|
||||
ModeReport::Insert => {
|
||||
matches!(
|
||||
event,
|
||||
KeyEvent(KeyCode::Right, ModKeys::NONE)
|
||||
)
|
||||
}
|
||||
ModeReport::Visual |
|
||||
ModeReport::Normal => {
|
||||
matches!(
|
||||
event,
|
||||
KeyEvent(KeyCode::Right, ModKeys::NONE)
|
||||
) ||
|
||||
(
|
||||
self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty() &&
|
||||
matches!(
|
||||
event,
|
||||
KeyEvent(KeyCode::Char('l'), ModKeys::NONE)
|
||||
)
|
||||
)
|
||||
}
|
||||
_ => unimplemented!()
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
self.editor = buf
|
||||
}
|
||||
pub fn should_accept_hint(&self, event: &KeyEvent) -> bool {
|
||||
flog!(DEBUG, self.editor.cursor_at_max());
|
||||
flog!(DEBUG, self.editor.cursor);
|
||||
if self.editor.cursor_at_max() && self.editor.has_hint() {
|
||||
match self.mode.report_mode() {
|
||||
ModeReport::Replace | ModeReport::Insert => {
|
||||
matches!(event, KeyEvent(KeyCode::Right, ModKeys::NONE))
|
||||
}
|
||||
ModeReport::Visual | ModeReport::Normal => {
|
||||
matches!(event, KeyEvent(KeyCode::Right, ModKeys::NONE))
|
||||
|| (self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty()
|
||||
&& matches!(event, KeyEvent(KeyCode::Char('l'), ModKeys::NONE)))
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_grab_history(&mut self, cmd: &ViCmd) -> bool {
|
||||
cmd.verb().is_none() &&
|
||||
(
|
||||
cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUpCharwise))) &&
|
||||
self.editor.start_of_line() == 0
|
||||
) ||
|
||||
(
|
||||
cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise))) &&
|
||||
self.editor.end_of_line() == self.editor.cursor_max() &&
|
||||
!self.history.cursor_entry().is_some_and(|ent| ent.is_new())
|
||||
)
|
||||
}
|
||||
pub fn should_grab_history(&mut self, cmd: &ViCmd) -> bool {
|
||||
cmd.verb().is_none()
|
||||
&& (cmd
|
||||
.motion()
|
||||
.is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUpCharwise)))
|
||||
&& self.editor.start_of_line() == 0)
|
||||
|| (cmd
|
||||
.motion()
|
||||
.is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise)))
|
||||
&& self.editor.end_of_line() == self.editor.cursor_max()
|
||||
&& !self.history.cursor_entry().is_some_and(|ent| ent.is_new()))
|
||||
}
|
||||
|
||||
pub fn print_line(&mut self) -> ShResult<()> {
|
||||
let new_layout = self.get_layout();
|
||||
if let Some(layout) = self.old_layout.as_ref() {
|
||||
self.writer.clear_rows(layout)?;
|
||||
}
|
||||
pub fn print_line(&mut self) -> ShResult<()> {
|
||||
let new_layout = self.get_layout();
|
||||
if let Some(layout) = self.old_layout.as_ref() {
|
||||
self.writer.clear_rows(layout)?;
|
||||
}
|
||||
|
||||
self.writer.redraw(
|
||||
&self.prompt,
|
||||
&self.editor,
|
||||
&new_layout
|
||||
)?;
|
||||
self
|
||||
.writer
|
||||
.redraw(&self.prompt, &self.editor, &new_layout)?;
|
||||
|
||||
self.writer.flush_write(&self.mode.cursor_style())?;
|
||||
self.writer.flush_write(&self.mode.cursor_style())?;
|
||||
|
||||
self.old_layout = Some(new_layout);
|
||||
Ok(())
|
||||
}
|
||||
self.old_layout = Some(new_layout);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> {
|
||||
let mut selecting = false;
|
||||
let mut is_insert_mode = false;
|
||||
if cmd.is_mode_transition() {
|
||||
let count = cmd.verb_count();
|
||||
let mut mode: Box<dyn ViMode> = match cmd.verb().unwrap().1 {
|
||||
Verb::Change |
|
||||
Verb::InsertModeLineBreak(_) |
|
||||
Verb::InsertMode => {
|
||||
is_insert_mode = true;
|
||||
Box::new(ViInsert::new().with_count(count as u16))
|
||||
}
|
||||
pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> {
|
||||
let mut selecting = false;
|
||||
let mut is_insert_mode = false;
|
||||
if cmd.is_mode_transition() {
|
||||
let count = cmd.verb_count();
|
||||
let mut mode: Box<dyn ViMode> = match cmd.verb().unwrap().1 {
|
||||
Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => {
|
||||
is_insert_mode = true;
|
||||
Box::new(ViInsert::new().with_count(count as u16))
|
||||
}
|
||||
|
||||
Verb::NormalMode => {
|
||||
Box::new(ViNormal::new())
|
||||
}
|
||||
Verb::NormalMode => Box::new(ViNormal::new()),
|
||||
|
||||
Verb::ReplaceMode => Box::new(ViReplace::new()),
|
||||
Verb::ReplaceMode => Box::new(ViReplace::new()),
|
||||
|
||||
Verb::VisualModeSelectLast => {
|
||||
if self.mode.report_mode() != ModeReport::Visual {
|
||||
self.editor.start_selecting(SelectMode::Char(SelectAnchor::End));
|
||||
}
|
||||
let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new());
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
|
||||
Verb::VisualModeSelectLast => {
|
||||
if self.mode.report_mode() != ModeReport::Visual {
|
||||
self
|
||||
.editor
|
||||
.start_selecting(SelectMode::Char(SelectAnchor::End));
|
||||
}
|
||||
let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new());
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
|
||||
|
||||
return self.editor.exec_cmd(cmd)
|
||||
}
|
||||
Verb::VisualMode => {
|
||||
selecting = true;
|
||||
Box::new(ViVisual::new())
|
||||
}
|
||||
return self.editor.exec_cmd(cmd);
|
||||
}
|
||||
Verb::VisualMode => {
|
||||
selecting = true;
|
||||
Box::new(ViVisual::new())
|
||||
}
|
||||
|
||||
_ => unreachable!()
|
||||
};
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
|
||||
if mode.is_repeatable() {
|
||||
self.repeat_action = mode.as_replay();
|
||||
}
|
||||
if mode.is_repeatable() {
|
||||
self.repeat_action = mode.as_replay();
|
||||
}
|
||||
|
||||
self.editor.exec_cmd(cmd)?;
|
||||
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
|
||||
self.editor.exec_cmd(cmd)?;
|
||||
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
|
||||
|
||||
if selecting {
|
||||
self.editor.start_selecting(SelectMode::Char(SelectAnchor::End));
|
||||
} else {
|
||||
self.editor.stop_selecting();
|
||||
}
|
||||
if is_insert_mode {
|
||||
self.editor.mark_insert_mode_start_pos();
|
||||
} else {
|
||||
self.editor.clear_insert_mode_start_pos();
|
||||
}
|
||||
return Ok(())
|
||||
} else if cmd.is_cmd_repeat() {
|
||||
let Some(replay) = self.repeat_action.clone() else {
|
||||
return Ok(())
|
||||
};
|
||||
let ViCmd { verb, .. } = cmd;
|
||||
let VerbCmd(count,_) = verb.unwrap();
|
||||
match replay {
|
||||
CmdReplay::ModeReplay { cmds, mut repeat } => {
|
||||
if count > 1 {
|
||||
repeat = count as u16;
|
||||
}
|
||||
for _ in 0..repeat {
|
||||
let cmds = cmds.clone();
|
||||
for cmd in cmds {
|
||||
self.editor.exec_cmd(cmd)?
|
||||
}
|
||||
}
|
||||
}
|
||||
CmdReplay::Single(mut cmd) => {
|
||||
if count > 1 {
|
||||
// Override the counts with the one passed to the '.' command
|
||||
if cmd.verb.is_some() {
|
||||
if let Some(v_mut) = cmd.verb.as_mut() {
|
||||
v_mut.0 = count
|
||||
}
|
||||
if let Some(m_mut) = cmd.motion.as_mut() {
|
||||
m_mut.0 = 1
|
||||
}
|
||||
} else {
|
||||
return Ok(()) // it has to have a verb to be repeatable, something weird happened
|
||||
}
|
||||
}
|
||||
self.editor.exec_cmd(cmd)?;
|
||||
}
|
||||
_ => unreachable!("motions should be handled in the other branch")
|
||||
}
|
||||
return Ok(())
|
||||
} else if cmd.is_motion_repeat() {
|
||||
match cmd.motion.as_ref().unwrap() {
|
||||
MotionCmd(count,Motion::RepeatMotion) => {
|
||||
let Some(motion) = self.repeat_motion.clone() else {
|
||||
return Ok(())
|
||||
};
|
||||
let repeat_cmd = ViCmd {
|
||||
register: RegisterName::default(),
|
||||
verb: None,
|
||||
motion: Some(motion),
|
||||
raw_seq: format!("{count};"),
|
||||
flags: CmdFlags::empty()
|
||||
};
|
||||
return self.editor.exec_cmd(repeat_cmd);
|
||||
}
|
||||
MotionCmd(count,Motion::RepeatMotionRev) => {
|
||||
let Some(motion) = self.repeat_motion.clone() else {
|
||||
return Ok(())
|
||||
};
|
||||
let mut new_motion = motion.invert_char_motion();
|
||||
new_motion.0 = *count;
|
||||
let repeat_cmd = ViCmd {
|
||||
register: RegisterName::default(),
|
||||
verb: None,
|
||||
motion: Some(new_motion),
|
||||
raw_seq: format!("{count},"),
|
||||
flags: CmdFlags::empty()
|
||||
};
|
||||
return self.editor.exec_cmd(repeat_cmd);
|
||||
}
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
if selecting {
|
||||
self
|
||||
.editor
|
||||
.start_selecting(SelectMode::Char(SelectAnchor::End));
|
||||
} else {
|
||||
self.editor.stop_selecting();
|
||||
}
|
||||
if is_insert_mode {
|
||||
self.editor.mark_insert_mode_start_pos();
|
||||
} else {
|
||||
self.editor.clear_insert_mode_start_pos();
|
||||
}
|
||||
return Ok(());
|
||||
} else if cmd.is_cmd_repeat() {
|
||||
let Some(replay) = self.repeat_action.clone() else {
|
||||
return Ok(());
|
||||
};
|
||||
let ViCmd { verb, .. } = cmd;
|
||||
let VerbCmd(count, _) = verb.unwrap();
|
||||
match replay {
|
||||
CmdReplay::ModeReplay { cmds, mut repeat } => {
|
||||
if count > 1 {
|
||||
repeat = count as u16;
|
||||
}
|
||||
for _ in 0..repeat {
|
||||
let cmds = cmds.clone();
|
||||
for cmd in cmds {
|
||||
self.editor.exec_cmd(cmd)?
|
||||
}
|
||||
}
|
||||
}
|
||||
CmdReplay::Single(mut cmd) => {
|
||||
if count > 1 {
|
||||
// Override the counts with the one passed to the '.' command
|
||||
if cmd.verb.is_some() {
|
||||
if let Some(v_mut) = cmd.verb.as_mut() {
|
||||
v_mut.0 = count
|
||||
}
|
||||
if let Some(m_mut) = cmd.motion.as_mut() {
|
||||
m_mut.0 = 1
|
||||
}
|
||||
} else {
|
||||
return Ok(()); // it has to have a verb to be repeatable,
|
||||
// something weird happened
|
||||
}
|
||||
}
|
||||
self.editor.exec_cmd(cmd)?;
|
||||
}
|
||||
_ => unreachable!("motions should be handled in the other branch"),
|
||||
}
|
||||
return Ok(());
|
||||
} else if cmd.is_motion_repeat() {
|
||||
match cmd.motion.as_ref().unwrap() {
|
||||
MotionCmd(count, Motion::RepeatMotion) => {
|
||||
let Some(motion) = self.repeat_motion.clone() else {
|
||||
return Ok(());
|
||||
};
|
||||
let repeat_cmd = ViCmd {
|
||||
register: RegisterName::default(),
|
||||
verb: None,
|
||||
motion: Some(motion),
|
||||
raw_seq: format!("{count};"),
|
||||
flags: CmdFlags::empty(),
|
||||
};
|
||||
return self.editor.exec_cmd(repeat_cmd);
|
||||
}
|
||||
MotionCmd(count, Motion::RepeatMotionRev) => {
|
||||
let Some(motion) = self.repeat_motion.clone() else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut new_motion = motion.invert_char_motion();
|
||||
new_motion.0 = *count;
|
||||
let repeat_cmd = ViCmd {
|
||||
register: RegisterName::default(),
|
||||
verb: None,
|
||||
motion: Some(new_motion),
|
||||
raw_seq: format!("{count},"),
|
||||
flags: CmdFlags::empty(),
|
||||
};
|
||||
return self.editor.exec_cmd(repeat_cmd);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.is_repeatable() {
|
||||
if self.mode.report_mode() == ModeReport::Visual {
|
||||
// The motion is assigned in the line buffer execution, so we also have to assign it here
|
||||
// in order to be able to repeat it
|
||||
let range = self.editor.select_range().unwrap();
|
||||
cmd.motion = Some(MotionCmd(1,Motion::Range(range.0, range.1)))
|
||||
}
|
||||
self.repeat_action = Some(CmdReplay::Single(cmd.clone()));
|
||||
}
|
||||
if cmd.is_repeatable() {
|
||||
if self.mode.report_mode() == ModeReport::Visual {
|
||||
// The motion is assigned in the line buffer execution, so we also have to
|
||||
// assign it here in order to be able to repeat it
|
||||
let range = self.editor.select_range().unwrap();
|
||||
cmd.motion = Some(MotionCmd(1, Motion::Range(range.0, range.1)))
|
||||
}
|
||||
self.repeat_action = Some(CmdReplay::Single(cmd.clone()));
|
||||
}
|
||||
|
||||
if cmd.is_char_search() {
|
||||
self.repeat_motion = cmd.motion.clone()
|
||||
}
|
||||
if cmd.is_char_search() {
|
||||
self.repeat_motion = cmd.motion.clone()
|
||||
}
|
||||
|
||||
self.editor.exec_cmd(cmd.clone())?;
|
||||
self.editor.exec_cmd(cmd.clone())?;
|
||||
|
||||
if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) {
|
||||
self.editor.stop_selecting();
|
||||
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) {
|
||||
self.editor.stop_selecting();
|
||||
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,166 +3,170 @@ use std::sync::Mutex;
|
||||
pub static REGISTERS: Mutex<Registers> = Mutex::new(Registers::new());
|
||||
|
||||
pub fn read_register(ch: Option<char>) -> Option<String> {
|
||||
let lock = REGISTERS.lock().unwrap();
|
||||
lock.get_reg(ch).map(|r| r.buf().clone())
|
||||
let lock = REGISTERS.lock().unwrap();
|
||||
lock.get_reg(ch).map(|r| r.buf().clone())
|
||||
}
|
||||
|
||||
pub fn write_register(ch: Option<char>, buf: String) {
|
||||
let mut lock = REGISTERS.lock().unwrap();
|
||||
if let Some(r) = lock.get_reg_mut(ch) { r.write(buf) }
|
||||
let mut lock = REGISTERS.lock().unwrap();
|
||||
if let Some(r) = lock.get_reg_mut(ch) {
|
||||
r.write(buf)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_register(ch: Option<char>, buf: String) {
|
||||
let mut lock = REGISTERS.lock().unwrap();
|
||||
if let Some(r) = lock.get_reg_mut(ch) { r.append(buf) }
|
||||
let mut lock = REGISTERS.lock().unwrap();
|
||||
if let Some(r) = lock.get_reg_mut(ch) {
|
||||
r.append(buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default,Debug)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Registers {
|
||||
default: Register,
|
||||
a: Register,
|
||||
b: Register,
|
||||
c: Register,
|
||||
d: Register,
|
||||
e: Register,
|
||||
f: Register,
|
||||
g: Register,
|
||||
h: Register,
|
||||
i: Register,
|
||||
j: Register,
|
||||
k: Register,
|
||||
l: Register,
|
||||
m: Register,
|
||||
n: Register,
|
||||
o: Register,
|
||||
p: Register,
|
||||
q: Register,
|
||||
r: Register,
|
||||
s: Register,
|
||||
t: Register,
|
||||
u: Register,
|
||||
v: Register,
|
||||
w: Register,
|
||||
x: Register,
|
||||
y: Register,
|
||||
z: Register,
|
||||
default: Register,
|
||||
a: Register,
|
||||
b: Register,
|
||||
c: Register,
|
||||
d: Register,
|
||||
e: Register,
|
||||
f: Register,
|
||||
g: Register,
|
||||
h: Register,
|
||||
i: Register,
|
||||
j: Register,
|
||||
k: Register,
|
||||
l: Register,
|
||||
m: Register,
|
||||
n: Register,
|
||||
o: Register,
|
||||
p: Register,
|
||||
q: Register,
|
||||
r: Register,
|
||||
s: Register,
|
||||
t: Register,
|
||||
u: Register,
|
||||
v: Register,
|
||||
w: Register,
|
||||
x: Register,
|
||||
y: Register,
|
||||
z: Register,
|
||||
}
|
||||
|
||||
impl Registers {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
default: Register(String::new()),
|
||||
a: Register(String::new()),
|
||||
b: Register(String::new()),
|
||||
c: Register(String::new()),
|
||||
d: Register(String::new()),
|
||||
e: Register(String::new()),
|
||||
f: Register(String::new()),
|
||||
g: Register(String::new()),
|
||||
h: Register(String::new()),
|
||||
i: Register(String::new()),
|
||||
j: Register(String::new()),
|
||||
k: Register(String::new()),
|
||||
l: Register(String::new()),
|
||||
m: Register(String::new()),
|
||||
n: Register(String::new()),
|
||||
o: Register(String::new()),
|
||||
p: Register(String::new()),
|
||||
q: Register(String::new()),
|
||||
r: Register(String::new()),
|
||||
s: Register(String::new()),
|
||||
t: Register(String::new()),
|
||||
u: Register(String::new()),
|
||||
v: Register(String::new()),
|
||||
w: Register(String::new()),
|
||||
x: Register(String::new()),
|
||||
y: Register(String::new()),
|
||||
z: Register(String::new()),
|
||||
}
|
||||
}
|
||||
pub fn get_reg(&self, ch: Option<char>) -> Option<&Register> {
|
||||
let Some(ch) = ch else {
|
||||
return Some(&self.default)
|
||||
};
|
||||
match ch {
|
||||
'a' => Some(&self.a),
|
||||
'b' => Some(&self.b),
|
||||
'c' => Some(&self.c),
|
||||
'd' => Some(&self.d),
|
||||
'e' => Some(&self.e),
|
||||
'f' => Some(&self.f),
|
||||
'g' => Some(&self.g),
|
||||
'h' => Some(&self.h),
|
||||
'i' => Some(&self.i),
|
||||
'j' => Some(&self.j),
|
||||
'k' => Some(&self.k),
|
||||
'l' => Some(&self.l),
|
||||
'm' => Some(&self.m),
|
||||
'n' => Some(&self.n),
|
||||
'o' => Some(&self.o),
|
||||
'p' => Some(&self.p),
|
||||
'q' => Some(&self.q),
|
||||
'r' => Some(&self.r),
|
||||
's' => Some(&self.s),
|
||||
't' => Some(&self.t),
|
||||
'u' => Some(&self.u),
|
||||
'v' => Some(&self.v),
|
||||
'w' => Some(&self.w),
|
||||
'x' => Some(&self.x),
|
||||
'y' => Some(&self.y),
|
||||
'z' => Some(&self.z),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
pub fn get_reg_mut(&mut self, ch: Option<char>) -> Option<&mut Register> {
|
||||
let Some(ch) = ch else {
|
||||
return Some(&mut self.default)
|
||||
};
|
||||
match ch {
|
||||
'a' => Some(&mut self.a),
|
||||
'b' => Some(&mut self.b),
|
||||
'c' => Some(&mut self.c),
|
||||
'd' => Some(&mut self.d),
|
||||
'e' => Some(&mut self.e),
|
||||
'f' => Some(&mut self.f),
|
||||
'g' => Some(&mut self.g),
|
||||
'h' => Some(&mut self.h),
|
||||
'i' => Some(&mut self.i),
|
||||
'j' => Some(&mut self.j),
|
||||
'k' => Some(&mut self.k),
|
||||
'l' => Some(&mut self.l),
|
||||
'm' => Some(&mut self.m),
|
||||
'n' => Some(&mut self.n),
|
||||
'o' => Some(&mut self.o),
|
||||
'p' => Some(&mut self.p),
|
||||
'q' => Some(&mut self.q),
|
||||
'r' => Some(&mut self.r),
|
||||
's' => Some(&mut self.s),
|
||||
't' => Some(&mut self.t),
|
||||
'u' => Some(&mut self.u),
|
||||
'v' => Some(&mut self.v),
|
||||
'w' => Some(&mut self.w),
|
||||
'x' => Some(&mut self.x),
|
||||
'y' => Some(&mut self.y),
|
||||
'z' => Some(&mut self.z),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
default: Register(String::new()),
|
||||
a: Register(String::new()),
|
||||
b: Register(String::new()),
|
||||
c: Register(String::new()),
|
||||
d: Register(String::new()),
|
||||
e: Register(String::new()),
|
||||
f: Register(String::new()),
|
||||
g: Register(String::new()),
|
||||
h: Register(String::new()),
|
||||
i: Register(String::new()),
|
||||
j: Register(String::new()),
|
||||
k: Register(String::new()),
|
||||
l: Register(String::new()),
|
||||
m: Register(String::new()),
|
||||
n: Register(String::new()),
|
||||
o: Register(String::new()),
|
||||
p: Register(String::new()),
|
||||
q: Register(String::new()),
|
||||
r: Register(String::new()),
|
||||
s: Register(String::new()),
|
||||
t: Register(String::new()),
|
||||
u: Register(String::new()),
|
||||
v: Register(String::new()),
|
||||
w: Register(String::new()),
|
||||
x: Register(String::new()),
|
||||
y: Register(String::new()),
|
||||
z: Register(String::new()),
|
||||
}
|
||||
}
|
||||
pub fn get_reg(&self, ch: Option<char>) -> Option<&Register> {
|
||||
let Some(ch) = ch else {
|
||||
return Some(&self.default);
|
||||
};
|
||||
match ch {
|
||||
'a' => Some(&self.a),
|
||||
'b' => Some(&self.b),
|
||||
'c' => Some(&self.c),
|
||||
'd' => Some(&self.d),
|
||||
'e' => Some(&self.e),
|
||||
'f' => Some(&self.f),
|
||||
'g' => Some(&self.g),
|
||||
'h' => Some(&self.h),
|
||||
'i' => Some(&self.i),
|
||||
'j' => Some(&self.j),
|
||||
'k' => Some(&self.k),
|
||||
'l' => Some(&self.l),
|
||||
'm' => Some(&self.m),
|
||||
'n' => Some(&self.n),
|
||||
'o' => Some(&self.o),
|
||||
'p' => Some(&self.p),
|
||||
'q' => Some(&self.q),
|
||||
'r' => Some(&self.r),
|
||||
's' => Some(&self.s),
|
||||
't' => Some(&self.t),
|
||||
'u' => Some(&self.u),
|
||||
'v' => Some(&self.v),
|
||||
'w' => Some(&self.w),
|
||||
'x' => Some(&self.x),
|
||||
'y' => Some(&self.y),
|
||||
'z' => Some(&self.z),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn get_reg_mut(&mut self, ch: Option<char>) -> Option<&mut Register> {
|
||||
let Some(ch) = ch else {
|
||||
return Some(&mut self.default);
|
||||
};
|
||||
match ch {
|
||||
'a' => Some(&mut self.a),
|
||||
'b' => Some(&mut self.b),
|
||||
'c' => Some(&mut self.c),
|
||||
'd' => Some(&mut self.d),
|
||||
'e' => Some(&mut self.e),
|
||||
'f' => Some(&mut self.f),
|
||||
'g' => Some(&mut self.g),
|
||||
'h' => Some(&mut self.h),
|
||||
'i' => Some(&mut self.i),
|
||||
'j' => Some(&mut self.j),
|
||||
'k' => Some(&mut self.k),
|
||||
'l' => Some(&mut self.l),
|
||||
'm' => Some(&mut self.m),
|
||||
'n' => Some(&mut self.n),
|
||||
'o' => Some(&mut self.o),
|
||||
'p' => Some(&mut self.p),
|
||||
'q' => Some(&mut self.q),
|
||||
'r' => Some(&mut self.r),
|
||||
's' => Some(&mut self.s),
|
||||
't' => Some(&mut self.t),
|
||||
'u' => Some(&mut self.u),
|
||||
'v' => Some(&mut self.v),
|
||||
'w' => Some(&mut self.w),
|
||||
'x' => Some(&mut self.x),
|
||||
'y' => Some(&mut self.y),
|
||||
'z' => Some(&mut self.z),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Default,Debug)]
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct Register(String);
|
||||
impl Register {
|
||||
pub fn buf(&self) -> &String {
|
||||
&self.0
|
||||
}
|
||||
pub fn write(&mut self, buf: String) {
|
||||
self.0 = buf
|
||||
}
|
||||
pub fn append(&mut self, buf: String) {
|
||||
self.0.push_str(&buf)
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
self.0.clear()
|
||||
}
|
||||
pub fn buf(&self) -> &String {
|
||||
&self.0
|
||||
}
|
||||
pub fn write(&mut self, buf: String) {
|
||||
self.0 = buf
|
||||
}
|
||||
pub fn append(&mut self, buf: String) {
|
||||
self.0.push_str(&buf)
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
self.0.clear()
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,438 +2,458 @@ use bitflags::bitflags;
|
||||
|
||||
use super::register::{append_register, read_register, write_register};
|
||||
|
||||
//TODO: write tests that take edit results and cursor positions from actual neovim edits and test them against the behavior of this editor
|
||||
//TODO: write tests that take edit results and cursor positions from actual
|
||||
// neovim edits and test them against the behavior of this editor
|
||||
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct RegisterName {
|
||||
name: Option<char>,
|
||||
count: usize,
|
||||
append: bool
|
||||
name: Option<char>,
|
||||
count: usize,
|
||||
append: bool,
|
||||
}
|
||||
|
||||
impl RegisterName {
|
||||
pub fn new(name: Option<char>, count: Option<usize>) -> Self {
|
||||
let Some(ch) = name else {
|
||||
return Self::default()
|
||||
};
|
||||
pub fn new(name: Option<char>, count: Option<usize>) -> Self {
|
||||
let Some(ch) = name else {
|
||||
return Self::default();
|
||||
};
|
||||
|
||||
let append = ch.is_uppercase();
|
||||
let name = ch.to_ascii_lowercase();
|
||||
Self {
|
||||
name: Some(name),
|
||||
count: count.unwrap_or(1),
|
||||
append
|
||||
}
|
||||
}
|
||||
pub fn name(&self) -> Option<char> {
|
||||
self.name
|
||||
}
|
||||
pub fn is_append(&self) -> bool {
|
||||
self.append
|
||||
}
|
||||
pub fn count(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
pub fn write_to_register(&self, buf: String) {
|
||||
if self.append {
|
||||
append_register(self.name, buf);
|
||||
} else {
|
||||
write_register(self.name, buf);
|
||||
}
|
||||
}
|
||||
pub fn read_from_register(&self) -> Option<String> {
|
||||
read_register(self.name)
|
||||
}
|
||||
let append = ch.is_uppercase();
|
||||
let name = ch.to_ascii_lowercase();
|
||||
Self {
|
||||
name: Some(name),
|
||||
count: count.unwrap_or(1),
|
||||
append,
|
||||
}
|
||||
}
|
||||
pub fn name(&self) -> Option<char> {
|
||||
self.name
|
||||
}
|
||||
pub fn is_append(&self) -> bool {
|
||||
self.append
|
||||
}
|
||||
pub fn count(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
pub fn write_to_register(&self, buf: String) {
|
||||
if self.append {
|
||||
append_register(self.name, buf);
|
||||
} else {
|
||||
write_register(self.name, buf);
|
||||
}
|
||||
}
|
||||
pub fn read_from_register(&self) -> Option<String> {
|
||||
read_register(self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RegisterName {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: None,
|
||||
count: 1,
|
||||
append: false
|
||||
}
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: None,
|
||||
count: 1,
|
||||
append: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct CmdFlags: u32 {
|
||||
const VISUAL = 1<<0;
|
||||
const VISUAL_LINE = 1<<1;
|
||||
const VISUAL_BLOCK = 1<<2;
|
||||
}
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct CmdFlags: u32 {
|
||||
const VISUAL = 1<<0;
|
||||
const VISUAL_LINE = 1<<1;
|
||||
const VISUAL_BLOCK = 1<<2;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Default,Debug)]
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct ViCmd {
|
||||
pub register: RegisterName,
|
||||
pub verb: Option<VerbCmd>,
|
||||
pub motion: Option<MotionCmd>,
|
||||
pub raw_seq: String,
|
||||
pub flags: CmdFlags,
|
||||
pub register: RegisterName,
|
||||
pub verb: Option<VerbCmd>,
|
||||
pub motion: Option<MotionCmd>,
|
||||
pub raw_seq: String,
|
||||
pub flags: CmdFlags,
|
||||
}
|
||||
|
||||
impl ViCmd {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn set_motion(&mut self, motion: MotionCmd) {
|
||||
self.motion = Some(motion)
|
||||
}
|
||||
pub fn set_verb(&mut self, verb: VerbCmd) {
|
||||
self.verb = Some(verb)
|
||||
}
|
||||
pub fn verb(&self) -> Option<&VerbCmd> {
|
||||
self.verb.as_ref()
|
||||
}
|
||||
pub fn motion(&self) -> Option<&MotionCmd> {
|
||||
self.motion.as_ref()
|
||||
}
|
||||
pub fn verb_count(&self) -> usize {
|
||||
self.verb.as_ref().map(|v| v.0).unwrap_or(1)
|
||||
}
|
||||
pub fn motion_count(&self) -> usize {
|
||||
self.motion.as_ref().map(|m| m.0).unwrap_or(1)
|
||||
}
|
||||
pub fn normalize_counts(&mut self) {
|
||||
let Some(verb) = self.verb.as_mut() else { return };
|
||||
let Some(motion) = self.motion.as_mut() else { return };
|
||||
let VerbCmd(v_count, _) = verb;
|
||||
let MotionCmd(m_count, _) = motion;
|
||||
let product = *v_count * *m_count;
|
||||
verb.0 = 1;
|
||||
motion.0 = product;
|
||||
}
|
||||
pub fn is_repeatable(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| v.1.is_repeatable())
|
||||
}
|
||||
pub fn is_cmd_repeat(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| matches!(v.1,Verb::RepeatLast))
|
||||
}
|
||||
pub fn is_motion_repeat(&self) -> bool {
|
||||
self.motion.as_ref().is_some_and(|m| matches!(m.1,Motion::RepeatMotion | Motion::RepeatMotionRev))
|
||||
}
|
||||
pub fn is_char_search(&self) -> bool {
|
||||
self.motion.as_ref().is_some_and(|m| matches!(m.1, Motion::CharSearch(..)))
|
||||
}
|
||||
pub fn should_submit(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::AcceptLineOrNewline))
|
||||
}
|
||||
pub fn is_undo_op(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo))
|
||||
}
|
||||
pub fn is_inplace_edit(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::ReplaceCharInplace(_,_) | Verb::ToggleCaseInplace(_))) &&
|
||||
self.motion.is_none()
|
||||
}
|
||||
pub fn is_line_motion(&self) -> bool {
|
||||
self.motion.as_ref().is_some_and(|m| {
|
||||
matches!(m.1,
|
||||
Motion::LineUp |
|
||||
Motion::LineDown |
|
||||
Motion::LineUpCharwise |
|
||||
Motion::LineDownCharwise
|
||||
)
|
||||
})
|
||||
}
|
||||
/// If a ViCmd has a linewise motion, but no verb, we change it to charwise
|
||||
pub fn alter_line_motion_if_no_verb(&mut self) {
|
||||
if self.is_line_motion() && self.verb.is_none() {
|
||||
if let Some(motion) = self.motion.as_mut() {
|
||||
match motion.1 {
|
||||
Motion::LineUp => motion.1 = Motion::LineUpCharwise,
|
||||
Motion::LineDown => motion.1 = Motion::LineDownCharwise,
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn is_mode_transition(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| {
|
||||
matches!(v.1,
|
||||
Verb::Change |
|
||||
Verb::InsertMode |
|
||||
Verb::InsertModeLineBreak(_) |
|
||||
Verb::NormalMode |
|
||||
Verb::VisualModeSelectLast |
|
||||
Verb::VisualMode |
|
||||
Verb::ReplaceMode
|
||||
)
|
||||
})
|
||||
}
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn set_motion(&mut self, motion: MotionCmd) {
|
||||
self.motion = Some(motion)
|
||||
}
|
||||
pub fn set_verb(&mut self, verb: VerbCmd) {
|
||||
self.verb = Some(verb)
|
||||
}
|
||||
pub fn verb(&self) -> Option<&VerbCmd> {
|
||||
self.verb.as_ref()
|
||||
}
|
||||
pub fn motion(&self) -> Option<&MotionCmd> {
|
||||
self.motion.as_ref()
|
||||
}
|
||||
pub fn verb_count(&self) -> usize {
|
||||
self.verb.as_ref().map(|v| v.0).unwrap_or(1)
|
||||
}
|
||||
pub fn motion_count(&self) -> usize {
|
||||
self.motion.as_ref().map(|m| m.0).unwrap_or(1)
|
||||
}
|
||||
pub fn normalize_counts(&mut self) {
|
||||
let Some(verb) = self.verb.as_mut() else {
|
||||
return;
|
||||
};
|
||||
let Some(motion) = self.motion.as_mut() else {
|
||||
return;
|
||||
};
|
||||
let VerbCmd(v_count, _) = verb;
|
||||
let MotionCmd(m_count, _) = motion;
|
||||
let product = *v_count * *m_count;
|
||||
verb.0 = 1;
|
||||
motion.0 = product;
|
||||
}
|
||||
pub fn is_repeatable(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| v.1.is_repeatable())
|
||||
}
|
||||
pub fn is_cmd_repeat(&self) -> bool {
|
||||
self
|
||||
.verb
|
||||
.as_ref()
|
||||
.is_some_and(|v| matches!(v.1, Verb::RepeatLast))
|
||||
}
|
||||
pub fn is_motion_repeat(&self) -> bool {
|
||||
self
|
||||
.motion
|
||||
.as_ref()
|
||||
.is_some_and(|m| matches!(m.1, Motion::RepeatMotion | Motion::RepeatMotionRev))
|
||||
}
|
||||
pub fn is_char_search(&self) -> bool {
|
||||
self
|
||||
.motion
|
||||
.as_ref()
|
||||
.is_some_and(|m| matches!(m.1, Motion::CharSearch(..)))
|
||||
}
|
||||
pub fn should_submit(&self) -> bool {
|
||||
self
|
||||
.verb
|
||||
.as_ref()
|
||||
.is_some_and(|v| matches!(v.1, Verb::AcceptLineOrNewline))
|
||||
}
|
||||
pub fn is_undo_op(&self) -> bool {
|
||||
self
|
||||
.verb
|
||||
.as_ref()
|
||||
.is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo))
|
||||
}
|
||||
pub fn is_inplace_edit(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| {
|
||||
matches!(
|
||||
v.1,
|
||||
Verb::ReplaceCharInplace(_, _) | Verb::ToggleCaseInplace(_)
|
||||
)
|
||||
}) && self.motion.is_none()
|
||||
}
|
||||
pub fn is_line_motion(&self) -> bool {
|
||||
self.motion.as_ref().is_some_and(|m| {
|
||||
matches!(
|
||||
m.1,
|
||||
Motion::LineUp | Motion::LineDown | Motion::LineUpCharwise | Motion::LineDownCharwise
|
||||
)
|
||||
})
|
||||
}
|
||||
/// If a ViCmd has a linewise motion, but no verb, we change it to charwise
|
||||
pub fn alter_line_motion_if_no_verb(&mut self) {
|
||||
if self.is_line_motion() && self.verb.is_none() {
|
||||
if let Some(motion) = self.motion.as_mut() {
|
||||
match motion.1 {
|
||||
Motion::LineUp => motion.1 = Motion::LineUpCharwise,
|
||||
Motion::LineDown => motion.1 = Motion::LineDownCharwise,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn is_mode_transition(&self) -> bool {
|
||||
self.verb.as_ref().is_some_and(|v| {
|
||||
matches!(
|
||||
v.1,
|
||||
Verb::Change
|
||||
| Verb::InsertMode
|
||||
| Verb::InsertModeLineBreak(_)
|
||||
| Verb::NormalMode
|
||||
| Verb::VisualModeSelectLast
|
||||
| Verb::VisualMode
|
||||
| Verb::ReplaceMode
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct VerbCmd(pub usize,pub Verb);
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct MotionCmd(pub usize,pub Motion);
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VerbCmd(pub usize, pub Verb);
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MotionCmd(pub usize, pub Motion);
|
||||
|
||||
impl MotionCmd {
|
||||
pub fn invert_char_motion(self) -> Self {
|
||||
let MotionCmd(count,Motion::CharSearch(dir, dest, ch)) = self else {
|
||||
unreachable!()
|
||||
};
|
||||
let new_dir = match dir {
|
||||
Direction::Forward => Direction::Backward,
|
||||
Direction::Backward => Direction::Forward,
|
||||
};
|
||||
MotionCmd(count,Motion::CharSearch(new_dir, dest, ch))
|
||||
}
|
||||
pub fn invert_char_motion(self) -> Self {
|
||||
let MotionCmd(count, Motion::CharSearch(dir, dest, ch)) = self else {
|
||||
unreachable!()
|
||||
};
|
||||
let new_dir = match dir {
|
||||
Direction::Forward => Direction::Backward,
|
||||
Direction::Backward => Direction::Forward,
|
||||
};
|
||||
MotionCmd(count, Motion::CharSearch(new_dir, dest, ch))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Verb {
|
||||
Delete,
|
||||
Change,
|
||||
Yank,
|
||||
Rot13, // lol
|
||||
ReplaceChar(char), // char to replace with, number of chars to replace
|
||||
ReplaceCharInplace(char,u16), // char to replace with, number of chars to replace
|
||||
ToggleCaseInplace(u16), // Number of chars to toggle
|
||||
ToggleCaseRange,
|
||||
ToLower,
|
||||
ToUpper,
|
||||
Complete,
|
||||
CompleteBackward,
|
||||
Undo,
|
||||
Redo,
|
||||
RepeatLast,
|
||||
Put(Anchor),
|
||||
ReplaceMode,
|
||||
InsertMode,
|
||||
InsertModeLineBreak(Anchor),
|
||||
NormalMode,
|
||||
VisualMode,
|
||||
VisualModeLine,
|
||||
VisualModeBlock, // dont even know if im going to implement this
|
||||
VisualModeSelectLast,
|
||||
SwapVisualAnchor,
|
||||
JoinLines,
|
||||
InsertChar(char),
|
||||
Insert(String),
|
||||
Indent,
|
||||
Dedent,
|
||||
Equalize,
|
||||
AcceptLineOrNewline,
|
||||
EndOfFile
|
||||
Delete,
|
||||
Change,
|
||||
Yank,
|
||||
Rot13, // lol
|
||||
ReplaceChar(char), // char to replace with, number of chars to replace
|
||||
ReplaceCharInplace(char, u16), // char to replace with, number of chars to replace
|
||||
ToggleCaseInplace(u16), // Number of chars to toggle
|
||||
ToggleCaseRange,
|
||||
ToLower,
|
||||
ToUpper,
|
||||
Complete,
|
||||
CompleteBackward,
|
||||
Undo,
|
||||
Redo,
|
||||
RepeatLast,
|
||||
Put(Anchor),
|
||||
ReplaceMode,
|
||||
InsertMode,
|
||||
InsertModeLineBreak(Anchor),
|
||||
NormalMode,
|
||||
VisualMode,
|
||||
VisualModeLine,
|
||||
VisualModeBlock, // dont even know if im going to implement this
|
||||
VisualModeSelectLast,
|
||||
SwapVisualAnchor,
|
||||
JoinLines,
|
||||
InsertChar(char),
|
||||
Insert(String),
|
||||
Indent,
|
||||
Dedent,
|
||||
Equalize,
|
||||
AcceptLineOrNewline,
|
||||
EndOfFile,
|
||||
}
|
||||
|
||||
|
||||
impl Verb {
|
||||
pub fn is_repeatable(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::Delete |
|
||||
Self::Change |
|
||||
Self::ReplaceChar(_) |
|
||||
Self::ReplaceCharInplace(_,_) |
|
||||
Self::ToLower |
|
||||
Self::ToUpper |
|
||||
Self::ToggleCaseRange |
|
||||
Self::ToggleCaseInplace(_) |
|
||||
Self::Put(_) |
|
||||
Self::ReplaceMode |
|
||||
Self::InsertModeLineBreak(_) |
|
||||
Self::JoinLines |
|
||||
Self::InsertChar(_) |
|
||||
Self::Insert(_) |
|
||||
Self::Indent |
|
||||
Self::Dedent |
|
||||
Self::Equalize
|
||||
)
|
||||
}
|
||||
pub fn is_edit(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::Delete |
|
||||
Self::Change |
|
||||
Self::ReplaceChar(_) |
|
||||
Self::ReplaceCharInplace(_,_) |
|
||||
Self::ToggleCaseRange |
|
||||
Self::ToggleCaseInplace(_) |
|
||||
Self::ToLower |
|
||||
Self::ToUpper |
|
||||
Self::RepeatLast |
|
||||
Self::Put(_) |
|
||||
Self::ReplaceMode |
|
||||
Self::InsertModeLineBreak(_) |
|
||||
Self::JoinLines |
|
||||
Self::InsertChar(_) |
|
||||
Self::Insert(_) |
|
||||
Self::Rot13 |
|
||||
Self::EndOfFile
|
||||
)
|
||||
}
|
||||
pub fn is_char_insert(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::Change |
|
||||
Self::InsertChar(_) |
|
||||
Self::ReplaceChar(_) |
|
||||
Self::ReplaceCharInplace(_,_)
|
||||
)
|
||||
}
|
||||
pub fn is_repeatable(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Delete
|
||||
| Self::Change
|
||||
| Self::ReplaceChar(_)
|
||||
| Self::ReplaceCharInplace(_, _)
|
||||
| Self::ToLower
|
||||
| Self::ToUpper
|
||||
| Self::ToggleCaseRange
|
||||
| Self::ToggleCaseInplace(_)
|
||||
| Self::Put(_)
|
||||
| Self::ReplaceMode
|
||||
| Self::InsertModeLineBreak(_)
|
||||
| Self::JoinLines
|
||||
| Self::InsertChar(_)
|
||||
| Self::Insert(_)
|
||||
| Self::Indent
|
||||
| Self::Dedent
|
||||
| Self::Equalize
|
||||
)
|
||||
}
|
||||
pub fn is_edit(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Delete
|
||||
| Self::Change
|
||||
| Self::ReplaceChar(_)
|
||||
| Self::ReplaceCharInplace(_, _)
|
||||
| Self::ToggleCaseRange
|
||||
| Self::ToggleCaseInplace(_)
|
||||
| Self::ToLower
|
||||
| Self::ToUpper
|
||||
| Self::RepeatLast
|
||||
| Self::Put(_)
|
||||
| Self::ReplaceMode
|
||||
| Self::InsertModeLineBreak(_)
|
||||
| Self::JoinLines
|
||||
| Self::InsertChar(_)
|
||||
| Self::Insert(_)
|
||||
| Self::Rot13
|
||||
| Self::EndOfFile
|
||||
)
|
||||
}
|
||||
pub fn is_char_insert(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Change | Self::InsertChar(_) | Self::ReplaceChar(_) | Self::ReplaceCharInplace(_, _)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum Motion {
|
||||
WholeLine,
|
||||
TextObj(TextObj),
|
||||
EndOfLastWord,
|
||||
BeginningOfFirstWord,
|
||||
BeginningOfLine,
|
||||
EndOfLine,
|
||||
WordMotion(To,Word,Direction),
|
||||
CharSearch(Direction,Dest,char),
|
||||
BackwardChar,
|
||||
ForwardChar,
|
||||
BackwardCharForced, // These two variants can cross line boundaries
|
||||
ForwardCharForced,
|
||||
LineUp,
|
||||
LineUpCharwise,
|
||||
ScreenLineUp,
|
||||
ScreenLineUpCharwise,
|
||||
LineDown,
|
||||
LineDownCharwise,
|
||||
ScreenLineDown,
|
||||
ScreenLineDownCharwise,
|
||||
BeginningOfScreenLine,
|
||||
FirstGraphicalOnScreenLine,
|
||||
HalfOfScreen,
|
||||
HalfOfScreenLineText,
|
||||
WholeBuffer,
|
||||
BeginningOfBuffer,
|
||||
EndOfBuffer,
|
||||
ToColumn,
|
||||
ToDelimMatch,
|
||||
ToBrace(Direction),
|
||||
ToBracket(Direction),
|
||||
ToParen(Direction),
|
||||
Range(usize,usize),
|
||||
RepeatMotion,
|
||||
RepeatMotionRev,
|
||||
Null
|
||||
WholeLine,
|
||||
TextObj(TextObj),
|
||||
EndOfLastWord,
|
||||
BeginningOfFirstWord,
|
||||
BeginningOfLine,
|
||||
EndOfLine,
|
||||
WordMotion(To, Word, Direction),
|
||||
CharSearch(Direction, Dest, char),
|
||||
BackwardChar,
|
||||
ForwardChar,
|
||||
BackwardCharForced, // These two variants can cross line boundaries
|
||||
ForwardCharForced,
|
||||
LineUp,
|
||||
LineUpCharwise,
|
||||
ScreenLineUp,
|
||||
ScreenLineUpCharwise,
|
||||
LineDown,
|
||||
LineDownCharwise,
|
||||
ScreenLineDown,
|
||||
ScreenLineDownCharwise,
|
||||
BeginningOfScreenLine,
|
||||
FirstGraphicalOnScreenLine,
|
||||
HalfOfScreen,
|
||||
HalfOfScreenLineText,
|
||||
WholeBuffer,
|
||||
BeginningOfBuffer,
|
||||
EndOfBuffer,
|
||||
ToColumn,
|
||||
ToDelimMatch,
|
||||
ToBrace(Direction),
|
||||
ToBracket(Direction),
|
||||
ToParen(Direction),
|
||||
Range(usize, usize),
|
||||
RepeatMotion,
|
||||
RepeatMotionRev,
|
||||
Null,
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,PartialEq,Eq,Debug)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum MotionBehavior {
|
||||
Exclusive,
|
||||
Inclusive,
|
||||
Linewise
|
||||
Exclusive,
|
||||
Inclusive,
|
||||
Linewise,
|
||||
}
|
||||
|
||||
impl Motion {
|
||||
pub fn behavior(&self) -> MotionBehavior {
|
||||
if self.is_linewise() {
|
||||
MotionBehavior::Linewise
|
||||
} else if self.is_exclusive() {
|
||||
MotionBehavior::Exclusive
|
||||
} else {
|
||||
MotionBehavior::Inclusive
|
||||
}
|
||||
}
|
||||
pub fn is_exclusive(&self) -> bool {
|
||||
matches!(&self,
|
||||
Self::BeginningOfLine |
|
||||
Self::BeginningOfFirstWord |
|
||||
Self::BeginningOfScreenLine |
|
||||
Self::FirstGraphicalOnScreenLine |
|
||||
Self::LineDownCharwise |
|
||||
Self::LineUpCharwise |
|
||||
Self::ScreenLineUpCharwise |
|
||||
Self::ScreenLineDownCharwise |
|
||||
Self::ToColumn |
|
||||
Self::TextObj(TextObj::Sentence(_)) |
|
||||
Self::TextObj(TextObj::Paragraph(_)) |
|
||||
Self::CharSearch(Direction::Backward, _, _) |
|
||||
Self::WordMotion(To::Start,_,_) |
|
||||
Self::ToBrace(_) |
|
||||
Self::ToBracket(_) |
|
||||
Self::ToParen(_) |
|
||||
Self::ScreenLineDown |
|
||||
Self::ScreenLineUp |
|
||||
Self::Range(_, _)
|
||||
)
|
||||
}
|
||||
pub fn is_linewise(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::WholeLine |
|
||||
Self::LineUp |
|
||||
Self::LineDown |
|
||||
Self::ScreenLineDown |
|
||||
Self::ScreenLineUp
|
||||
)
|
||||
}
|
||||
pub fn behavior(&self) -> MotionBehavior {
|
||||
if self.is_linewise() {
|
||||
MotionBehavior::Linewise
|
||||
} else if self.is_exclusive() {
|
||||
MotionBehavior::Exclusive
|
||||
} else {
|
||||
MotionBehavior::Inclusive
|
||||
}
|
||||
}
|
||||
pub fn is_exclusive(&self) -> bool {
|
||||
matches!(
|
||||
&self,
|
||||
Self::BeginningOfLine
|
||||
| Self::BeginningOfFirstWord
|
||||
| Self::BeginningOfScreenLine
|
||||
| Self::FirstGraphicalOnScreenLine
|
||||
| Self::LineDownCharwise
|
||||
| Self::LineUpCharwise
|
||||
| Self::ScreenLineUpCharwise
|
||||
| Self::ScreenLineDownCharwise
|
||||
| Self::ToColumn
|
||||
| Self::TextObj(TextObj::Sentence(_))
|
||||
| Self::TextObj(TextObj::Paragraph(_))
|
||||
| Self::CharSearch(Direction::Backward, _, _)
|
||||
| Self::WordMotion(To::Start, _, _)
|
||||
| Self::ToBrace(_)
|
||||
| Self::ToBracket(_)
|
||||
| Self::ToParen(_)
|
||||
| Self::ScreenLineDown
|
||||
| Self::ScreenLineUp
|
||||
| Self::Range(_, _)
|
||||
)
|
||||
}
|
||||
pub fn is_linewise(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::WholeLine | Self::LineUp | Self::LineDown | Self::ScreenLineDown | Self::ScreenLineUp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum Anchor {
|
||||
After,
|
||||
Before
|
||||
After,
|
||||
Before,
|
||||
}
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum TextObj {
|
||||
/// `iw`, `aw` — inner word, around word
|
||||
Word(Word, Bound),
|
||||
/// `iw`, `aw` — inner word, around word
|
||||
Word(Word, Bound),
|
||||
|
||||
/// `)`, `(` — forward, backward
|
||||
Sentence(Direction),
|
||||
/// `)`, `(` — forward, backward
|
||||
Sentence(Direction),
|
||||
|
||||
/// `}`, `{` — forward, backward
|
||||
Paragraph(Direction),
|
||||
/// `}`, `{` — forward, backward
|
||||
Paragraph(Direction),
|
||||
|
||||
WholeSentence(Bound),
|
||||
WholeParagraph(Bound),
|
||||
WholeSentence(Bound),
|
||||
WholeParagraph(Bound),
|
||||
|
||||
/// `i"`, `a"` — inner/around double quotes
|
||||
DoubleQuote(Bound),
|
||||
/// `i'`, `a'`
|
||||
SingleQuote(Bound),
|
||||
/// `i\``, `a\``
|
||||
BacktickQuote(Bound),
|
||||
/// `i"`, `a"` — inner/around double quotes
|
||||
DoubleQuote(Bound),
|
||||
/// `i'`, `a'`
|
||||
SingleQuote(Bound),
|
||||
/// `i\``, `a\``
|
||||
BacktickQuote(Bound),
|
||||
|
||||
/// `i)`, `a)` — round parens
|
||||
Paren(Bound),
|
||||
/// `i]`, `a]`
|
||||
Bracket(Bound),
|
||||
/// `i}`, `a}`
|
||||
Brace(Bound),
|
||||
/// `i<`, `a<`
|
||||
Angle(Bound),
|
||||
/// `i)`, `a)` — round parens
|
||||
Paren(Bound),
|
||||
/// `i]`, `a]`
|
||||
Bracket(Bound),
|
||||
/// `i}`, `a}`
|
||||
Brace(Bound),
|
||||
/// `i<`, `a<`
|
||||
Angle(Bound),
|
||||
|
||||
/// `it`, `at` — HTML/XML tags
|
||||
Tag(Bound),
|
||||
/// `it`, `at` — HTML/XML tags
|
||||
Tag(Bound),
|
||||
|
||||
/// Custom user-defined objects maybe?
|
||||
Custom(char),
|
||||
/// Custom user-defined objects maybe?
|
||||
Custom(char),
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum Word {
|
||||
Big,
|
||||
Normal
|
||||
Big,
|
||||
Normal,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum Bound {
|
||||
Inside,
|
||||
Around
|
||||
Inside,
|
||||
Around,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum Direction {
|
||||
#[default]
|
||||
Forward,
|
||||
Backward
|
||||
#[default]
|
||||
Forward,
|
||||
Backward,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum Dest {
|
||||
On,
|
||||
Before,
|
||||
After
|
||||
On,
|
||||
Before,
|
||||
After,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum To {
|
||||
Start,
|
||||
End
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user