Added -j flag to 'complete' for completing job names/pids

This commit is contained in:
2026-02-27 11:03:56 -05:00
parent e141e39c7e
commit c508180228
44 changed files with 3259 additions and 2853 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,10 @@ use std::{
use crate::{
libsh::term::{Style, StyleSet, Styled},
readline::{annotate_input, markers::{self, is_marker}},
readline::{
annotate_input,
markers::{self, is_marker},
},
state::{read_logic, read_meta, read_shopts},
};
@@ -20,10 +23,10 @@ use crate::{
pub struct Highlighter {
input: String,
output: String,
linebuf_cursor_pos: usize,
linebuf_cursor_pos: usize,
style_stack: Vec<StyleSet>,
last_was_reset: bool,
in_selection: bool
in_selection: bool,
}
impl Highlighter {
@@ -32,10 +35,10 @@ impl Highlighter {
Self {
input: String::new(),
output: String::new(),
linebuf_cursor_pos: 0,
linebuf_cursor_pos: 0,
style_stack: Vec::new(),
last_was_reset: true, // start as true so we don't emit a leading reset
in_selection: false
in_selection: false,
}
}
@@ -46,18 +49,18 @@ impl Highlighter {
pub fn load_input(&mut self, input: &str, linebuf_cursor_pos: usize) {
let input = annotate_input(input);
self.input = input;
self.linebuf_cursor_pos = linebuf_cursor_pos;
self.linebuf_cursor_pos = linebuf_cursor_pos;
}
pub fn strip_markers(str: &str) -> String {
let mut out = String::new();
for ch in str.chars() {
if !is_marker(ch) {
out.push(ch);
}
}
out
}
pub fn strip_markers(str: &str) -> String {
let mut out = String::new();
for ch in str.chars() {
if !is_marker(ch) {
out.push(ch);
}
}
out
}
/// Processes the annotated input and generates ANSI-styled output
///
@@ -69,14 +72,14 @@ impl Highlighter {
let mut input_chars = input.chars().peekable();
while let Some(ch) = input_chars.next() {
match ch {
markers::VISUAL_MODE_START => {
self.emit_style(Style::BgWhite | Style::Black);
self.in_selection = true;
}
markers::VISUAL_MODE_END => {
self.reapply_style();
self.in_selection = false;
}
markers::VISUAL_MODE_START => {
self.emit_style(Style::BgWhite | Style::Black);
self.in_selection = true;
}
markers::VISUAL_MODE_END => {
self.reapply_style();
self.in_selection = false;
}
markers::STRING_DQ_END
| markers::STRING_SQ_END
| markers::VAR_SUB_END
@@ -96,16 +99,16 @@ impl Highlighter {
if ch == markers::RESET {
break;
}
if !is_marker(ch) {
cmd_name.push(ch);
}
if !is_marker(ch) {
cmd_name.push(ch);
}
}
match cmd_name.as_str() {
"continue" | "return" | "break" => self.push_style(Style::Magenta),
_ => self.push_style(Style::Green),
}
}
match cmd_name.as_str() {
"continue" | "return" | "break" => self.push_style(Style::Magenta),
_ => self.push_style(Style::Green),
}
}
markers::CASE_PAT => self.push_style(Style::Blue),
markers::COMMENT => self.push_style(Style::BrightBlack),
@@ -114,7 +117,6 @@ impl Highlighter {
markers::REDIRECT | markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold),
markers::ASSIGNMENT => {
let mut var_name = String::new();
@@ -140,28 +142,30 @@ impl Highlighter {
markers::ARG => {
let mut arg = String::new();
let is_last_arg = !input_chars.clone().any(|c| c == markers::ARG || c.is_whitespace());
let is_last_arg = !input_chars
.clone()
.any(|c| c == markers::ARG || c.is_whitespace());
if !is_last_arg {
self.push_style(Style::White);
} else {
let mut chars_clone = input_chars.clone();
while let Some(ch) = chars_clone.next() {
if ch == markers::RESET {
break;
}
arg.push(ch);
}
if !is_last_arg {
self.push_style(Style::White);
} else {
let mut chars_clone = input_chars.clone();
while let Some(ch) = chars_clone.next() {
if ch == markers::RESET {
break;
}
arg.push(ch);
}
let style = if Self::is_filename(&Self::strip_markers(&arg)) {
Style::White | Style::Underline
} else {
Style::White.into()
};
let style = if Self::is_filename(&Self::strip_markers(&arg)) {
Style::White | Style::Underline
} else {
Style::White.into()
};
self.push_style(style);
self.last_was_reset = false;
}
self.push_style(style);
self.last_was_reset = false;
}
}
markers::COMMAND => {
@@ -173,9 +177,12 @@ impl Highlighter {
}
cmd_name.push(ch);
}
let style = if matches!(Self::strip_markers(&cmd_name).as_str(), "break" | "continue" | "return") {
Style::Magenta.into()
} else if Self::is_valid(&Self::strip_markers(&cmd_name)) {
let style = if matches!(
Self::strip_markers(&cmd_name).as_str(),
"break" | "continue" | "return"
) {
Style::Magenta.into()
} else if Self::is_valid(&Self::strip_markers(&cmd_name)) {
Style::Green.into()
} else {
Style::Red | Style::Bold
@@ -292,21 +299,21 @@ impl Highlighter {
fn is_valid(command: &str) -> bool {
let cmd_path = Path::new(&command);
if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) {
// this is a directory and autocd is enabled
return true;
}
if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) {
// this is a directory and autocd is enabled
return true;
}
if cmd_path.is_absolute() {
// the user has given us an absolute path
let Ok(meta) = cmd_path.metadata() else {
return false;
};
// this is a file that is executable by someone
meta.permissions().mode() & 0o111 != 0
} else {
read_meta(|m| m.cached_cmds().get(command).is_some())
}
if cmd_path.is_absolute() {
// the user has given us an absolute path
let Ok(meta) = cmd_path.metadata() else {
return false;
};
// this is a file that is executable by someone
meta.permissions().mode() & 0o111 != 0
} else {
read_meta(|m| m.cached_cmds().get(command).is_some())
}
}
fn is_filename(arg: &str) -> bool {
@@ -316,9 +323,10 @@ impl Highlighter {
return true;
}
if path.is_absolute()
&& let Some(parent_dir) = path.parent()
&& let Ok(entries) = parent_dir.read_dir() {
if path.is_absolute()
&& let Some(parent_dir) = path.parent()
&& let Ok(entries) = parent_dir.read_dir()
{
let files = entries
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string())
@@ -334,17 +342,17 @@ impl Highlighter {
return true;
}
}
}
}
read_meta(|m| {
let files = m.cwd_cache();
for file in files {
if file.starts_with(arg) {
return true;
}
}
false
})
read_meta(|m| {
let files = m.cwd_cache();
for file in files {
if file.starts_with(arg) {
return true;
}
}
false
})
}
/// Emits a reset ANSI code to the output, with deduplication
@@ -363,10 +371,10 @@ impl Highlighter {
/// Unconditionally appends the ANSI escape sequence for the given style
/// and marks that we're no longer in a reset state.
fn emit_style(&mut self, style: StyleSet) {
let mut style = style;
if !style.styles().contains(&Style::BgWhite) {
style = style.add_style(Style::BgBlack);
}
let mut style = style;
if !style.styles().contains(&Style::BgWhite) {
style = style.add_style(Style::BgBlack);
}
self.output.push_str(&style.to_string());
self.last_was_reset = false;
}
@@ -378,9 +386,9 @@ impl Highlighter {
pub fn push_style(&mut self, style: impl Into<StyleSet>) {
let set: StyleSet = style.into();
self.style_stack.push(set.clone());
if !self.in_selection {
self.emit_style(set.clone());
}
if !self.in_selection {
self.emit_style(set.clone());
}
}
/// Pops a style from the stack and restores the previous style
@@ -405,18 +413,18 @@ impl Highlighter {
/// the default terminal color between independent commands.
pub fn clear_styles(&mut self) {
self.style_stack.clear();
if !self.in_selection {
self.emit_reset();
}
if !self.in_selection {
self.emit_reset();
}
}
pub fn reapply_style(&mut self) {
if let Some(style) = self.style_stack.last().cloned() {
self.emit_style(style);
} else {
self.emit_reset();
}
}
pub fn reapply_style(&mut self) {
if let Some(style) = self.style_stack.last().cloned() {
self.emit_style(style);
} else {
self.emit_reset();
}
}
/// Simple marker-to-ANSI replacement (unused in favor of stack-based
/// highlighting)

View File

@@ -14,7 +14,14 @@ use crate::{
libsh::{
error::ShResult,
term::{Style, Styled},
}, parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}, prelude::*, readline::{markers, register::{write_register, RegisterContent}}, state::read_shopts
},
parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule},
prelude::*,
readline::{
markers,
register::{RegisterContent, write_register},
},
state::read_shopts,
};
const PUNCTUATION: [&str; 3] = ["?", "!", "."];
@@ -326,7 +333,7 @@ pub struct LineBuf {
pub insert_mode_start_pos: Option<usize>,
pub saved_col: Option<usize>,
pub auto_indent_level: usize,
pub auto_indent_level: usize,
pub undo_stack: Vec<Edit>,
pub redo_stack: Vec<Edit>,
@@ -384,12 +391,12 @@ impl LineBuf {
pub fn set_cursor_clamp(&mut self, yn: bool) {
self.cursor.exclusive = yn;
}
pub fn move_cursor_to_end(&mut self) {
self.move_cursor(MotionKind::To(self.grapheme_indices().len()))
}
pub fn move_cursor_to_start(&mut self) {
self.move_cursor(MotionKind::To(0))
}
pub fn move_cursor_to_end(&mut self) {
self.move_cursor(MotionKind::To(self.grapheme_indices().len()))
}
pub fn move_cursor_to_start(&mut self) {
self.move_cursor(MotionKind::To(0))
}
pub fn cursor_byte_pos(&mut self) -> usize {
self.index_byte_pos(self.cursor.get())
}
@@ -496,12 +503,12 @@ impl LineBuf {
pub fn grapheme_at_cursor(&mut self) -> Option<&str> {
self.grapheme_at(self.cursor.get())
}
pub fn grapheme_before_cursor(&mut self) -> Option<&str> {
if self.cursor.get() == 0 {
return None;
}
self.grapheme_at(self.cursor.ret_sub(1))
}
pub fn grapheme_before_cursor(&mut self) -> Option<&str> {
if self.cursor.get() == 0 {
return None;
}
self.grapheme_at(self.cursor.ret_sub(1))
}
pub fn mark_insert_mode_start_pos(&mut self) {
self.insert_mode_start_pos = Some(self.cursor.get())
}
@@ -542,7 +549,7 @@ impl LineBuf {
}
pub fn slice_to(&mut self, end: usize) -> Option<&str> {
self.update_graphemes_lazy();
self.read_slice_to(end)
self.read_slice_to(end)
}
pub fn read_slice_to(&self, end: usize) -> Option<&str> {
let grapheme_index = self.grapheme_indices().get(end).copied().or_else(|| {
@@ -596,9 +603,9 @@ impl LineBuf {
self.update_graphemes();
drained
}
pub fn drain_inclusive(&mut self, range: RangeInclusive<usize>) -> String {
self.drain(*range.start()..range.end().saturating_add(1))
}
pub fn drain_inclusive(&mut self, range: RangeInclusive<usize>) -> String {
self.drain(*range.start()..range.end().saturating_add(1))
}
pub fn push(&mut self, ch: char) {
self.buffer.push(ch);
self.update_graphemes();
@@ -620,30 +627,31 @@ impl LineBuf {
self.update_graphemes();
}
pub fn select_range(&self) -> Option<(usize, usize)> {
match self.select_mode? {
SelectMode::Char(_) => {
self.select_range
}
SelectMode::Line(_) => {
let (start, end) = self.select_range?;
let start = self.pos_line_number(start);
let end = self.pos_line_number(end);
let (select_start,_) = self.line_bounds(start);
let (_,select_end) = self.line_bounds(end);
if self.read_grapheme_before(select_end).is_some_and(|gr| gr == "\n") {
Some((select_start, select_end - 1))
} else {
Some((select_start, select_end))
}
}
SelectMode::Block(_) => todo!(),
}
match self.select_mode? {
SelectMode::Char(_) => self.select_range,
SelectMode::Line(_) => {
let (start, end) = self.select_range?;
let start = self.pos_line_number(start);
let end = self.pos_line_number(end);
let (select_start, _) = self.line_bounds(start);
let (_, select_end) = self.line_bounds(end);
if self
.read_grapheme_before(select_end)
.is_some_and(|gr| gr == "\n")
{
Some((select_start, select_end - 1))
} else {
Some((select_start, select_end))
}
}
SelectMode::Block(_) => todo!(),
}
}
pub fn start_selecting(&mut self, mode: SelectMode) {
let range_start = self.cursor;
let mut range_end = self.cursor;
range_end.add(1);
self.select_range = Some((range_start.get(), range_end.get()));
let range_start = self.cursor;
let mut range_end = self.cursor;
range_end.add(1);
self.select_range = Some((range_start.get(), range_end.get()));
self.select_mode = Some(mode);
}
pub fn stop_selecting(&mut self) {
@@ -656,11 +664,12 @@ impl LineBuf {
self.buffer.graphemes(true).filter(|g| *g == "\n").count()
}
pub fn pos_line_number(&self, pos: usize) -> usize {
self.read_slice_to(pos)
.map(|slice| slice.graphemes(true).filter(|g| *g == "\n").count())
.unwrap_or(0)
}
pub fn pos_line_number(&self, pos: usize) -> usize {
self
.read_slice_to(pos)
.map(|slice| slice.graphemes(true).filter(|g| *g == "\n").count())
.unwrap_or(0)
}
pub fn cursor_line_number(&self) -> usize {
self
.read_slice_to_cursor()
@@ -771,14 +780,14 @@ impl LineBuf {
}
Some(self.line_bounds(line_no))
}
pub fn this_line_exclusive(&mut self) -> (usize, usize) {
let line_no = self.cursor_line_number();
let (start, mut end) = self.line_bounds(line_no);
if self.read_grapheme_before(end).is_some_and(|gr| gr == "\n") {
end = end.saturating_sub(1);
}
(start, end)
}
pub fn this_line_exclusive(&mut self) -> (usize, usize) {
let line_no = self.cursor_line_number();
let (start, mut end) = self.line_bounds(line_no);
if self.read_grapheme_before(end).is_some_and(|gr| gr == "\n") {
end = end.saturating_sub(1);
}
(start, end)
}
pub fn this_line(&mut self) -> (usize, usize) {
let line_no = self.cursor_line_number();
self.line_bounds(line_no)
@@ -789,9 +798,9 @@ impl LineBuf {
pub fn end_of_line(&mut self) -> usize {
self.this_line().1
}
pub fn end_of_line_exclusive(&mut self) -> usize {
self.this_line_exclusive().1
}
pub fn end_of_line_exclusive(&mut self) -> usize {
self.this_line_exclusive().1
}
pub fn select_lines_up(&mut self, n: usize) -> Option<(usize, usize)> {
if self.start_of_line() == 0 {
return None;
@@ -1929,33 +1938,34 @@ impl LineBuf {
let end = start + gr.len();
self.buffer.replace_range(start..end, new);
}
pub fn calc_indent_level(&mut self) {
let to_cursor = self
.slice_to_cursor()
.map(|s| s.to_string())
.unwrap_or(self.buffer.clone());
let input = Arc::new(to_cursor);
let Ok(tokens) = LexStream::new(input, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>() else {
log::error!("Failed to lex buffer for indent calculation");
return;
};
let mut level: usize = 0;
for tk in tokens {
if tk.flags.contains(TkFlags::KEYWORD) {
match tk.as_str() {
"then" | "do" | "in" => level += 1,
"done" | "fi" | "esac" => level = level.saturating_sub(1),
_ => { /* Continue */ }
}
} else if tk.class == TkRule::BraceGrpStart {
level += 1;
} else if tk.class == TkRule::BraceGrpEnd {
level = level.saturating_sub(1);
}
}
pub fn calc_indent_level(&mut self) {
let to_cursor = self
.slice_to_cursor()
.map(|s| s.to_string())
.unwrap_or(self.buffer.clone());
let input = Arc::new(to_cursor);
let Ok(tokens) = LexStream::new(input, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()
else {
log::error!("Failed to lex buffer for indent calculation");
return;
};
let mut level: usize = 0;
for tk in tokens {
if tk.flags.contains(TkFlags::KEYWORD) {
match tk.as_str() {
"then" | "do" | "in" => level += 1,
"done" | "fi" | "esac" => level = level.saturating_sub(1),
_ => { /* Continue */ }
}
} else if tk.class == TkRule::BraceGrpStart {
level += 1;
} else if tk.class == TkRule::BraceGrpEnd {
level = level.saturating_sub(1);
}
}
self.auto_indent_level = level;
}
self.auto_indent_level = level;
}
pub fn eval_motion(&mut self, verb: Option<&Verb>, motion: MotionCmd) -> MotionKind {
let buffer = self.buffer.clone();
if self.has_hint() {
@@ -1965,7 +1975,7 @@ impl LineBuf {
let eval = match motion {
MotionCmd(count, motion @ (Motion::WholeLineInclusive | Motion::WholeLineExclusive)) => {
let exclusive = matches!(motion, Motion::WholeLineExclusive);
let exclusive = matches!(motion, Motion::WholeLineExclusive);
let Some((start, mut end)) = (if count == 1 {
Some(self.this_line())
@@ -1975,9 +1985,9 @@ impl LineBuf {
return MotionKind::Null;
};
if exclusive && self.grapheme_before(end).is_some_and(|gr| gr == "\n") {
end = end.saturating_sub(1);
}
if exclusive && self.grapheme_before(end).is_some_and(|gr| gr == "\n") {
end = end.saturating_sub(1);
}
let target_col = if let Some(col) = self.saved_col {
col
@@ -1994,7 +2004,8 @@ impl LineBuf {
if self.cursor.exclusive
&& line.ends_with("\n")
&& self.grapheme_at(target_pos) == Some("\n")
&& line != "\n" // Allow landing on newline for empty lines
&& line != "\n"
// Allow landing on newline for empty lines
{
target_pos = target_pos.saturating_sub(1); // Don't land on the
// newline
@@ -2155,7 +2166,7 @@ impl LineBuf {
MotionCmd(_, Motion::BeginningOfLine) => MotionKind::On(self.start_of_line()),
MotionCmd(count, Motion::EndOfLine) => {
let pos = if count == 1 {
self.end_of_line()
self.end_of_line()
} else if let Some((_, end)) = self.select_lines_down(count) {
end
} else {
@@ -2228,14 +2239,15 @@ impl LineBuf {
};
let Some(line) = self.slice(start..end).map(|s| s.to_string()) else {
log::warn!("Failed to get line slice for motion, start: {start}, end: {end}");
log::warn!("Failed to get line slice for motion, start: {start}, end: {end}");
return MotionKind::Null;
};
let mut target_pos = self.grapheme_index_for_display_col(&line, target_col);
if self.cursor.exclusive
&& line.ends_with("\n")
&& self.grapheme_at(target_pos) == Some("\n")
&& line != "\n" // Allow landing on newline for empty lines
&& line != "\n"
// Allow landing on newline for empty lines
{
target_pos = target_pos.saturating_sub(1); // Don't land on the
// newline
@@ -2247,7 +2259,6 @@ impl LineBuf {
_ => unreachable!(),
};
MotionKind::InclusiveWithTargetCol((start, end), target_pos)
}
MotionCmd(count, Motion::LineDownCharwise) | MotionCmd(count, Motion::LineUpCharwise) => {
@@ -2428,13 +2439,13 @@ impl LineBuf {
pub fn range_from_motion(&mut self, motion: &MotionKind) -> Option<(usize, usize)> {
let range = match motion {
MotionKind::On(pos) => {
let cursor_pos = self.cursor.get();
if cursor_pos == *pos {
ordered(cursor_pos, pos + 1) // scary
} else {
ordered(cursor_pos, *pos)
}
}
let cursor_pos = self.cursor.get();
if cursor_pos == *pos {
ordered(cursor_pos, pos + 1) // scary
} else {
ordered(cursor_pos, *pos)
}
}
MotionKind::Onto(pos) => {
// For motions which include the character at the cursor during operations
// but exclude the character during movements
@@ -2478,29 +2489,32 @@ impl LineBuf {
) -> ShResult<()> {
match verb {
Verb::Delete | Verb::Yank | Verb::Change => {
log::debug!("Executing verb: {verb:?} with motion: {motion:?}");
log::debug!("Executing verb: {verb:?} with motion: {motion:?}");
let Some((mut start, mut end)) = self.range_from_motion(&motion) else {
log::debug!("No range from motion, nothing to do");
log::debug!("No range from motion, nothing to do");
return Ok(());
};
log::debug!("Initial range from motion: ({start}, {end})");
log::debug!("self.grapheme_indices().len(): {}", self.grapheme_indices().len());
log::debug!("Initial range from motion: ({start}, {end})");
log::debug!(
"self.grapheme_indices().len(): {}",
self.grapheme_indices().len()
);
let mut do_indent = false;
if verb == Verb::Change && (start,end) == self.this_line_exclusive() {
do_indent = read_shopts(|o| o.prompt.auto_indent);
}
let mut do_indent = false;
if verb == Verb::Change && (start, end) == self.this_line_exclusive() {
do_indent = read_shopts(|o| o.prompt.auto_indent);
}
let mut text = if verb == Verb::Yank {
self
.slice(start..end)
.map(|c| c.to_string())
.unwrap_or_default()
} else if start == self.grapheme_indices().len() && end == self.grapheme_indices().len() {
// user is in normal mode and pressed 'x' on the last char in the buffer
let drained = self.drain(end.saturating_sub(1)..end);
self.update_graphemes();
drained
} else if start == self.grapheme_indices().len() && end == self.grapheme_indices().len() {
// user is in normal mode and pressed 'x' on the last char in the buffer
let drained = self.drain(end.saturating_sub(1)..end);
self.update_graphemes();
drained
} else {
let drained = self.drain(start..end);
self.update_graphemes();
@@ -2508,30 +2522,30 @@ impl LineBuf {
};
let is_linewise = matches!(
motion,
MotionKind::InclusiveWithTargetCol(..) |
MotionKind::ExclusiveWithTargetCol(..)
MotionKind::InclusiveWithTargetCol(..) | MotionKind::ExclusiveWithTargetCol(..)
) || matches!(self.select_mode, Some(SelectMode::Line(_)));
let register_content = if is_linewise {
if !text.ends_with('\n') && !text.is_empty() {
text.push('\n');
}
if !text.ends_with('\n') && !text.is_empty() {
text.push('\n');
}
RegisterContent::Line(text)
} else {
RegisterContent::Span(text)
};
register.write_to_register(register_content);
self.cursor.set(start);
if do_indent {
self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t');
for tab in tabs {
self.insert_at_cursor(tab);
self.cursor.add(1);
}
} else if verb != Verb::Change
&& let MotionKind::InclusiveWithTargetCol((_,_), col) = motion {
self.cursor.add(col);
}
self.cursor.set(start);
if do_indent {
self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t');
for tab in tabs {
self.insert_at_cursor(tab);
self.cursor.add(1);
}
} else if verb != Verb::Change
&& let MotionKind::InclusiveWithTargetCol((_, _), col) = motion
{
self.cursor.add(col);
}
}
Verb::Rot13 => {
let Some((start, end)) = self.range_from_motion(&motion) else {
@@ -2682,7 +2696,7 @@ impl LineBuf {
self.buffer.replace_range(pos..pos + new.len(), &old);
let new_cursor_pos = self.cursor.get();
self.cursor.set(cursor_pos);
self.cursor.set(cursor_pos);
let new_edit = Edit {
pos,
cursor_pos: new_cursor_pos,
@@ -2701,17 +2715,17 @@ impl LineBuf {
if content.is_empty() {
return Ok(());
}
if let Some(range) = self.select_range() {
let register_text = self.drain_inclusive(range.0..=range.1);
write_register(None, RegisterContent::Span(register_text)); // swap deleted text into register
if let Some(range) = self.select_range() {
let register_text = self.drain_inclusive(range.0..=range.1);
write_register(None, RegisterContent::Span(register_text)); // swap deleted text into register
let text = content.as_str();
self.insert_str_at(range.0, text);
self.cursor.set(range.0 + content.char_count());
self.select_range = None;
self.update_graphemes();
return Ok(());
}
let text = content.as_str();
self.insert_str_at(range.0, text);
self.cursor.set(range.0 + content.char_count());
self.select_range = None;
self.update_graphemes();
return Ok(());
}
match content {
RegisterContent::Span(ref text) => {
let insert_idx = match anchor {
@@ -2726,7 +2740,9 @@ impl LineBuf {
Anchor::After => self.end_of_line(),
Anchor::Before => self.start_of_line(),
};
let needs_newline = self.grapheme_before(insert_idx).is_some_and(|gr| gr != "\n");
let needs_newline = self
.grapheme_before(insert_idx)
.is_some_and(|gr| gr != "\n");
if needs_newline {
let full = format!("\n{}", text);
self.insert_str_at(insert_idx, &full);
@@ -2788,11 +2804,11 @@ impl LineBuf {
let Some((start, end)) = self.range_from_motion(&motion) else {
return Ok(());
};
let move_cursor = self.cursor.get() == start;
let move_cursor = self.cursor.get() == start;
self.insert_at(start, '\t');
if move_cursor {
self.cursor.add(1);
}
if move_cursor {
self.cursor.add(1);
}
let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter();
while let Some(idx) = range_indices.next() {
let gr = self.grapheme_at(idx).unwrap();
@@ -2822,7 +2838,7 @@ impl LineBuf {
if self.grapheme_at(start) == Some("\t") {
self.remove(start);
}
end = end.min(self.grapheme_indices().len().saturating_sub(1));
end = end.min(self.grapheme_indices().len().saturating_sub(1));
let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter();
while let Some(idx) = range_indices.next() {
let gr = self.grapheme_at(idx).unwrap();
@@ -2852,29 +2868,29 @@ impl LineBuf {
Verb::Equalize => todo!(),
Verb::InsertModeLineBreak(anchor) => {
let (mut start, end) = self.this_line();
let auto_indent = read_shopts(|o| o.prompt.auto_indent);
let auto_indent = read_shopts(|o| o.prompt.auto_indent);
if start == 0 && end == self.cursor.max {
match anchor {
Anchor::After => {
self.push('\n');
if auto_indent {
self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t');
for tab in tabs {
self.push(tab);
}
}
if auto_indent {
self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t');
for tab in tabs {
self.push(tab);
}
}
self.cursor.set(self.cursor_max());
return Ok(());
}
Anchor::Before => {
if auto_indent {
self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t');
for tab in tabs {
self.insert_at(0, tab);
}
}
if auto_indent {
self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t');
for tab in tabs {
self.insert_at(0, tab);
}
}
self.insert_at(0, '\n');
self.cursor.set(0);
return Ok(());
@@ -2888,52 +2904,52 @@ impl LineBuf {
self.cursor.set(end);
self.insert_at_cursor('\n');
self.cursor.add(1);
if auto_indent {
self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t');
for tab in tabs {
self.insert_at_cursor(tab);
self.cursor.add(1);
}
}
if auto_indent {
self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t');
for tab in tabs {
self.insert_at_cursor(tab);
self.cursor.add(1);
}
}
}
Anchor::Before => {
self.cursor.set(start);
self.insert_at_cursor('\n');
self.cursor.add(1);
if auto_indent {
self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t');
for tab in tabs {
self.insert_at_cursor(tab);
self.cursor.add(1);
}
}
if auto_indent {
self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t');
for tab in tabs {
self.insert_at_cursor(tab);
self.cursor.add(1);
}
}
}
}
}
Verb::AcceptLineOrNewline => {
// If this verb has reached this function, it means we have incomplete input
// and therefore must insert a newline instead of accepting the input
if self.cursor.exclusive {
// in this case we are in normal/visual mode, so we don't insert anything
// and just move down a line
let motion = self.eval_motion(None, MotionCmd(1, Motion::LineDownCharwise));
self.apply_motion(motion);
return Ok(());
}
let auto_indent = read_shopts(|o| o.prompt.auto_indent);
self.insert_at_cursor('\n');
self.cursor.add(1);
if auto_indent {
self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t');
for tab in tabs {
self.insert_at_cursor(tab);
self.cursor.add(1);
}
}
}
// If this verb has reached this function, it means we have incomplete input
// and therefore must insert a newline instead of accepting the input
if self.cursor.exclusive {
// in this case we are in normal/visual mode, so we don't insert anything
// and just move down a line
let motion = self.eval_motion(None, MotionCmd(1, Motion::LineDownCharwise));
self.apply_motion(motion);
return Ok(());
}
let auto_indent = read_shopts(|o| o.prompt.auto_indent);
self.insert_at_cursor('\n');
self.cursor.add(1);
if auto_indent {
self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t');
for tab in tabs {
self.insert_at_cursor(tab);
self.cursor.add(1);
}
}
}
Verb::Complete
| Verb::EndOfFile
@@ -2951,7 +2967,11 @@ impl LineBuf {
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() || cmd.verb.as_ref().is_some_and(|v| v.1 == Verb::AcceptLineOrNewline);
let is_line_motion = cmd.is_line_motion()
|| cmd
.verb
.as_ref()
.is_some_and(|v| v.1 == Verb::AcceptLineOrNewline);
let is_undo_op = cmd.is_undo_op();
let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging);
@@ -3024,13 +3044,14 @@ impl LineBuf {
self.apply_motion(motion_eval);
}
if self.cursor.exclusive
&& self.grapheme_at_cursor().is_some_and(|gr| gr == "\n")
&& self.grapheme_before_cursor().is_some_and(|gr| gr != "\n") {
// we landed on a newline, and we aren't inbetween two newlines.
self.cursor.sub(1);
self.update_select_range();
}
if self.cursor.exclusive
&& self.grapheme_at_cursor().is_some_and(|gr| gr == "\n")
&& self.grapheme_before_cursor().is_some_and(|gr| gr != "\n")
{
// we landed on a newline, and we aren't inbetween two newlines.
self.cursor.sub(1);
self.update_select_range();
}
/* Done executing, do some cleanup */
@@ -3070,10 +3091,10 @@ impl LineBuf {
let text = self
.hint
.clone()
.map(|h| format!("\x1b[90m{h}\x1b[0m"))
.map(|h| format!("\x1b[90m{h}\x1b[0m"))
.unwrap_or_default();
text.replace("\n", "\n\x1b[90m")
text.replace("\n", "\n\x1b[90m")
}
}
@@ -3085,20 +3106,29 @@ impl Display for LineBuf {
let start_byte = self.read_idx_byte_pos(start);
let end_byte = self.read_idx_byte_pos(end).min(full_buf.len());
match mode.anchor() {
SelectAnchor::Start => {
let mut inclusive = start_byte..=end_byte;
if *inclusive.end() == full_buf.len() {
inclusive = start_byte..=end_byte.saturating_sub(1);
}
let selected = format!("{}{}{}", markers::VISUAL_MODE_START, &full_buf[inclusive.clone()], markers::VISUAL_MODE_END)
.replace("\n", format!("\n{}",markers::VISUAL_MODE_START).as_str());
let selected = format!(
"{}{}{}",
markers::VISUAL_MODE_START,
&full_buf[inclusive.clone()],
markers::VISUAL_MODE_END
)
.replace("\n", format!("\n{}", markers::VISUAL_MODE_START).as_str());
full_buf.replace_range(inclusive, &selected);
}
SelectAnchor::End => {
let selected = format!("{}{}{}", markers::VISUAL_MODE_START, &full_buf[start_byte..end_byte], markers::VISUAL_MODE_END)
.replace("\n", format!("\n{}",markers::VISUAL_MODE_START).as_str());
let selected = format!(
"{}{}{}",
markers::VISUAL_MODE_START,
&full_buf[start_byte..end_byte],
markers::VISUAL_MODE_END
)
.replace("\n", format!("\n{}", markers::VISUAL_MODE_START).as_str());
full_buf.replace_range(start_byte..end_byte, &selected);
}
}

View File

@@ -36,12 +36,12 @@ pub mod vimode;
pub mod markers {
use super::Marker;
/*
* These are invisible Unicode characters used to annotate
* strings with various contextual metadata.
*/
/*
* These are invisible Unicode characters used to annotate
* strings with various contextual metadata.
*/
/* Highlight Markers */
/* Highlight Markers */
// token-level (derived from token class)
pub const COMMAND: Marker = '\u{e100}';
@@ -71,36 +71,36 @@ pub mod markers {
pub const ESCAPE: Marker = '\u{e116}';
pub const GLOB: Marker = '\u{e117}';
// other
pub const VISUAL_MODE_START: Marker = '\u{e118}';
pub const VISUAL_MODE_END: Marker = '\u{e119}';
// other
pub const VISUAL_MODE_START: Marker = '\u{e118}';
pub const VISUAL_MODE_END: Marker = '\u{e119}';
pub const RESET: Marker = '\u{e11a}';
pub const NULL: Marker = '\u{e11b}';
/* Expansion Markers */
/// Double quote '"' marker
pub const DUB_QUOTE: Marker = '\u{e001}';
/// Single quote '\\'' marker
pub const SNG_QUOTE: Marker = '\u{e002}';
/// Tilde sub marker
pub const TILDE_SUB: Marker = '\u{e003}';
/// Input process sub marker
pub const PROC_SUB_IN: Marker = '\u{e005}';
/// Output process sub marker
pub const PROC_SUB_OUT: Marker = '\u{e006}';
/// Marker for null expansion
/// This is used for when "$@" or "$*" are used in quotes and there are no
/// arguments Without this marker, it would be handled like an empty string,
/// which breaks some commands
pub const NULL_EXPAND: Marker = '\u{e007}';
/// Explicit marker for argument separation
/// This is used to join the arguments given by "$@", and preserves exact formatting
/// of the original arguments, including quoting
pub const ARG_SEP: Marker = '\u{e008}';
/* Expansion Markers */
/// Double quote '"' marker
pub const DUB_QUOTE: Marker = '\u{e001}';
/// Single quote '\\'' marker
pub const SNG_QUOTE: Marker = '\u{e002}';
/// Tilde sub marker
pub const TILDE_SUB: Marker = '\u{e003}';
/// Input process sub marker
pub const PROC_SUB_IN: Marker = '\u{e005}';
/// Output process sub marker
pub const PROC_SUB_OUT: Marker = '\u{e006}';
/// Marker for null expansion
/// This is used for when "$@" or "$*" are used in quotes and there are no
/// arguments Without this marker, it would be handled like an empty string,
/// which breaks some commands
pub const NULL_EXPAND: Marker = '\u{e007}';
/// Explicit marker for argument separation
/// This is used to join the arguments given by "$@", and preserves exact
/// formatting of the original arguments, including quoting
pub const ARG_SEP: Marker = '\u{e008}';
pub const VI_SEQ_EXP: Marker = '\u{e009}';
pub const VI_SEQ_EXP: Marker = '\u{e009}';
pub const END_MARKERS: [Marker; 7] = [
VAR_SUB_END,
@@ -116,10 +116,10 @@ pub mod markers {
];
pub const SUB_TOKEN: [Marker; 6] = [VAR_SUB, CMD_SUB, PROC_SUB, STRING_DQ, STRING_SQ, GLOB];
pub const MISC: [Marker; 3] = [ESCAPE, VISUAL_MODE_START, VISUAL_MODE_END];
pub const MISC: [Marker; 3] = [ESCAPE, VISUAL_MODE_START, VISUAL_MODE_END];
pub fn is_marker(c: Marker) -> bool {
('\u{e000}'..'\u{efff}').contains(&c)
('\u{e000}'..'\u{efff}').contains(&c)
}
}
type Marker = char;
@@ -135,66 +135,73 @@ pub enum ReadlineEvent {
}
pub struct Prompt {
ps1_expanded: String,
ps1_raw: String,
psr_expanded: Option<String>,
psr_raw: Option<String>,
ps1_expanded: String,
ps1_raw: String,
psr_expanded: Option<String>,
psr_raw: Option<String>,
}
impl Prompt {
const DEFAULT_PS1: &str = "\\e[0m\\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 ";
pub fn new() -> Self {
let Ok(ps1_raw) = env::var("PS1") else {
return Self::default();
};
let Ok(ps1_expanded) = expand_prompt(&ps1_raw) else {
return Self::default();
};
let psr_raw = env::var("PSR").ok();
let psr_expanded = psr_raw.clone().map(|r| expand_prompt(&r)).transpose().ok().flatten();
Self {
ps1_expanded,
ps1_raw,
psr_expanded,
psr_raw,
}
}
const DEFAULT_PS1: &str =
"\\e[0m\\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 ";
pub fn new() -> Self {
let Ok(ps1_raw) = env::var("PS1") else {
return Self::default();
};
let Ok(ps1_expanded) = expand_prompt(&ps1_raw) else {
return Self::default();
};
let psr_raw = env::var("PSR").ok();
let psr_expanded = psr_raw
.clone()
.map(|r| expand_prompt(&r))
.transpose()
.ok()
.flatten();
Self {
ps1_expanded,
ps1_raw,
psr_expanded,
psr_raw,
}
}
pub fn get_ps1(&self) -> &str {
&self.ps1_expanded
}
pub fn set_ps1(&mut self, ps1_raw: String) -> ShResult<()> {
self.ps1_expanded = expand_prompt(&ps1_raw)?;
self.ps1_raw = ps1_raw;
Ok(())
}
pub fn set_psr(&mut self, psr_raw: String) -> ShResult<()> {
self.psr_expanded = Some(expand_prompt(&psr_raw)?);
self.psr_raw = Some(psr_raw);
Ok(())
}
pub fn get_psr(&self) -> Option<&str> {
self.psr_expanded.as_deref()
}
pub fn get_ps1(&self) -> &str {
&self.ps1_expanded
}
pub fn set_ps1(&mut self, ps1_raw: String) -> ShResult<()> {
self.ps1_expanded = expand_prompt(&ps1_raw)?;
self.ps1_raw = ps1_raw;
Ok(())
}
pub fn set_psr(&mut self, psr_raw: String) -> ShResult<()> {
self.psr_expanded = Some(expand_prompt(&psr_raw)?);
self.psr_raw = Some(psr_raw);
Ok(())
}
pub fn get_psr(&self) -> Option<&str> {
self.psr_expanded.as_deref()
}
pub fn refresh(&mut self) -> ShResult<()> {
self.ps1_expanded = expand_prompt(&self.ps1_raw)?;
if let Some(psr_raw) = &self.psr_raw {
self.psr_expanded = Some(expand_prompt(psr_raw)?);
}
Ok(())
}
pub fn refresh(&mut self) -> ShResult<()> {
self.ps1_expanded = expand_prompt(&self.ps1_raw)?;
if let Some(psr_raw) = &self.psr_raw {
self.psr_expanded = Some(expand_prompt(psr_raw)?);
}
Ok(())
}
}
impl Default for Prompt {
fn default() -> Self {
Self {
ps1_expanded: expand_prompt(Self::DEFAULT_PS1).unwrap_or_else(|_| Self::DEFAULT_PS1.to_string()),
ps1_raw: Self::DEFAULT_PS1.to_string(),
psr_expanded: None,
psr_raw: None,
}
}
fn default() -> Self {
Self {
ps1_expanded: expand_prompt(Self::DEFAULT_PS1)
.unwrap_or_else(|_| Self::DEFAULT_PS1.to_string()),
ps1_raw: Self::DEFAULT_PS1.to_string(),
psr_expanded: None,
psr_raw: None,
}
}
}
pub struct ShedVi {
@@ -232,7 +239,7 @@ impl ShedVi {
history: History::new()?,
needs_redraw: true,
};
new.writer.flush_write("\n")?; // ensure we start on a new line, in case the previous command didn't end with a newline
new.writer.flush_write("\n")?; // ensure we start on a new line, in case the previous command didn't end with a newline
new.print_line(false)?;
Ok(new)
}
@@ -255,58 +262,55 @@ impl ShedVi {
self.needs_redraw = true;
}
/// Reset readline state for a new prompt
pub fn reset(&mut self, full_redraw: bool) -> ShResult<()> {
// Clear old display before resetting state — old_layout must survive
// so print_line can call clear_rows with the full multi-line layout
self.prompt = Prompt::new();
// Clear old display before resetting state — old_layout must survive
// so print_line can call clear_rows with the full multi-line layout
self.prompt = Prompt::new();
self.editor = Default::default();
self.mode = Box::new(ViInsert::new());
self.needs_redraw = true;
if full_redraw {
self.old_layout = None;
}
if full_redraw {
self.old_layout = None;
}
self.history.pending = None;
self.history.reset();
self.print_line(false)
self.print_line(false)
}
pub fn prompt(&self) -> &Prompt {
&self.prompt
}
pub fn prompt(&self) -> &Prompt {
&self.prompt
}
pub fn prompt_mut(&mut self) -> &mut Prompt {
&mut self.prompt
}
pub fn prompt_mut(&mut self) -> &mut Prompt {
&mut self.prompt
}
fn should_submit(&mut self) -> ShResult<bool> {
if self.mode.report_mode() == ModeReport::Normal {
return Ok(true);
}
let input = Arc::new(self.editor.buffer.clone());
self.editor.calc_indent_level();
let lex_result1 = LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<_>>>();
let lex_result2 = LexStream::new(Arc::clone(&input), LexFlags::empty()).collect::<ShResult<Vec<_>>>();
let is_top_level = self.editor.auto_indent_level == 0;
fn should_submit(&mut self) -> ShResult<bool> {
if self.mode.report_mode() == ModeReport::Normal {
return Ok(true);
}
let input = Arc::new(self.editor.buffer.clone());
self.editor.calc_indent_level();
let lex_result1 =
LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<_>>>();
let lex_result2 =
LexStream::new(Arc::clone(&input), LexFlags::empty()).collect::<ShResult<Vec<_>>>();
let is_top_level = self.editor.auto_indent_level == 0;
let is_complete = match (lex_result1.is_err(), lex_result2.is_err()) {
(true, true) => {
return Err(lex_result2.unwrap_err());
}
(true, false) => {
return Err(lex_result1.unwrap_err());
}
(false, true) => {
false
}
(false, false) => {
true
}
};
let is_complete = match (lex_result1.is_err(), lex_result2.is_err()) {
(true, true) => {
return Err(lex_result2.unwrap_err());
}
(true, false) => {
return Err(lex_result1.unwrap_err());
}
(false, true) => false,
(false, false) => true,
};
Ok(is_complete && is_top_level)
}
Ok(is_complete && is_top_level)
}
/// Process any available input and return readline event
/// This is non-blocking - returns Pending if no complete line yet
@@ -362,8 +366,8 @@ impl ShedVi {
self.editor.set_hint(hint);
}
None => {
self.writer.send_bell().ok();
},
self.writer.send_bell().ok();
}
}
self.needs_redraw = true;
@@ -385,9 +389,11 @@ impl ShedVi {
continue;
}
if cmd.is_submit_action() && (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete)) {
if cmd.is_submit_action()
&& (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete))
{
self.editor.set_hint(None);
self.editor.cursor.set(self.editor.cursor_max()); // Move the cursor to the very end
self.editor.cursor.set(self.editor.cursor_max()); // Move the cursor to the very end
self.print_line(true)?; // Redraw
self.writer.flush_write("\n")?;
let buf = self.editor.take_buf();
@@ -407,13 +413,13 @@ impl ShedVi {
return Ok(ReadlineEvent::Eof);
} else {
self.editor = LineBuf::new();
self.mode = Box::new(ViInsert::new());
self.mode = Box::new(ViInsert::new());
self.needs_redraw = true;
continue;
}
}
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit());
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit());
let before = self.editor.buffer.clone();
self.exec_cmd(cmd)?;
@@ -424,8 +430,8 @@ impl ShedVi {
.history
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
} else if before == after && has_edit_verb {
self.writer.send_bell().ok(); // bell on no-op commands with a verb (e.g., 'x' on empty line)
}
self.writer.send_bell().ok(); // bell on no-op commands with a verb (e.g., 'x' on empty line)
}
let hint = self.history.get_hint();
self.editor.set_hint(hint);
@@ -462,21 +468,21 @@ impl ShedVi {
};
let entry = self.history.scroll(count);
if let Some(entry) = entry {
let editor = std::mem::take(&mut self.editor);
let editor = std::mem::take(&mut self.editor);
self.editor.set_buffer(entry.command().to_string());
if self.history.pending.is_none() {
self.history.pending = Some(editor);
}
self.editor.set_hint(None);
self.editor.move_cursor_to_end();
self.editor.move_cursor_to_end();
} else if let Some(pending) = self.history.pending.take() {
self.editor = pending;
} else {
// If we are here it should mean we are on our pending command
// And the user tried to scroll history down
// Since there is no "future" history, we should just bell and do nothing
self.writer.send_bell().ok();
}
// If we are here it should mean we are on our pending command
// And the user tried to scroll history down
// Since there is no "future" history, we should just bell and do nothing
self.writer.send_bell().ok();
}
}
pub fn should_accept_hint(&self, event: &KeyEvent) -> bool {
if self.editor.cursor_at_max() && self.editor.has_hint() {
@@ -512,7 +518,9 @@ impl ShedVi {
let line = self.editor.to_string();
let hint = self.editor.get_hint_text();
if crate::state::read_shopts(|s| s.prompt.highlight) {
self.highlighter.load_input(&line,self.editor.cursor_byte_pos());
self
.highlighter
.load_input(&line, self.editor.cursor_byte_pos());
self.highlighter.highlight();
let highlighted = self.highlighter.take();
format!("{highlighted}{hint}")
@@ -524,53 +532,84 @@ impl ShedVi {
pub fn print_line(&mut self, final_draw: bool) -> ShResult<()> {
let line = self.line_text();
let new_layout = self.get_layout(&line);
let pending_seq = self.mode.pending_seq();
let mut prompt_string_right = self.prompt.psr_expanded.clone();
let pending_seq = self.mode.pending_seq();
let mut prompt_string_right = self.prompt.psr_expanded.clone();
if prompt_string_right.as_ref().is_some_and(|psr| psr.lines().count() > 1) {
log::warn!("PSR has multiple lines, truncating to one line");
prompt_string_right = prompt_string_right.map(|psr| psr.lines().next().unwrap_or_default().to_string());
}
let row0_used = self.prompt
.get_ps1()
.lines()
.next()
.map(|l| Layout::calc_pos(self.writer.t_cols, l, Pos { col: 0, row: 0 }, 0))
.map(|p| p.col)
.unwrap_or_default() as usize;
let one_line = new_layout.end.row == 0;
if prompt_string_right
.as_ref()
.is_some_and(|psr| psr.lines().count() > 1)
{
log::warn!("PSR has multiple lines, truncating to one line");
prompt_string_right =
prompt_string_right.map(|psr| psr.lines().next().unwrap_or_default().to_string());
}
let row0_used = self
.prompt
.get_ps1()
.lines()
.next()
.map(|l| Layout::calc_pos(self.writer.t_cols, l, Pos { col: 0, row: 0 }, 0))
.map(|p| p.col)
.unwrap_or_default() as usize;
let one_line = new_layout.end.row == 0;
if let Some(layout) = self.old_layout.as_ref() {
self.writer.clear_rows(layout)?;
}
self.writer.redraw(self.prompt.get_ps1(), &line, &new_layout)?;
self
.writer
.redraw(self.prompt.get_ps1(), &line, &new_layout)?;
let seq_fits = pending_seq.as_ref().is_some_and(|seq| row0_used + 1 < self.writer.t_cols as usize - seq.width());
let psr_fits = prompt_string_right.as_ref().is_some_and(|psr| new_layout.end.col as usize + 1 < (self.writer.t_cols as usize).saturating_sub(psr.width()));
let seq_fits = pending_seq
.as_ref()
.is_some_and(|seq| row0_used + 1 < self.writer.t_cols as usize - seq.width());
let psr_fits = prompt_string_right.as_ref().is_some_and(|psr| {
new_layout.end.col as usize + 1 < (self.writer.t_cols as usize).saturating_sub(psr.width())
});
if !final_draw && let Some(seq) = pending_seq && !seq.is_empty() && !(prompt_string_right.is_some() && one_line) && seq_fits {
let to_col = self.writer.t_cols - calc_str_width(&seq);
let up = new_layout.cursor.row; // rows to move up from cursor to top line of prompt
if !final_draw
&& let Some(seq) = pending_seq
&& !seq.is_empty()
&& !(prompt_string_right.is_some() && one_line)
&& seq_fits
{
let to_col = self.writer.t_cols - calc_str_width(&seq);
let up = new_layout.cursor.row; // rows to move up from cursor to top line of prompt
let move_up = if up > 0 { format!("\x1b[{up}A") } else { String::new() };
let move_up = if up > 0 {
format!("\x1b[{up}A")
} else {
String::new()
};
// Save cursor, move up to top row, move right to column, write sequence, restore cursor
self.writer.flush_write(&format!("\x1b7{move_up}\x1b[{to_col}G{seq}\x1b8"))?;
} else if !final_draw && let Some(psr) = prompt_string_right && psr_fits {
let to_col = self.writer.t_cols - calc_str_width(&psr);
let down = new_layout.end.row - new_layout.cursor.row;
let move_down = if down > 0 { format!("\x1b[{down}B") } else { String::new() };
// Save cursor, move up to top row, move right to column, write sequence,
// restore cursor
self
.writer
.flush_write(&format!("\x1b7{move_up}\x1b[{to_col}G{seq}\x1b8"))?;
} else if !final_draw
&& let Some(psr) = prompt_string_right
&& psr_fits
{
let to_col = self.writer.t_cols - calc_str_width(&psr);
let down = new_layout.end.row - new_layout.cursor.row;
let move_down = if down > 0 {
format!("\x1b[{down}B")
} else {
String::new()
};
self.writer.flush_write(&format!("\x1b7{move_down}\x1b[{to_col}G{psr}\x1b8"))?;
}
self
.writer
.flush_write(&format!("\x1b7{move_down}\x1b[{to_col}G{psr}\x1b8"))?;
}
self.writer.flush_write(&self.mode.cursor_style())?;
self.old_layout = Some(new_layout);
self.needs_redraw = false;
self.needs_redraw = false;
Ok(())
}
@@ -853,19 +892,22 @@ pub fn get_insertions(input: &str) -> Vec<(usize, Marker)> {
/// - Unimplemented features (comments, brace groups)
pub fn marker_for(class: &TkRule) -> Option<Marker> {
match class {
TkRule::Pipe |
TkRule::ErrPipe |
TkRule::And |
TkRule::Or |
TkRule::Bg |
TkRule::BraceGrpStart |
TkRule::BraceGrpEnd => {
Some(markers::OPERATOR)
}
TkRule::Pipe
| TkRule::ErrPipe
| TkRule::And
| TkRule::Or
| TkRule::Bg
| TkRule::BraceGrpStart
| TkRule::BraceGrpEnd => Some(markers::OPERATOR),
TkRule::Sep => Some(markers::CMD_SEP),
TkRule::Redir => Some(markers::REDIRECT),
TkRule::Comment => Some(markers::COMMENT),
TkRule::Expanded { exp: _ } | TkRule::EOI | TkRule::SOI | TkRule::Null | TkRule::Str | TkRule::CasePattern => None,
TkRule::Expanded { exp: _ }
| TkRule::EOI
| TkRule::SOI
| TkRule::Null
| TkRule::Str
| TkRule::CasePattern => None,
}
}
@@ -880,9 +922,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
std::cmp::Ordering::Equal => {
let priority = |m: Marker| -> u8 {
match m {
markers::VISUAL_MODE_END
| markers::VISUAL_MODE_START
| markers::RESET => 0,
markers::VISUAL_MODE_END | markers::VISUAL_MODE_START | markers::RESET => 0,
markers::VAR_SUB
| markers::VAR_SUB_END
| markers::CMD_SUB
@@ -911,9 +951,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
std::cmp::Ordering::Equal => {
let priority = |m: Marker| -> u8 {
match m {
markers::VISUAL_MODE_END
| markers::VISUAL_MODE_START
| markers::RESET => 0,
markers::VISUAL_MODE_END | markers::VISUAL_MODE_START | markers::RESET => 0,
markers::VAR_SUB
| markers::VAR_SUB_END
| markers::CMD_SUB
@@ -926,7 +964,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
| markers::STRING_SQ_END
| markers::SUBSH_END => 2,
| markers::ARG => 3, // Lowest priority - processed first, overridden by sub-tokens
markers::ARG => 3, // Lowest priority - processed first, overridden by sub-tokens
_ => 1,
}
};
@@ -960,11 +998,11 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
insertions.push((token.span.start, markers::SUBSH));
return insertions;
} else if token.class == TkRule::CasePattern {
insertions.push((token.span.end, markers::RESET));
insertions.push((token.span.end - 1, markers::CASE_PAT));
insertions.push((token.span.start, markers::OPERATOR));
return insertions;
}
insertions.push((token.span.end, markers::RESET));
insertions.push((token.span.end - 1, markers::CASE_PAT));
insertions.push((token.span.start, markers::OPERATOR));
return insertions;
}
let token_raw = token.span.as_str();
let mut token_chars = token_raw.char_indices().peekable();
@@ -1144,17 +1182,17 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
}
}
'*' | '?' if (!in_dub_qt && !in_sng_qt) => {
let glob_ch = *ch;
let glob_ch = *ch;
token_chars.next(); // consume the first glob char
if !in_context(markers::COMMAND, &insertions) {
let offset = if glob_ch == '*' && token_chars.peek().is_some_and(|(_, c)| *c == '*') {
// it's one of these probably: ./dir/**/*.txt
token_chars.next(); // consume the second *
2
} else {
// just a regular glob char
1
};
let offset = if glob_ch == '*' && token_chars.peek().is_some_and(|(_, c)| *c == '*') {
// it's one of these probably: ./dir/**/*.txt
token_chars.next(); // consume the second *
2
} else {
// just a regular glob char
1
};
insertions.push((span_start + index + offset, markers::RESET));
insertions.push((span_start + index, markers::GLOB));
}

View File

@@ -101,35 +101,41 @@ pub fn get_win_size(fd: RawFd) -> (Col, Row) {
}
fn enumerate_lines(s: &str, left_pad: usize) -> String {
let total_lines = s.lines().count();
let max_num_len = total_lines.to_string().len();
s.lines()
.enumerate()
.fold(String::new(), |mut acc, (i, ln)| {
if i == 0 {
acc.push_str(ln);
acc.push('\n');
} else {
let num = (i + 1).to_string();
let num_pad = max_num_len - num.len();
// " 2 | " — num + padding + " | "
let prefix_len = max_num_len + 3; // "N | "
let trail_pad = left_pad.saturating_sub(prefix_len);
if i == total_lines - 1 {
// Don't add a newline to the last line
write!(acc, "\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}",
" ".repeat(num_pad),
" ".repeat(trail_pad),
).unwrap();
} else {
writeln!(acc, "\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}",
" ".repeat(num_pad),
" ".repeat(trail_pad),
).unwrap();
}
}
acc
})
let total_lines = s.lines().count();
let max_num_len = total_lines.to_string().len();
s.lines()
.enumerate()
.fold(String::new(), |mut acc, (i, ln)| {
if i == 0 {
acc.push_str(ln);
acc.push('\n');
} else {
let num = (i + 1).to_string();
let num_pad = max_num_len - num.len();
// " 2 | " — num + padding + " | "
let prefix_len = max_num_len + 3; // "N | "
let trail_pad = left_pad.saturating_sub(prefix_len);
if i == total_lines - 1 {
// Don't add a newline to the last line
write!(
acc,
"\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}",
" ".repeat(num_pad),
" ".repeat(trail_pad),
)
.unwrap();
} else {
writeln!(
acc,
"\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}",
" ".repeat(num_pad),
" ".repeat(trail_pad),
)
.unwrap();
}
}
acc
})
}
fn write_all(fd: RawFd, buf: &str) -> nix::Result<()> {
@@ -171,8 +177,8 @@ fn ends_with_newline(s: &str) -> bool {
}
pub fn calc_str_width(s: &str) -> u16 {
let mut esc_seq = 0;
s.graphemes(true).map(|g| width(g, &mut esc_seq)).sum()
let mut esc_seq = 0;
s.graphemes(true).map(|g| width(g, &mut esc_seq)).sum()
}
// Big credit to rustyline for this

View File

@@ -1,6 +1,6 @@
use bitflags::bitflags;
use super::register::{append_register, read_register, write_register, RegisterContent};
use super::register::{RegisterContent, 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
@@ -383,7 +383,12 @@ impl Motion {
pub fn is_linewise(&self) -> bool {
matches!(
self,
Self::WholeLineInclusive | Self::WholeLineExclusive | Self::LineUp | Self::LineDown | Self::ScreenLineDown | Self::ScreenLineUp
Self::WholeLineInclusive
| Self::WholeLineExclusive
| Self::LineUp
| Self::LineDown
| Self::ScreenLineDown
| Self::ScreenLineUp
)
}
}

View File

@@ -1020,15 +1020,13 @@ impl ViNormal {
impl ViMode for ViNormal {
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
let mut cmd = match key {
E(K::Char('V'), M::NONE) => {
Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::VisualModeLine)),
motion: None,
raw_seq: "".into(),
flags: self.flags(),
})
}
E(K::Char('V'), M::NONE) => Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::VisualModeLine)),
motion: None,
raw_seq: "".into(),
flags: self.flags(),
}),
E(K::Char(ch), M::NONE) => self.try_parse(ch),
E(K::Backspace, M::NONE) => Some(ViCmd {
register: Default::default(),
@@ -1405,8 +1403,8 @@ impl ViVisual {
break 'motion_parse Some(MotionCmd(count, Motion::WholeLineInclusive));
}
('c', Some(VerbCmd(_, Verb::Change))) => {
break 'motion_parse Some(MotionCmd(count, Motion::WholeLineExclusive));
}
break 'motion_parse Some(MotionCmd(count, Motion::WholeLineExclusive));
}
_ => {}
}
match ch {