diff --git a/src/readline/linebuf.rs b/src/readline/linebuf.rs index 8252f68..92f962b 100644 --- a/src/readline/linebuf.rs +++ b/src/readline/linebuf.rs @@ -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(()) diff --git a/src/readline/mod.rs b/src/readline/mod.rs index 30b0c52..a8030ae 100644 --- a/src/readline/mod.rs +++ b/src/readline/mod.rs @@ -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(); + self.editor.set_hint(None); + { + 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; diff --git a/src/readline/term.rs b/src/readline/term.rs index d11ae19..fea81d2 100644 --- a/src/readline/term.rs +++ b/src/readline/term.rs @@ -893,6 +893,7 @@ impl Default for Layout { } } +#[derive(Clone, Debug, Default)] pub struct TermWriter { last_bell: Option, out: RawFd, diff --git a/src/readline/vimode/ex.rs b/src/readline/vimode/ex.rs index 56618d0..faf6c3f 100644 --- a/src/readline/vimode/ex.rs +++ b/src/readline/vimode/ex.rs @@ -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>) -> Result, Option Result> { - 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)) } diff --git a/src/readline/vimode/mod.rs b/src/readline/vimode/mod.rs index 35c64a0..13bf8eb 100644 --- a/src/readline/vimode/mod.rs +++ b/src/readline/vimode/mod.rs @@ -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; fn cursor_style(&self) -> String; fn pending_seq(&self) -> Option; - fn pending_cursor(&self) -> Option { - None - } + fn pending_cursor(&self) -> Option { 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;