Implemented visual line mode

This commit is contained in:
2026-02-25 21:15:44 -05:00
parent e82f45f2ea
commit 37cf9625b3
6 changed files with 186 additions and 52 deletions

View File

@@ -321,7 +321,7 @@ pub struct LineBuf {
pub cursor: ClampedUsize, // Used to index grapheme_indices
pub select_mode: Option<SelectMode>,
pub select_range: Option<(usize, usize)>,
select_range: Option<(usize, usize)>,
pub last_selection: Option<(usize, usize)>,
pub insert_mode_start_pos: Option<usize>,
@@ -542,6 +542,9 @@ impl LineBuf {
}
pub fn slice_to(&mut self, end: usize) -> Option<&str> {
self.update_graphemes_lazy();
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(|| {
if end == self.grapheme_indices().len() {
Some(self.buffer.len())
@@ -559,6 +562,9 @@ impl LineBuf {
pub fn slice_to_cursor(&mut self) -> Option<&str> {
self.slice_to(self.cursor.get())
}
pub fn read_slice_to_cursor(&self) -> Option<&str> {
self.read_slice_to(self.cursor.get())
}
pub fn slice_to_cursor_inclusive(&mut self) -> Option<&str> {
self.slice_to(self.cursor.ret_add(1))
}
@@ -614,14 +620,31 @@ impl LineBuf {
self.update_graphemes();
}
pub fn select_range(&self) -> Option<(usize, usize)> {
self.select_range
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()));
self.select_mode = Some(mode);
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()));
}
pub fn stop_selecting(&mut self) {
self.select_mode = None;
@@ -629,12 +652,18 @@ impl LineBuf {
self.last_selection = self.select_range.take();
}
}
pub fn total_lines(&mut self) -> usize {
pub fn total_lines(&self) -> usize {
self.buffer.graphemes(true).filter(|g| *g == "\n").count()
}
pub fn cursor_line_number(&mut self) -> usize {
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
.slice_to_cursor()
.read_slice_to_cursor()
.map(|slice| slice.graphemes(true).filter(|g| *g == "\n").count())
.unwrap_or(0)
}
@@ -772,7 +801,7 @@ impl LineBuf {
Some((start, end))
}
pub fn line_bounds(&mut self, n: usize) -> (usize, usize) {
pub fn line_bounds(&self, n: usize) -> (usize, usize) {
if n > self.total_lines() {
panic!(
"Attempted to find line {n} when there are only {} lines",
@@ -2321,7 +2350,7 @@ impl LineBuf {
return;
};
match mode {
SelectMode::Char(anchor) => match anchor {
SelectMode::Line(anchor) | SelectMode::Char(anchor) => match anchor {
SelectAnchor::Start => {
start = self.cursor.get();
}
@@ -2329,14 +2358,6 @@ impl LineBuf {
end = self.cursor.get();
}
},
SelectMode::Line(anchor) => match anchor {
SelectAnchor::Start => {
start = self.start_of_line();
}
SelectAnchor::End => {
end = self.end_of_line();
}
}
SelectMode::Block(anchor) => todo!(),
}
if start >= end {
@@ -2381,7 +2402,7 @@ impl LineBuf {
}
}
MotionKind::Exclusive((start,end)) => {
if self.select_range().is_none() {
if self.select_range.is_none() {
self.cursor.set(start)
} else {
let end = end.saturating_sub(1);
@@ -2395,7 +2416,14 @@ impl LineBuf {
}
pub fn range_from_motion(&mut self, motion: &MotionKind) -> Option<(usize, usize)> {
let range = match motion {
MotionKind::On(pos) => ordered(self.cursor.get(), *pos),
MotionKind::On(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
@@ -2439,20 +2467,29 @@ impl LineBuf {
) -> ShResult<()> {
match verb {
Verb::Delete | Verb::Yank | Verb::Change => {
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");
return Ok(());
};
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() {
do_indent = read_shopts(|o| o.prompt.auto_indent);
}
let text = if verb == Verb::Yank {
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 {
let drained = self.drain(start..end);
self.update_graphemes();
@@ -2460,9 +2497,13 @@ 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');
}
RegisterContent::Line(text)
} else {
RegisterContent::Span(text)
@@ -2649,7 +2690,7 @@ impl LineBuf {
if content.is_empty() {
return Ok(());
}
if let Some(range) = self.select_range {
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
@@ -2688,7 +2729,7 @@ impl LineBuf {
}
}
Verb::SwapVisualAnchor => {
if let Some((start, end)) = self.select_range()
if let Some((start, end)) = self.select_range
&& let Some(mut mode) = self.select_mode
{
mode.invert_anchor();
@@ -2960,7 +3001,7 @@ impl LineBuf {
.map(|m| self.eval_motion(verb_ref.as_ref(), m))
.unwrap_or({
self
.select_range
.select_range()
.map(MotionKind::Inclusive)
.unwrap_or(MotionKind::Null)
})
@@ -3028,15 +3069,11 @@ impl LineBuf {
impl Display for LineBuf {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut full_buf = self.buffer.clone();
if let Some((start, end)) = self.select_range {
if let Some((start, end)) = self.select_range() {
let mode = self.select_mode.unwrap();
let start_byte = self.read_idx_byte_pos(start);
let end_byte = self.read_idx_byte_pos(end);
let end_byte = self.read_idx_byte_pos(end).min(full_buf.len());
if start_byte >= full_buf.len() || end_byte >= full_buf.len() {
log::warn!("Selection range '{:?}' is out of bounds for buffer of length {}, clearing selection", (start, end), full_buf.len());
return write!(f, "{}", full_buf);
}
match mode.anchor() {
SelectAnchor::Start => {
@@ -3044,11 +3081,13 @@ impl Display for LineBuf {
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);
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..end], markers::VISUAL_MODE_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());
full_buf.replace_range(start_byte..end_byte, &selected);
}
}

View File

@@ -561,7 +561,7 @@ impl ShedVi {
}
pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> {
let mut selecting = false;
let mut select_mode = None;
let mut is_insert_mode = false;
if cmd.is_mode_transition() {
let count = cmd.verb_count();
@@ -588,7 +588,11 @@ impl ShedVi {
return self.editor.exec_cmd(cmd);
}
Verb::VisualMode => {
selecting = true;
select_mode = Some(SelectMode::Char(SelectAnchor::End));
Box::new(ViVisual::new())
}
Verb::VisualModeLine => {
select_mode = Some(SelectMode::Line(SelectAnchor::End));
Box::new(ViVisual::new())
}
@@ -606,10 +610,8 @@ impl ShedVi {
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
self.editor.exec_cmd(cmd)?;
if selecting {
self
.editor
.start_selecting(SelectMode::Char(SelectAnchor::End));
if let Some(sel_mode) = select_mode {
self.editor.start_selecting(sel_mode);
} else {
self.editor.stop_selecting();
}

View File

@@ -117,12 +117,12 @@ fn enumerate_lines(s: &str, left_pad: usize) -> String {
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[90m{}{num} |\x1b[0m {}{ln}",
write!(acc, "\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}",
" ".repeat(num_pad),
" ".repeat(trail_pad),
).unwrap();
} else {
writeln!(acc, "\x1b[90m{}{num} |\x1b[0m {}{ln}",
writeln!(acc, "\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}",
" ".repeat(num_pad),
" ".repeat(trail_pad),
).unwrap();

View File

@@ -182,6 +182,7 @@ impl ViCmd {
| Verb::NormalMode
| Verb::VisualModeSelectLast
| Verb::VisualMode
| Verb::VisualModeLine
| Verb::ReplaceMode
)
})

View File

@@ -1018,6 +1018,15 @@ 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(ch), M::NONE) => self.try_parse(ch),
E(K::Backspace, M::NONE) => Some(ViCmd {
register: Default::default(),
@@ -1041,15 +1050,6 @@ impl ViMode for ViNormal {
self.clear_cmd();
None
}
E(K::Char('V'), M::SHIFT) => {
Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::VisualModeLine)),
motion: None,
raw_seq: "".into(),
flags: self.flags() | CmdFlags::VISUAL_LINE,
})
}
_ => {
if let Some(cmd) = common_cmds(key) {
self.clear_cmd();