mod banner;
mod editor_setup;
pub(super) mod history;
mod history_runtime;
#[cfg(test)]
mod history_runtime_tests;
mod meta_command;
mod pager_ui;
mod prompt;
mod r_mainloop;
mod read_console;
pub(crate) mod reprex;
mod shell;
mod standalone;
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, ConfigFileInfo, EditorMode, FormatterBackend, HelpViewer,
ModeIndicatorPosition, RSourceStatus, ReprexMode, ResolvedHistoryLocation,
};
use crate::editor::hinter::RLanguageHinter;
use crate::editor::mode::new_editor_state_ref;
use crate::editor::prompt::PromptFormatter;
use crate::highlighter::{CombinedHighlighter, MetaCommandHighlighter};
use crate::history::HistoryRuntime;
use anyhow::Result;
use crossterm::{
ExecutableCommand,
style::Stylize,
terminal::{self, ClearType},
};
use nu_ansi_term::{Color, Style};
use reedline::{
AutoPairs, DefaultHinter, Emacs, HistorySessionId, IdeMenu, ListMenu, MenuBuilder, Reedline,
ReedlineMenu, Signal, Vi, default_emacs_keybindings, default_vi_insert_keybindings,
default_vi_normal_keybindings, default_vi_visual_keybindings,
};
use std::cell::RefCell;
use std::collections::HashMap;
use std::io;
use std::sync::atomic::{AtomicU16, Ordering};
use crate::editor::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::finalize_history;
#[cfg(test)]
#[allow(unused_imports)]
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::ReprexRuntime;
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,
history_location: ResolvedHistoryLocation,
formatter_backend: Option<FormatterBackend>,
config_file_info: ConfigFileInfo,
r_source_status: RSourceStatus,
r_home: Option<std::path::PathBuf>,
r_initialized: bool,
prompt_formatter: PromptFormatter,
session_id: Option<HistorySessionId>,
prepared_r_history: Option<HistoryRuntime>,
prepared_shell_history: Option<HistoryRuntime>,
}
impl Repl {
pub fn new(
config: Config,
config_file_info: ConfigFileInfo,
r_source_status: RSourceStatus,
r_home: Option<std::path::PathBuf>,
session_id: Option<HistorySessionId>,
) -> Result<Self> {
let history_location = crate::config::resolved_history_location(&config.history.mode);
let formatter_backend =
crate::external::formatter::resolve_formatter(config.reprex.formatter);
let r_initialized = arf_libr::r_library().is_ok();
let prompt_formatter = PromptFormatter::new();
if config.startup.reprex != ReprexMode::Off {
arf_libr::set_reprex_mode(true, &config.reprex.comment);
}
Ok(Repl {
config,
history_location,
formatter_backend,
config_file_info,
r_source_status,
r_home,
r_initialized,
prompt_formatter,
session_id,
prepared_r_history: None,
prepared_shell_history: None,
})
}
pub(crate) fn r_home_for_ipc(&self) -> Option<String> {
self.r_home.as_ref().map(|path| path.display().to_string())
}
pub fn run(&mut self) -> Result<()> {
self.prepare_history();
if self.config.startup.show_banner {
let banner = format_banner(
&self.config,
self.r_initialized,
self.r_source_status.override_info(),
self.formatter_backend,
);
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(())
}
}
enum MetaAction {
Continue,
Exit,
}
struct SessionInfoContext<'a> {
prompt_config: &'a PromptRuntimeConfig,
reprex: &'a ReprexRuntime,
config_file_info: &'a ConfigFileInfo,
history_location: &'a ResolvedHistoryLocation,
r_history: &'a HistoryRuntime,
shell_history: &'a HistoryRuntime,
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.reprex,
ctx.config_file_info,
ctx.r_history,
ctx.shell_history,
ctx.r_source_status,
);
});
MetaAction::Continue
}
MetaCommandResult::ShowChangelog => {
with_ipc_alternate_guard(crate::pager::display_changelog);
MetaAction::Continue
}
MetaCommandResult::ShowHistoryBrowser { store, mode } => {
run_pager_history_browser(&store, mode);
MetaAction::Continue
}
MetaCommandResult::ClearHistory { stores } => {
let mut cleared_count = 0i64;
for (name, store) in stores {
match store.count_all() {
Ok(count) if count > 0 => match store.clear() {
Ok(()) => cleared_count += count,
Err(error) => arf_println!("Failed to clear {} history: {}", name, error),
},
Ok(_) => {}
Err(error) => arf_println!("Failed to read {} history: {}", name, error),
}
}
arf_println!("Cleared {} history entries.", cleared_count);
MetaAction::Continue
}
MetaCommandResult::ShowHistorySchema => {
if let Err(e) = with_ipc_alternate_guard(|| {
crate::pager::history_schema::show_schema_pager(ctx.history_location)
}) {
arf_println!("Error: {}", e);
}
MetaAction::Continue
}
}
}