implemented ex mode :w/:e commands

implemented tab completion and history search for the ex mode prompt as well

fixed paths not expanding correctly in ex mode command arguments
This commit is contained in:
2026-03-16 18:15:01 -04:00
parent ec9795c781
commit 958dad9942
5 changed files with 140 additions and 48 deletions

View File

@@ -12,21 +12,15 @@ use super::vicmd::{
ViCmd, Word,
};
use crate::{
expand::expand_cmd_sub,
libsh::{error::ShResult, guards::var_ctx_guard},
parse::{
execute::exec_input,
lex::{LexFlags, LexStream, QuoteState, Tk, TkRule},
},
prelude::*,
readline::{
expand::expand_cmd_sub, libsh::{error::ShResult, guards::var_ctx_guard}, parse::{
Redir, RedirType, execute::exec_input, lex::{LexFlags, LexStream, QuoteState, Tk, TkFlags, TkRule}
}, prelude::*, procio::{IoFrame, IoMode, IoStack}, readline::{
history::History,
markers,
register::{RegisterContent, write_register},
term::RawModeGuard,
vicmd::ReadSrc,
},
state::{VarFlags, VarKind, read_shopts, write_meta, write_vars},
vicmd::{ReadSrc, WriteDest},
}, state::{VarFlags, VarKind, read_shopts, write_meta, write_vars}
};
const PUNCTUATION: [&str; 3] = ["?", "!", "."];
@@ -3358,8 +3352,54 @@ impl LineBuf {
self.cursor.add(grapheme_count);
}
},
Verb::Write(dest) => {}
Verb::Edit(path) => {}
Verb::Write(dest) => {
match dest {
WriteDest::FileAppend(ref path_buf) |
WriteDest::File(ref path_buf) => {
let Ok(mut file) = (if matches!(dest, WriteDest::File(_)) {
OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(path_buf)
} else {
OpenOptions::new()
.create(true)
.append(true)
.open(path_buf)
}) else {
write_meta(|m| {
m.post_system_message(format!("Failed to open file {}", path_buf.display()))
});
return Ok(());
};
if let Err(e) = file.write_all(self.as_str().as_bytes()) {
write_meta(|m| {
m.post_system_message(format!("Failed to write to file {}: {e}", path_buf.display()))
});
}
return Ok(());
}
WriteDest::Cmd(cmd) => {
let buf = self.as_str().to_string();
let io_mode = IoMode::Buffer {
tgt_fd: STDIN_FILENO,
buf,
flags: TkFlags::IS_HEREDOC | TkFlags::LIT_HEREDOC,
};
let redir = Redir::new(io_mode, RedirType::Input);
let mut frame = IoFrame::new();
frame.push(redir);
let mut stack = IoStack::new();
stack.push_frame(frame);
exec_input(cmd, Some(stack), false, Some("ex write".into()))?;
}
}
}
Verb::Edit(path) => {
let input = format!("$EDITOR {}",path.display());
exec_input(input, None, true, Some("ex edit".into()))?;
}
Verb::Normal(_) | Verb::Substitute(..) | Verb::RepeatSubstitute | Verb::RepeatGlobal => {}
}
Ok(())

View File

@@ -346,6 +346,18 @@ impl ShedVi {
self
}
/// A mutable reference to the currently focused editor
/// This includes the main LineBuf, and sub-editors for modes like Ex mode.
pub fn focused_editor(&mut self) -> &mut LineBuf {
self.mode.editor().unwrap_or(&mut self.editor)
}
/// A mutable reference to the currently focused history, if any.
/// This includes the main history struct, and history for sub-editors like Ex mode.
pub fn focused_history(&mut self) -> &mut History {
self.mode.history().unwrap_or(&mut self.history)
}
/// Feed raw bytes from stdin into the reader's buffer
pub fn feed_bytes(&mut self, bytes: &[u8]) {
self.reader.feed_bytes(bytes);
@@ -367,8 +379,8 @@ impl ShedVi {
self.completer.reset_stay_active();
self.needs_redraw = true;
Ok(())
} else if self.history.fuzzy_finder.is_active() {
self.history.fuzzy_finder.reset_stay_active();
} else if self.focused_history().fuzzy_finder.is_active() {
self.focused_history().fuzzy_finder.reset_stay_active();
self.needs_redraw = true;
Ok(())
} else {
@@ -457,20 +469,28 @@ impl ShedVi {
// Process all available keys
while let Some(key) = self.reader.read_key()? {
// If completer or history search are active, delegate input to it
if self.history.fuzzy_finder.is_active() {
if self.focused_history().fuzzy_finder.is_active() {
self.print_line(false)?;
match self.history.fuzzy_finder.handle_key(key)? {
match self.focused_history().fuzzy_finder.handle_key(key)? {
SelectorResponse::Accept(cmd) => {
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistorySelect));
self.editor.set_buffer(cmd.to_string());
self.editor.move_cursor_to_end();
{
let editor = self.focused_editor();
editor.set_buffer(cmd.to_string());
editor.move_cursor_to_end();
}
self
.history
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
self.editor.set_hint(None);
self.history.fuzzy_finder.clear(&mut self.writer)?;
self.history.fuzzy_finder.reset();
{
let mut writer = std::mem::take(&mut self.writer);
self.focused_history().fuzzy_finder.clear(&mut writer)?;
self.writer = writer;
}
self.focused_history().fuzzy_finder.reset();
with_vars([("_HIST_ENTRY".into(), cmd.clone())], || {
post_cmds.exec_with(&cmd);
@@ -493,7 +513,11 @@ impl ShedVi {
post_cmds.exec();
self.editor.set_hint(None);
self.history.fuzzy_finder.clear(&mut self.writer)?;
{
let mut writer = std::mem::take(&mut self.writer);
self.focused_history().fuzzy_finder.clear(&mut writer)?;
self.writer = writer;
}
write_vars(|v| {
v.set_var(
"SHED_VI_MODE",
@@ -520,8 +544,8 @@ impl ShedVi {
let span_start = self.completer.token_span().0;
let new_cursor = span_start + candidate.len();
let line = self.completer.get_completed_line(&candidate);
self.editor.set_buffer(line);
self.editor.cursor.set(new_cursor);
self.focused_editor().set_buffer(line);
self.focused_editor().cursor.set(new_cursor);
// Don't reset yet — clear() needs old_layout to erase the selector.
if !self.history.at_pending() {
@@ -650,7 +674,8 @@ impl ShedVi {
}
if let KeyEvent(KeyCode::Tab, mod_keys) = key {
if self.editor.attempt_history_expansion(&self.history) {
if self.mode.report_mode() != ModeReport::Ex
&& self.editor.attempt_history_expansion(&self.history) {
// If history expansion occurred, don't attempt completion yet
// allow the user to see the expanded command and accept or edit it before completing
return Ok(None);
@@ -660,8 +685,8 @@ impl ShedVi {
ModKeys::SHIFT => -1,
_ => 1,
};
let line = self.editor.as_str().to_string();
let cursor_pos = self.editor.cursor_byte_pos();
let line = self.focused_editor().as_str().to_string();
let cursor_pos = self.focused_editor().cursor_byte_pos();
match self.completer.complete(line, cursor_pos, direction) {
Err(e) => {
@@ -685,8 +710,8 @@ impl ShedVi {
.map(|c| c.len())
.unwrap_or_default();
self.editor.set_buffer(line.clone());
self.editor.cursor.set(new_cursor);
self.focused_editor().set_buffer(line.clone());
self.focused_editor().cursor.set(new_cursor);
if !self.history.at_pending() {
self.history.reset_to_pending();
@@ -748,18 +773,18 @@ impl ShedVi {
self.needs_redraw = true;
return Ok(None);
} else if let KeyEvent(KeyCode::Char('R'), ModKeys::CTRL) = key
&& self.mode.report_mode() == ModeReport::Insert
&& matches!(self.mode.report_mode(), ModeReport::Insert | ModeReport::Ex)
{
let initial = self.editor.as_str();
match self.history.start_search(initial) {
let initial = self.focused_editor().as_str().to_string();
match self.focused_history().start_search(&initial) {
Some(entry) => {
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistorySelect));
with_vars([("_HIST_ENTRY".into(), entry.clone())], || {
post_cmds.exec_with(&entry);
});
self.editor.set_buffer(entry);
self.editor.move_cursor_to_end();
self.focused_editor().set_buffer(entry);
self.focused_editor().move_cursor_to_end();
self
.history
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
@@ -767,9 +792,9 @@ impl ShedVi {
}
None => {
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistoryOpen));
let entries = self.history.fuzzy_finder.candidates();
let entries = self.focused_history().fuzzy_finder.candidates().to_vec();
let matches = self
.history
.focused_history()
.fuzzy_finder
.filtered()
.iter()
@@ -792,7 +817,7 @@ impl ShedVi {
},
);
if self.history.fuzzy_finder.is_active() {
if self.focused_history().fuzzy_finder.is_active() {
write_vars(|v| {
v.set_var(
"SHED_VI_MODE",
@@ -849,10 +874,10 @@ impl ShedVi {
}
if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) {
if self.editor.buffer.is_empty() {
if self.focused_editor().buffer.is_empty() {
return Ok(Some(ReadlineEvent::Eof));
} else {
self.editor = LineBuf::new();
*self.focused_editor() = LineBuf::new();
self.mode = Box::new(ViInsert::new());
self.needs_redraw = true;
return Ok(None);
@@ -1007,7 +1032,11 @@ impl ShedVi {
let one_line = new_layout.end.row == 0;
self.completer.clear(&mut self.writer)?;
self.history.fuzzy_finder.clear(&mut self.writer)?;
{
let mut writer = std::mem::take(&mut self.writer);
self.focused_history().fuzzy_finder.clear(&mut writer)?;
self.writer = writer;
}
if let Some(layout) = self.old_layout.as_ref() {
self.writer.clear_rows(layout)?;
@@ -1100,10 +1129,15 @@ impl ShedVi {
self.completer.draw(&mut self.writer)?;
self
.history
.focused_history()
.fuzzy_finder
.set_prompt_line_context(preceding_width, new_layout.cursor.col);
self.history.fuzzy_finder.draw(&mut self.writer)?;
{
let mut writer = std::mem::take(&mut self.writer);
self.focused_history().fuzzy_finder.draw(&mut writer)?;
self.writer = writer;
}
self.old_layout = Some(new_layout);
self.needs_redraw = false;

View File

@@ -893,6 +893,7 @@ impl Default for Layout {
}
}
#[derive(Clone, Debug, Default)]
pub struct TermWriter {
last_bell: Option<Instant>,
out: RawFd,

View File

@@ -7,6 +7,8 @@ use itertools::Itertools;
use crate::bitflags;
use crate::expand::{Expander, expand_raw};
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
use crate::parse::lex::TkFlags;
use crate::readline::complete::SimpleCompleter;
use crate::readline::history::History;
use crate::readline::keys::KeyEvent;
use crate::readline::linebuf::LineBuf;
@@ -152,6 +154,14 @@ impl ViMode for ViEx {
None
}
fn editor(&mut self) -> Option<&mut LineBuf> {
Some(&mut self.pending_cmd.buf)
}
fn history(&mut self) -> Option<&mut History> {
Some(&mut self.pending_cmd.history)
}
fn cursor_style(&self) -> String {
"\x1b[3 q".to_string()
}
@@ -328,8 +338,13 @@ fn parse_read(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<St
}
fn get_path(path: &str) -> Result<PathBuf, Option<String>> {
let expanded = expand_raw(&mut path.chars().peekable())
.map_err(|e| Some(format!("Error expanding path: {}", e)))?;
log::debug!("Expanding path: {}", path);
let expanded = Expander::from_raw(path, TkFlags::empty())
.map_err(|e| Some(format!("Error expanding path: {}", e)))?
.expand()
.map_err(|e| Some(format!("Error expanding path: {}", e)))?
.join(" ");
log::debug!("Expanded path: {}", expanded);
Ok(PathBuf::from(&expanded))
}

View File

@@ -3,7 +3,9 @@ use std::fmt::Display;
use unicode_segmentation::UnicodeSegmentation;
use crate::libsh::error::ShResult;
use crate::readline::history::History;
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use crate::readline::linebuf::LineBuf;
use crate::readline::vicmd::{Motion, MotionCmd, To, Verb, VerbCmd, ViCmd};
pub mod ex;
@@ -79,9 +81,9 @@ pub trait ViMode {
fn as_replay(&self) -> Option<CmdReplay>;
fn cursor_style(&self) -> String;
fn pending_seq(&self) -> Option<String>;
fn pending_cursor(&self) -> Option<usize> {
None
}
fn pending_cursor(&self) -> Option<usize> { None }
fn editor(&mut self) -> Option<&mut LineBuf> { None }
fn history(&mut self) -> Option<&mut History> { None }
fn move_cursor_on_undo(&self) -> bool;
fn clamp_cursor(&self) -> bool;
fn hist_scroll_start_pos(&self) -> Option<To>;