reimplemented visual mode and text objects

This commit is contained in:
2026-03-19 17:12:22 -04:00
parent 406fd57b5a
commit 22113cbdfd
6 changed files with 1271 additions and 922 deletions

View File

@@ -1368,13 +1368,12 @@ pub fn unescape_str(raw: &str) -> String {
} }
} }
'$' => { '$' => {
if chars.peek() == Some(&'$') if chars.peek() == Some(&'$') || chars.peek().is_none_or(|ch| ch.is_whitespace()) {
|| chars.peek().is_none_or(|ch| ch.is_whitespace()) {
chars.next(); chars.next();
result.push('$'); result.push('$');
} else { } else {
result.push(markers::VAR_SUB); result.push(markers::VAR_SUB);
} }
} }
'`' => { '`' => {
result.push(markers::VAR_SUB); result.push(markers::VAR_SUB);

View File

@@ -311,7 +311,9 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
let mut exec_if_timeout = None; let mut exec_if_timeout = None;
let timeout = if readline.pending_keymap.is_empty() { let timeout = if readline.pending_keymap.is_empty() {
let screensaver_cmd = read_shopts(|o| o.prompt.screensaver_cmd.clone()); let screensaver_cmd = read_shopts(|o| o.prompt.screensaver_cmd.clone())
.trim()
.to_string();
let screensaver_idle_time = read_shopts(|o| o.prompt.screensaver_idle_time); let screensaver_idle_time = read_shopts(|o| o.prompt.screensaver_idle_time);
if screensaver_idle_time > 0 && !screensaver_cmd.is_empty() { if screensaver_idle_time > 0 && !screensaver_cmd.is_empty() {
exec_if_timeout = Some(screensaver_cmd); exec_if_timeout = Some(screensaver_cmd);

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
use history::History; use history::History;
use keys::{KeyCode, KeyEvent, ModKeys}; use keys::{KeyCode, KeyEvent, ModKeys};
use linebuf::{LineBuf, SelectAnchor, SelectMode}; use linebuf::{LineBuf, SelectMode};
use std::fmt::Write; use std::fmt::Write;
use term::{KeyReader, Layout, LineWriter, PollReader, TermWriter, get_win_size}; use term::{KeyReader, Layout, LineWriter, PollReader, TermWriter, get_win_size};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
@@ -605,6 +605,14 @@ impl ShedVi {
} }
CompResponse::Passthrough => { /* fall through to normal handling below */ } CompResponse::Passthrough => { /* fall through to normal handling below */ }
} }
} else if self.mode.pending_seq().is_some_and(|seq| !seq.is_empty()) {
// Vi mode is waiting for more input (e.g. after 'f', 'd', etc.)
// Bypass keymap matching and send directly to the mode handler
if let Some(event) = self.handle_key(key)? {
return Ok(event);
}
self.needs_redraw = true;
continue;
} else { } else {
let keymap_flags = self.curr_keymap_flags(); let keymap_flags = self.curr_keymap_flags();
self.pending_keymap.push(key.clone()); self.pending_keymap.push(key.clone());
@@ -634,6 +642,11 @@ impl ShedVi {
self.needs_redraw = true; self.needs_redraw = true;
continue; continue;
} else { } else {
log::debug!(
"Ambiguous key sequence: {:?}, matches: {:?}",
self.pending_keymap,
matches
);
// There is ambiguity. Allow the timeout in the main loop to handle this. // There is ambiguity. Allow the timeout in the main loop to handle this.
continue; continue;
} }
@@ -961,8 +974,8 @@ impl ShedVi {
// Since there is no "future" history, we should just bell and do nothing // Since there is no "future" history, we should just bell and do nothing
self.writer.send_bell().ok(); self.writer.send_bell().ok();
} }
self.editor.set_cursor_clamp(self.mode.clamp_cursor()); self.editor.set_cursor_clamp(self.mode.clamp_cursor());
self.editor.fix_cursor(); self.editor.fix_cursor();
} }
pub fn should_accept_hint(&self, event: &KeyEvent) -> bool { pub fn should_accept_hint(&self, event: &KeyEvent) -> bool {
if self.editor.cursor_at_max() && self.editor.has_hint() { if self.editor.cursor_at_max() && self.editor.has_hint() {
@@ -1173,7 +1186,7 @@ impl ShedVi {
post_mode_change.exec(); post_mode_change.exec();
} }
fn exec_mode_transition(&mut self, cmd: ViCmd, from_replay: bool) -> ShResult<()> { fn exec_mode_transition(&mut self, mut cmd: ViCmd, from_replay: bool) -> ShResult<()> {
let mut is_insert_mode = false; let mut is_insert_mode = false;
let count = cmd.verb_count(); let count = cmd.verb_count();
@@ -1255,11 +1268,22 @@ impl ShedVi {
self.repeat_action = mode.as_replay(); self.repeat_action = mode.as_replay();
} }
if let Some(range) = self.editor.select_range() {
cmd.motion = Some(MotionCmd(1, range))
} else {
log::warn!("You're in visual mode with no select range??");
};
// Set cursor clamp BEFORE executing the command so that motions // Set cursor clamp BEFORE executing the command so that motions
// (like EndOfLine for 'A') can reach positions valid in the new mode // (like EndOfLine for 'A') can reach positions valid in the new mode
log::debug!("cmd: {:?}", cmd);
self.editor.set_cursor_clamp(self.mode.clamp_cursor()); self.editor.set_cursor_clamp(self.mode.clamp_cursor());
self.editor.exec_cmd(cmd)?; self.editor.exec_cmd(cmd)?;
if mode.report_mode() == ModeReport::Visual && self.editor.select_range().is_some() {
self.editor.stop_selecting();
}
if is_insert_mode { if is_insert_mode {
self.editor.mark_insert_mode_start_pos(); self.editor.mark_insert_mode_start_pos();
} else { } else {
@@ -1401,7 +1425,7 @@ impl ShedVi {
// The motion is assigned in the line buffer execution, so we also have to // 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 // assign it here in order to be able to repeat it
if let Some(range) = self.editor.select_range() { if let Some(range) = self.editor.select_range() {
cmd.motion = Some(MotionCmd(1, Motion::Range(range.0, range.1))) cmd.motion = Some(MotionCmd(1, range))
} else { } else {
log::warn!("You're in visual mode with no select range??"); log::warn!("You're in visual mode with no select range??");
}; };

View File

@@ -45,7 +45,7 @@ pub fn append_register(ch: Option<char>, buf: RegisterContent) {
pub enum RegisterContent { pub enum RegisterContent {
Span(Vec<Line>), Span(Vec<Line>),
Line(Vec<Line>), Line(Vec<Line>),
Block(Vec<Line>), Block(Vec<Line>),
#[default] #[default]
Empty, Empty,
} }
@@ -53,11 +53,16 @@ pub enum RegisterContent {
impl Display for RegisterContent { impl Display for RegisterContent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::Block(s) | Self::Block(s) | Self::Line(s) | Self::Span(s) => {
Self::Line(s) | write!(
Self::Span(s) => { f,
write!(f, "{}", s.iter().map(|l| l.to_string()).collect::<Vec<_>>().join("\n")) "{}",
} s.iter()
.map(|l| l.to_string())
.collect::<Vec<_>>()
.join("\n")
)
}
Self::Empty => write!(f, ""), Self::Empty => write!(f, ""),
} }
} }
@@ -65,13 +70,11 @@ impl Display for RegisterContent {
impl RegisterContent { impl RegisterContent {
pub fn clear(&mut self) { pub fn clear(&mut self) {
*self = Self::Empty *self = Self::Empty
} }
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
match self { match self {
Self::Span(s) | Self::Span(s) | Self::Line(s) | Self::Block(s) => s.len(),
Self::Line(s) |
Self::Block(s) => s.len(),
Self::Empty => 0, Self::Empty => 0,
} }
} }
@@ -79,13 +82,13 @@ impl RegisterContent {
match self { match self {
Self::Span(s) => s.is_empty(), Self::Span(s) => s.is_empty(),
Self::Line(s) => s.is_empty(), Self::Line(s) => s.is_empty(),
Self::Block(s) => s.is_empty(), Self::Block(s) => s.is_empty(),
Self::Empty => true, Self::Empty => true,
} }
} }
pub fn is_block(&self) -> bool { pub fn is_block(&self) -> bool {
matches!(self, Self::Block(_)) matches!(self, Self::Block(_))
} }
pub fn is_line(&self) -> bool { pub fn is_line(&self) -> bool {
matches!(self, Self::Line(_)) matches!(self, Self::Line(_))
} }
@@ -250,13 +253,13 @@ impl Register {
pub fn append(&mut self, mut buf: RegisterContent) { pub fn append(&mut self, mut buf: RegisterContent) {
match buf { match buf {
RegisterContent::Empty => {} RegisterContent::Empty => {}
RegisterContent::Span(ref mut s) | RegisterContent::Span(ref mut s)
RegisterContent::Block(ref mut s) | | RegisterContent::Block(ref mut s)
RegisterContent::Line(ref mut s) => match &mut self.content { | RegisterContent::Line(ref mut s) => match &mut self.content {
RegisterContent::Empty => self.content = buf, RegisterContent::Empty => self.content = buf,
RegisterContent::Span(existing) | RegisterContent::Span(existing)
RegisterContent::Line(existing) | | RegisterContent::Line(existing)
RegisterContent::Block(existing) => existing.append(s), | RegisterContent::Block(existing) => existing.append(s),
}, },
} }
} }

View File

@@ -2,7 +2,10 @@ use std::path::PathBuf;
use bitflags::bitflags; use bitflags::bitflags;
use crate::readline::{linebuf::Grapheme, vimode::ex::SubFlags}; use crate::readline::{
linebuf::{Grapheme, Pos},
vimode::ex::SubFlags,
};
use super::register::{RegisterContent, append_register, read_register, write_register}; use super::register::{RegisterContent, append_register, read_register, write_register};
@@ -335,7 +338,9 @@ pub enum Motion {
ToBrace(Direction), ToBrace(Direction),
ToBracket(Direction), ToBracket(Direction),
ToParen(Direction), ToParen(Direction),
Range(usize, usize), CharRange(Pos, Pos),
LineRange(usize, usize),
BlockRange(Pos, Pos),
RepeatMotion, RepeatMotion,
RepeatMotionRev, RepeatMotionRev,
Null, Null,
@@ -374,7 +379,7 @@ impl Motion {
| Self::ToBrace(_) | Self::ToBrace(_)
| Self::ToBracket(_) | Self::ToBracket(_)
| Self::ToParen(_) | Self::ToParen(_)
| Self::Range(_, _) | Self::CharRange(_, _)
) )
} }
pub fn is_linewise(&self) -> bool { pub fn is_linewise(&self) -> bool {