mod banner;
pub(super) mod history;
mod meta_command;
mod pager_ui;
mod prompt;
mod read_console;
mod reprex;
mod shell;
pub(crate) mod state;
use crate::completion::completer::CombinedCompleter;
use crate::completion::menu::{FunctionAwareMenu, StateSyncHistoryMenu};
use crate::completion::shell::ShellCompleter;
use crate::config::{
AutoSuggestions, Config, ConfigStatus, EditorMode, ModeIndicatorPosition, RSourceStatus,
history_dir,
};
use crate::editor::hinter::RLanguageHinter;
use crate::editor::mode::new_editor_state_ref;
use crate::editor::prompt::PromptFormatter;
use crate::highlighter::{CombinedHighlighter, MetaCommandHighlighter};
use anyhow::Result;
use crossterm::{
ExecutableCommand,
style::Stylize,
terminal::{self, ClearType},
};
use nu_ansi_term::{Color, Style};
use reedline::{
DefaultHinter, Emacs, HistorySessionId, IdeMenu, ListMenu, MenuBuilder, Reedline, ReedlineMenu,
Signal, Vi, default_emacs_keybindings, default_vi_insert_keybindings,
default_vi_normal_keybindings,
};
use std::cell::RefCell;
use std::collections::HashMap;
use std::io;
use std::sync::atomic::{AtomicU16, Ordering};
use crate::editor::keybindings::{
add_auto_match_keybindings, add_common_keybindings, add_key_map_keybindings,
add_shell_semicolon_keybinding, wrap_edit_mode_with_conditional_rules,
};
use crate::editor::validator::RValidator;
use banner::{format_banner, format_override_line};
use history::setup_history;
use meta_command::{MetaCommandResult, process_meta_command};
use pager_ui::{run_pager_help_browser, run_pager_history_browser, with_ipc_alternate_guard};
use prompt::RPrompt;
use read_console::read_console_callback;
use reprex::{clear_input_lines, strip_reprex_output};
use shell::{execute_shell_command, restart_process};
use state::{PendingHistoryContext, PromptRuntimeConfig, ReplState};
thread_local! {
pub(super) static REPL_STATE: RefCell<Option<ReplState>> = const { RefCell::new(None) };
}
static LAST_TERMINAL_WIDTH: AtomicU16 = AtomicU16::new(0);
const MIN_R_WIDTH: u16 = 20;
const MAX_R_WIDTH: u16 = 10000;
fn sync_r_width() {
let prev = LAST_TERMINAL_WIDTH.load(Ordering::Relaxed);
let (cols, _) = match terminal::size() {
Ok(size) => size,
Err(e) => {
if prev != 0 {
log::debug!(
"Failed to read terminal size (transient); keeping previous width: {:?}",
e
);
return;
}
log::debug!(
"Failed to read terminal size; falling back to default width: {:?}",
e
);
(80, 24)
}
};
let clamped = cols.clamp(MIN_R_WIDTH, MAX_R_WIDTH);
if prev != clamped {
let code = format!("options(width = {})", clamped);
match arf_harp::eval_string_with_visibility(&code) {
Ok(_) => {
LAST_TERMINAL_WIDTH.store(clamped, Ordering::Relaxed);
}
Err(e) => log::debug!("Failed to set R width option: {:?}", e),
}
}
}
pub(crate) fn install_r_interrupt_handler() {
#[cfg(unix)]
{
use nix::sys::signal;
extern "C" fn handle_sigint(_signum: std::ffi::c_int) {
if !arf_libr::is_r_awaiting_console_input() {
arf_libr::set_r_interrupt_pending();
}
}
let action = signal::SigAction::new(
signal::SigHandler::Handler(handle_sigint),
signal::SaFlags::SA_RESTART,
signal::SigSet::empty(),
);
if let Err(e) = unsafe { signal::sigaction(signal::Signal::SIGINT, &action) } {
log::warn!("Could not set Ctrl+C handler: {e}");
}
}
#[cfg(windows)]
if let Err(e) = ctrlc::set_handler(|| {
if !arf_libr::is_r_awaiting_console_input() {
arf_libr::set_r_interrupt_pending();
}
}) {
log::warn!("Could not set Ctrl+C handler: {e}");
}
}
#[cfg(unix)]
pub(crate) fn restore_default_sigint_handler() {
use nix::sys::signal;
let action = signal::SigAction::new(
signal::SigHandler::SigDfl,
signal::SaFlags::empty(),
signal::SigSet::empty(),
);
if let Err(e) = unsafe { signal::sigaction(signal::Signal::SIGINT, &action) } {
log::warn!("Could not restore default Ctrl+C handler: {e}");
}
}
pub(crate) const ARF_PREFIX: &str = "# [arf]";
macro_rules! arf_println {
($($arg:tt)*) => {
println!("{} {}", $crate::repl::ARF_PREFIX, format_args!($($arg)*))
};
}
macro_rules! arf_eprintln {
($($arg:tt)*) => {
eprintln!("{} {}", $crate::repl::ARF_PREFIX, format_args!($($arg)*))
};
}
pub(crate) use arf_eprintln;
pub(crate) use arf_println;
pub struct Repl {
config: Config,
config_path: Option<std::path::PathBuf>,
config_status: ConfigStatus,
r_source_status: RSourceStatus,
r_home: Option<std::path::PathBuf>,
r_initialized: bool,
prompt_formatter: PromptFormatter,
session_id: Option<HistorySessionId>,
}
impl Repl {
pub fn new(
config: Config,
config_path: Option<std::path::PathBuf>,
config_status: ConfigStatus,
r_source_status: RSourceStatus,
r_home: Option<std::path::PathBuf>,
session_id: Option<HistorySessionId>,
) -> Result<Self> {
let r_initialized = arf_libr::r_library().is_ok();
let prompt_formatter = PromptFormatter::new();
if config.startup.mode.reprex {
arf_libr::set_reprex_mode(true, &config.mode.reprex.comment);
}
Ok(Repl {
config,
config_path,
config_status,
r_source_status,
r_home,
r_initialized,
prompt_formatter,
session_id,
})
}
fn history_session_id_raw(&self) -> Option<i64> {
self.session_id.map(i64::from)
}
fn r_history_path(&self) -> Option<std::path::PathBuf> {
if self.config.history.disabled {
return None;
}
let dir = self.config.history.dir.clone().or_else(history_dir);
dir.map(|d| d.join("r.db"))
}
fn shell_history_path(&self) -> Option<std::path::PathBuf> {
if self.config.history.disabled {
return None;
}
let dir = self.config.history.dir.clone().or_else(history_dir);
dir.map(|d| d.join("shell.db"))
}
fn create_r_hinter(&self) -> Option<Box<RLanguageHinter>> {
match self.config.editor.auto_suggestions {
AutoSuggestions::None => None,
AutoSuggestions::All => Some(Box::new(
RLanguageHinter::new().with_style(Style::new().italic().fg(Color::DarkGray)),
)),
AutoSuggestions::Cwd => Some(Box::new(
RLanguageHinter::new()
.with_style(Style::new().italic().fg(Color::DarkGray))
.with_cwd_aware(true),
)),
}
}
pub fn run(&mut self) -> Result<()> {
if self.config.startup.show_banner {
let banner = format_banner(
&self.config,
self.r_initialized,
self.r_source_status.override_info(),
);
if !self.r_initialized {
for line in banner.lines() {
if line.contains("R is not initialized") {
println!(
"# {}",
"R is not initialized. Commands will not be evaluated.".yellow()
);
} else {
println!("{}", line);
}
}
} else {
print!("{}", banner);
}
} else if self.r_initialized
&& let Some(info) = self.r_source_status.override_info()
{
eprintln!("{}", format_override_line(info));
}
if self.r_initialized {
self.run_with_r_mainloop()?;
} else {
self.run_standalone()?;
}
Ok(())
}
fn run_with_r_mainloop(&self) -> Result<()> {
let line_editor = Reedline::create().use_bracketed_paste(true);
let (mut line_editor, r_history_ok) =
setup_history(line_editor, self.r_history_path(), self.session_id);
let editor_state = new_editor_state_ref();
line_editor = match self.config.editor.mode {
EditorMode::Vi => {
let mut insert_keybindings = default_vi_insert_keybindings();
add_common_keybindings(&mut insert_keybindings);
if self.config.editor.auto_match {
add_auto_match_keybindings(&mut insert_keybindings);
}
if self.config.experimental.shell_semicolon_shortcut {
add_shell_semicolon_keybinding(&mut insert_keybindings);
}
add_key_map_keybindings(&mut insert_keybindings, &self.config.editor.key_map);
let vi = Vi::new(insert_keybindings, default_vi_normal_keybindings());
line_editor.with_edit_mode(wrap_edit_mode_with_conditional_rules(
vi,
editor_state.clone(),
self.config.editor.auto_match,
self.config.experimental.completion_min_chars,
self.config.experimental.shell_semicolon_shortcut,
))
}
EditorMode::Emacs => {
let mut keybindings = default_emacs_keybindings();
add_common_keybindings(&mut keybindings);
if self.config.editor.auto_match {
add_auto_match_keybindings(&mut keybindings);
}
if self.config.experimental.shell_semicolon_shortcut {
add_shell_semicolon_keybinding(&mut keybindings);
}
add_key_map_keybindings(&mut keybindings, &self.config.editor.key_map);
let emacs = Emacs::new(keybindings);
line_editor.with_edit_mode(wrap_edit_mode_with_conditional_rules(
emacs,
editor_state.clone(),
self.config.editor.auto_match,
self.config.experimental.completion_min_chars,
self.config.experimental.shell_semicolon_shortcut,
))
}
};
if self.config.completion.enabled {
let completer = Box::new(CombinedCompleter::with_settings_full(
self.config.completion.timeout_ms,
self.config.completion.debounce_ms,
self.config.completion.auto_paren_limit,
self.r_source_status.rig_enabled(),
self.config.experimental.r_completion.fuzzy,
self.config
.experimental
.r_completion
.package_functions
.clone(),
));
line_editor = line_editor.with_completer(completer);
let ide_menu = IdeMenu::default()
.with_name("completion_menu")
.with_max_completion_height(self.config.completion.max_height);
let completion_menu =
Box::new(FunctionAwareMenu::new(ide_menu).with_editor_state(editor_state.clone()));
line_editor = line_editor.with_menu(ReedlineMenu::EngineCompleter(completion_menu));
}
let (_, rows) = terminal::size().unwrap_or((80, 24));
let terminal_based_size = rows.saturating_sub(5) as usize;
let config_max_height = self.config.history.menu_max_height as usize;
let history_page_size = terminal_based_size.min(config_max_height).max(3);
let list_menu = ListMenu::default()
.with_name("history_menu")
.with_only_buffer_difference(false)
.with_page_size(history_page_size);
let history_menu =
Box::new(StateSyncHistoryMenu::new(list_menu).with_editor_state(editor_state.clone()));
line_editor = line_editor.with_menu(ReedlineMenu::HistoryMenu(history_menu));
line_editor = line_editor.with_validator(Box::new(
RValidator::new().with_editor_state(editor_state.clone()),
));
let highlighter = CombinedHighlighter::new(
self.config.colors.clone(),
self.config.editor.highlight_matching_bracket,
)
.with_editor_state(editor_state.clone());
line_editor = line_editor.with_highlighter(Box::new(highlighter));
if let Some(hinter) = self.create_r_hinter() {
line_editor = line_editor.with_hinter(hinter);
}
let auto_width = self.config.r.auto_width;
line_editor = line_editor
.with_break_signal(crate::ipc::break_signal())
.with_poll_interval(std::time::Duration::from_millis(33))
.with_idle_callback(Box::new(move || {
arf_libr::process_r_events();
if auto_width {
sync_r_width();
}
crate::ipc::poll_ipc_requests();
}));
let (shell_line_editor, shell_history_ok) = self.create_shell_line_editor();
if !r_history_ok && !shell_history_ok {
crate::ipc::clear_history_session_id();
}
let prompt_config = PromptRuntimeConfig::builder(
self.prompt_formatter.clone(),
self.config.prompt.format.clone(),
self.config.prompt.continuation.clone(),
self.config.prompt.shell_format.clone(),
)
.mode_indicator_position(self.config.prompt.mode_indicator)
.reprex(
self.config.startup.mode.reprex,
self.config.mode.reprex.comment.clone(),
)
.indicators(self.config.prompt.indicators.clone())
.autoformat(self.config.startup.mode.autoformat)
.main_color(self.config.colors.prompt.main)
.continuation_color(self.config.colors.prompt.continuation)
.shell_color(self.config.colors.prompt.shell)
.mode_indicator_color(self.config.colors.prompt.indicator)
.status(
self.config.prompt.status.clone(),
self.config.colors.prompt.status.clone(),
)
.duration(
self.config.experimental.prompt_duration.clone(),
self.config.colors.prompt.duration,
)
.spinner(self.config.experimental.prompt_spinner.clone())
.vi(
self.config.prompt.vi.clone(),
self.config.colors.prompt.vi.clone(),
)
.build();
let r_history_path = self.r_history_path();
let shell_history_path = self.shell_history_path();
REPL_STATE.with(|state| {
*state.borrow_mut() = Some(ReplState {
line_editor,
shell_line_editor,
prompt_config,
should_exit: false,
config_path: self.config_path.clone(),
config_status: self.config_status,
r_history_path,
shell_history_path,
r_source_status: self.r_source_status.clone(),
r_home: self.r_home.clone(),
forget_config: self.config.experimental.history_forget.clone(),
sponge_queue: state::SpongeQueue::new(),
dir_stack: Vec::new(),
history_session_id: if r_history_ok || shell_history_ok {
self.session_id
} else {
None
},
pending_history_context: PendingHistoryContext::None,
});
});
let error_handler_code = arf_libr::global_error_handler_code();
match arf_harp::eval_string_with_visibility(error_handler_code) {
Ok(_) => {
log::info!("Global error handler initialized");
arf_libr::mark_global_error_handler_initialized();
}
Err(e) => {
log::warn!("Failed to initialize global error handler: {:?}", e);
}
}
#[cfg(unix)]
{
let askpass_handler_code = arf_libr::askpass_handler_code();
match arf_harp::eval_string_with_visibility(askpass_handler_code) {
Ok(_) => {
log::info!("Askpass handler initialized");
}
Err(e) => {
log::warn!("Failed to initialize askpass handler: {:?}", e);
}
}
}
if self.config.r.auto_width {
sync_r_width();
}
arf_libr::set_read_console_callback(read_console_callback);
unsafe {
arf_libr::run_r_mainloop();
}
REPL_STATE.with(|state| {
if let Some(ref mut repl_state) = *state.borrow_mut()
&& repl_state.forget_config.enabled
&& !repl_state.sponge_queue.is_empty()
{
for id_to_delete in repl_state.sponge_queue.drain_failed_ids() {
let _ = repl_state.line_editor.history_mut().delete(id_to_delete);
}
let _ = repl_state.line_editor.sync_history();
}
});
REPL_STATE.with(|state| {
*state.borrow_mut() = None;
});
println!("\nGoodbye!");
Ok(())
}
fn run_standalone(&self) -> Result<()> {
let line_editor = Reedline::create().use_bracketed_paste(true);
let (mut line_editor, history_ok) =
setup_history(line_editor, self.r_history_path(), self.session_id);
if !history_ok {
crate::ipc::clear_history_session_id();
}
let history_session_id = if history_ok {
self.history_session_id_raw()
} else {
None
};
let editor_state = new_editor_state_ref();
line_editor = match self.config.editor.mode {
EditorMode::Vi => {
let mut insert_keybindings = default_vi_insert_keybindings();
add_common_keybindings(&mut insert_keybindings);
if self.config.editor.auto_match {
add_auto_match_keybindings(&mut insert_keybindings);
}
if self.config.experimental.shell_semicolon_shortcut {
add_shell_semicolon_keybinding(&mut insert_keybindings);
}
add_key_map_keybindings(&mut insert_keybindings, &self.config.editor.key_map);
let vi = Vi::new(insert_keybindings, default_vi_normal_keybindings());
line_editor.with_edit_mode(wrap_edit_mode_with_conditional_rules(
vi,
editor_state.clone(),
self.config.editor.auto_match,
self.config.experimental.completion_min_chars,
self.config.experimental.shell_semicolon_shortcut,
))
}
EditorMode::Emacs => {
let mut keybindings = default_emacs_keybindings();
add_common_keybindings(&mut keybindings);
if self.config.editor.auto_match {
add_auto_match_keybindings(&mut keybindings);
}
if self.config.experimental.shell_semicolon_shortcut {
add_shell_semicolon_keybinding(&mut keybindings);
}
add_key_map_keybindings(&mut keybindings, &self.config.editor.key_map);
let emacs = Emacs::new(keybindings);
line_editor.with_edit_mode(wrap_edit_mode_with_conditional_rules(
emacs,
editor_state.clone(),
self.config.editor.auto_match,
self.config.experimental.completion_min_chars,
self.config.experimental.shell_semicolon_shortcut,
))
}
};
if let Some(hinter) = self.create_r_hinter() {
line_editor = line_editor.with_hinter(hinter);
}
let mode_position = self.config.prompt.mode_indicator;
let mode_indicator =
if self.config.startup.mode.reprex && mode_position != ModeIndicatorPosition::None {
Some(self.config.prompt.indicators.reprex.clone())
} else {
None
};
let prompt = RPrompt::new(
self.prompt_formatter.format(&self.config.prompt.format),
self.prompt_formatter
.format(&self.config.prompt.continuation),
)
.with_mode_indicator(mode_indicator, mode_position)
.with_colors(
self.config.colors.prompt.main,
self.config.colors.prompt.continuation,
self.config.colors.prompt.indicator,
);
let mut prompt_config =
PromptRuntimeConfig::builder(self.prompt_formatter.clone(), "R > ", "+ ", "$ ")
.mode_indicator_position(ModeIndicatorPosition::None)
.main_color(self.config.colors.prompt.main)
.continuation_color(self.config.colors.prompt.continuation)
.shell_color(self.config.colors.prompt.shell)
.mode_indicator_color(self.config.colors.prompt.indicator)
.status(
self.config.prompt.status.clone(),
self.config.colors.prompt.status.clone(),
)
.duration(
self.config.experimental.prompt_duration.clone(),
self.config.colors.prompt.duration,
)
.spinner(self.config.experimental.prompt_spinner.clone())
.vi(
self.config.prompt.vi.clone(),
self.config.colors.prompt.vi.clone(),
)
.build();
let r_history_path = self.r_history_path();
let shell_history_path = self.shell_history_path();
let mut dir_stack: Vec<std::path::PathBuf> = Vec::new();
loop {
match line_editor.read_line(&prompt) {
Ok(Signal::Success(line)) => {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Some(result) = process_meta_command(
&line,
&mut prompt_config,
&r_history_path,
&shell_history_path,
&self.r_source_status,
&mut dir_stack,
history_session_id,
self.r_home.as_deref(),
) {
prompt_config.clear_command_duration();
let ctx = SessionInfoContext {
prompt_config: &prompt_config,
config_path: &self.config_path,
config_status: self.config_status,
r_history_path: &r_history_path,
shell_history_path: &shell_history_path,
r_source_status: &self.r_source_status,
};
match handle_meta_command_result(result, &ctx) {
MetaAction::Continue => continue,
MetaAction::Exit => {
println!("\nGoodbye!");
return Ok(());
}
}
}
println!("{}", format!("[R not initialized] {}", line).dark_grey());
}
Ok(Signal::CtrlC) => {
let _ = io::stdout().execute(terminal::Clear(ClearType::FromCursorDown));
println!("^C");
continue;
}
Ok(Signal::CtrlD) => {
let _ = io::stdout().execute(terminal::Clear(ClearType::FromCursorDown));
println!("\nGoodbye!");
break;
}
Ok(_) => {
continue;
}
Err(err) => {
eprintln!("Error: {}", err);
break;
}
}
}
Ok(())
}
fn create_shell_line_editor(&self) -> (Reedline, bool) {
let shell_editor = Reedline::create().use_bracketed_paste(true);
let (mut shell_editor, history_ok) =
setup_history(shell_editor, self.shell_history_path(), self.session_id);
shell_editor = match self.config.editor.mode {
EditorMode::Vi => {
let mut insert_keybindings = default_vi_insert_keybindings();
add_common_keybindings(&mut insert_keybindings);
add_key_map_keybindings(&mut insert_keybindings, &self.config.editor.key_map);
shell_editor.with_edit_mode(Box::new(Vi::new(
insert_keybindings,
default_vi_normal_keybindings(),
)))
}
EditorMode::Emacs => {
let mut keybindings = default_emacs_keybindings();
add_common_keybindings(&mut keybindings);
add_key_map_keybindings(&mut keybindings, &self.config.editor.key_map);
shell_editor.with_edit_mode(Box::new(Emacs::new(keybindings)))
}
};
if self.config.completion.enabled {
let completer = Box::new(ShellCompleter::new(
self.config.experimental.shell_completion.command_names,
));
shell_editor = shell_editor.with_completer(completer);
let completion_menu = Box::new(
IdeMenu::default()
.with_name("completion_menu")
.with_max_completion_height(self.config.completion.max_height),
);
shell_editor = shell_editor.with_menu(ReedlineMenu::EngineCompleter(completion_menu));
}
let (_, rows) = terminal::size().unwrap_or((80, 24));
let terminal_based_size = rows.saturating_sub(5) as usize;
let config_max_height = self.config.history.menu_max_height as usize;
let history_page_size = terminal_based_size.min(config_max_height).max(3);
let history_menu = Box::new(
ListMenu::default()
.with_name("history_menu")
.with_only_buffer_difference(false)
.with_page_size(history_page_size),
);
shell_editor = shell_editor.with_menu(ReedlineMenu::HistoryMenu(history_menu));
shell_editor = shell_editor.with_highlighter(Box::new(MetaCommandHighlighter::new(
self.config.colors.meta.clone(),
)));
if !matches!(self.config.editor.auto_suggestions, AutoSuggestions::None) {
let hinter =
DefaultHinter::default().with_style(Style::new().italic().fg(Color::DarkGray));
shell_editor = shell_editor.with_hinter(Box::new(hinter));
}
if !self.config.experimental.shell_abbreviations.is_empty() {
let abbrs: HashMap<String, String> = self
.config
.experimental
.shell_abbreviations
.clone()
.into_iter()
.collect();
shell_editor = shell_editor.with_abbreviations(abbrs);
}
shell_editor = shell_editor
.with_poll_interval(std::time::Duration::from_millis(33))
.with_idle_callback(Box::new(|| {
arf_libr::process_r_events();
}));
(shell_editor, history_ok)
}
}
enum MetaAction {
Continue,
Exit,
}
struct SessionInfoContext<'a> {
prompt_config: &'a PromptRuntimeConfig,
config_path: &'a Option<std::path::PathBuf>,
config_status: ConfigStatus,
r_history_path: &'a Option<std::path::PathBuf>,
shell_history_path: &'a Option<std::path::PathBuf>,
r_source_status: &'a RSourceStatus,
}
fn handle_meta_command_result(
result: MetaCommandResult,
ctx: &SessionInfoContext<'_>,
) -> MetaAction {
match result {
MetaCommandResult::Handled | MetaCommandResult::ShellExecuted => MetaAction::Continue,
MetaCommandResult::Exit => MetaAction::Exit,
MetaCommandResult::Unknown(cmd) => {
arf_println!(
"Unknown command: {}. Type :commands for available commands.",
cmd
);
MetaAction::Continue
}
MetaCommandResult::Restart(version) => {
restart_process(version.as_deref());
MetaAction::Continue
}
MetaCommandResult::ShowHelpBrowser(query) => {
run_pager_help_browser(&query);
MetaAction::Continue
}
MetaCommandResult::ShowSessionInfo => {
with_ipc_alternate_guard(|| {
crate::pager::display_session_info(
ctx.prompt_config,
ctx.config_path,
ctx.config_status,
ctx.r_history_path,
ctx.shell_history_path,
ctx.r_source_status,
);
});
MetaAction::Continue
}
MetaCommandResult::ShowChangelog => {
with_ipc_alternate_guard(crate::pager::display_changelog);
MetaAction::Continue
}
MetaCommandResult::ShowHistoryBrowser { path, mode } => {
run_pager_history_browser(&path, mode);
MetaAction::Continue
}
MetaCommandResult::ShowHistorySchema => {
if let Err(e) =
with_ipc_alternate_guard(crate::pager::history_schema::show_schema_pager)
{
arf_println!("Error: {}", e);
}
MetaAction::Continue
}
}
}