use super::{
history_search::{smartcase_flags, ReaderHistorySearch, SearchMode},
iothreads::{self, Debouncers},
word_motion::{MoveWordDir, MoveWordStateMachine, MoveWordStyle},
};
use crate::{
abbrs::{self, abbrs_match},
ast::{self, is_same_node, Kind},
builtins::shared::{ErrorCode, STATUS_CMD_ERROR, STATUS_CMD_OK},
common::{get_program_name, shell_modes},
complete::{
complete, complete_load, sort_and_prioritize, CompleteFlags, Completion, CompletionList,
CompletionRequestOptions,
},
editable_line::{line_at_cursor, range_of_line_at_cursor, Edit, EditableLine},
env::{EnvMode, EnvStack, Environment, Statuses},
env_dispatch::{handle_emoji_width, MIDNIGHT_COMMANDER_SID},
event,
exec::exec_subshell,
expand::{expand_one, expand_string, expand_tilde, ExpandFlags, ExpandResultCode},
fd_readable_set::poll_fd_readable,
fds::{make_fd_blocking, wopen_cloexec},
flog::{flog, flogf},
function,
global_safety::RelaxedAtomicBool,
highlight::{
autosuggest_validate_from_history, highlight_shell, parse_text_face_for_highlight,
HighlightRole, HighlightSpec,
},
history::{
history_session_id, in_private_mode, History, HistorySearch, PersistenceMode,
SearchDirection, SearchFlags, SearchType,
},
input_common::{
stop_query, BackgroundColorQuery, CharEvent, CharInputStyle, CursorPositionQuery,
CursorPositionQueryReason, ImplicitEvent, InputData, InputEventQueue,
InputEventQueuer as _, QueryResponse, QueryResultEvent, ReadlineCmd, RecurrentQuery,
TerminalQuery, LONG_READ_TIMEOUT,
},
io::IoChain,
key::ViewportPosition,
kill::{kill_add, kill_replace, kill_yank, kill_yank_rotate},
nix::isatty,
operation_context::{get_bg_context, OperationContext},
pager::{PageRendering, Pager, SelectionMotion},
panic::AT_EXIT,
parse_constants::{ParseIssue, ParseTreeFlags, SourceRange},
parse_util::{
compute_indents, contains_wildcards, detect_parse_errors, escape_wildcards,
get_cmdsubst_extent, get_line_from_offset, get_offset, get_offset_from_line,
get_process_extent, get_process_first_token_offset, get_token_extent, lineno,
locate_cmdsubst_range, MaybeParentheses, SPACES_PER_INDENT,
},
parser::{BlockType, EvalRes, Parser, ParserEnvSetMode},
portable_atomic::AtomicU64,
prelude::*,
proc::{
hup_jobs, is_interactive_session, job_reap, jobs_requiring_warning_on_exit,
print_exit_warning_for_jobs, proc_update_jiffies, HAVE_PROC_STAT,
},
reader::word_motion::bigword_class,
screen::{is_dumb, screen_force_clear_to_end, CharOffset, Screen},
should_flog,
signal::{
signal_check_cancel, signal_clear_cancel, signal_reset_handlers, signal_set_handlers,
signal_set_handlers_once,
},
terminal::{
BufferedOutputter, Outputter,
TerminalCommand::{
self, ClearScreen, DecrstAlternateScreenBuffer, DecsetAlternateScreenBuffer,
DecsetShowCursor, Osc0WindowTitle, Osc133CommandFinished, Osc133CommandStart,
Osc1TabTitle, QueryBackgroundColor, QueryCursorPosition,
QueryKittyKeyboardProgressiveEnhancements, QueryPrimaryDeviceAttribute, QueryXtgettcap,
QueryXtversion,
},
},
termsize::{signal_safe_termsize_invalidate_tty, termsize_last, termsize_update},
text_face::{parse_text_face, TextFace},
threads::{assert_is_background_thread, assert_is_main_thread},
tokenizer::{
quote_end, tok_command, variable_assignment_equals_pos, TokenType, Tokenizer,
TOK_ACCEPT_UNFINISHED, TOK_SHOW_COMMENTS,
},
tty_handoff::{
deactivate_tty_protocols, get_tty_protocols_active, initialize_tty_protocols, TtyHandoff,
SCROLL_CONTENT_UP_TERMINFO_CODE, XTGETTCAP_QUERY_OS_NAME,
},
wildcard::wildcard_has,
wutil::{fstat, perror_nix, wstat},
};
use assert_matches::assert_matches;
use errno::{errno, Errno};
use fish_common::{
escape, escape_string, escape_string_with_quote, exit_without_destructors,
get_obfuscation_read_char, help_section, restore_term_foreground_process_group_for_exit,
write_loop, EscapeFlags, EscapeStringStyle, ScopeGuard, ScopeGuarding,
};
use fish_fallback::{fish_wcwidth, lowercase};
use fish_feature_flags::FeatureFlag;
use fish_util::{perror, write_to_fd};
use fish_wcstringutil::{
count_preceding_backslashes, is_prefix, join_strings, string_prefixes_string,
string_prefixes_string_case_insensitive, string_prefixes_string_maybe_case_insensitive,
CaseSensitivity, IsPrefix, StringFuzzyMatch,
};
use fish_widestring::{bytes2wcstring, ELLIPSIS_CHAR, UTF8_BOM_WCHAR};
use libc::{
c_char, _POSIX_VDISABLE, EIO, EISDIR, ENOTTY, ESRCH, O_NONBLOCK, O_RDONLY, SIGINT,
STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO, VMIN, VQUIT, VSUSP, VTIME,
};
use nix::{
fcntl::OFlag,
sys::{
signal::{killpg, Signal},
stat::Mode,
termios::{self, tcgetattr, tcsetattr, SetArg, Termios},
},
unistd::{getpgrp, getpid, setpgid},
};
use std::{
borrow::Cow,
cell::UnsafeCell,
cmp,
io::BufReader,
num::NonZeroUsize,
ops::{ControlFlow, Range},
os::fd::{AsRawFd as _, BorrowedFd, FromRawFd as _, OwnedFd, RawFd},
pin::Pin,
sync::{
atomic::{AtomicI32, AtomicU32, AtomicU8, Ordering},
Arc, LazyLock, Mutex, MutexGuard, OnceLock,
},
time::{Duration, Instant},
};
#[repr(u8)]
enum ExitState {
None,
RunningHandlers,
FinishedHandlers,
}
static EXIT_STATE: AtomicU8 = AtomicU8::new(ExitState::None as u8);
fn zeroed_termios() -> Termios {
let termios: libc::termios = unsafe { std::mem::zeroed() };
termios.into()
}
pub static SHELL_MODES: LazyLock<Mutex<Termios>> = LazyLock::new(|| Mutex::new(zeroed_termios()));
static TERMINAL_MODE_ON_STARTUP: OnceLock<libc::termios> = OnceLock::new();
static TTY_MODES_FOR_EXTERNAL_CMDS: LazyLock<Mutex<Termios>> =
LazyLock::new(|| Mutex::new(zeroed_termios()));
static RUN_COUNT: AtomicU64 = AtomicU64::new(0);
static STATUS_COUNT: AtomicU64 = AtomicU64::new(0);
static INTERRUPTED: AtomicI32 = AtomicI32::new(0);
static EXIT_SIGNAL: AtomicI32 = AtomicI32::new(0);
pub fn get_terminal_mode_on_startup() -> Option<&'static libc::termios> {
TERMINAL_MODE_ON_STARTUP.get()
}
fn commandline_state_snapshot() -> MutexGuard<'static, CommandlineState> {
static STATE: Mutex<CommandlineState> = Mutex::new(CommandlineState::new());
STATE.lock().unwrap()
}
static GENERATION: AtomicU32 = AtomicU32::new(0);
fn redirect_tty_after_sighup() {
use std::fs::OpenOptions;
static TTY_REDIRECTED: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
if TTY_REDIRECTED.swap(true) {
return;
}
let Ok(devnull) = OpenOptions::new().read(true).write(true).open("/dev/null") else {
return;
};
let fd = devnull.as_raw_fd();
for stdfd in [STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO] {
if matches!(
tcgetattr(unsafe { BorrowedFd::borrow_raw(stdfd) }),
Err(nix::Error::EIO | nix::Error::ENOTTY)
) {
unsafe { libc::dup2(fd, stdfd) };
}
}
}
fn querying_allowed(vars: &dyn Environment) -> bool {
fish_feature_flags::feature_test(FeatureFlag::QueryTerm)
&& !is_dumb()
&& {
vars.get(MIDNIGHT_COMMANDER_SID).is_none()
}
&& {
isatty(STDOUT_FILENO)
}
}
pub struct TerminalInitResult {
pub input_queue: InputEventQueue,
pub background_color: Option<xterm_color::Color>,
}
pub fn terminal_init(vars: &dyn Environment, inputfd: RawFd) -> TerminalInitResult {
assert!(isatty(inputfd));
reader_interactive_init();
let mut input_queue = InputEventQueue::new(inputfd, Some(LONG_READ_TIMEOUT));
let mut background_color = None;
let _init_tty_metadata = ScopeGuard::new((), |()| {
initialize_tty_protocols(vars);
});
if !querying_allowed(vars) {
return TerminalInitResult {
input_queue,
background_color,
};
}
set_shell_modes(inputfd, "initial query");
{
let mut out = BufferedOutputter::new(Outputter::stdoutput());
out.write_command(QueryKittyKeyboardProgressiveEnhancements);
out.write_command(QueryXtversion);
out.write_command(QueryBackgroundColor);
query_capabilities_via_dcs(&mut out, vars);
out.write_command(QueryPrimaryDeviceAttribute);
}
input_queue.blocking_query().replace(TerminalQuery::Initial);
while !check_exit_loop_maybe_warning(None) {
use CharEvent::{Command, Implicit, Key, Readline};
use ImplicitEvent::{CheckExit, Eof};
use QueryResultEvent::*;
match input_queue.readch() {
Implicit(Eof) => signal_safe_reader_set_exit_signal(libc::SIGHUP),
Implicit(CheckExit) => {}
CharEvent::QueryResult(Response(QueryResponse::PrimaryDeviceAttribute)) => {
break;
}
CharEvent::QueryResult(Response(QueryResponse::BackgroundColor(bg))) => {
if background_color.is_none() {
background_color = Some(bg);
}
}
CharEvent::QueryResult(Response(QueryResponse::CursorPosition(_))) => (),
CharEvent::QueryResult(Timeout) => {
let program = get_program_name();
flog!(
warning,
wgettext_fmt!(
"%s could not read response to Primary Device Attribute query after waiting for %d seconds. \
This is often due to a missing feature in your terminal. \
See 'help %s' or 'man fish-terminal-compatibility'. \
This %s process will no longer wait for outstanding queries, \
which disables some optional features.",
program,
LONG_READ_TIMEOUT.as_secs(),
help_section!("terminal-compatibility"),
program
),
);
input_queue
.get_input_data_mut()
.blocking_query_timeout
.replace(Duration::from_millis(30));
break;
}
CharEvent::QueryResult(Interrupted) => break,
Key(_) | Readline(_) | Command(_) | Implicit(_) => panic!(),
}
}
stop_query(input_queue.blocking_query());
let input_data = input_queue.get_input_data();
assert!(input_data.input_function_args.is_empty());
assert!(input_data.event_storage.is_empty());
flogf!(
reader,
"Returning %u pending input events",
input_data.queue.len()
);
TerminalInitResult {
input_queue,
background_color,
}
}
fn reader_data_stack() -> &'static mut Vec<Pin<Box<ReaderData>>> {
struct ReaderDataStack(UnsafeCell<Vec<Pin<Box<ReaderData>>>>);
unsafe impl Sync for ReaderDataStack {}
static READER_DATA_STACK: ReaderDataStack = ReaderDataStack(UnsafeCell::new(vec![]));
assert_is_main_thread();
unsafe { &mut *READER_DATA_STACK.0.get() }
}
pub fn reader_in_interactive_read() -> bool {
reader_data_stack()
.iter()
.rev()
.any(|reader| reader.conf.exit_on_interrupt)
}
pub fn current_data() -> Option<&'static mut ReaderData> {
reader_data_stack()
.last_mut()
.map(|data| unsafe { Pin::get_unchecked_mut(Pin::as_mut(data)) })
}
pub use current_data as reader_current_data;
use fish_widestring::word_char::{is_blank, WordCharClass};
pub fn reader_push<'a>(parser: &'a Parser, history_name: &wstr, conf: ReaderConfig) -> Reader<'a> {
assert_is_main_thread();
let inputfd = conf.inputfd;
let input_data = if !parser.interactive_initialized.swap(true) {
let TerminalInitResult {
mut input_queue,
background_color,
} = terminal_init(parser.vars(), inputfd);
let input_data = input_queue.get_input_data_mut();
handle_emoji_width(parser.vars());
parser.libdata_mut().status_vars.command = L!("fish").to_owned();
parser.set_one(
L!("_"),
ParserEnvSetMode::new(EnvMode::GLOBAL),
L!("fish").to_owned(),
);
let old = parser
.blocking_query_timeout
.replace(input_data.blocking_query_timeout);
assert!(old.is_none());
parser.set_color_theme(background_color.as_ref());
std::mem::take(input_data)
} else {
InputData::new(inputfd, *parser.blocking_query_timeout.borrow())
};
let hist = History::with_name(history_name);
hist.resolve_pending();
let data = ReaderData::new(input_data, hist, conf, reader_data_stack().is_empty());
reader_data_stack().push(data);
let data = current_data().unwrap();
data.command_line_changed(EditableLineTag::Commandline, AutosuggestionUpdate::Remove);
Reader { data, parser }
}
pub fn reader_pop() {
assert_is_main_thread();
reader_data_stack().pop().unwrap();
if let Some(new_reader) = current_data() {
new_reader
.screen
.reset_abandoning_line(Some(termsize_last().width()));
} else {
Outputter::stdoutput().borrow_mut().reset_text_face();
*commandline_state_snapshot() = CommandlineState::new();
}
}
pub fn fake_scoped_reader<'a>(parser: &'a Parser) -> impl ScopeGuarding<Target = Reader<'a>> + 'a {
let inputfd = -1;
let conf = ReaderConfig {
inputfd,
..Default::default()
};
let hist = History::with_name(L!(""));
let input_data = InputData::new(inputfd, None);
let data = ReaderData::new(input_data, hist, conf, reader_data_stack().is_empty());
reader_data_stack().push(data);
let data = current_data().unwrap();
let reader = Reader { data, parser };
ScopeGuard::new(reader, |_reader| {
reader_data_stack().pop().unwrap();
})
}
#[derive(Default)]
pub struct ReaderConfig {
pub left_prompt_cmd: WString,
pub right_prompt_cmd: WString,
pub event: &'static wstr,
pub complete_ok: bool,
pub highlight_ok: bool,
pub syntax_check_ok: bool,
pub autosuggest_ok: bool,
pub transient_prompt: bool,
pub expand_abbrev_ok: bool,
pub exit_on_interrupt: bool,
pub read_prompt_str_is_empty: bool,
pub in_silent_mode: bool,
pub inputfd: RawFd,
}
#[derive(Clone, Default)]
pub struct CommandlineState {
pub text: WString,
pub cursor_pos: usize,
pub selection: Option<Range<usize>>,
pub history: Option<Arc<History>>,
pub pager_mode: bool,
pub pager_fully_disclosed: bool,
pub search_field: Option<(WString, usize)>,
pub search_mode: bool,
}
impl CommandlineState {
const fn new() -> Self {
Self {
text: WString::new(),
cursor_pos: 0,
selection: None,
history: None,
pager_mode: false,
pager_fully_disclosed: false,
search_field: None,
search_mode: false,
}
}
}
#[derive(Eq, PartialEq)]
pub enum CursorSelectionMode {
Exclusive,
Inclusive,
}
#[derive(Eq, PartialEq)]
pub enum CursorEndMode {
Exclusive,
Inclusive,
}
enum Kill {
Append,
Prepend,
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub enum JumpDirection {
Forward,
Backward,
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub enum JumpPrecision {
Till,
To,
}
#[derive(Copy, Clone, Eq, PartialEq)]
enum CompletionAction {
ShownAmbiguous,
InsertedUnique,
}
struct ReadlineLoopState {
last_cmd: Option<ReadlineCmd>,
yank_len: usize,
completion_action: Option<CompletionAction>,
finished: bool,
nchars: Option<NonZeroUsize>,
}
impl ReadlineLoopState {
fn new() -> Self {
Self {
last_cmd: None,
yank_len: 0,
completion_action: None,
finished: false,
nchars: None,
}
}
}
#[derive(Clone, Copy, Default, Eq, PartialEq)]
struct SelectionData {
begin: usize,
start: usize,
stop: usize,
}
#[derive(Clone, Default)]
struct LayoutData {
text: WString,
colors: Vec<HighlightSpec>,
position: usize,
pager_search_field_position: Option<usize>,
selection: Option<SelectionData>,
autosuggestion: WString,
history_search_range: Option<SourceRange>,
left_prompt_buff: WString,
mode_prompt_buff: WString,
right_prompt_buff: WString,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum EditableLineTag {
Commandline,
SearchField,
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum TransientEdit {
Pager,
HistorySearch,
}
pub struct ReaderData {
conf: ReaderConfig,
command_line: EditableLine,
command_line_transient_edit: Option<TransientEdit>,
rendered_layout: LayoutData,
autosuggestion: Autosuggestion,
saved_autosuggestion: Option<Autosuggestion>,
pager: Pager,
current_page_rendering: PageRendering,
suppress_autosuggestion: bool,
reset_loop_state: bool,
first_prompt: bool,
last_flash: Option<Instant>,
flash_autosuggestion: bool,
screen: Screen,
pub input_data: InputData,
queued_repaint: bool,
history: Arc<History>,
history_search: ReaderHistorySearch,
history_pager: Option<Range<usize>>,
cursor_selection_mode: CursorSelectionMode,
cursor_end_mode: CursorEndMode,
selection: Option<SelectionData>,
left_prompt_buff: WString,
mode_prompt_buff: WString,
right_prompt_buff: WString,
cycle_command_line: WString,
cycle_cursor_pos: usize,
exit_loop_requested: bool,
did_warn_for_bg_jobs: bool,
kill_item: WString,
force_exec_prompt_and_repaint: bool,
last_jump_target: Option<char>,
last_jump_direction: JumpDirection,
last_jump_precision: JumpPrecision,
in_flight_highlight_request: WString,
in_flight_autosuggest_request: WString,
rls: Option<ReadlineLoopState>,
pub(super) debouncers: Debouncers,
}
pub struct Reader<'a> {
pub data: &'a mut ReaderData,
pub parser: &'a Parser,
}
impl<'a> std::ops::Deref for Reader<'a> {
type Target = ReaderData;
fn deref(&self) -> &ReaderData {
self.data
}
}
impl<'a> std::ops::DerefMut for Reader<'a> {
fn deref_mut(&mut self) -> &mut ReaderData {
self.data
}
}
impl<'a> Reader<'a> {
fn vars(&self) -> &dyn Environment {
self.parser.vars()
}
pub(super) fn service_debounced_results(&mut self) {
if let Some(r) = self.debouncers.autosuggestions.take_result() {
self.autosuggest_completed(r);
}
if let Some(r) = self.debouncers.highlight.take_result() {
self.highlight_completed(r);
}
if let Some(cb) = self.debouncers.history_pager.take_result() {
cb(self);
}
}
}
pub fn reader_read(parser: &Parser, fd: RawFd, io: &IoChain) -> Result<(), ErrorCode> {
let interactive = (fd == STDIN_FILENO) && isatty(STDIN_FILENO);
let _interactive_push = parser.push_scope(|s| s.is_interactive = interactive);
signal_set_handlers_once(interactive);
let res = if interactive {
read_i(parser);
Ok(())
} else {
read_ni(parser, fd, io)
};
parser.libdata_mut().exit_current_script = false;
res
}
fn read_i(parser: &Parser) {
assert_is_main_thread();
let mut conf = ReaderConfig {
event: L!("fish_prompt"),
complete_ok: true,
highlight_ok: true,
syntax_check_ok: true,
expand_abbrev_ok: true,
autosuggest_ok: check_bool_var(parser.vars(), L!("fish_autosuggestion_enabled"), true),
transient_prompt: check_bool_var(parser.vars(), L!("fish_transient_prompt"), false),
..Default::default()
};
if parser.is_breakpoint() && function::exists(DEBUG_PROMPT_FUNCTION_NAME, parser) {
conf.left_prompt_cmd = DEBUG_PROMPT_FUNCTION_NAME.to_owned();
conf.right_prompt_cmd.clear();
} else {
conf.left_prompt_cmd = LEFT_PROMPT_FUNCTION_NAME.to_owned();
conf.right_prompt_cmd = RIGHT_PROMPT_FUNCTION_NAME.to_owned();
}
let mut data = reader_push(parser, &history_session_id(parser.vars()), conf);
data.import_history_if_necessary();
let mut tty = TtyHandoff::new(reader_save_screen_state);
while !check_exit_loop_maybe_warning(Some(&mut data)) {
RUN_COUNT.fetch_add(1, Ordering::Relaxed);
let Some(command) = data.readline(set_shell_modes_temporarily(data.conf.inputfd), None)
else {
continue;
};
if command.is_empty() {
continue;
}
tty.disable_tty_protocols();
data.clear(EditableLineTag::Commandline);
data.update_buff_pos(EditableLineTag::Commandline, None);
BufferedOutputter::new(Outputter::stdoutput()).write_command(Osc133CommandStart(&command));
event::fire_generic(parser, L!("fish_preexec").to_owned(), vec![command.clone()]);
let eval_res = reader_run_command(parser, &command);
signal_clear_cancel();
if !eval_res.no_status {
STATUS_COUNT.fetch_add(1, Ordering::Relaxed);
}
data.exit_loop_requested |= parser.libdata().exit_current_script;
parser.libdata_mut().exit_current_script = false;
BufferedOutputter::new(Outputter::stdoutput()).write_command(Osc133CommandFinished {
exit_status: parser.get_last_status(),
});
event::fire_generic(parser, L!("fish_postexec").to_owned(), vec![command]);
data.history.resolve_pending();
data.screen.write_command(DecsetShowCursor);
let already_warned = data.did_warn_for_bg_jobs;
if check_exit_loop_maybe_warning(Some(&mut data)) {
break;
}
if already_warned {
data.did_warn_for_bg_jobs = false;
}
}
reader_pop();
if reader_exit_signal() == libc::SIGHUP {
redirect_tty_after_sighup();
}
if reader_data_stack().is_empty() {
EXIT_STATE.store(ExitState::RunningHandlers as u8, Ordering::Relaxed);
event::fire_generic(parser, L!("fish_exit").to_owned(), vec![]);
EXIT_STATE.store(ExitState::FinishedHandlers as u8, Ordering::Relaxed);
hup_jobs(&parser.jobs());
}
}
fn read_ni(parser: &Parser, fd: RawFd, io: &IoChain) -> Result<(), ErrorCode> {
let md = match fstat(fd) {
Ok(md) => md,
Err(err) => {
flog!(
error,
wgettext_fmt!("Unable to read input file: %s", err.to_string())
);
return Err(STATUS_CMD_ERROR);
}
};
if fd != STDIN_FILENO && md.is_dir() {
flog!(
error,
wgettext_fmt!("Unable to read input file: %s", Errno(EISDIR).to_string())
);
return Err(STATUS_CMD_ERROR);
}
let mut fd_contents = Vec::with_capacity(usize::try_from(md.len()).unwrap());
loop {
let mut buff = [0_u8; 4096];
match nix::unistd::read(unsafe { BorrowedFd::borrow_raw(fd) }, &mut buff) {
Ok(0) => {
break;
}
Ok(amt) => {
fd_contents.extend_from_slice(&buff[..amt]);
}
Err(err) => {
if err == nix::Error::EINTR {
continue;
} else if (err == nix::Error::EAGAIN || err == nix::Error::EWOULDBLOCK)
&& make_fd_blocking(fd).is_ok()
{
continue;
} else {
flog!(
error,
wgettext_fmt!("Unable to read input file: %s", err.to_string())
);
return Err(STATUS_CMD_ERROR);
}
}
}
}
let mut s = bytes2wcstring(&fd_contents);
drop(fd_contents);
if s.chars().next() == Some(UTF8_BOM_WCHAR) {
s.remove(0);
}
match parser.eval_wstr(s, io, None, BlockType::top) {
Ok(_) => Ok(()),
Err(msg) => {
eprintf!("%s", msg);
Err(STATUS_CMD_ERROR)
}
}
}
const FLOW_CONTROL_FLAGS: termios::InputFlags = {
use termios::InputFlags;
InputFlags::IXON.union(InputFlags::IXOFF)
};
pub fn reader_init(will_restore_foreground_pgroup: bool) {
let terminal_mode_on_startup = match tcgetattr(unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) })
{
Ok(modes) => {
TERMINAL_MODE_ON_STARTUP.get_or_init(|| libc::termios::from(modes.clone()));
modes
}
Err(_) => zeroed_termios(),
};
if !cfg!(test) {
assert!(AT_EXIT.get().is_none());
}
AT_EXIT.get_or_init(|| Box::new(move || reader_deinit(will_restore_foreground_pgroup)));
let mut external_modes = terminal_mode_on_startup;
term_fix_external_modes(&mut external_modes);
external_modes.input_flags &= !FLOW_CONTROL_FLAGS;
{
let mut shell_modes = shell_modes();
*shell_modes = external_modes.clone();
term_fix_shell_modes(&mut shell_modes);
}
*TTY_MODES_FOR_EXTERNAL_CMDS.lock().unwrap() = external_modes;
if is_interactive_session() && getpgrp().as_raw() == unsafe { libc::tcgetpgrp(STDIN_FILENO) } {
term_donate( true);
}
}
pub fn reader_deinit(restore_foreground_pgroup: bool) {
restore_term_mode();
deactivate_tty_protocols();
if restore_foreground_pgroup {
restore_term_foreground_process_group_for_exit();
}
}
pub fn restore_term_mode() {
if !is_interactive_session() || getpgrp().as_raw() != unsafe { libc::tcgetpgrp(STDIN_FILENO) } {
return;
}
if let Some(modes) = get_terminal_mode_on_startup() {
unsafe { libc::tcsetattr(STDIN_FILENO, libc::TCSANOW, modes) };
}
}
pub fn reader_change_history(name: &wstr) {
let Some(data) = current_data() else {
return;
};
data.history.save();
data.history = History::with_name(name);
commandline_state_snapshot().history = Some(data.history.clone());
}
pub fn reader_change_cursor_selection_mode(selection_mode: CursorSelectionMode) {
if let Some(data) = current_data() {
if data.cursor_selection_mode == selection_mode {
return;
}
let invalidates_selection = data.selection.is_some();
data.cursor_selection_mode = selection_mode;
if invalidates_selection {
data.update_buff_pos(EditableLineTag::Commandline, None);
}
}
}
pub fn reader_change_cursor_end_mode(end_mode: CursorEndMode) {
if let Some(data) = current_data() {
if data.cursor_end_mode == end_mode {
return;
}
let invalidates_end = data.selection.is_some();
data.cursor_end_mode = end_mode;
if invalidates_end {
data.update_buff_pos(EditableLineTag::Commandline, None);
}
}
}
fn check_bool_var(vars: &dyn Environment, name: &wstr, default: bool) -> bool {
vars.get(name)
.map(|v| v.as_string())
.map_or(default, |v| v != L!("0"))
}
pub fn reader_set_autosuggestion_enabled(vars: &dyn Environment) {
if let Some(data) = current_data() {
let enable = check_bool_var(vars, L!("fish_autosuggestion_enabled"), true);
if data.conf.autosuggest_ok != enable {
data.conf.autosuggest_ok = enable;
data.schedule_prompt_repaint();
}
}
}
pub fn reader_set_transient_prompt(vars: &dyn Environment) {
if let Some(data) = current_data() {
data.conf.transient_prompt = check_bool_var(vars, L!("fish_transient_prompt"), false);
}
}
pub fn reader_schedule_prompt_repaint() {
assert_is_main_thread();
let Some(data) = current_data() else {
return;
};
data.schedule_prompt_repaint();
}
pub fn reader_update_termsize(parser: &Parser) {
let last = termsize_last();
let new = termsize_update(parser);
if new.height() == last.height() {
return;
}
let Some(data) = current_data() else {
return;
};
let mut data = Reader { parser, data };
data.push_front(CharEvent::Implicit(ImplicitEvent::NewWindowHeight));
}
pub fn reader_execute_readline_cmd(parser: &Parser, ch: CharEvent) {
if parser.scope().readonly_commandline {
return;
}
let Some(data) = current_data() else {
return;
};
let mut data = Reader { parser, data };
let CharEvent::Readline(readline_cmd_evt) = &ch else {
panic!()
};
if matches!(
readline_cmd_evt.cmd,
ReadlineCmd::ClearScreenAndRepaint
| ReadlineCmd::RepaintMode
| ReadlineCmd::Repaint
| ReadlineCmd::ForceRepaint
) {
data.queued_repaint = true;
}
if data.queued_repaint {
data.input_data.queue_char(ch);
return;
}
if data.rls.is_none() {
data.rls = Some(ReadlineLoopState::new());
}
data.save_screen_state();
let _ = data.handle_char_event(Some(ch));
}
pub fn reader_jump(direction: JumpDirection, precision: JumpPrecision, target: char) -> bool {
let Some(data) = current_data() else {
return false;
};
data.save_screen_state();
let elt = data.active_edit_line_tag();
data.jump_and_remember_last_jump(direction, precision, elt, target, false)
}
pub fn reader_showing_suggestion(parser: &Parser) -> bool {
if !is_interactive_session() {
return false;
}
if let Some(data) = current_data() {
let reader = Reader { parser, data };
let suggestion = &reader.autosuggestion.text;
let is_single_space = suggestion.ends_with(L!(" "))
&& line_at_cursor(reader.command_line.text(), reader.command_line.position())
== suggestion[..suggestion.len() - 1];
!suggestion.is_empty() && !is_single_space
} else {
false
}
}
pub fn reader_reading_interrupted(data: &mut ReaderData) -> i32 {
let res = reader_test_and_clear_interrupted();
if res == 0 {
return 0;
}
if data.conf.exit_on_interrupt {
data.exit_loop_requested = true;
return 0;
}
res
}
pub fn reader_readline(
parser: &Parser,
old_modes: Option<Termios>,
nchars: Option<NonZeroUsize>,
) -> Option<WString> {
let data = current_data().unwrap();
let mut reader = Reader { parser, data };
reader.readline(old_modes, nchars)
}
pub fn commandline_get_state(sync: bool) -> CommandlineState {
if sync {
current_data().map(|data| data.update_commandline_state());
}
commandline_state_snapshot().clone()
}
pub fn commandline_set_buffer(parser: &Parser, text: Option<WString>, cursor_pos: Option<usize>) {
if parser.scope().readonly_commandline {
return;
}
{
let mut state = commandline_state_snapshot();
if let Some(text) = text {
state.text = text;
}
state.cursor_pos = cmp::min(cursor_pos.unwrap_or(usize::MAX), state.text.len());
}
current_data().map(|data| data.apply_commandline_state_changes());
}
pub fn commandline_set_search_field(parser: &Parser, text: WString, cursor_pos: Option<usize>) {
if parser.scope().readonly_commandline {
return;
}
{
let mut state = commandline_state_snapshot();
assert!(state.search_field.is_some());
let new_pos = cmp::min(cursor_pos.unwrap_or(usize::MAX), text.len());
state.search_field = Some((text, new_pos));
}
current_data().map(|data| data.apply_commandline_state_changes());
}
pub fn reader_run_count() -> u64 {
RUN_COUNT.load(Ordering::Relaxed)
}
pub fn reader_status_count() -> u64 {
STATUS_COUNT.load(Ordering::Relaxed)
}
const ENV_CMD_DURATION: &wstr = L!("CMD_DURATION");
const PREFIX_MAX_LEN: usize = 9;
const DEFAULT_PROMPT: &wstr = L!("echo -n \"$USER@$hostname $PWD \"'> '");
const LEFT_PROMPT_FUNCTION_NAME: &wstr = L!("fish_prompt");
const RIGHT_PROMPT_FUNCTION_NAME: &wstr = L!("fish_right_prompt");
const DEBUG_PROMPT_FUNCTION_NAME: &wstr = L!("fish_breakpoint_prompt");
const MODE_PROMPT_FUNCTION_NAME: &wstr = L!("fish_mode_prompt");
const DEFAULT_TITLE: &wstr = L!("echo (status current-command) \" \" $PWD");
const READAHEAD_MAX: usize = 256;
pub fn read_generation_count() -> u32 {
GENERATION.load(Ordering::Relaxed)
}
const HIGHLIGHT_TIMEOUT_FOR_EXECUTION: Duration = Duration::from_millis(250);
pub fn signal_safe_reader_handle_sigint() {
INTERRUPTED.store(SIGINT, Ordering::Relaxed);
}
pub fn reader_reset_interrupted() {
INTERRUPTED.store(0, Ordering::Relaxed);
}
pub fn reader_test_and_clear_interrupted() -> i32 {
let res = INTERRUPTED.load(Ordering::Relaxed);
if res != 0 {
INTERRUPTED.store(0, Ordering::Relaxed);
}
res
}
pub fn signal_safe_reader_set_exit_signal(sig: i32) {
EXIT_SIGNAL.store(sig, Ordering::Relaxed);
}
pub fn reader_exit_signal() -> i32 {
EXIT_SIGNAL.load(Ordering::Relaxed)
}
fn reader_received_exit_signal() -> bool {
reader_exit_signal() != 0
}
impl ReaderData {
fn new(
input_data: InputData,
history: Arc<History>,
conf: ReaderConfig,
is_top_level: bool,
) -> Pin<Box<Self>> {
let mut command_line = EditableLine::default();
if is_top_level {
let state = commandline_state_snapshot();
command_line.push_edit(Edit::new(0..0, state.text.clone()), false);
command_line.set_position(state.cursor_pos);
}
Pin::new(Box::new(Self {
conf,
command_line,
command_line_transient_edit: None,
rendered_layout: Default::default(),
autosuggestion: Default::default(),
saved_autosuggestion: Default::default(),
pager: Default::default(),
current_page_rendering: Default::default(),
suppress_autosuggestion: Default::default(),
reset_loop_state: Default::default(),
first_prompt: true,
last_flash: Default::default(),
flash_autosuggestion: false,
screen: Screen::default(),
input_data,
queued_repaint: false,
history,
history_search: Default::default(),
history_pager: None,
cursor_selection_mode: CursorSelectionMode::Exclusive,
cursor_end_mode: CursorEndMode::Exclusive,
selection: Default::default(),
left_prompt_buff: Default::default(),
mode_prompt_buff: Default::default(),
right_prompt_buff: Default::default(),
cycle_command_line: Default::default(),
cycle_cursor_pos: Default::default(),
exit_loop_requested: Default::default(),
did_warn_for_bg_jobs: Default::default(),
kill_item: Default::default(),
force_exec_prompt_and_repaint: Default::default(),
last_jump_target: Default::default(),
last_jump_direction: JumpDirection::Forward,
last_jump_precision: JumpPrecision::To,
in_flight_highlight_request: Default::default(),
in_flight_autosuggest_request: Default::default(),
rls: None,
debouncers: Debouncers::new(),
}))
}
pub fn save_screen_state(&mut self) {
self.screen.save_status();
}
fn is_navigating_pager_contents(&self) -> bool {
self.pager.is_navigating_contents() || self.history_pager.is_some()
}
fn edit_line(&self, elt: EditableLineTag) -> &EditableLine {
match elt {
EditableLineTag::Commandline => &self.command_line,
EditableLineTag::SearchField => &self.pager.search_field_line,
}
}
fn edit_line_mut(&mut self, elt: EditableLineTag) -> &mut EditableLine {
match elt {
EditableLineTag::Commandline => &mut self.command_line,
EditableLineTag::SearchField => &mut self.pager.search_field_line,
}
}
fn active_edit_line_tag(&self) -> EditableLineTag {
if self.is_navigating_pager_contents() && self.pager.is_search_field_shown() {
return EditableLineTag::SearchField;
}
EditableLineTag::Commandline
}
fn active_edit_line(&self) -> (EditableLineTag, &EditableLine) {
let elt = self.active_edit_line_tag();
(elt, self.edit_line(elt))
}
fn active_edit_line_mut(&mut self) -> (EditableLineTag, &mut EditableLine) {
let elt = self.active_edit_line_tag();
(elt, self.edit_line_mut(elt))
}
fn rls(&self) -> &ReadlineLoopState {
self.rls.as_ref().unwrap()
}
fn rls_mut(&mut self) -> &mut ReadlineLoopState {
self.rls.as_mut().unwrap()
}
fn command_line_changed(
&mut self,
elt: EditableLineTag,
autosuggestion_update: AutosuggestionUpdate,
) {
assert_is_main_thread();
match elt {
EditableLineTag::Commandline => {
GENERATION.fetch_add(1, Ordering::Relaxed);
let saved_autosuggestion = self.saved_autosuggestion.take();
use AutosuggestionUpdate::*;
match autosuggestion_update {
Preserve => (),
Remove => self.autosuggestion.clear(),
RemoveAndSave => {
self.saved_autosuggestion = Some(std::mem::take(&mut self.autosuggestion));
}
Restore => {
self.autosuggestion = saved_autosuggestion.unwrap();
self.suppress_autosuggestion = false;
}
}
}
EditableLineTag::SearchField => {
if self.history_pager.is_some() {
self.fill_history_pager(
HistoryPagerInvocation::Anew,
Some(SelectionMotion::Next),
SearchDirection::Backward,
);
return;
}
if self.pager.is_empty() {
return;
}
self.pager.refilter_completions();
self.pager_selection_changed();
}
}
}
fn update_commandline_state(&self) {
let mut snapshot = commandline_state_snapshot();
if snapshot.text != self.command_line.text() {
snapshot.text = self.command_line.text().to_owned();
}
snapshot.cursor_pos = self.command_line.position();
snapshot.history = Some(self.history.clone());
snapshot.selection = self.get_selection();
snapshot.pager_mode = !self.pager.is_empty();
snapshot.pager_fully_disclosed = self.current_page_rendering.remaining_to_disclose == 0;
if (snapshot.search_field.is_some() != self.pager.search_field_shown)
|| snapshot
.search_field
.as_ref()
.is_some_and(|(text, position)| {
text != self.pager.search_field_line.text()
|| *position != self.pager.search_field_line.position()
})
{
snapshot.search_field = self.pager.search_field_shown.then(|| {
(
self.pager.search_field_line.text().to_owned(),
self.pager.search_field_line.position(),
)
});
}
snapshot.search_mode = self.history_search.active();
}
fn apply_commandline_state_changes(&mut self) {
let state = commandline_get_state(false);
if state.text != self.command_line.text()
|| state.cursor_pos != self.command_line.position()
{
self.clear_pager();
self.set_buffer_maintaining_pager(&state.text, state.cursor_pos);
self.reset_loop_state = true;
} else if let Some((new_search_field, new_cursor_pos)) = state.search_field {
if !self.pager.search_field_shown {
return; }
if new_search_field == self.pager.search_field_line.text()
&& new_cursor_pos == self.pager.search_field_line.position()
{
return;
}
self.push_edit(
EditableLineTag::SearchField,
Edit::new(0..self.pager.search_field_line.len(), new_search_field),
);
self.pager.search_field_line.set_position(new_cursor_pos);
}
}
fn update_buff_pos(&mut self, elt: EditableLineTag, mut new_pos: Option<usize>) -> bool {
let el = self.edit_line(elt);
if self.cursor_end_mode == CursorEndMode::Inclusive {
let mut pos = new_pos.unwrap_or(el.position());
if !el.is_empty() && pos == el.len() {
pos = el.len() - 1;
if el.position() == pos {
return false;
}
new_pos = Some(pos);
}
}
let old_pos = el.position();
if let Some(pos) = new_pos {
self.edit_line_mut(elt).set_position(pos);
}
if elt != EditableLineTag::Commandline {
return true;
}
if let Some(new_pos) = new_pos {
let range = if new_pos <= old_pos {
new_pos..old_pos
} else {
old_pos..new_pos
};
if self.command_line.text()[range].contains('\n') {
self.suppress_autosuggestion = true;
}
}
let buff_pos = self.command_line.position();
let target_char = if self.cursor_selection_mode == CursorSelectionMode::Inclusive {
1
} else {
0
};
let Some(selection) = self.selection.as_mut() else {
return true;
};
if selection.begin <= buff_pos {
selection.start = selection.begin;
selection.stop = buff_pos + target_char;
} else {
selection.start = buff_pos;
selection.stop = selection.begin + target_char;
}
true
}
pub fn mouse_left_click(&mut self, click_position: ViewportPosition) {
flogf!(
reader,
"Received left mouse click at %u",
format!("{:?}", click_position),
);
match self.screen.offset_in_cmdline_given_cursor(click_position) {
CharOffset::Cmd(new_pos) | CharOffset::Pointer(new_pos) => {
let (elt, _el) = self.active_edit_line();
self.update_buff_pos(elt, Some(new_pos));
}
CharOffset::Pager(idx) if self.pager.selected_completion_idx != Some(idx) => {
self.pager.selected_completion_idx = Some(idx);
self.pager_selection_changed();
}
CharOffset::Pager(_) | CharOffset::None => {}
}
}
pub fn schedule_prompt_repaint(&mut self) {
if self.force_exec_prompt_and_repaint {
return;
}
self.force_exec_prompt_and_repaint = true;
self.input_data
.queue_char(CharEvent::from_readline(ReadlineCmd::Repaint));
}
}
pub fn reader_save_screen_state() {
current_data().map(|data| data.save_screen_state());
}
fn combine_command_and_autosuggestion(
cmdline: &wstr,
line_range: Range<usize>,
autosuggestion: &wstr,
) -> WString {
let pos = line_range.end;
let full_line;
assert!(!autosuggestion.is_empty());
assert!(autosuggestion.len() >= line_range.len());
let available = autosuggestion.len() - line_range.len();
let line = &cmdline[line_range];
if !string_prefixes_string(line, autosuggestion) && line.len() < autosuggestion.len() {
assert!(string_prefixes_string_case_insensitive(
line,
autosuggestion
));
let (tok, _) = get_token_extent(cmdline, cmdline.len() - 1);
let last_token_contains_uppercase = cmdline[tok].chars().any(|c| c.is_uppercase());
if !last_token_contains_uppercase {
let start: usize = unsafe {
std::ptr::from_ref(line.as_char_slice().first().unwrap())
.offset_from(&cmdline.as_char_slice()[0])
}
.try_into()
.unwrap();
full_line = cmdline[..start].to_owned() + autosuggestion + &cmdline[pos..];
return full_line;
}
}
cmdline[..pos].to_owned()
+ &autosuggestion[autosuggestion.len() - available..]
+ &cmdline[pos..]
}
impl<'a> Reader<'a> {
fn query(&mut self, query_state: RecurrentQuery) {
assert_ne!(query_state, RecurrentQuery::default());
if self
.vars()
.get_unless_empty(L!("FISH_TEST_NO_RECURRENT_QUERIES"))
.is_some()
{
return;
}
if !querying_allowed(self.vars()) {
return;
}
let mut query = self.blocking_query();
assert!(query.is_none());
{
let mut out = Outputter::stdoutput().borrow_mut();
out.begin_buffering();
if query_state.background_color.is_some() {
out.write_command(QueryBackgroundColor);
}
if query_state.cursor_position.is_some() {
out.write_command(QueryCursorPosition);
}
out.write_command(QueryPrimaryDeviceAttribute);
out.end_buffering();
}
*query = Some(TerminalQuery::Recurrent(query_state));
drop(query);
self.save_screen_state();
}
fn is_repaint_needed(&self, mcolors: Option<&[HighlightSpec]>) -> bool {
let check = |val: bool, reason: &str| {
if val {
flog!(reader_render, "repaint needed because", reason, "change");
}
val
};
let focused_on_pager = self.active_edit_line_tag() == EditableLineTag::SearchField;
let pager_search_field_position = focused_on_pager.then(|| self.pager.cursor_position());
let last = &self.rendered_layout;
check(self.force_exec_prompt_and_repaint, "forced")
|| check(self.command_line.text() != last.text, "text")
|| check(
mcolors.is_some_and(|colors| colors != last.colors),
"highlight",
)
|| check(self.selection != last.selection, "selection")
|| check(self.command_line.position() != last.position, "position")
|| check(
pager_search_field_position != last.pager_search_field_position,
"pager_search_field_position",
)
|| check(
self.history_search.search_range_if_active() != last.history_search_range,
"history search",
)
|| check(
self.autosuggestion.text != last.autosuggestion,
"autosuggestion",
)
|| check(
self.left_prompt_buff != last.left_prompt_buff,
"left_prompt",
)
|| check(
self.mode_prompt_buff != last.mode_prompt_buff,
"mode_prompt",
)
|| check(
self.right_prompt_buff != last.right_prompt_buff,
"right_prompt",
)
|| check(
self.pager
.rendering_needs_update(&self.current_page_rendering),
"pager",
)
}
fn make_layout_data(&self) -> LayoutData {
let mut result = LayoutData::default();
let focused_on_pager = self.active_edit_line_tag() == EditableLineTag::SearchField;
result.text = self.command_line.text().to_owned();
result.colors = self.command_line.colors().to_vec();
assert_eq!(result.text.len(), result.colors.len());
result.position = self.command_line.position();
result.pager_search_field_position = focused_on_pager.then(|| self.pager.cursor_position());
result.selection = self.selection;
result.history_search_range = self.history_search.search_range_if_active();
result.autosuggestion = self.autosuggestion.text.clone();
result.left_prompt_buff = self.left_prompt_buff.clone();
result.mode_prompt_buff = self.mode_prompt_buff.clone();
result.right_prompt_buff = self.right_prompt_buff.clone();
result
}
fn layout_and_repaint(&mut self, reason: &wstr) {
self.rendered_layout = self.make_layout_data();
self.paint_layout(reason, false);
}
fn layout_and_repaint_before_execution(&mut self) {
self.rendered_layout = self.make_layout_data();
self.paint_layout(L!("prepare to execute"), true);
}
fn paint_layout(&mut self, reason: &wstr, is_final_rendering: bool) {
flogf!(reader_render, "Repainting from %s", reason);
let cmd_line = &self.data.command_line;
let (full_line, autosuggested_range) = if self.conf.in_silent_mode {
(
Cow::Owned(
wstr::from_char_slice(&[get_obfuscation_read_char()]).repeat(cmd_line.len()),
),
None,
)
} else if self.is_at_line_with_autosuggestion() {
let autosuggestion = &self.autosuggestion;
let search_string_range = &autosuggestion.search_string_range;
let autosuggested_start = search_string_range.end;
let autosuggested_end = search_string_range.start + autosuggestion.text.len();
(
Cow::Owned(combine_command_and_autosuggestion(
cmd_line.text(),
autosuggestion.search_string_range.clone(),
&autosuggestion.text,
)),
Some(autosuggested_start..autosuggested_end),
)
} else {
(Cow::Borrowed(cmd_line.text()), None)
};
let autosuggested_range = autosuggested_range.unwrap_or(full_line.len()..full_line.len());
let data = &self.data.rendered_layout;
let mut colors = data.colors.clone();
if let Some(range) = data.history_search_range {
if !self.conf.in_silent_mode {
let mut range = range.as_usize();
if range.end > colors.len() {
range.start = range.start.min(colors.len());
range.end = colors.len();
}
let explicit_foreground = self
.vars()
.get_unless_empty(L!("fish_color_search_match"))
.is_some_and(|var| parse_text_face(var.as_list()).fg.is_some());
for color in &mut colors[range] {
if explicit_foreground {
color.foreground = HighlightRole::search_match;
}
color.background = HighlightRole::search_match;
}
}
}
if let Some(selection) = data.selection {
let selection_color = HighlightSpec::with_both(HighlightRole::selection);
let end = std::cmp::min(selection.stop, colors.len());
for color in &mut colors[selection.start.min(end)..end] {
*color = selection_color;
}
}
let pos = autosuggested_range.start;
colors.splice(
pos..pos,
vec![
if self.flash_autosuggestion {
HighlightSpec::with_both(HighlightRole::search_match)
} else {
HighlightSpec::with_fg(HighlightRole::autosuggestion)
};
autosuggested_range.len()
],
);
let indents = compute_indents(&full_line);
let screen = &mut self.data.screen;
let pager = &mut self.data.pager;
let current_page_rendering = &mut self.data.current_page_rendering;
let curr_termsize = termsize_last();
screen.write(
curr_termsize,
&(self.data.mode_prompt_buff.clone() + &self.data.left_prompt_buff[..]),
&self.data.right_prompt_buff,
&full_line,
autosuggested_range,
colors,
indents,
data.position,
data.pager_search_field_position,
self.parser.vars(),
pager,
current_page_rendering,
is_final_rendering,
);
screen.autoscroll(curr_termsize.height());
}
}
enum AutosuggestionUpdate {
Preserve,
Remove,
RemoveAndSave,
Restore,
}
impl ReaderData {
fn kill(&mut self, elt: EditableLineTag, range: Range<usize>, mode: Kill, newv: bool) {
let text = match elt {
EditableLineTag::Commandline => &self.command_line,
EditableLineTag::SearchField => &self.pager.search_field_line,
}
.text();
let kill_item = &mut self.kill_item;
if newv {
*kill_item = text[range.clone()].to_owned();
kill_add(kill_item.clone());
} else {
let old = kill_item.to_owned();
match mode {
Kill::Append => kill_item.push_utfstr(&text[range.clone()]),
Kill::Prepend => {
*kill_item = text[range.clone()].to_owned();
kill_item.push_utfstr(&old);
}
}
kill_replace(&old, kill_item.clone());
}
self.erase_substring(elt, range);
}
fn insert_string(&mut self, elt: EditableLineTag, s: &wstr) {
let history_search_active = self.history_search.active();
let el = self.edit_line(elt);
self.push_edit_internal(
elt,
Edit::new(el.position()..el.position(), s.to_owned()),
!history_search_active,
);
if elt == EditableLineTag::Commandline {
self.command_line_transient_edit = None;
self.suppress_autosuggestion = false;
}
}
fn erase_substring(&mut self, elt: EditableLineTag, range: Range<usize>) {
self.push_edit(elt, Edit::new(range, L!("").to_owned()));
}
fn clear(&mut self, elt: EditableLineTag) {
let el = self.edit_line(elt);
if !el.is_empty() {
self.erase_substring(elt, 0..el.len());
}
}
fn replace_substring(
&mut self,
elt: EditableLineTag,
range: Range<usize>,
replacement: WString,
) {
self.push_edit(elt, Edit::new(range, replacement));
}
fn push_edit(&mut self, elt: EditableLineTag, edit: Edit) {
self.push_edit_internal(elt, edit, false);
}
fn insert_char(&mut self, elt: EditableLineTag, c: char) {
self.insert_string(elt, &WString::from_chars([c]));
}
fn set_command_line_and_position(
&mut self,
elt: EditableLineTag,
new_str: WString,
pos: usize,
) {
self.push_edit(elt, Edit::new(0..self.edit_line(elt).len(), new_str));
self.edit_line_mut(elt).set_position(pos);
self.update_buff_pos(elt, Some(pos));
}
}
fn try_apply_edit_to_autosuggestion(
autosuggestion: &mut Autosuggestion,
command_line_text: &wstr,
edit: &Edit,
) -> bool {
if autosuggestion.is_empty() {
return false;
}
assert!(string_prefixes_string_maybe_case_insensitive(
autosuggestion.icase_matched_codepoints.is_some(),
&command_line_text[autosuggestion.search_string_range.clone()],
&autosuggestion.text
));
let search_string_range = autosuggestion.search_string_range.clone();
let replacement = &edit.replacement;
let mut matched_codepoints_delta = 0_isize;
if edit.range.start < search_string_range.start
|| edit.range.end != search_string_range.end
|| {
let suggestion = autosuggestion.text.chars();
let unchanged_prefix = autosuggestion.icase_matched_codepoints.map_or(
search_string_range
.len()
.checked_sub(edit.range.len())
.unwrap(),
|matched_codepoints| {
let unmatched_codepoints =
lowercase(command_line_text[edit.range.clone()].chars()).count();
matched_codepoints_delta -= isize::try_from(unmatched_codepoints).unwrap();
matched_codepoints
.checked_sub(unmatched_codepoints)
.unwrap()
},
);
if autosuggestion.icase_matched_codepoints.is_some() {
let replacement_lower = || lowercase(replacement.chars());
matched_codepoints_delta += isize::try_from(replacement_lower().count()).unwrap();
is_prefix(
replacement_lower(),
lowercase(suggestion).skip(unchanged_prefix),
)
} else {
is_prefix(replacement.chars(), suggestion.skip(unchanged_prefix))
}
} != Some(IsPrefix::Prefix)
{
return false;
}
autosuggestion.search_string_range.end =
search_string_range.end - edit.range.len().min(search_string_range.end) + replacement.len();
if let Some(matched_codepoints) = &mut autosuggestion.icase_matched_codepoints {
*matched_codepoints = matched_codepoints
.checked_add_signed(matched_codepoints_delta)
.unwrap();
}
true
}
impl ReaderData {
fn push_edit_internal(&mut self, elt: EditableLineTag, edit: Edit, allow_coalesce: bool) {
let mut autosuggestion_update = AutosuggestionUpdate::Remove;
if elt == EditableLineTag::Commandline {
let preserves_autosuggestion = try_apply_edit_to_autosuggestion(
&mut self.autosuggestion,
self.command_line.text(),
&edit,
);
if preserves_autosuggestion {
autosuggestion_update = AutosuggestionUpdate::Preserve;
} else if !self.autosuggestion.is_empty()
&& edit.range.start == self.autosuggestion.search_string_range.end
&& edit.range.is_empty()
&& !edit.replacement.is_empty()
{
autosuggestion_update = AutosuggestionUpdate::RemoveAndSave;
} else if self
.saved_autosuggestion
.as_ref()
.is_some_and(|saved_autosuggestion| {
self.conf.autosuggest_ok
&& self.history_search.is_at_present()
&& edit.replacement.is_empty()
&& edit.range.start == saved_autosuggestion.search_string_range.end
&& !edit.range.is_empty()
})
{
autosuggestion_update = AutosuggestionUpdate::Restore;
}
}
self.edit_line_mut(elt).push_edit(edit, allow_coalesce);
self.command_line_changed(elt, autosuggestion_update);
}
fn undo(&mut self, elt: EditableLineTag) -> bool {
let ok = self.edit_line_mut(elt).undo();
if ok {
self.command_line_changed(elt, AutosuggestionUpdate::Remove);
}
ok
}
fn redo(&mut self, elt: EditableLineTag) -> bool {
let ok = self.edit_line_mut(elt).redo();
if ok {
self.command_line_changed(elt, AutosuggestionUpdate::Remove);
}
ok
}
fn clear_transient_edit(&mut self) {
if self.command_line_transient_edit.is_none() {
return;
}
self.undo(EditableLineTag::Commandline);
self.update_buff_pos(EditableLineTag::Commandline, None);
self.command_line_transient_edit = None;
}
fn replace_current_token(&mut self, new_token: WString) {
let (elt, el) = self.active_edit_line();
let (token_range, _) = get_token_extent(el.text(), el.position());
self.replace_substring(elt, token_range, new_token);
}
fn update_command_line_from_history_search(&mut self) {
assert!(self.history_search.active());
if let Some(transient_edit) = self.command_line_transient_edit.take() {
if transient_edit == TransientEdit::HistorySearch {
self.undo(EditableLineTag::Commandline);
}
}
if !self.history_search.is_at_present() {
let new_text = self.history_search.current_result().to_owned();
if self.history_search.by_token() {
self.replace_current_token(new_text);
} else {
self.replace_substring(
EditableLineTag::Commandline,
0..self.command_line.len(),
new_text,
);
if self.history_search.by_prefix()
&& !self.history_search.search_string().is_empty()
{
self.command_line
.set_position(self.history_search.search_string().len());
}
}
self.command_line_transient_edit = Some(TransientEdit::HistorySearch);
}
self.update_buff_pos(EditableLineTag::Commandline, None);
}
fn delete_char(&mut self, backward: bool ) {
let (elt, el) = self.active_edit_line();
let mut pos = el.position();
if !backward {
pos += 1;
}
let pos_end = pos;
if el.position() == 0 && backward {
return;
}
loop {
pos -= 1;
if fish_wcwidth(el.text().char_at(pos)).is_none_or(|w| w != 0) || pos == 0 {
break;
}
}
self.suppress_autosuggestion = true;
self.erase_substring(elt, pos..pos_end);
self.update_buff_pos(elt, None);
}
}
impl ReaderData {
fn move_word(
&mut self,
elt: EditableLineTag,
direction: MoveWordDir,
erase: bool,
style: MoveWordStyle,
newv: bool,
to_word_end: bool,
) {
let move_right = direction == MoveWordDir::Right;
let state_machine_dir = if to_word_end {
match direction {
MoveWordDir::Left => MoveWordDir::Right,
MoveWordDir::Right => MoveWordDir::Left,
}
} else {
direction
};
let el = self.edit_line(elt);
let boundary = if move_right { el.len() } else { 0 };
if el.position() == boundary {
return;
}
let mut state = MoveWordStateMachine::new(style, state_machine_dir);
let start_buff_pos = el.position();
let mut buff_pos = el.position();
let end = if move_right {
if to_word_end {
el.len() - 1
} else {
el.len()
}
} else if to_word_end {
usize::MAX
} else {
0
};
while buff_pos != end {
if buff_pos == el.len() && (move_right || to_word_end) {
if !move_right && to_word_end && buff_pos != 0 {
buff_pos -= 1;
} else {
break;
}
}
let char_pos = if move_right {
if to_word_end {
buff_pos + 1
} else {
buff_pos
}
} else if to_word_end {
buff_pos
} else {
buff_pos.wrapping_sub(1)
};
let consumed = state.consume_char(el.text(), char_pos);
if consumed {
buff_pos = if move_right {
buff_pos + 1
} else {
buff_pos.wrapping_sub(1)
};
} else {
break;
}
}
if buff_pos == usize::MAX {
buff_pos = 0;
}
if erase {
if elt == EditableLineTag::Commandline {
self.suppress_autosuggestion = true;
}
if move_right {
self.kill(
elt,
start_buff_pos..(buff_pos + if to_word_end { 1 } else { 0 }),
Kill::Append,
newv,
);
} else {
self.kill(
elt,
buff_pos..start_buff_pos + if to_word_end { 1 } else { 0 },
Kill::Prepend,
newv,
);
}
} else {
self.update_buff_pos(elt, Some(buff_pos));
}
}
fn delete_a_word(&mut self, elt: EditableLineTag, style: MoveWordStyle, newv: bool) {
let el = self.edit_line(elt);
let pos = el.position();
if pos == el.len() {
return;
}
let text_slice = el.text().as_char_slice();
let on_blank = is_blank(text_slice[pos]);
if on_blank {
let mut begin_pos = 0;
for idx in (0..pos).rev() {
if !is_blank(text_slice[idx]) {
begin_pos = idx + 1;
break;
}
}
self.update_buff_pos(elt, Some(begin_pos));
self.move_word(elt, MoveWordDir::Right, true, style, newv, true);
} else {
if pos < el.len() - 1 {
self.update_buff_pos(elt, Some(el.position() + 1));
}
self.move_word(
elt,
MoveWordDir::Left,
false,
style,
newv,
false,
);
let word_start = self.edit_line(elt).position();
self.move_word(
elt,
MoveWordDir::Right,
false,
style,
newv,
true,
);
let el = self.edit_line(elt);
let word_end = el.position() + 1;
let text_slice = el.text().as_char_slice();
let len = el.len();
let kill_range = if word_end < len && is_blank(text_slice[word_end]) {
let mut end_pos = len;
for (idx, &c) in text_slice.iter().enumerate().skip(word_end + 1) {
if !is_blank(c) {
end_pos = idx;
break;
}
}
word_start..end_pos
} else if word_start > 0 && is_blank(text_slice[word_start - 1]) {
let mut begin_pos = 0;
for idx in (0..word_start - 1).rev() {
if !is_blank(text_slice[idx]) {
begin_pos = idx + 1;
break;
}
}
begin_pos..word_end
} else {
word_start..word_end
};
self.update_buff_pos(elt, Some(word_start));
self.kill(elt, kill_range, Kill::Append, newv);
}
}
fn delete_inner_word(&mut self, elt: EditableLineTag, style: MoveWordStyle, newv: bool) {
let el = self.edit_line(elt);
let len = el.len();
let pos = el.position();
if pos == len {
return;
}
let text_slice = el.text().as_char_slice();
if is_blank(text_slice[pos]) {
let mut begin_pos = 0;
for idx in (0..pos).rev() {
if !is_blank(text_slice[idx]) {
begin_pos = idx + 1;
break;
}
}
let mut end_pos = len;
for (idx, &c) in text_slice.iter().enumerate().skip(pos) {
if !is_blank(c) {
end_pos = idx;
break;
}
}
self.kill(elt, begin_pos..end_pos, Kill::Append, newv);
} else {
if el.position() != 0 {
self.update_buff_pos(elt, Some(el.position() + 1));
self.move_word(
elt,
MoveWordDir::Left,
false,
style,
newv,
false,
);
}
self.move_word(
elt,
MoveWordDir::Right,
true,
style,
newv,
true,
);
}
}
fn jump_to_matching_bracket(
&mut self,
precision: JumpPrecision,
elt: EditableLineTag,
jump_from: usize,
l_bracket: char,
r_bracket: char,
) -> bool {
let el = self.edit_line(elt);
let mut tmp_r_pos: usize = 0;
let mut brackets_stack = Vec::new();
while tmp_r_pos < el.len() {
if el.at(tmp_r_pos) == l_bracket {
brackets_stack.push(tmp_r_pos);
} else if el.at(tmp_r_pos) == r_bracket {
match brackets_stack.pop() {
Some(tmp_l_pos) if jump_from == tmp_l_pos => {
return match precision {
JumpPrecision::Till => self.update_buff_pos(elt, Some(tmp_r_pos - 1)),
JumpPrecision::To => self.update_buff_pos(elt, Some(tmp_r_pos)),
};
}
Some(tmp_l_pos) if jump_from == tmp_r_pos => {
return match precision {
JumpPrecision::Till => self.update_buff_pos(elt, Some(tmp_l_pos + 1)),
JumpPrecision::To => self.update_buff_pos(elt, Some(tmp_l_pos)),
};
}
_ => {}
}
}
tmp_r_pos += 1;
}
false
}
fn jump_and_remember_last_jump(
&mut self,
direction: JumpDirection,
precision: JumpPrecision,
elt: EditableLineTag,
target: char,
skip_till: bool,
) -> bool {
self.last_jump_target = Some(target);
self.last_jump_direction = direction;
self.last_jump_precision = precision;
self.jump(direction, precision, elt, vec![target], skip_till)
}
fn jump(
&mut self,
direction: JumpDirection,
precision: JumpPrecision,
elt: EditableLineTag,
targets: Vec<char>,
skip_till: bool,
) -> bool {
let el = self.edit_line(elt);
match direction {
JumpDirection::Backward => {
let mut tmp_pos = el.position();
if precision == JumpPrecision::Till && skip_till && tmp_pos > 0 {
tmp_pos -= 1;
}
loop {
if tmp_pos == 0 {
return false;
}
tmp_pos -= 1;
if targets.iter().any(|&target| el.at(tmp_pos) == target) {
if precision == JumpPrecision::Till {
tmp_pos = std::cmp::min(el.len() - 1, tmp_pos + 1);
}
self.update_buff_pos(elt, Some(tmp_pos));
return true;
}
}
}
JumpDirection::Forward => {
let mut tmp_pos = el.position() + 1;
if precision == JumpPrecision::Till && skip_till && tmp_pos < el.len() - 1 {
tmp_pos += 1;
}
while tmp_pos < el.len() {
if targets.iter().any(|&target| el.at(tmp_pos) == target) {
if precision == JumpPrecision::Till {
tmp_pos -= 1;
}
self.update_buff_pos(elt, Some(tmp_pos));
return true;
}
tmp_pos += 1;
}
false
}
}
}
}
impl<'a> Reader<'a> {
fn readline(
&mut self,
old_modes: Option<Termios>,
nchars: Option<NonZeroUsize>,
) -> Option<WString> {
let mut tty = TtyHandoff::new(reader_save_screen_state);
self.rls = Some(ReadlineLoopState::new());
let _restore = self.parser.push_scope(|s| s.suppress_fish_trace = true);
self.rls_mut().nchars = nchars;
self.cycle_command_line.clear();
self.cycle_cursor_pos = 0;
self.history_search.reset();
if !self.first_prompt || !self.conf.read_prompt_str_is_empty {
let trusted_width = (!self.first_prompt).then_some(termsize_last().width());
self.screen.reset_abandoning_line(trusted_width);
}
self.first_prompt = false;
if !self.conf.event.is_empty() {
event::fire_generic(self.parser, self.conf.event.to_owned(), vec![]);
}
self.exec_prompt(true, false);
self.force_exec_prompt_and_repaint = true;
self.query(RecurrentQuery {
cursor_position: Some(CursorPositionQuery::new(
CursorPositionQueryReason::NewPrompt,
)),
background_color: Some(BackgroundColorQuery::default()),
});
while !check_exit_loop_maybe_warning(Some(self)) {
tty.enable_tty_protocols();
if self.handle_char_event(None).is_break() || self.rls().finished {
break;
}
}
tty.disable_tty_protocols();
if self.conf.transient_prompt {
self.exec_prompt(true, true);
}
if self.is_repaint_needed(None) || self.screen.scrolled || self.conf.inputfd != STDIN_FILENO
{
self.layout_and_repaint_before_execution();
}
if self.rls().finished {
self.finish_highlighting_before_exec();
}
if !self.screen.cursor_is_wrapped_to_own_line() {
let _ = write_to_fd(b"\n", STDOUT_FILENO);
}
if self.conf.inputfd != STDIN_FILENO {
let _ = write_loop(&STDOUT_FILENO, b"\r");
}
if !self.pager.is_empty() {
screen_force_clear_to_end();
self.clear_pager();
}
if EXIT_STATE.load(Ordering::Relaxed) != ExitState::FinishedHandlers as _ {
if let Some(old_modes) = old_modes {
if let Err(err) = tcsetattr(
unsafe { BorrowedFd::borrow_raw(self.conf.inputfd) },
SetArg::TCSANOW,
&old_modes,
) {
if is_interactive_session() {
perror_nix("tcsetattr", err);
}
}
}
Outputter::stdoutput().borrow_mut().reset_text_face();
}
let result = self
.rls()
.finished
.then(|| self.command_line.text().to_owned());
self.rls = None;
result
}
fn eval_bind_cmd(&mut self, cmd: &wstr) {
let last_statuses = self.parser.vars().get_last_statuses();
let mut scoped_tty = TtyHandoff::new(reader_save_screen_state);
scoped_tty.disable_tty_protocols();
self.parser.eval(cmd, &IoChain::new());
self.parser.set_last_statuses(last_statuses);
}
fn run_input_command_scripts(&mut self, cmd: &wstr) {
self.eval_bind_cmd(cmd);
set_shell_modes(STDIN_FILENO, "bind scripts");
signal_safe_termsize_invalidate_tty();
}
fn read_normal_chars(&mut self) -> Option<CharEvent> {
let mut event_needing_handling = None;
let limit = std::cmp::min(
self.rls().nchars.map_or(usize::MAX, |nchars| {
usize::from(nchars) - self.command_line_len()
}),
READAHEAD_MAX,
);
let mut accumulated_chars = WString::new();
while accumulated_chars.len() < limit {
let evt = self.read_char();
let CharEvent::Key(kevt) = &evt else {
event_needing_handling = Some(evt);
break;
};
if !poll_fd_readable(self.conf.inputfd) {
event_needing_handling = Some(evt);
break;
}
if kevt.input_style == CharInputStyle::NotFirst
&& accumulated_chars.is_empty()
&& self.active_edit_line().1.position() == 0
{
continue;
}
if let Some(c) = kevt.key.codepoint_text() {
accumulated_chars.push(c);
} else {
continue;
}
}
if !accumulated_chars.is_empty() {
let (elt, _el) = self.active_edit_line();
self.insert_string(elt, &accumulated_chars);
if elt == EditableLineTag::Commandline {
self.clear_pager();
}
self.rls_mut().last_cmd = None;
}
event_needing_handling
}
fn color_suggest_repaint_now(&mut self) {
if self.conf.inputfd == STDIN_FILENO {
self.update_autosuggestion();
self.super_highlight_me_plenty();
}
if self.is_repaint_needed(None) {
self.layout_and_repaint(L!("toplevel"));
}
self.force_exec_prompt_and_repaint = false;
}
fn handle_char_event(&mut self, injected_event: Option<CharEvent>) -> ControlFlow<()> {
if self.reset_loop_state {
self.reset_loop_state = false;
self.rls_mut().last_cmd = None;
self.rls_mut().completion_action = None;
}
reader_update_termsize(self.parser);
self.color_suggest_repaint_now();
if self
.rls()
.nchars
.is_some_and(|nchars| usize::from(nchars) <= self.command_line_len())
{
self.rls_mut().finished = true;
return ControlFlow::Break(());
}
let event_needing_handling = injected_event.or_else(|| loop {
let event_needing_handling = self.read_normal_chars();
if event_needing_handling.is_some() {
break event_needing_handling;
}
if self
.rls()
.nchars
.is_some_and(|nchars| usize::from(nchars) <= self.command_line_len())
{
break None;
}
});
self.exit_loop_requested |= self.parser.libdata().exit_current_script;
self.parser.libdata_mut().exit_current_script = false;
if self.exit_loop_requested {
return ControlFlow::Continue(());
}
let Some(event_needing_handling) = event_needing_handling else {
return ControlFlow::Continue(());
};
match event_needing_handling {
CharEvent::Readline(readline_cmd_evt) => {
if !matches!(
self.rls().last_cmd,
Some(ReadlineCmd::Yank | ReadlineCmd::YankPop)
) {
self.rls_mut().yank_len = 0;
}
let readline_cmd = readline_cmd_evt.cmd;
if readline_cmd == ReadlineCmd::Cancel && self.is_navigating_pager_contents() {
self.clear_transient_edit();
}
let focused_on_search_field =
self.active_edit_line_tag() == EditableLineTag::SearchField;
if !self.history_search.active()
&& command_ends_paging(readline_cmd, focused_on_search_field)
{
self.clear_pager();
}
self.handle_readline_command(readline_cmd);
if self.history_search.active() && command_ends_history_search(readline_cmd) {
if readline_cmd == ReadlineCmd::Cancel {
self.clear_transient_edit();
}
self.history_search.reset();
self.command_line_transient_edit = None;
}
self.rls_mut().last_cmd = Some(readline_cmd);
}
CharEvent::Command(command) => {
self.run_input_command_scripts(&command);
}
CharEvent::Key(kevt) => {
if kevt.input_style == CharInputStyle::NotFirst
&& self.active_edit_line().1.position() == 0
{
} else {
let (elt, _el) = self.active_edit_line();
if let Some(c) = kevt.key.codepoint_text() {
self.insert_char(elt, c);
if elt == EditableLineTag::Commandline {
self.clear_pager();
self.history_search.reset();
}
}
}
self.rls_mut().last_cmd = None;
}
CharEvent::Implicit(implicit_event) => {
use ImplicitEvent::*;
match implicit_event {
Eof => signal_safe_reader_set_exit_signal(libc::SIGHUP),
CheckExit => (),
FocusIn => {
event::fire_generic(self.parser, L!("fish_focus_in").to_owned(), vec![]);
self.save_screen_state();
}
FocusOut => {
event::fire_generic(self.parser, L!("fish_focus_out").to_owned(), vec![]);
self.save_screen_state();
}
MouseLeft(position) => {
flog!(reader, "Mouse left click", position);
self.mouse_left_click(position);
}
NewColorTheme => {
self.query(RecurrentQuery {
background_color: Some(BackgroundColorQuery::default()),
..Default::default()
});
}
NewWindowHeight => {
flog!(reader, "Handling window height change");
self.query(RecurrentQuery {
cursor_position: Some(CursorPositionQuery::new(
CursorPositionQueryReason::WindowHeightChange,
)),
..Default::default()
});
}
}
}
CharEvent::QueryResult(query_result) => {
let mut maybe_query = self.blocking_query();
let query = &mut maybe_query;
use QueryResponse::*;
use QueryResultEvent::*;
let query = match (&mut **query, query_result) {
(Some(TerminalQuery::Initial), _) => panic!(),
(
Some(TerminalQuery::Recurrent(RecurrentQuery {
background_color: Some(color_query),
..
})),
Response(BackgroundColor(background_color)),
) => {
color_query.result = Some(background_color);
return ControlFlow::Continue(());
}
(
Some(TerminalQuery::Recurrent(RecurrentQuery {
cursor_position: Some(cursor_pos_query),
..
})),
Response(CursorPosition(cursor_pos)),
) => {
cursor_pos_query.result = Some(cursor_pos);
return ControlFlow::Continue(());
}
(
Some(TerminalQuery::Recurrent(query_state)),
Response(PrimaryDeviceAttribute) | Timeout | Interrupted,
) => {
let query = query_state.clone();
drop(maybe_query);
if let Some(cursor_pos_query) = query.cursor_position {
let cursor_pos = cursor_pos_query.result;
use CursorPositionQueryReason::*;
let reason = cursor_pos_query.reason;
let whence = match reason {
NewPrompt => "cursor position query on new prompt",
WindowHeightChange => {
"cursor position query on window height change"
}
};
let y = cursor_pos.map(|cursor_pos| match reason {
NewPrompt => cursor_pos.y,
WindowHeightChange => {
self.screen.command_line_y_given_cursor_y(cursor_pos.y)
}
});
self.screen.set_position_in_viewport(whence, y);
}
if let Some(background_color_query) = query.background_color {
if let Some(background_color) = &background_color_query.result {
self.parser.set_color_theme(Some(background_color));
}
}
self.blocking_query()
}
(_, _) => return ControlFlow::Continue(()),
};
let ok = stop_query(query);
assert!(ok);
}
}
ControlFlow::Continue(())
}
}
fn send_xtgettcap_query(out: &mut Outputter, cap: &'static str) {
if should_flog!(reader) {
flog!(reader, format!("Sending XTGETTCAP request for {}:", cap));
}
out.write_command(QueryXtgettcap(cap));
}
fn query_capabilities_via_dcs(out: &mut Outputter, vars: &dyn Environment) {
if vars.get_unless_empty(L!("STY")).is_some()
|| vars.get_unless_empty(L!("TERM")).is_some_and(|term| {
let term = &term.as_list()[0];
term == "screen" || term == "screen-256color"
})
{
return;
}
out.write_command(DecsetAlternateScreenBuffer); send_xtgettcap_query(out, SCROLL_CONTENT_UP_TERMINFO_CODE);
send_xtgettcap_query(out, XTGETTCAP_QUERY_OS_NAME);
out.write_command(DecrstAlternateScreenBuffer); }
impl<'a> Reader<'a> {
fn command_line_len(&self) -> usize {
self.data.command_line.len()
}
fn update_buff_pos(&mut self, elt: EditableLineTag, new_pos: Option<usize>) -> bool {
self.data.update_buff_pos(elt, new_pos)
}
fn push_edit(&mut self, elt: EditableLineTag, edit: Edit) {
self.data.push_edit(elt, edit);
}
fn handle_readline_command(&mut self, c: ReadlineCmd) {
#[allow(non_camel_case_types)]
type rl = ReadlineCmd;
match c {
rl::BeginningOfLine => {
loop {
let (elt, el) = self.active_edit_line();
let position = {
let position = el.position();
if position == 0 || el.text().char_at(position - 1) == '\n' {
break;
}
position
};
self.update_buff_pos(elt, Some(position - 1));
}
}
rl::EndOfLine => {
if self.is_at_autosuggestion() {
self.accept_autosuggestion(AutosuggestionPortion::Line);
} else if !self.is_at_end() {
loop {
let position = {
let (_elt, el) = self.active_edit_line();
let position = el.position();
if position == el.len() {
break;
}
if el.text().char_at(position) == '\n' {
break;
}
position
};
if !self
.data
.update_buff_pos(self.active_edit_line_tag(), Some(position + 1))
{
break;
}
}
}
}
rl::BeginningOfBuffer => {
self.data
.update_buff_pos(EditableLineTag::Commandline, Some(0));
}
rl::EndOfBuffer => {
if self.is_at_autosuggestion() {
self.accept_autosuggestion(AutosuggestionPortion::Count(usize::MAX));
} else {
self.data.update_buff_pos(
EditableLineTag::Commandline,
Some(self.command_line_len()),
);
}
}
rl::CancelCommandline | rl::ClearCommandline => {
if self.conf.exit_on_interrupt {
self.parser
.set_last_statuses(Statuses::just(STATUS_CMD_ERROR));
self.exit_loop_requested = true;
return;
}
if self.command_line.is_empty() {
return;
}
if c == rl::CancelCommandline {
let end = self.command_line.len();
{
let tmp =
std::mem::replace(&mut self.cursor_end_mode, CursorEndMode::Exclusive);
self.update_buff_pos(EditableLineTag::Commandline, Some(end));
self.cursor_end_mode = tmp;
}
self.autosuggestion.clear();
if self.is_repaint_needed(None) {
self.layout_and_repaint(L!("cancel"));
}
let mut outp = Outputter::stdoutput().borrow_mut();
if let Some(fish_color_cancel) = self.vars().get(L!("fish_color_cancel")) {
outp.set_text_face(
parse_text_face_for_highlight(&fish_color_cancel)
.unwrap_or(TextFace::terminal_default()),
);
}
outp.write_wstr(L!("^C"));
outp.reset_text_face();
outp.push(b'\n');
}
self.push_edit(
EditableLineTag::Commandline,
Edit::new(0..self.command_line_len(), L!("").to_owned()),
);
if c == rl::CancelCommandline {
self.screen
.reset_abandoning_line(Some(termsize_last().width()));
}
event::fire_generic(self.parser, L!("fish_cancel").to_owned(), vec![]);
}
rl::Cancel => {
if self.rls().completion_action == Some(CompletionAction::InsertedUnique)
&& matches!(
self.rls().last_cmd,
Some(rl::Complete | rl::CompleteAndSearch)
)
{
let (elt, _el) = self.active_edit_line();
self.undo(elt);
self.update_buff_pos(elt, None);
}
}
rl::RepaintMode | rl::ForceRepaint | rl::Repaint => {
self.queued_repaint = false;
self.parser.libdata_mut().is_repaint = true;
if c == rl::RepaintMode {
self.exec_prompt(false, false);
if !self.mode_prompt_buff.is_empty() {
if self.is_repaint_needed(None) {
self.screen.reset_line( true);
self.layout_and_repaint(L!("mode"));
}
self.parser.libdata_mut().is_repaint = false;
return;
}
}
self.exec_prompt(true, false);
self.screen.reset_line( true);
self.layout_and_repaint(L!("readline"));
self.force_exec_prompt_and_repaint = false;
self.parser.libdata_mut().is_repaint = false;
}
rl::Complete | rl::CompleteAndSearch => {
if !self.conf.complete_ok {
return;
}
if self.is_navigating_pager_contents()
|| (self.rls().completion_action == Some(CompletionAction::ShownAmbiguous)
&& self.rls().last_cmd == Some(rl::Complete))
{
if self.current_page_rendering.remaining_to_disclose != 0 {
self.pager.set_fully_disclosed();
} else {
self.select_completion_in_direction(
if c == rl::Complete {
SelectionMotion::Next
} else {
SelectionMotion::Prev
},
false,
);
}
} else {
let mut tty = TtyHandoff::new(reader_save_screen_state);
tty.disable_tty_protocols();
self.compute_and_apply_completions(c);
}
}
rl::PagerToggleSearch => {
if let Some(history_pager) = &self.history_pager {
if history_pager.start == 0 {
self.flash(0..self.command_line.len());
return;
}
self.fill_history_pager(
HistoryPagerInvocation::Advance,
Some(SelectionMotion::Next),
SearchDirection::Forward,
);
return;
}
if !self.pager.is_empty() {
let sfs = self.pager.is_search_field_shown();
self.pager.set_search_field_shown(!sfs);
self.pager.set_fully_disclosed();
if self.pager.is_search_field_shown() && !self.is_navigating_pager_contents() {
self.select_completion_in_direction(SelectionMotion::South, false);
}
}
}
rl::KillLine => {
let (elt, el) = self.active_edit_line();
let position = el.position();
let begin = position;
let mut end = begin
+ el.text()[begin..]
.chars()
.take_while(|&c| c != '\n')
.count();
if end == begin && end < el.len() {
end += 1;
}
let range = begin..end;
if !range.is_empty() {
self.data.kill(
elt,
range,
Kill::Append,
self.rls().last_cmd != Some(rl::KillLine),
);
}
}
rl::BackwardKillLine => {
let (elt, el) = self.active_edit_line();
let position = el.position();
if position == 0 {
return;
}
let text = el.text();
let end = position;
let mut begin = position;
begin -= 1;
while begin != 0 && text.as_char_slice()[begin] != '\n' {
begin -= 1;
}
if text.as_char_slice()[begin] == '\n' {
begin += 1;
}
assert!(end >= begin);
let len = std::cmp::max(end - begin, 1);
if elt == EditableLineTag::Commandline {
self.suppress_autosuggestion = true;
}
self.data.kill(
elt,
end - len..end,
Kill::Prepend,
self.rls().last_cmd != Some(rl::BackwardKillLine),
);
}
rl::KillWholeLine | rl::KillInnerLine => {
let (elt, el) = self.active_edit_line();
let text = el.text();
let position = el.position();
let mut begin = position
- text[..position]
.chars()
.rev()
.take_while(|&c| c != '\n')
.count();
let mut end = position;
loop {
if end == text.len() {
if c == rl::KillWholeLine && begin > 0 {
begin -= 1;
}
break;
}
if text.as_char_slice()[end] == '\n' {
if c == rl::KillWholeLine {
end += 1;
}
break;
}
end += 1;
}
assert!(end >= begin);
if end > begin {
self.data.kill(
elt,
begin..end,
Kill::Append,
self.rls().last_cmd != Some(c),
);
}
}
rl::Yank => {
let yank_str = kill_yank();
self.data
.insert_string(self.active_edit_line_tag(), &yank_str);
self.rls_mut().yank_len = yank_str.len();
if !yank_str.is_empty() && self.cursor_end_mode == CursorEndMode::Inclusive {
let (_elt, el) = self.active_edit_line();
self.update_buff_pos(self.active_edit_line_tag(), Some(el.position() - 1));
}
}
rl::YankPop => {
if self.rls().yank_len != 0 {
let (elt, el) = self.active_edit_line();
let yank_str = kill_yank_rotate();
let new_yank_len = yank_str.len();
let bias = if self.cursor_end_mode == CursorEndMode::Inclusive {
1
} else {
0
};
let begin = el.position() + bias - self.rls().yank_len;
let end = el.position() + bias;
self.suppress_autosuggestion = true;
self.replace_substring(elt, begin..end, yank_str);
self.update_buff_pos(elt, None);
self.rls_mut().yank_len = new_yank_len;
}
}
rl::BackwardDeleteChar => {
self.delete_char(true);
}
rl::Exit => {
self.parser.set_last_statuses(Statuses::just(STATUS_CMD_OK));
self.exit_loop_requested = true;
check_exit_loop_maybe_warning(Some(self));
}
rl::DeleteOrExit | rl::DeleteChar => {
let (_elt, el) = self.active_edit_line();
if el.position() < el.len() {
self.delete_char(false);
} else if c == rl::DeleteOrExit && el.is_empty() {
self.parser.set_last_statuses(Statuses::just(STATUS_CMD_OK));
self.exit_loop_requested = true;
check_exit_loop_maybe_warning(Some(self));
}
}
rl::Execute => {
if !self.handle_execute() {
event::fire_generic(
self.parser,
L!("fish_posterror").to_owned(),
vec![self.command_line.text().to_owned()],
);
self.screen
.reset_abandoning_line(Some(termsize_last().width()));
}
}
rl::HistoryPrefixSearchBackward
| rl::HistoryPrefixSearchForward
| rl::HistorySearchBackward
| rl::HistorySearchForward
| rl::HistoryTokenSearchBackward
| rl::HistoryTokenSearchForward
| rl::HistoryLastTokenSearchBackward
| rl::HistoryLastTokenSearchForward => {
let mode = match c {
rl::HistoryTokenSearchBackward | rl::HistoryTokenSearchForward => {
SearchMode::Token
}
rl::HistoryLastTokenSearchBackward | rl::HistoryLastTokenSearchForward => {
SearchMode::LastToken
}
rl::HistoryPrefixSearchBackward | rl::HistoryPrefixSearchForward => {
SearchMode::Prefix
}
rl::HistorySearchBackward | rl::HistorySearchForward => SearchMode::Line,
_ => unreachable!(),
};
let was_active_before = self.history_search.active();
if self.history_search.is_at_present() && mode != self.history_search.mode() {
let el = &self.data.command_line;
if matches!(mode, SearchMode::Token | SearchMode::LastToken) {
let (token_range, _) = get_token_extent(el.text(), el.position());
self.data.history_search.reset_to_mode(
el.text()[token_range.clone()].to_owned(),
self.history.clone(),
mode,
token_range.start,
);
} else {
self.data.history_search.reset_to_mode(
el.text().to_owned(),
self.history.clone(),
mode,
0,
);
let suggest = &self.data.autosuggestion.text;
if !suggest.is_empty()
&& !self.data.screen.autosuggestion_is_truncated
&& mode != SearchMode::Prefix
{
self.data.history_search.add_skip(suggest.clone());
}
}
}
assert!(self.history_search.active());
let dir = match c {
rl::HistorySearchBackward
| rl::HistoryTokenSearchBackward
| rl::HistoryLastTokenSearchBackward
| rl::HistoryPrefixSearchBackward => SearchDirection::Backward,
rl::HistorySearchForward
| rl::HistoryTokenSearchForward
| rl::HistoryLastTokenSearchForward
| rl::HistoryPrefixSearchForward => SearchDirection::Forward,
_ => unreachable!(),
};
let found = self.history_search.move_in_direction(dir);
if !found {
let result_range = self.history_search.search_result_range();
self.flash(if !result_range.is_empty() {
result_range
} else {
0..self.command_line.len()
});
}
if found {
self.update_command_line_from_history_search();
} else if !was_active_before {
self.history_search.reset();
}
}
rl::HistoryPager => {
if let Some(history_pager) = &self.history_pager {
if history_pager.end > self.history.size() {
self.flash(0..self.command_line.len());
return;
}
self.fill_history_pager(
HistoryPagerInvocation::Advance,
Some(SelectionMotion::Next),
SearchDirection::Backward,
);
return;
}
self.cycle_command_line = self.command_line.text().to_owned();
self.cycle_cursor_pos = self.command_line.position();
self.history_pager = Some(0..1);
self.pager.set_search_field_shown(true);
self.pager.set_prefix(Cow::Borrowed(L!("► ")), false);
let search_string = if !self.history_search.active()
|| self.history_search.search_string().is_empty()
{
let cmdsub =
get_cmdsubst_extent(self.command_line.text(), self.command_line.position());
let cmdsub = &self.command_line.text()[cmdsub];
let needle = if !cmdsub.contains('\n') {
cmdsub
} else {
line_at_cursor(self.command_line.text(), self.command_line.position())
};
escape_wildcards(needle)
} else {
self.history_search.search_string().to_owned()
};
self.insert_string(EditableLineTag::SearchField, &search_string);
}
#[allow(deprecated)]
rl::HistoryDelete | rl::HistoryPagerDelete => {
let is_history_search = !self.history_search.is_at_present();
let is_autosuggestion = self.is_at_autosuggestion();
if is_history_search || is_autosuggestion {
self.input_data.function_set_status(true);
if is_autosuggestion && !self.autosuggestion.is_whole_item_from_history {
self.flash_autosuggestion = true;
self.flash(0..0);
return;
}
self.history.remove(if is_history_search {
self.history_search.current_result()
} else {
&self.autosuggestion.text
});
self.history.save();
if is_history_search {
self.history_search.handle_deletion();
self.update_command_line_from_history_search();
} else {
self.autosuggestion.clear();
}
return;
}
if self.history_pager.is_none() {
self.input_data.function_set_status(false);
return;
}
self.input_data.function_set_status(true);
if let Some(completion) =
self.pager.selected_completion(&self.current_page_rendering)
{
self.history.remove(&completion.completion);
self.history.save();
self.fill_history_pager(
HistoryPagerInvocation::Refresh,
None,
SearchDirection::Backward,
);
}
}
rl::BackwardChar => {
let (elt, el) = self.active_edit_line();
if self.is_navigating_pager_contents() {
self.select_completion_in_direction(SelectionMotion::West, false);
} else if el.position() != 0 {
self.update_buff_pos(elt, Some(el.position() - 1));
}
}
rl::BackwardCharPassive => {
let (elt, el) = self.active_edit_line();
if el.position() != 0
&& (elt == EditableLineTag::SearchField || !self.is_navigating_pager_contents())
{
self.update_buff_pos(elt, Some(el.position() - 1));
}
}
rl::ForwardChar | rl::ForwardSingleChar => {
if self.is_navigating_pager_contents() {
self.select_completion_in_direction(SelectionMotion::East, false);
} else if self.is_at_autosuggestion() {
self.accept_autosuggestion(AutosuggestionPortion::Count(
if c == rl::ForwardSingleChar {
1
} else {
usize::MAX
},
));
} else if !self.is_at_end() {
let (elt, el) = self.active_edit_line();
self.update_buff_pos(elt, Some(el.position() + 1));
}
}
rl::ForwardCharPassive => {
if !self.is_at_end() {
let (elt, el) = self.active_edit_line();
if elt == EditableLineTag::SearchField || !self.is_navigating_pager_contents() {
self.update_buff_pos(elt, Some(el.position() + 1));
}
}
}
rl::ForwardWordEmacs
| rl::ForwardBigwordEmacs
| rl::KillWordEmacs
| rl::KillBigwordEmacs
| rl::NextdOrForwardWordEmacs => {
if c == rl::NextdOrForwardWordEmacs && self.command_line.is_empty() {
self.eval_bind_cmd(L!("nextd"));
self.schedule_prompt_repaint();
return;
}
let (elt, el) = self.active_edit_line();
let is_kill = matches!(c, rl::KillWordEmacs | rl::KillBigwordEmacs);
let style = match c {
rl::ForwardWordEmacs | rl::NextdOrForwardWordEmacs | rl::KillWordEmacs => {
MoveWordStyle::Punctuation
}
rl::ForwardBigwordEmacs | rl::KillBigwordEmacs => MoveWordStyle::Whitespace,
_ => unreachable!(),
};
let is_at_word_end = el.position() + 1 < el.len() && {
let class = match style {
MoveWordStyle::Punctuation => WordCharClass::from_char,
MoveWordStyle::Whitespace => bigword_class,
MoveWordStyle::PathComponents => unreachable!(),
};
let pos = el.position();
let cur_class = class(el.at(pos));
let next_class = class(el.at(pos + 1));
!matches!(cur_class, WordCharClass::Blank | WordCharClass::Newline)
&& next_class != cur_class
};
if self.is_at_autosuggestion() {
self.accept_autosuggestion(AutosuggestionPortion::PerMoveWordStyle {
style,
to_word_end: true,
});
} else if is_at_word_end {
if is_kill {
self.delete_char( false);
} else {
let pos = el.position();
self.update_buff_pos(elt, Some(pos + 1));
}
} else {
self.data.move_word(
elt,
MoveWordDir::Right,
is_kill,
style,
self.rls().last_cmd != Some(c),
true,
);
if !is_kill {
let el = self.edit_line(elt);
let pos = el.position();
if pos < el.len() {
self.update_buff_pos(elt, Some(pos + 1));
}
}
}
}
rl::BackwardKillWord | rl::BackwardKillPathComponent | rl::BackwardKillBigword => {
let style = match c {
rl::BackwardKillWord => MoveWordStyle::Punctuation,
rl::BackwardKillBigword => MoveWordStyle::Whitespace,
rl::BackwardKillPathComponent => MoveWordStyle::PathComponents,
_ => unreachable!(),
};
let newv = !matches!(
self.rls().last_cmd,
Some(rl::BackwardKillWord | rl::BackwardKillBigword)
);
let elt = self.active_edit_line_tag();
self.data.move_word(
elt,
MoveWordDir::Left,
true,
style,
newv,
false,
);
}
rl::KillWordVi | rl::KillBigwordVi | rl::KillPathComponent => {
let style = match c {
rl::KillWordVi => MoveWordStyle::Punctuation,
rl::KillBigwordVi => MoveWordStyle::Whitespace,
rl::KillPathComponent => MoveWordStyle::PathComponents,
_ => unreachable!(),
};
self.data.move_word(
self.active_edit_line_tag(),
MoveWordDir::Right,
true,
style,
self.rls().last_cmd != Some(c),
false,
);
}
rl::KillInnerWord | rl::KillInnerBigWord => {
let style = match c {
rl::KillInnerBigWord => MoveWordStyle::Whitespace,
rl::KillInnerWord => MoveWordStyle::Punctuation,
_ => unreachable!(),
};
let elt = self.active_edit_line_tag();
let newv = self.rls().last_cmd != Some(c);
self.data.delete_inner_word(elt, style, newv);
}
rl::KillAWord | rl::KillABigWord => {
let style = match c {
rl::KillABigWord => MoveWordStyle::Whitespace,
rl::KillAWord => MoveWordStyle::Punctuation,
_ => unreachable!(),
};
let elt = self.active_edit_line_tag();
let newv = self.rls().last_cmd != Some(c);
self.data.delete_a_word(elt, style, newv);
}
rl::BackwardKillToken => {
let Some(new_position) = self.backward_token() else {
return;
};
let (elt, _el) = self.active_edit_line();
if elt == EditableLineTag::Commandline {
self.suppress_autosuggestion = true;
}
let (elt, el) = self.active_edit_line();
self.data.kill(
elt,
new_position..el.position(),
Kill::Prepend,
self.rls().last_cmd != Some(rl::BackwardKillToken),
);
}
rl::BackwardToken => {
let Some(new_position) = self.backward_token() else {
return;
};
let (elt, _el) = self.active_edit_line();
self.update_buff_pos(elt, Some(new_position));
}
rl::KillToken => {
let Some(new_position) = self.forward_token(false) else {
return;
};
let (elt, _el) = self.active_edit_line();
if elt == EditableLineTag::Commandline {
self.suppress_autosuggestion = true;
}
let (elt, el) = self.active_edit_line();
self.data.kill(
elt,
el.position()..new_position,
Kill::Append,
self.rls().last_cmd != Some(rl::KillToken),
);
}
rl::ForwardToken => {
if self.is_at_autosuggestion() {
let Some(new_position) = self.forward_token(true) else {
return;
};
let (_elt, el) = self.active_edit_line();
let search_string_range = range_of_line_at_cursor(el.text(), el.position());
self.accept_autosuggestion(AutosuggestionPortion::Count(
new_position - search_string_range.end,
));
} else if !self.is_at_end() {
let Some(new_position) = self.forward_token(false) else {
return;
};
let (elt, _el) = self.active_edit_line();
self.update_buff_pos(elt, Some(new_position));
}
}
rl::BackwardWord
| rl::BackwardWordEnd
| rl::BackwardPathComponent
| rl::BackwardBigword
| rl::BackwardBigwordEnd
| rl::PrevdOrBackwardWord => {
if c == rl::PrevdOrBackwardWord && self.command_line.is_empty() {
self.eval_bind_cmd(L!("prevd"));
self.schedule_prompt_repaint();
return;
}
let to_word_end = matches!(c, rl::BackwardWordEnd | rl::BackwardBigwordEnd);
let style = match c {
rl::BackwardWord | rl::BackwardWordEnd | rl::PrevdOrBackwardWord => {
MoveWordStyle::Punctuation
}
rl::BackwardBigword | rl::BackwardBigwordEnd => MoveWordStyle::Whitespace,
rl::BackwardPathComponent => MoveWordStyle::PathComponents,
_ => unreachable!(),
};
self.data.move_word(
self.active_edit_line_tag(),
MoveWordDir::Left,
false,
style,
false,
to_word_end,
);
}
rl::ForwardWordVi
| rl::ForwardBigwordVi
| rl::ForwardWordEnd
| rl::ForwardBigwordEnd
| rl::ForwardPathComponent => {
let style = match c {
rl::ForwardWordVi | rl::ForwardWordEnd => MoveWordStyle::Punctuation,
rl::ForwardBigwordVi | rl::ForwardBigwordEnd => MoveWordStyle::Whitespace,
rl::ForwardPathComponent => MoveWordStyle::PathComponents,
_ => unreachable!(),
};
let to_word_end = matches!(c, rl::ForwardWordEnd | rl::ForwardBigwordEnd);
if self.is_at_autosuggestion() {
self.accept_autosuggestion(AutosuggestionPortion::PerMoveWordStyle {
style,
to_word_end,
});
} else if !self.is_at_end() {
let (elt, _el) = self.active_edit_line();
self.move_word(
elt,
MoveWordDir::Right,
false,
style,
false,
to_word_end,
);
}
}
rl::BeginningOfHistory | rl::EndOfHistory => {
let up = c == rl::BeginningOfHistory;
if self.is_navigating_pager_contents() {
self.select_completion_in_direction(
if up {
SelectionMotion::PageNorth
} else {
SelectionMotion::PageSouth
},
false,
);
} else if self.history_search.active() {
if up {
self.history_search.go_to_oldest();
} else {
self.history_search.go_to_present();
}
self.update_command_line_from_history_search();
}
}
rl::UpLine | rl::DownLine => {
if self.is_navigating_pager_contents() {
let direction = if c == rl::DownLine {
SelectionMotion::South
} else if self.selection_is_at_top() {
SelectionMotion::Deselect
} else {
SelectionMotion::North
};
self.select_completion_in_direction(direction, false);
} else if !self.pager.is_empty() {
self.select_completion_in_direction(
if c == rl::DownLine {
SelectionMotion::South
} else {
SelectionMotion::North
},
false,
);
} else {
let (elt, el) = self.active_edit_line();
let line_old =
i32::try_from(get_line_from_offset(el.text(), el.position())).unwrap();
let line_new = if c == rl::UpLine {
line_old - 1
} else {
line_old + 1
};
let line_count = lineno(el.text(), el.len()) - 1;
if (0..=i32::try_from(line_count).unwrap()).contains(&line_new) {
let indents = compute_indents(el.text());
let base_pos_new = get_offset_from_line(el.text(), line_new).unwrap();
let base_pos_old = get_offset_from_line(el.text(), line_old).unwrap();
let indent_old = indents[std::cmp::min(indents.len() - 1, base_pos_old)];
let indent_new = indents[std::cmp::min(indents.len() - 1, base_pos_new)];
let indent_old = isize::try_from(indent_old).unwrap();
let indent_new = isize::try_from(indent_new).unwrap();
let line_offset_old =
isize::try_from(el.position() - base_pos_old).unwrap();
let total_offset_new = get_offset(
el.text(),
line_new,
line_offset_old
- isize::try_from(SPACES_PER_INDENT).unwrap()
* (indent_new - indent_old),
);
self.update_buff_pos(elt, total_offset_new);
}
}
}
rl::SuppressAutosuggestion => {
self.suppress_autosuggestion = true;
let success = self.is_at_line_with_autosuggestion();
self.autosuggestion.clear();
self.input_data.function_set_status(success);
}
rl::AcceptAutosuggestion => {
let success = self.is_at_line_with_autosuggestion();
if success {
self.accept_autosuggestion(AutosuggestionPortion::Count(usize::MAX));
}
self.input_data.function_set_status(success);
}
rl::TransposeChars => {
let (elt, el) = self.active_edit_line();
if el.len() < 2 {
return;
}
if el.position() == el.len() {
self.update_buff_pos(elt, Some(el.position() - 1));
}
let (elt, el) = self.active_edit_line();
if el.position() > 0 {
let mut local_cmd = el.text().to_owned();
local_cmd
.as_char_slice_mut()
.swap(el.position(), el.position() - 1);
self.data
.set_command_line_and_position(elt, local_cmd, el.position() + 1);
}
}
rl::TransposeWords => {
let (elt, el) = self.active_edit_line();
let buff_pos = el.position()
+ el.text()[el.position()..]
.chars()
.take_while(|c| !c.is_alphanumeric())
.count();
self.update_buff_pos(elt, Some(buff_pos));
let (elt, el) = self.active_edit_line();
let text = el.text();
let (mut tok, mut prev_tok) = get_token_extent(text, el.position());
if tok.start == el.len() {
let pos = prev_tok.end;
(tok, prev_tok) = get_token_extent(text, pos);
}
if !prev_tok.is_empty() && !tok.is_empty() && tok.start > prev_tok.start {
let prev = &text[prev_tok.clone()];
let sep = &text[prev_tok.end..tok.start];
let trail = &text[tok.end..];
let mut new_text = text[..prev_tok.start].to_owned();
new_text.push_utfstr(&text[tok.clone()]);
new_text.push_utfstr(sep);
new_text.push_utfstr(prev);
new_text.push_utfstr(trail);
self.set_command_line_and_position(elt, new_text, tok.end);
}
}
rl::TogglecaseChar => {
let (elt, el) = self.active_edit_line();
let buff_pos = el.position();
if buff_pos != el.len() {
let chr = el.text().as_char_slice()[buff_pos];
let make_uppercase = chr.is_lowercase();
let replacement = if make_uppercase {
WString::from_iter(chr.to_uppercase())
} else {
WString::from_iter(chr.to_lowercase())
};
self.replace_substring(elt, buff_pos..buff_pos + 1, replacement);
self.update_buff_pos(elt, Some(buff_pos));
}
}
rl::TogglecaseSelection => {
let (elt, el) = self.active_edit_line();
if let Some(selection) = self.get_selection() {
let mut replacement = WString::new();
for pos in selection.clone() {
if pos >= el.len() {
break;
}
let chr = el.text().as_char_slice()[pos];
let make_uppercase = chr.is_lowercase();
if make_uppercase {
replacement.extend(chr.to_uppercase());
} else {
replacement.extend(chr.to_lowercase());
}
}
let buff_pos = el.position();
self.replace_substring(elt, selection, replacement);
self.update_buff_pos(elt, Some(buff_pos));
}
}
rl::UpcaseSelection | rl::DowncaseSelection => {
let (elt, el) = self.active_edit_line();
if let Some(selection) = self.get_selection() {
let text = &el.text().as_char_slice()[selection.clone()];
let replacement = if c == rl::UpcaseSelection {
WString::from_iter(text.iter().flat_map(|c| c.to_uppercase()))
} else {
WString::from_iter(text.iter().flat_map(|c| c.to_lowercase()))
};
let buff_pos = el.position();
self.replace_substring(elt, selection, replacement);
self.update_buff_pos(elt, Some(buff_pos));
}
}
rl::UpcaseWord | rl::DowncaseWord | rl::CapitalizeWord => {
let (elt, el) = self.active_edit_line();
let mut capitalized_first = false;
let mut pos = el.position();
let init_pos = pos;
self.move_word(
elt,
MoveWordDir::Right,
false,
MoveWordStyle::Punctuation,
false,
false,
);
let (elt, el) = self.active_edit_line();
let mut replacement = WString::new();
while pos
< if self.cursor_selection_mode == CursorSelectionMode::Inclusive
&& self.is_at_end()
{
el.len()
} else {
el.position()
}
{
let chr = el.text().as_char_slice()[pos];
let make_uppercase = if c == rl::CapitalizeWord {
!capitalized_first && chr.is_alphanumeric()
} else {
c == rl::UpcaseWord
};
if make_uppercase {
replacement.extend(chr.to_uppercase());
} else {
replacement.extend(chr.to_lowercase());
}
capitalized_first = capitalized_first || make_uppercase;
pos += 1;
}
self.replace_substring(elt, init_pos..pos, replacement);
self.update_buff_pos(elt, None);
}
rl::BeginSelection => {
let mut selection = SelectionData::default();
let pos = self.command_line.position();
selection.begin = pos;
selection.start = pos;
selection.stop = pos
+ if self.cursor_selection_mode == CursorSelectionMode::Inclusive {
1
} else {
0
};
self.selection = Some(selection);
}
rl::EndSelection => {
self.selection = None;
}
rl::SwapSelectionStartStop => {
let position = self.command_line.position();
let Some(selection) = &mut self.selection else {
return;
};
let tmp = selection.begin;
selection.begin = position;
selection.start = position;
self.update_buff_pos(self.active_edit_line_tag(), Some(tmp));
}
rl::KillSelection => {
let newv = self.rls().last_cmd != Some(rl::KillSelection);
if let Some(selection) = self.get_selection() {
self.kill(EditableLineTag::Commandline, selection, Kill::Append, newv);
}
if self.is_at_end() {
self.update_buff_pos(self.active_edit_line_tag(), None);
}
}
rl::InsertLineOver => {
let elt = loop {
let (elt, el) = self.active_edit_line();
if el.position() == 0 || el.text().as_char_slice()[el.position() - 1] == '\n' {
break elt;
}
if !self.update_buff_pos(elt, Some(el.position() - 1)) {
break elt;
}
};
self.insert_char(elt, '\n');
let (elt, el) = self.active_edit_line();
self.update_buff_pos(elt, Some(el.position() - 1));
}
rl::InsertLineUnder => {
let elt = loop {
let (elt, el) = self.active_edit_line();
if el.position() == el.len() || el.text().as_char_slice()[el.position()] == '\n'
{
break elt;
}
if !self.update_buff_pos(elt, Some(el.position() + 1)) {
break elt;
}
};
self.insert_char(elt, '\n');
}
rl::ForwardJump | rl::BackwardJump | rl::ForwardJumpTill | rl::BackwardJumpTill => {
let direction = match c {
rl::ForwardJump | rl::ForwardJumpTill => JumpDirection::Forward,
rl::BackwardJump | rl::BackwardJumpTill => JumpDirection::Backward,
_ => unreachable!(),
};
let precision = match c {
rl::ForwardJump | rl::BackwardJump => JumpPrecision::To,
rl::ForwardJumpTill | rl::BackwardJumpTill => JumpPrecision::Till,
_ => unreachable!(),
};
let (elt, _el) = self.active_edit_line();
if let Some(target) = self.function_pop_arg() {
let success =
self.jump_and_remember_last_jump(direction, precision, elt, target, false);
self.input_data.function_set_status(success);
}
}
rl::JumpToMatchingBracket | rl::JumpTillMatchingBracket => {
let (elt, _el) = self.active_edit_line();
let el = self.edit_line(elt);
let l_brackets = ['(', '[', '{'];
let r_brackets = [')', ']', '}'];
let cursor = el.position();
let precision = match c {
rl::JumpToMatchingBracket => JumpPrecision::To,
rl::JumpTillMatchingBracket => JumpPrecision::Till,
_ => unreachable!(),
};
let jump_from_pos = match c {
_ if l_brackets.contains(&el.at(cursor))
|| r_brackets.contains(&el.at(cursor)) =>
{
Some(cursor)
}
rl::JumpTillMatchingBracket
if cursor > 0 && l_brackets.contains(&el.at(cursor - 1)) =>
{
Some(cursor - 1)
}
rl::JumpTillMatchingBracket
if cursor < el.len() && r_brackets.contains(&el.at(cursor + 1)) =>
{
Some(cursor + 1)
}
_ => None,
};
let success = match jump_from_pos {
Some(jump_from_pos) => {
let l_bracket = match el.at(jump_from_pos) {
'(' | ')' => '(',
'[' | ']' => '[',
'{' | '}' => '{',
_ => unreachable!(),
};
let r_bracket = match l_bracket {
'(' => ')',
'[' => ']',
'{' => '}',
_ => unreachable!(),
};
self.jump_to_matching_bracket(
precision,
elt,
jump_from_pos,
l_bracket,
r_bracket,
)
}
None => self.jump(
JumpDirection::Forward,
precision,
elt,
r_brackets.to_vec(),
false,
),
};
self.input_data.function_set_status(success);
}
rl::RepeatJump => {
let (elt, _el) = self.active_edit_line();
let mut success = false;
if let Some(target) = self.last_jump_target {
success = self.data.jump_and_remember_last_jump(
self.data.last_jump_direction,
self.data.last_jump_precision,
elt,
target,
true,
);
}
self.input_data.function_set_status(success);
}
rl::ReverseRepeatJump => {
let (elt, _el) = self.active_edit_line();
let mut success = false;
let original_dir = self.last_jump_direction;
let dir = if self.last_jump_direction == JumpDirection::Forward {
JumpDirection::Backward
} else {
JumpDirection::Forward
};
if let Some(last_target) = self.last_jump_target {
success = self.data.jump_and_remember_last_jump(
dir,
self.data.last_jump_precision,
elt,
last_target,
true,
);
}
self.last_jump_direction = original_dir;
self.input_data.function_set_status(success);
}
rl::ExpandAbbr => {
if self.expand_abbreviation_at_cursor(1) {
self.input_data.function_set_status(true);
} else {
self.input_data.function_set_status(false);
}
}
rl::Undo | rl::Redo => {
let (elt, _el) = self.active_edit_line();
let ok = if c == rl::Undo {
self.undo(elt)
} else {
self.redo(elt)
};
if !ok {
self.flash(0..self.command_line.len());
return;
}
self.suppress_autosuggestion = false;
if elt == EditableLineTag::Commandline {
self.clear_pager();
}
self.update_buff_pos(elt, None);
}
rl::BeginUndoGroup => {
let (_elt, el) = self.active_edit_line_mut();
el.begin_edit_group();
}
rl::EndUndoGroup => {
let (_elt, el) = self.active_edit_line_mut();
el.end_edit_group();
}
rl::ClearScreenAndRepaint => {
self.clear_screen_and_repaint();
}
rl::ScrollbackPush => {
self.screen.push_to_scrollback();
}
rl::SelfInsert | rl::SelfInsertNotFirst | rl::GetKey | rl::FuncAnd | rl::FuncOr => {
}
}
}
fn clear_screen_and_repaint(&mut self) {
self.parser.libdata_mut().is_repaint = true;
Outputter::stdoutput()
.borrow_mut()
.write_command(ClearScreen);
self.screen
.set_position_in_viewport("screen clear", Some(0));
self.screen.reset_line( true);
self.layout_and_repaint(L!("readline"));
self.exec_prompt(true, false);
self.screen.reset_line( true);
self.layout_and_repaint(L!("readline"));
self.force_exec_prompt_and_repaint = false;
self.parser.libdata_mut().is_repaint = false;
}
fn backward_token(&mut self) -> Option<usize> {
let (_elt, el) = self.active_edit_line();
let pos = el.position();
if pos == 0 {
return None;
}
let (tok, prev_tok) = get_token_extent(el.text(), el.position());
let new_position = if tok.start == pos {
if prev_tok.start == pos {
let cmdsub = get_cmdsubst_extent(el.text(), prev_tok.start);
cmdsub.start.saturating_sub(1)
} else {
prev_tok.start
}
} else {
tok.start
};
Some(new_position)
}
fn forward_token(&self, autosuggest: bool) -> Option<usize> {
let (elt, el) = self.active_edit_line();
let pos = el.position();
let buffer = if autosuggest {
assert_eq!(elt, EditableLineTag::Commandline);
assert!(self.is_at_line_with_autosuggestion());
let autosuggestion = &self.autosuggestion;
Cow::Owned(combine_command_and_autosuggestion(
el.text(),
autosuggestion.search_string_range.clone(),
&autosuggestion.text,
))
} else {
Cow::Borrowed(el.text())
};
if pos == buffer.len() {
return None;
}
let cmdsubst_range = get_cmdsubst_extent(&buffer, pos);
for token in Tokenizer::new(&buffer[cmdsubst_range.clone()], TOK_ACCEPT_UNFINISHED) {
if token.type_ != TokenType::String {
continue;
}
let tok_end = cmdsubst_range.start + token.end();
if tok_end > pos {
return Some(tok_end);
}
}
Some(el.len())
}
}
fn text_ends_in_comment(text: &wstr) -> bool {
Tokenizer::new(text, TOK_ACCEPT_UNFINISHED | TOK_SHOW_COMMENTS)
.last()
.is_some_and(|token| token.type_ == TokenType::Comment)
}
impl<'a> Reader<'a> {
fn handle_execute(&mut self) -> bool {
if self.is_navigating_pager_contents() {
let search_field = &self.data.pager.search_field_line;
if self.history_pager.is_some() && self.pager.selected_completion_idx.is_none() {
let range = 0..self.command_line.len();
let offset_from_end = search_field.len() - search_field.position();
let mut cursor = self.command_line.position();
let updated = replace_line_at_cursor(
self.command_line.text(),
&mut cursor,
search_field.text(),
);
self.replace_substring(EditableLineTag::Commandline, range, updated);
self.command_line.set_position(cursor - offset_from_end);
} else if self
.pager
.selected_completion(&self.data.current_page_rendering)
.is_none()
{
let failed_search = search_field.text().to_owned();
self.insert_string(EditableLineTag::Commandline, &failed_search);
}
self.clear_pager();
return true;
}
self.clear_pager();
let elt = EditableLineTag::Commandline;
let el = &mut self.command_line;
let mut continue_on_next_line = false;
if el.position() >= el.len() {
continue_on_next_line =
is_backslashed(el.text(), el.position()) && !text_ends_in_comment(el.text());
} else {
if is_backslashed(el.text(), el.position())
&& el.text().as_char_slice()[el.position()].is_whitespace()
{
continue_on_next_line = true;
} else if is_backslashed(el.text(), el.len()) && !text_ends_in_comment(el.text()) {
el.set_position(el.len());
continue_on_next_line = true;
}
}
if continue_on_next_line {
self.insert_char(elt, '\n');
return true;
}
let test_res = self.expand_for_execute();
if let Err(p) = test_res {
if p.error {
return false;
} else if p.incomplete {
self.insert_char(elt, '\n');
return true;
}
unreachable!();
}
self.autosuggestion.clear();
self.add_to_history();
self.rls_mut().finished = true;
self.command_line.pending_position = Some(self.command_line.position());
self.update_buff_pos(elt, Some(self.command_line_len()));
true
}
fn expand_for_execute(&mut self) -> Result<(), ParseIssue> {
let el = &self.command_line;
let mut test_res = Ok(());
if self.conf.syntax_check_ok {
test_res = reader_shell_test(self.parser, el.text());
if test_res.is_err_and(|p| p.error) {
return test_res;
}
}
if self.expand_abbreviation_at_cursor(0) {
self.super_highlight_me_plenty();
if self.conf.syntax_check_ok {
let el = &self.command_line;
test_res = reader_shell_test(self.parser, el.text());
}
}
test_res
}
}
impl ReaderData {
fn clear_pager(&mut self) {
self.pager.clear();
self.history_pager = None;
self.clear(EditableLineTag::SearchField);
self.command_line_transient_edit = None;
}
fn get_selection(&self) -> Option<Range<usize>> {
let selection = self.selection?;
let start = std::cmp::min(selection.start, self.command_line.len());
let end = std::cmp::min(selection.stop, self.command_line.len());
if start == end {
return None;
}
Some(start..end)
}
fn selection_is_at_top(&self) -> bool {
let pager = &self.pager;
let row = pager.get_selected_row(&self.current_page_rendering);
if row.is_some_and(|row| row != 0) {
return false;
}
let col = pager.get_selected_column(&self.current_page_rendering);
col.is_none_or(|col| col == 0)
}
}
impl<'a> Reader<'a> {
fn flash(&mut self, mut flash_range: Range<usize>) {
let now = Instant::now();
if self
.last_flash
.is_some_and(|last_flash| now.duration_since(last_flash) < Duration::from_millis(50))
|| flash_range.is_empty() && !self.flash_autosuggestion
{
self.last_flash = Some(now);
return;
}
let mut data = self.make_layout_data();
let saved_colors = data.colors.clone();
if flash_range.end > data.colors.len() {
flash_range.start = flash_range.start.min(data.colors.len());
flash_range.end = data.colors.len();
}
for color in &mut data.colors[flash_range] {
color.foreground = HighlightRole::search_match;
color.background = HighlightRole::search_match;
}
self.rendered_layout = data;
self.paint_layout(L!("flash"), false);
self.flash_autosuggestion = false;
std::thread::sleep(Duration::from_millis(100));
self.rendered_layout.colors = saved_colors;
self.paint_layout(L!("unflash"), false);
self.last_flash = Some(Instant::now());
}
}
impl ReaderData {
fn pager_selection_changed(&mut self) {
assert_is_main_thread();
let mut cursor_pos = self.cycle_cursor_pos;
if let Some(transient_edit) = self.command_line_transient_edit.take() {
if transient_edit == TransientEdit::Pager {
self.undo(EditableLineTag::Commandline);
}
}
if let Some(completion) = self.pager.selected_completion(&self.current_page_rendering) {
let new_cmd_line = completion_apply_to_command_line(
&OperationContext::background_interruptible(EnvStack::globals()), &completion.completion,
completion.flags,
&self.cycle_command_line,
&mut cursor_pos,
false,
false, );
if new_cmd_line != self.command_line.text() && new_cmd_line != self.cycle_command_line {
self.set_buffer_maintaining_pager(&new_cmd_line, cursor_pos);
self.command_line_transient_edit = Some(TransientEdit::Pager);
}
} else {
self.update_buff_pos(EditableLineTag::Commandline, None);
}
}
fn set_buffer_maintaining_pager(&mut self, new_cmd_line: &wstr, pos: usize) {
self.replace_substring(
EditableLineTag::Commandline,
0..self.command_line.len(),
new_cmd_line.to_owned(),
);
self.update_buff_pos(
EditableLineTag::Commandline,
Some(pos.min(new_cmd_line.len())),
);
self.history_search.reset();
}
fn select_completion_in_direction(
&mut self,
dir: SelectionMotion,
force_selection_change: bool,
) {
let selection_changed = self
.pager
.select_next_completion_in_direction(dir, &self.current_page_rendering);
if force_selection_change || selection_changed {
self.pager_selection_changed();
}
}
}
fn term_fix_oflag(modes: &mut Termios) {
modes.output_flags |= {
use termios::OutputFlags;
OutputFlags::OPOST
| OutputFlags::ONLCR
};
}
fn term_fix_shell_modes(modes: &mut Termios) {
modes.input_flags &= {
use termios::InputFlags;
!InputFlags::ICRNL
& !InputFlags::INLCR
};
modes.local_flags &= {
use termios::LocalFlags;
let echo = LocalFlags::ECHO;
let flusho = LocalFlags::FLUSHO;
let icanon = LocalFlags::ICANON;
let iexten = LocalFlags::IEXTEN;
!echo
& !icanon
& !iexten & !flusho
};
term_fix_oflag(modes);
let c_cc = &mut modes.control_chars;
c_cc[VMIN] = 1;
c_cc[VTIME] = 0;
let disabling_char = _POSIX_VDISABLE;
c_cc[VSUSP] = disabling_char;
c_cc[VQUIT] = disabling_char;
}
fn term_fix_external_modes(modes: &mut Termios) {
term_fix_oflag(modes);
modes.local_flags = {
use termios::LocalFlags;
let echo = LocalFlags::ECHO;
let flusho = LocalFlags::FLUSHO;
let icanon = LocalFlags::ICANON;
let iexten = LocalFlags::IEXTEN;
(modes.local_flags | echo | icanon | iexten) & !flusho
};
modes.input_flags = {
use termios::InputFlags;
let icrnl = InputFlags::ICRNL;
let inlcr = InputFlags::INLCR;
(modes.input_flags | icrnl) & !inlcr
};
}
fn term_donate(quiet: bool ) {
loop {
match tcsetattr(
unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) },
SetArg::TCSANOW,
&TTY_MODES_FOR_EXTERNAL_CMDS.lock().unwrap(),
) {
Ok(_) => (),
Err(nix::Error::EINTR) => continue,
Err(err) => {
if !quiet {
flog!(
warning,
wgettext!("Could not set terminal mode for new job")
);
perror_nix("tcsetattr", err);
}
break;
}
}
break;
}
}
pub fn term_copy_modes() {
let mut external_modes = tcgetattr(unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) })
.unwrap_or_else(|_| zeroed_termios());
term_fix_external_modes(&mut external_modes);
let external_flow_control = external_modes.input_flags & FLOW_CONTROL_FLAGS;
*TTY_MODES_FOR_EXTERNAL_CMDS.lock().unwrap() = external_modes;
let mut shell_modes = shell_modes();
shell_modes.input_flags =
(shell_modes.input_flags & !FLOW_CONTROL_FLAGS) | external_flow_control;
}
pub fn set_shell_modes(fd: RawFd, whence: &str) -> bool {
loop {
match tcsetattr(
unsafe { BorrowedFd::borrow_raw(fd) },
SetArg::TCSANOW,
&shell_modes(),
) {
Ok(_) => return true,
Err(nix::Error::EINTR) => continue,
Err(err) => {
perror_nix("tcsetattr", err);
flog!(
warning,
wgettext_fmt!("Failed to set terminal mode (%s)", whence)
);
return false;
}
}
}
}
pub fn set_shell_modes_temporarily(inputfd: RawFd) -> Option<Termios> {
unsafe { libc::tcsetpgrp(inputfd, libc::getpgrp()) };
let old_modes = tcgetattr(unsafe { BorrowedFd::borrow_raw(inputfd) }).ok();
set_shell_modes(inputfd, "readline");
old_modes
}
fn term_steal(copy_modes: bool) {
if copy_modes {
term_copy_modes();
}
set_shell_modes(STDIN_FILENO, "shell");
signal_safe_termsize_invalidate_tty();
}
fn acquire_tty_or_exit(shell_pgid: libc::pid_t) {
assert_is_main_thread();
let mut owner = unsafe { libc::tcgetpgrp(STDIN_FILENO) };
if owner == shell_pgid {
return;
}
if owner == getpid().as_raw() {
unsafe { libc::setpgid(owner, owner) };
return;
}
signal_reset_handlers();
let _restore_sigs = ScopeGuard::new((), |()| signal_set_handlers(true));
for loop_count in 0.. {
owner = unsafe { libc::tcgetpgrp(STDIN_FILENO) };
if owner == 0 {
unsafe { libc::tcsetpgrp(STDIN_FILENO, shell_pgid) };
owner = unsafe { libc::tcgetpgrp(STDIN_FILENO) };
}
if owner == -1 && errno().0 == ENOTTY {
if !is_interactive_session() {
break;
}
flog!(
warning,
wgettext!("No TTY for interactive shell (tcgetpgrp failed)")
);
perror("setpgid");
exit_without_destructors(1);
}
if owner == shell_pgid {
break; } else {
if check_for_orphaned_process(loop_count, shell_pgid) {
let pid = getpid();
flog!(
warning,
sprintf!(
"I appear to be an orphaned process, so I am quitting politely. My pid is %d.",
pid.as_raw()
)
);
exit_without_destructors(1);
}
if let Err(err) = killpg(nix::unistd::Pid::from_raw(shell_pgid), Signal::SIGTTIN) {
perror_nix("killpg(shell_pgid, SIGTTIN)", err);
exit_without_destructors(1);
}
}
}
}
fn reader_interactive_init() {
assert_is_main_thread();
let mut shell_pgid = getpgrp();
let shell_pid = getpid();
signal_set_handlers_once(true);
acquire_tty_or_exit(shell_pgid.as_raw());
if shell_pgid.as_raw() == 0 || (is_interactive_session() && shell_pgid != shell_pid) {
shell_pgid = shell_pid;
if let Err(e) = setpgid(shell_pgid, shell_pgid) {
if e != nix::errno::Errno::EPERM {
flog!(
error,
wgettext!("Failed to assign shell to its own process group")
);
perror_nix("setpgid", e);
exit_without_destructors(1);
}
}
if unsafe { libc::tcsetpgrp(STDIN_FILENO, shell_pgid.as_raw()) } == -1 {
flog!(error, wgettext!("Failed to take control of the terminal"));
perror("tcsetpgrp");
exit_without_destructors(1);
}
set_shell_modes(STDIN_FILENO, "startup");
}
signal_safe_termsize_invalidate_tty();
}
pub fn fish_is_unwinding_for_exit() -> bool {
let exit_state = EXIT_STATE.load(Ordering::Relaxed);
let exit_state: ExitState = unsafe { std::mem::transmute(exit_state) };
match exit_state {
ExitState::None => reader_received_exit_signal(),
ExitState::RunningHandlers => {
false
}
ExitState::FinishedHandlers => {
true
}
}
}
pub fn reader_write_title(
cmd: &wstr,
parser: &Parser,
reset_cursor_position: bool,
) {
fn write_title(
parser: &Parser,
out: &mut BufferedOutputter,
cmd: &wstr,
osc: fn(&[WString]) -> TerminalCommand<'_>,
function_name: &wstr,
fallback_title: Option<&wstr>,
) -> bool {
let mut title_function_call;
let mut title_command = fallback_title;
if function::exists(function_name, parser) {
title_function_call = function_name.to_owned();
if !cmd.is_empty() {
title_function_call.push(' ');
title_function_call.push_utfstr(&escape_string(
cmd,
EscapeStringStyle::Script(EscapeFlags::NO_QUOTED | EscapeFlags::NO_TILDE),
));
}
title_command = Some(&title_function_call);
}
let Some(title_command) = title_command else {
return false;
};
let mut title_buffer = vec![];
let _ = exec_subshell(
title_command,
parser,
Some(&mut title_buffer),
false,
);
if !title_buffer.is_empty() {
out.write_command(osc(&title_buffer));
return true;
}
false
}
let _scoped = parser.push_scope(|s| {
s.is_interactive = false;
s.suppress_fish_trace = true;
});
let mut out = BufferedOutputter::new(Outputter::stdoutput());
let mut written = false;
written |= write_title(
parser,
&mut out,
cmd,
|title_buffer| Osc0WindowTitle(title_buffer),
L!("fish_title"),
Some(DEFAULT_TITLE),
);
written |= write_title(
parser,
&mut out,
cmd,
|title_buffer| Osc1TabTitle(title_buffer),
L!("fish_tab_title"),
None,
);
out.reset_text_face();
if reset_cursor_position && written {
out.write_bytes(b"\r");
}
}
impl<'a> Reader<'a> {
fn exec_prompt_cmd(&self, prompt_cmd: &wstr, final_prompt: bool) -> Vec<WString> {
let mut output = vec![];
let prompt_cmd = if final_prompt && function::exists(prompt_cmd, self.parser) {
Cow::Owned(prompt_cmd.to_owned() + L!(" --final-rendering"))
} else {
Cow::Borrowed(prompt_cmd)
};
let _ = exec_subshell(&prompt_cmd, self.parser, Some(&mut output), false);
output
}
fn exec_prompt(&mut self, full_prompt: bool, final_prompt: bool) {
let _suppress_trace = self.parser.push_scope(|s| s.suppress_fish_trace = true);
let _noninteractive = self.parser.push_scope(|s| s.is_interactive = false);
let mut scoped_tty = TtyHandoff::new(reader_save_screen_state);
scoped_tty.disable_tty_protocols();
reader_update_termsize(self.parser);
self.mode_prompt_buff.clear();
if function::exists(MODE_PROMPT_FUNCTION_NAME, self.parser) {
self.mode_prompt_buff =
WString::from_iter(self.exec_prompt_cmd(MODE_PROMPT_FUNCTION_NAME, final_prompt));
}
if full_prompt {
self.left_prompt_buff.clear();
self.right_prompt_buff.clear();
if !self.conf.left_prompt_cmd.is_empty() {
let prompt_cmd = if self.conf.left_prompt_cmd != LEFT_PROMPT_FUNCTION_NAME
|| function::exists(&self.conf.left_prompt_cmd, self.parser)
{
&self.conf.left_prompt_cmd
} else {
DEFAULT_PROMPT
};
self.left_prompt_buff =
join_strings(&self.exec_prompt_cmd(prompt_cmd, final_prompt), '\n');
if let Some(prefix) = self.vars().get_unless_empty(L!("SHELL_PROMPT_PREFIX")) {
self.left_prompt_buff.insert_utfstr(0, &prefix.as_string());
}
if let Some(suffix) = self.vars().get_unless_empty(L!("SHELL_PROMPT_SUFFIX")) {
self.left_prompt_buff.push_utfstr(&suffix.as_string());
}
}
if !self.conf.right_prompt_cmd.is_empty()
&& (self.conf.right_prompt_cmd != RIGHT_PROMPT_FUNCTION_NAME
|| function::exists(&self.conf.right_prompt_cmd, self.parser))
{
self.right_prompt_buff = WString::from_iter(
self.exec_prompt_cmd(&self.conf.right_prompt_cmd, final_prompt),
);
}
}
reader_write_title(L!(""), self.parser, false);
job_reap(self.parser, true, None);
let exit_current_script = self.parser.libdata().exit_current_script;
self.exit_loop_requested |= exit_current_script;
self.parser.libdata_mut().exit_current_script = false;
}
}
#[derive(Default, Clone, PartialEq, Debug)]
pub(super) struct Autosuggestion {
text: WString,
search_string_range: Range<usize>,
icase_matched_codepoints: Option<usize>,
is_whole_item_from_history: bool,
}
impl Autosuggestion {
fn clear(&mut self) {
self.text.clear();
}
fn is_empty(&self) -> bool {
self.text.is_empty()
}
}
#[derive(Default)]
pub(super) struct AutosuggestionResult {
autosuggestion: Autosuggestion,
command_line: WString,
needs_load: Vec<WString>,
}
impl std::ops::Deref for AutosuggestionResult {
type Target = Autosuggestion;
fn deref(&self) -> &Self::Target {
&self.autosuggestion
}
}
impl AutosuggestionResult {
fn new(
command_line: WString,
search_string_range: Range<usize>,
text: WString,
icase_matched_codepoints: Option<usize>,
is_whole_item_from_history: bool,
) -> Self {
Self {
autosuggestion: Autosuggestion {
text,
search_string_range,
icase_matched_codepoints,
is_whole_item_from_history,
},
command_line,
needs_load: vec![],
}
}
fn search_string(&self) -> &wstr {
&self.command_line[self.search_string_range.clone()]
}
}
fn get_autosuggestion_performer(
parser: &Parser,
command_line: WString,
cursor_pos: usize,
history: Arc<History>,
) -> impl FnOnce() -> AutosuggestionResult + use<> {
let generation_count = read_generation_count();
let vars = parser.vars().snapshot();
let working_directory = parser.vars().get_pwd_slash();
move || {
assert_is_background_thread();
let nothing = AutosuggestionResult::default();
let ctx = get_bg_context(&vars, generation_count);
if ctx.check_cancel() {
return nothing;
}
let mut icase_history_result = None;
let line_range = range_of_line_at_cursor(&command_line, cursor_pos);
for (search_type, range) in [
(SearchType::Prefix, 0..command_line.len()),
(SearchType::LinePrefix, line_range.clone()),
] {
if range.is_empty() {
continue;
}
let search_string = &command_line[range.clone()];
if search_type == SearchType::LinePrefix {
let cursor_line_has_process_start = {
let mut tokens = vec![];
get_process_extent(&command_line, cursor_pos, Some(&mut tokens));
range_of_line_at_cursor(
&command_line,
get_process_first_token_offset(&command_line, cursor_pos)
.unwrap_or(cursor_pos),
) == range
};
if !cursor_line_has_process_start {
continue;
}
}
let mut searcher = HistorySearch::new_with(
history.clone(),
search_string.to_owned(),
search_type,
SearchFlags::IGNORE_CASE,
0,
);
while !ctx.check_cancel() && searcher.go_to_next_match(SearchDirection::Backward) {
let item = searcher.current_item();
let full = item.str();
let (suggested_range, icase) = if search_type == SearchType::Prefix {
let mut suggested_range =
full.starts_with(search_string).then_some(0..full.len());
let mut icase = false;
if suggested_range.is_none() && icase_history_result.is_none() {
icase = true;
suggested_range =
string_prefixes_string_case_insensitive(search_string, full)
.then_some(0..full.len());
}
(suggested_range, icase)
} else {
let newlines = full
.char_indices()
.filter_map(|(i, c)| (c == '\n').then_some(i));
let line_ranges = std::iter::once(0)
.chain(newlines.clone().map(|i| i + 1))
.zip(newlines.chain(std::iter::once(full.char_count())))
.map(|(start, end)| start..end);
let mut icase = false;
let mut suggested_range = line_ranges
.clone()
.find(|range| full[range.clone()].starts_with(search_string));
if suggested_range.is_none() && icase_history_result.is_none() {
icase = true;
suggested_range = line_ranges.into_iter().find(|range| {
string_prefixes_string_case_insensitive(
search_string,
&full[range.clone()],
)
});
}
(suggested_range, icase)
};
let Some(suggested_range) = suggested_range else {
assert!(
icase_history_result.is_some(),
"couldn't find line matching search {search_string:?} in history item {item:?} (did history search yield a bogus result?)"
);
continue;
};
if autosuggest_validate_from_history(
full,
suggested_range.clone(),
item.get_required_paths(),
&working_directory,
&ctx,
) {
let is_whole = suggested_range.len() == item.str().len();
let result = AutosuggestionResult::new(
command_line.clone(),
range.clone(),
full[suggested_range].into(),
icase.then(|| searcher.canon_term().char_count()),
is_whole,
);
if icase {
icase_history_result = Some(result);
} else {
return result;
}
}
}
}
if ctx.check_cancel() {
return nothing;
}
let Some(last_char) = command_line[line_range.clone()].chars().next_back() else {
return nothing;
};
let cursor_at_end =
cursor_pos == command_line.len() || command_line.as_char_slice()[cursor_pos] == '\n';
if !cursor_at_end && last_char.is_whitespace() {
return nothing;
}
if matches!(last_char, '\'' | '"') && cursor_at_end {
return nothing;
}
let complete_flags = CompletionRequestOptions::autosuggest();
let mut would_be_cursor = line_range.end;
let (mut completions, needs_load) =
complete(&command_line[..would_be_cursor], complete_flags, &ctx);
let suggestion = if completions.is_empty() {
if let Some(result) = icase_history_result {
return result;
}
WString::new()
} else {
sort_and_prioritize(&mut completions, complete_flags);
let comp = &completions[0];
if let (Some(result), CaseSensitivity::Smart | CaseSensitivity::Insensitive) =
(icase_history_result, comp.r#match.case_fold)
{
return result;
}
let full_line = completion_apply_to_command_line(
&OperationContext::background_interruptible(&vars),
&comp.completion,
comp.flags,
&command_line,
&mut would_be_cursor,
true,
false,
);
line_at_cursor(&full_line, would_be_cursor).to_owned()
};
let lowercase_char_count = lowercase(command_line[line_range.clone()].chars()).count();
let mut result = AutosuggestionResult::new(
command_line,
line_range,
suggestion,
Some(lowercase_char_count), false,
);
result.needs_load = needs_load;
result
}
}
enum AutosuggestionPortion {
Count(usize),
Line,
PerMoveWordStyle {
style: MoveWordStyle,
to_word_end: bool,
},
}
impl<'a> Reader<'a> {
fn can_autosuggest(&self) -> bool {
let (elt, el) = self.active_edit_line();
self.conf.autosuggest_ok
&& !self.suppress_autosuggestion
&& self.history_search.is_at_present()
&& elt == EditableLineTag::Commandline
&& el
.text()
.chars()
.any(|c| !matches!(c, ' ' | '\t' | '\r' | '\n' | '\x0B'))
}
fn autosuggest_completed(&mut self, result: AutosuggestionResult) {
assert_is_main_thread();
if result.command_line == self.data.in_flight_autosuggest_request {
self.data.in_flight_autosuggest_request.clear();
}
if result.command_line != self.command_line.text() {
return;
}
let mut loaded_new = false;
for to_load in &result.needs_load {
if complete_load(to_load, self.parser) {
flogf!(
complete,
"Autosuggest found new completions for %s, restarting",
to_load
);
loaded_new = true;
}
}
if loaded_new {
self.update_autosuggestion();
} else if !result.is_empty()
&& self.can_autosuggest()
&& string_prefixes_string_maybe_case_insensitive(
result.icase_matched_codepoints.is_some(),
result.search_string(),
&result.text,
)
{
self.autosuggestion = result.autosuggestion;
if self.is_repaint_needed(None) {
self.layout_and_repaint(L!("autosuggest"));
}
}
}
fn update_autosuggestion(&mut self) {
if !self.can_autosuggest() {
self.data.in_flight_autosuggest_request.clear();
self.data.autosuggestion.clear();
return;
}
let el = &self.data.command_line;
if self.is_at_line_with_autosuggestion() {
let autosuggestion = &self.autosuggestion;
assert!(string_prefixes_string_maybe_case_insensitive(
autosuggestion.icase_matched_codepoints.is_some(),
&el.text()[autosuggestion.search_string_range.clone()],
&autosuggestion.text
));
return;
}
if el.text() == self.in_flight_autosuggest_request {
return;
}
self.data.in_flight_autosuggest_request = el.text().to_owned();
flog!(reader_render, "Autosuggesting");
self.data.autosuggestion.clear();
let performer = get_autosuggestion_performer(
self.parser,
el.text().to_owned(),
el.position(),
self.history.clone(),
);
self.debouncers.autosuggestions.perform(performer);
}
fn is_at_end(&self) -> bool {
let (_elt, el) = self.active_edit_line();
match self.cursor_end_mode {
CursorEndMode::Exclusive => el.position() == el.len(),
CursorEndMode::Inclusive => el.position() + 1 >= el.len(),
}
}
fn is_at_autosuggestion(&self) -> bool {
if self.active_edit_line_tag() != EditableLineTag::Commandline {
return false;
}
let autosuggestion = &self.autosuggestion;
if autosuggestion.is_empty() {
return false;
}
let el = &self.command_line;
(match self.cursor_end_mode {
CursorEndMode::Exclusive => el.position(),
CursorEndMode::Inclusive => el.position() + 1,
}) == autosuggestion.search_string_range.end
}
fn is_at_line_with_autosuggestion(&self) -> bool {
if self.active_edit_line_tag() != EditableLineTag::Commandline {
return false;
}
let autosuggestion = &self.autosuggestion;
if autosuggestion.is_empty() {
return false;
}
let el = &self.command_line;
let search_string_range = &autosuggestion.search_string_range;
range_of_line_at_cursor(el.text(), el.position()) == *search_string_range || {
search_string_range.start == 0 && el.position() <= search_string_range.end
}
}
fn accept_autosuggestion(&mut self, amount: AutosuggestionPortion) {
assert!(self.is_at_line_with_autosuggestion());
self.clear_pager();
let autosuggestion = &self.autosuggestion;
let autosuggestion_text = &autosuggestion.text;
let search_string_range = autosuggestion.search_string_range.clone();
let (range, replacement) = match amount {
AutosuggestionPortion::Count(count) => {
if count == usize::MAX {
(search_string_range, autosuggestion_text.clone())
} else {
let pos = search_string_range.end;
let available = autosuggestion_text.len() - search_string_range.len();
let count = count.min(available);
if count == 0 {
return;
}
let start = autosuggestion_text.len() - available;
(
pos..pos,
autosuggestion_text[start..start + count].to_owned(),
)
}
}
AutosuggestionPortion::Line => {
let suggested = &autosuggestion_text[search_string_range.len()..];
let line_end = suggested
.chars()
.position(|c| c == '\n')
.unwrap_or(suggested.len());
if line_end == 0 {
return;
}
(
search_string_range.end..search_string_range.end,
suggested[..line_end].to_owned(),
)
}
AutosuggestionPortion::PerMoveWordStyle { style, to_word_end } => {
let state_machine_dir = if to_word_end {
MoveWordDir::Left
} else {
MoveWordDir::Right
};
let mut state = MoveWordStateMachine::new(style, state_machine_dir);
let have = search_string_range.len();
let mut want = have;
while want < autosuggestion_text.len() {
let consumed = state.consume_char(autosuggestion_text, want);
if consumed {
want += 1;
} else {
break;
}
}
(
search_string_range.end..search_string_range.end,
autosuggestion_text[have..want].to_owned(),
)
}
};
self.data
.replace_substring(EditableLineTag::Commandline, range, replacement);
self.update_buff_pos(self.active_edit_line_tag(), None);
}
}
#[derive(Default)]
pub(super) struct HighlightResult {
colors: Vec<HighlightSpec>,
text: WString,
}
fn get_highlight_performer(
parser: &Parser,
el: &EditableLine,
io_ok: bool,
) -> impl FnOnce() -> HighlightResult + use<> {
let vars = parser.vars().snapshot();
let generation_count = read_generation_count();
let position = el.position();
let text = el.text().to_owned();
move || {
if text.is_empty() {
return HighlightResult::default();
}
let ctx = get_bg_context(&vars, generation_count);
let mut colors = vec![];
highlight_shell(&text, &mut colors, &ctx, io_ok, Some(position));
HighlightResult { colors, text }
}
}
impl<'a> Reader<'a> {
fn highlight_completed(&mut self, result: HighlightResult) {
assert_is_main_thread();
self.in_flight_highlight_request.clear();
if result.text == self.command_line.text() {
assert_eq!(result.colors.len(), self.command_line.len());
if self.is_repaint_needed(Some(&result.colors)) {
self.command_line.set_colors(result.colors);
self.layout_and_repaint(L!("highlight"));
}
}
}
fn super_highlight_me_plenty(&mut self) {
if !self.conf.highlight_ok {
return;
}
if self.command_line.text() == self.in_flight_highlight_request {
return;
}
self.in_flight_highlight_request = self.command_line.text().to_owned();
flog!(reader_render, "Highlighting");
let highlight_performer =
get_highlight_performer(self.parser, &self.command_line, true);
self.debouncers.highlight.perform(highlight_performer);
}
fn finish_highlighting_before_exec(&mut self) {
if !self.conf.highlight_ok {
return;
}
let mut current_highlight_ok = false;
if self.in_flight_highlight_request.is_empty() {
current_highlight_ok = self.rendered_layout.text == self.command_line.text();
} else if self.in_flight_highlight_request == self.command_line.text() {
let mut now = Instant::now();
let deadline = now + HIGHLIGHT_TIMEOUT_FOR_EXECUTION;
while now < deadline {
let timeout = deadline - now;
if let Some(result) = self.debouncers.highlight.take_result_with_timeout(timeout) {
self.highlight_completed(result);
}
if self.in_flight_highlight_request.is_empty() {
break;
}
now = Instant::now();
}
current_highlight_ok = self.in_flight_highlight_request.is_empty();
}
if !current_highlight_ok {
let highlight_no_io =
get_highlight_performer(self.parser, &self.command_line, false);
self.highlight_completed(highlight_no_io());
}
}
}
pub(super) struct HistoryPagerResult {
matched_commands: Vec<Completion>,
range: Range<usize>,
first_shown: usize,
motion: Option<SelectionMotion>,
}
#[derive(Eq, PartialEq)]
pub(super) enum HistoryPagerInvocation {
Anew,
Advance,
Refresh,
}
fn history_pager_search(
history: &Arc<History>,
direction: SearchDirection,
motion: Option<SelectionMotion>,
history_index: usize,
search_string: &wstr,
) -> HistoryPagerResult {
let page_size = cmp::max(termsize_last().height() / 2 - 2, 12);
let mut completions = Vec::with_capacity(page_size);
let new_search = |search_type| {
HistorySearch::new_with(
history.clone(),
search_string.to_owned(),
search_type,
smartcase_flags(search_string),
history_index,
)
};
let mut search = new_search(SearchType::ContainsGlob);
if !search.go_to_next_match(direction) && !contains_wildcards(search_string) {
search = new_search(SearchType::ContainsSubsequence);
search.go_to_next_match(direction);
}
search.search_forward(match direction {
SearchDirection::Forward => page_size,
SearchDirection::Backward => 0,
});
let first_index = search.current_index();
let mut next_match_found = search.go_to_next_match(SearchDirection::Backward);
let first_shown = search.current_index();
while completions.len() < page_size && next_match_found {
let item = search.current_item();
completions.push(Completion::new(
item.str().to_owned(),
L!("").to_owned(),
StringFuzzyMatch::exact_match(),
CompleteFlags::REPLACES_LINE | CompleteFlags::DONT_ESCAPE | CompleteFlags::DONT_SORT,
));
next_match_found = search.go_to_next_match(SearchDirection::Backward);
}
let last_index = search.current_index();
let range = first_index..last_index;
if completions.is_empty() && range != (0..history.size() + 1) {
history_pager_search(
history,
SearchDirection::Forward,
Some(SelectionMotion::Prev),
history.size() + 1,
search_string,
)
} else {
HistoryPagerResult {
matched_commands: completions,
range,
first_shown,
motion,
}
}
}
impl ReaderData {
fn fill_history_pager(
&mut self,
why: HistoryPagerInvocation,
motion: Option<SelectionMotion>,
mut direction: SearchDirection,
) {
let index;
let mut old_pager_index = None;
match why {
HistoryPagerInvocation::Anew => {
assert_eq!(direction, SearchDirection::Backward);
index = 0;
}
HistoryPagerInvocation::Advance => {
let history_pager = self.history_pager.as_ref().unwrap();
index = match direction {
SearchDirection::Forward => history_pager.start + 1,
SearchDirection::Backward => history_pager.end - 1,
}
}
HistoryPagerInvocation::Refresh => {
let history_pager = self.history_pager.as_ref().unwrap();
direction = SearchDirection::Backward;
index = history_pager.start;
old_pager_index = self.pager.selected_completion_index();
}
}
let search_term = self.pager.search_field_line.text().to_owned();
let history = self.history.clone();
let search_term = search_term.clone();
let performer = move || -> iothreads::Callback {
let result = history_pager_search(&history, direction, motion, index, &search_term);
Box::new(move |r: &mut Reader| {
r.fill_history_pager_complete(result, why, old_pager_index);
})
};
self.debouncers.history_pager.perform(performer);
}
}
impl<'a> Reader<'a> {
fn fill_history_pager_complete(
&mut self,
result: HistoryPagerResult,
why: HistoryPagerInvocation,
old_pager_index: Option<usize>,
) {
let history_size = self.history.size();
let Some(history_pager) = self.history_pager.as_mut() else {
return; };
assert!(result.range.start < result.range.end);
*history_pager = result.range;
self.pager.extra_progress_text =
if !result.matched_commands.is_empty() && *history_pager != (0..history_size + 1) {
wgettext_fmt!(
"Items %u to %u of %u",
match history_pager.start {
0 => 1,
_ => result.first_shown,
},
history_pager.end - 1,
history_size
)
} else {
L!("").to_owned()
};
self.pager.set_completions(&result.matched_commands, false);
if why == HistoryPagerInvocation::Refresh {
self.pager.set_selected_completion_index(old_pager_index);
self.pager_selection_changed();
}
if let Some(motion) = result.motion {
self.select_completion_in_direction(motion, true);
}
self.super_highlight_me_plenty();
self.layout_and_repaint(L!("history-pager"));
}
}
fn expand_replacer(
range: SourceRange,
token: &wstr,
repl: &abbrs::Replacer,
parser: &Parser,
) -> Option<abbrs::Replacement> {
if !repl.is_function {
flogf!(
abbrs,
"Expanded literal abbreviation <%s> -> <%s>",
token,
&repl.replacement
);
return Some(abbrs::Replacement::new(
range,
repl.replacement.clone(),
repl.set_cursor_marker.clone(),
));
}
let mut cmd = escape(&repl.replacement);
cmd.push(' ');
cmd.push_utfstr(&escape(token));
let _not_interactive = parser.push_scope(|s| {
s.is_interactive = false;
s.readonly_commandline = true;
});
let mut outputs = vec![];
if exec_subshell(
&cmd,
parser,
Some(&mut outputs),
false,
)
.is_err()
{
return None;
}
let result = join_strings(&outputs, '\n');
flogf!(
abbrs,
"Expanded function abbreviation <%s> -> <%s>",
token,
result
);
Some(abbrs::Replacement::new(
range,
result,
repl.set_cursor_marker.clone(),
))
}
struct PositionedToken {
range: SourceRange,
is_cmd: bool,
}
fn extract_tokens(s: &wstr) -> Vec<PositionedToken> {
let ast_flags = ParseTreeFlags {
continue_after_error: true,
accept_incomplete_tokens: true,
leave_unterminated: true,
..ParseTreeFlags::default()
};
let ast = ast::parse(s, ast_flags, None);
let mut result = vec![];
let mut traversal = ast.walk();
while let Some(node) = traversal.next() {
if node.as_leaf().is_none() {
continue;
}
let range = node.source_range();
if range.length() == 0 {
continue;
}
let mut has_cmd_subs = false;
let mut cmdsub_cursor = range.start();
loop {
match locate_cmdsubst_range(
s,
&mut cmdsub_cursor,
true,
None,
None,
) {
MaybeParentheses::Error | MaybeParentheses::None => break,
MaybeParentheses::CommandSubstitution(parens) => {
if parens.start() >= range.end() {
break;
}
has_cmd_subs = true;
for mut t in extract_tokens(&s[parens.command()]) {
t.range.start += u32::try_from(parens.command().start).unwrap();
result.push(t);
}
}
}
}
if !has_cmd_subs {
let mut is_cmd = false;
if let Kind::DecoratedStatement(stmt) = traversal.parent(node).kind() {
is_cmd = is_same_node(node, &stmt.command);
}
result.push(PositionedToken { range, is_cmd });
}
}
result
}
pub fn reader_expand_abbreviation_at_cursor(
cmdline: &wstr,
cursor_pos: usize,
parser: &Parser,
) -> Option<abbrs::Replacement> {
let tokens = extract_tokens(cmdline);
let mut token: Option<_> = None;
let mut cmdtok: Option<_> = None;
for t in tokens.into_iter().rev() {
let range = t.range;
let is_cmd = t.is_cmd;
if t.range.contains_inclusive(cursor_pos) {
token = Some(t);
}
if token.is_some() && is_cmd {
cmdtok = Some(range);
break;
}
}
let token = token?;
let range = token.range;
let position = if token.is_cmd {
abbrs::Position::Command
} else {
abbrs::Position::Anywhere
};
let cmd = if !token.is_cmd {
cmdtok.map(|t| &cmdline[Range::<usize>::from(t)])
} else {
None
};
let token_str = &cmdline[Range::<usize>::from(range)];
let replacers = abbrs_match(token_str, position, cmd.unwrap_or(L!("")));
for replacer in replacers {
if let Some(replacement) = expand_replacer(range, token_str, &replacer, parser) {
return Some(replacement);
}
}
None
}
impl<'a> Reader<'a> {
fn expand_abbreviation_at_cursor(&mut self, cursor_backtrack: usize) -> bool {
let (elt, el) = self.active_edit_line();
if self.conf.expand_abbrev_ok && elt == EditableLineTag::Commandline {
let cursor_pos = el.position().saturating_sub(cursor_backtrack);
if let Some(replacement) =
reader_expand_abbreviation_at_cursor(el.text(), cursor_pos, self.parser)
{
self.push_edit(elt, Edit::new(replacement.range.into(), replacement.text));
self.update_buff_pos(elt, replacement.cursor);
return true;
}
}
false
}
}
fn command_ends_paging(c: ReadlineCmd, focused_on_search_field: bool) -> bool {
#[allow(non_camel_case_types)]
type rl = ReadlineCmd;
match c {
rl::HistoryPrefixSearchBackward
| rl::HistoryPrefixSearchForward
| rl::HistorySearchBackward
| rl::HistorySearchForward
| rl::HistoryTokenSearchBackward
| rl::HistoryTokenSearchForward
| rl::HistoryLastTokenSearchBackward
| rl::HistoryLastTokenSearchForward
| rl::AcceptAutosuggestion
| rl::DeleteOrExit
| rl::CancelCommandline
| rl::ClearCommandline
| rl::Cancel =>
{
true
}
rl::Complete
| rl::CompleteAndSearch
| rl::HistoryPager
| rl::BackwardChar
| rl::BackwardCharPassive
| rl::ForwardChar
| rl::ForwardCharPassive
| rl::ForwardSingleChar
| rl::UpLine
| rl::DownLine
| rl::Repaint
| rl::SuppressAutosuggestion
| rl::BeginningOfHistory
| rl::EndOfHistory =>
{
false
}
rl::Execute =>
{
false
}
rl::BeginningOfLine
| rl::EndOfLine
| rl::ForwardBigwordVi
| rl::ForwardWordVi
| rl::KillBigwordVi
| rl::KillWordVi
| rl::ForwardWordEmacs
| rl::ForwardBigwordEmacs
| rl::KillWordEmacs
| rl::KillBigwordEmacs
| rl::BackwardWord
| rl::BackwardBigword
| rl::ForwardToken
| rl::BackwardToken
| rl::NextdOrForwardWordEmacs
| rl::PrevdOrBackwardWord
| rl::DeleteChar
| rl::BackwardDeleteChar
| rl::KillLine
| rl::Yank
| rl::YankPop
| rl::BackwardKillLine
| rl::KillWholeLine
| rl::KillInnerLine
| rl::KillToken
| rl::BackwardKillWord
| rl::BackwardKillPathComponent
| rl::BackwardKillBigword
| rl::BackwardKillToken
| rl::SelfInsert
| rl::GetKey
| rl::SelfInsertNotFirst
| rl::TransposeChars
| rl::TransposeWords
| rl::UpcaseWord
| rl::DowncaseWord
| rl::CapitalizeWord
| rl::BeginningOfBuffer
| rl::EndOfBuffer
| rl::Undo
| rl::Redo =>
{
!focused_on_search_field
}
_ => false,
}
}
fn command_ends_history_search(c: ReadlineCmd) -> bool {
#[allow(non_camel_case_types)]
type rl = ReadlineCmd;
#[allow(deprecated)]
!matches!(
c,
rl::HistoryPrefixSearchBackward
| rl::HistoryPrefixSearchForward
| rl::HistorySearchBackward
| rl::HistorySearchForward
| rl::HistoryTokenSearchBackward
| rl::HistoryTokenSearchForward
| rl::HistoryLastTokenSearchBackward
| rl::HistoryLastTokenSearchForward
| rl::HistoryDelete
| rl::HistoryPagerDelete
| rl::BeginningOfHistory
| rl::EndOfHistory
| rl::ScrollbackPush
| rl::ClearScreenAndRepaint
| rl::Repaint
| rl::ForceRepaint
)
}
fn check_for_orphaned_process(loop_count: usize, shell_pgid: libc::pid_t) -> bool {
let mut we_think_we_are_orphaned = false;
if loop_count % 64 == 0 && unsafe { libc::kill(shell_pgid, 0) } < 0 && errno().0 == ESRCH {
we_think_we_are_orphaned = true;
}
if !we_think_we_are_orphaned && loop_count % 128 == 0 {
unsafe extern "C" {
unsafe fn ctermid(buf: *mut c_char) -> *mut c_char;
}
let tty = unsafe { ctermid(std::ptr::null_mut()) };
if tty.is_null() {
perror("ctermid");
exit_without_destructors(1);
}
let tty_fd = {
let res = unsafe { libc::open(tty, O_RDONLY | O_NONBLOCK) };
if res < 0 {
perror("open");
exit_without_destructors(1);
}
unsafe { OwnedFd::from_raw_fd(res) }
};
let mut tmp = 0 as libc::c_char;
if unsafe { libc::read(tty_fd.as_raw_fd(), (&raw mut tmp).cast(), 1) } < 0
&& errno().0 == EIO
{
we_think_we_are_orphaned = true;
}
}
if loop_count > 4096 {
we_think_we_are_orphaned = true;
}
we_think_we_are_orphaned
}
fn reader_run_command(parser: &Parser, cmd: &wstr) -> EvalRes {
assert!(
!get_tty_protocols_active(),
"TTY protocols should not be active"
);
let ft = tok_command(cmd);
if !ft.is_empty() {
parser.libdata_mut().status_vars.command = ft.clone();
parser.libdata_mut().status_vars.commandline = cmd.to_owned();
parser.set_one(L!("_"), ParserEnvSetMode::new(EnvMode::GLOBAL), ft.clone());
}
reader_write_title(cmd, parser, true);
Outputter::stdoutput()
.borrow_mut()
.set_text_face(TextFace::terminal_default());
term_donate(false);
let time_before = Instant::now();
let eval_res = parser.eval(cmd, &IoChain::new());
job_reap(parser, true, None);
if !ft.is_empty() {
let time_after = Instant::now();
let duration = time_after.duration_since(time_before);
parser.set_one(
ENV_CMD_DURATION,
ParserEnvSetMode::new(EnvMode::UNEXPORT),
duration.as_millis().to_wstring(),
);
}
term_steal(eval_res.status.is_success());
parser.libdata_mut().status_vars.command = get_program_name().to_owned();
parser.set_one(
L!("_"),
ParserEnvSetMode::new(EnvMode::GLOBAL),
get_program_name().to_owned(),
);
parser.libdata_mut().status_vars.commandline = L!("").to_owned();
if *HAVE_PROC_STAT {
proc_update_jiffies(parser);
}
eval_res
}
fn reader_shell_test(parser: &Parser, bstr: &wstr) -> Result<(), ParseIssue> {
let mut errors = vec![];
let res = detect_parse_errors(bstr, Some(&mut errors), true);
if res.is_err_and(|p| p.error) {
let mut error_desc = parser.get_backtrace(bstr, &errors);
if !error_desc.ends_with('\n') {
error_desc.push('\n');
}
eprintf!("\n%s", error_desc);
reader_schedule_prompt_repaint();
}
res
}
impl<'a> Reader<'a> {
fn import_history_if_necessary(&mut self) {
if self.history.is_empty() {
self.history.populate_from_config_path();
}
if self.history.is_empty() && self.history.is_default() {
let var = self.vars().get(L!("HISTFILE"));
let mut path =
var.map_or_else(|| L!("~/.bash_history").to_owned(), |var| var.as_string());
expand_tilde(&mut path, self.vars());
let Ok(file) = wopen_cloexec(&path, OFlag::O_RDONLY, Mode::empty()) else {
return;
};
self.history.populate_from_bash(BufReader::new(file));
}
}
fn should_add_to_history(&mut self, text: &wstr) -> bool {
let parser = self.parser;
if !function::exists(L!("fish_should_add_to_history"), parser) {
return text.as_char_slice()[0] != ' ';
}
let mut cmd: WString = L!("fish_should_add_to_history ").into();
cmd.push_utfstr(&escape(text));
let _not_interactive = parser.push_scope(|s| s.is_interactive = false);
exec_subshell(&cmd, parser, None, false).is_ok()
}
fn add_to_history(&mut self) {
if self.conf.in_silent_mode {
return;
}
let mut text = self.command_line.text().to_owned();
while text
.chars()
.next_back()
.is_some_and(|c| matches!(c, ' ' | '\n'))
&& count_preceding_backslashes(&text, text.len() - 1) % 2 == 0
{
text.pop();
}
self.history.remove_ephemeral_items();
if text.is_empty() {
return;
}
let mode = if !self.should_add_to_history(&text) {
PersistenceMode::Ephemeral
} else if in_private_mode(self.vars()) {
PersistenceMode::Memory
} else {
PersistenceMode::Disk
};
self.history
.add_pending_with_file_detection(&text, &self.parser.variables, mode);
}
fn try_warn_on_background_jobs(&mut self) -> bool {
assert_is_main_thread();
if self.did_warn_for_bg_jobs {
return false;
}
if reader_data_stack().len() > 1 {
return false;
}
let bg_jobs = jobs_requiring_warning_on_exit(self.parser);
if bg_jobs.is_empty() {
return false;
}
print_exit_warning_for_jobs(&bg_jobs);
self.did_warn_for_bg_jobs = true;
true
}
}
pub fn check_exit_loop_maybe_warning(data: Option<&mut Reader>) -> bool {
if reader_received_exit_signal() {
return true;
}
let Some(data) = data else {
return false;
};
if !data.exit_loop_requested {
return false;
}
if data.try_warn_on_background_jobs() {
data.exit_loop_requested = false;
return false;
}
true
}
fn try_expand_wildcard(
parser: &Parser,
wc: WString,
position: usize,
result: &mut WString,
) -> ExpandResultCode {
let is_path_sep =
|offset| wc.char_at(offset) == '/' && count_preceding_backslashes(&wc, offset) % 2 == 0;
let mut comp_start = position;
while comp_start > 0 && !is_path_sep(comp_start - 1) {
comp_start -= 1;
}
let mut comp_end = position;
while comp_end < wc.len() && !is_path_sep(comp_end) {
comp_end += 1;
}
if !wildcard_has(&wc[comp_start..comp_end]) {
return ExpandResultCode::wildcard_no_match;
}
result.clear();
const TAB_COMPLETE_WILDCARD_MAX_EXPANSION: usize = 256;
let ctx = OperationContext::background_with_cancel_checker(
&parser.variables,
Box::new(|| signal_check_cancel() != 0),
TAB_COMPLETE_WILDCARD_MAX_EXPANSION,
);
let flags = ExpandFlags::FAIL_ON_CMDSUBST
| ExpandFlags::SKIP_VARIABLES
| ExpandFlags::PRESERVE_HOME_TILDES;
let mut expanded = CompletionList::new();
let ret = expand_string(wc, &mut expanded, flags, &ctx, None);
if ret.result != ExpandResultCode::ok {
return ret.result;
}
let mut joined = WString::new();
for r#match in expanded {
if r#match.flags.contains(CompleteFlags::DONT_ESCAPE) {
joined.push_utfstr(&r#match.completion);
} else {
let tildeflag = if r#match.flags.contains(CompleteFlags::DONT_ESCAPE_TILDES) {
EscapeFlags::NO_TILDE
} else {
EscapeFlags::default()
};
joined.push_utfstr(&escape_string(
&r#match.completion,
EscapeStringStyle::Script(EscapeFlags::NO_QUOTED | tildeflag),
));
}
joined.push(' ');
}
*result = joined;
ExpandResultCode::ok
}
pub(crate) fn is_backslashed(s: &wstr, pos: usize) -> bool {
if pos > s.len() {
return false;
}
let mut count = 0;
for idx in (0..pos).rev() {
if s.as_char_slice()[idx] != '\\' {
break;
}
count += 1;
}
count % 2 == 1
}
fn unescaped_quote(s: &wstr, pos: usize) -> Option<char> {
let mut result = None;
if pos < s.len() {
let c = s.as_char_slice()[pos];
if matches!(c, '\'' | '"') && !is_backslashed(s, pos) {
result = Some(c);
}
}
result
}
fn replace_line_at_cursor(
text: &wstr,
inout_cursor_pos: &mut usize,
replacement: &wstr,
) -> WString {
let cursor = *inout_cursor_pos;
let start = text[0..cursor]
.as_char_slice()
.iter()
.rposition(|&c| c == '\n')
.map_or(0, |newline| newline + 1);
let end = text[cursor..]
.as_char_slice()
.iter()
.position(|&c| c == '\n')
.map_or(text.len(), |pos| cursor + pos);
*inout_cursor_pos = start + replacement.len();
text[..start].to_owned() + replacement + &text[end..]
}
pub(crate) fn get_quote(cmd_str: &wstr, len: usize) -> Option<char> {
let cmd = cmd_str.as_char_slice();
let mut i = 0;
while i < cmd.len() {
if cmd[i] == '\\' {
i += 1;
if i == cmd_str.len() {
return None;
}
i += 1;
} else if cmd[i] == '\'' || cmd[i] == '"' {
match quote_end(cmd_str, i, cmd[i]) {
Some(end) => {
if end > len {
return Some(cmd[i]);
}
i = end + 1;
}
None => return Some(cmd[i]),
}
} else {
i += 1;
}
}
None
}
pub fn completion_apply_to_command_line(
ctx: &OperationContext,
val_str: &wstr,
flags: CompleteFlags,
command_line: &wstr,
inout_cursor_pos: &mut usize,
append_only: bool,
is_unique: bool,
) -> WString {
let mut trailer = (!flags.contains(CompleteFlags::NO_SPACE)).then_some(' ');
let do_replace_token = flags.contains(CompleteFlags::REPLACES_TOKEN);
let do_replace_line = flags.contains(CompleteFlags::REPLACES_LINE);
let do_escape = !flags.contains(CompleteFlags::DONT_ESCAPE);
let no_tilde = flags.contains(CompleteFlags::DONT_ESCAPE_TILDES);
let keep_variable_override = flags.contains(CompleteFlags::KEEP_VARIABLE_OVERRIDE_PREFIX);
let is_variable_name = flags.contains(CompleteFlags::VARIABLE_NAME);
let cursor_pos = *inout_cursor_pos;
let mut back_into_trailing_quote = false;
assert!(!is_variable_name || command_line.char_at(cursor_pos) != '/');
let have_trailer = command_line.char_at(cursor_pos) == ' ';
if do_replace_line {
assert!(!do_escape, "unsupported completion flag");
let cmdsub = get_cmdsubst_extent(command_line, cursor_pos);
return if !command_line[cmdsub.clone()].contains('\n') {
*inout_cursor_pos = cmdsub.start + val_str.len();
command_line[..cmdsub.start].to_owned() + val_str + &command_line[cmdsub.end..]
} else {
replace_line_at_cursor(command_line, inout_cursor_pos, val_str)
};
}
let mut escape_flags = EscapeFlags::empty();
if append_only || !is_unique || trailer.is_none() {
escape_flags.insert(EscapeFlags::NO_QUOTED);
}
if no_tilde {
escape_flags.insert(EscapeFlags::NO_TILDE);
}
let maybe_add_slash = |trailer: &mut char, token: &wstr| {
let mut expanded = token.to_owned();
if expand_one(&mut expanded, ExpandFlags::FAIL_ON_CMDSUBST, ctx, None)
&& wstat(&expanded).is_ok_and(|md| md.is_dir())
{
*trailer = '/';
}
};
if do_replace_token {
if is_variable_name {
assert!(!do_escape);
if let Some(trailer) = trailer.as_mut() {
maybe_add_slash(trailer, val_str);
}
}
let mut move_cursor = 0;
let (range, _) = get_token_extent(command_line, cursor_pos);
let mut sb = command_line[..range.start].to_owned();
if keep_variable_override {
let tok = &command_line[range.clone()];
let separator_pos = variable_assignment_equals_pos(tok).unwrap();
let key = &tok[..=separator_pos];
sb.push_utfstr(&key);
move_cursor += key.len();
}
if do_escape {
let escaped = escape_string(val_str, EscapeStringStyle::Script(escape_flags));
sb.push_utfstr(&escaped);
move_cursor += escaped.len();
} else {
sb.push_utfstr(val_str);
move_cursor += val_str.len();
}
if let Some(trailer) = trailer {
if !have_trailer {
sb.push(trailer);
}
move_cursor += 1;
}
sb.push_utfstr(&command_line[range.end..]);
let new_cursor_pos = range.start + move_cursor;
*inout_cursor_pos = new_cursor_pos;
return sb;
}
let mut quote = None;
let replaced = if do_escape {
let (tok, _) = get_token_extent(command_line, cursor_pos);
let mut have_token = false;
if tok.contains(&cursor_pos) || cursor_pos == tok.end {
quote = get_quote(&command_line[tok.clone()], cursor_pos - tok.start);
have_token = !tok.is_empty();
}
if quote.is_none() && !append_only && cursor_pos > 0 {
let trailing_quote = unescaped_quote(command_line, cursor_pos - 1);
if trailing_quote.is_some() {
quote = trailing_quote;
back_into_trailing_quote = true;
}
}
if have_token {
escape_flags.insert(EscapeFlags::NO_QUOTED);
}
escape_string_with_quote(val_str, quote, escape_flags)
} else {
val_str.to_owned()
};
let mut insertion_point = cursor_pos;
if back_into_trailing_quote {
insertion_point = insertion_point.checked_sub(1).unwrap();
}
let mut result = command_line.to_owned();
result.insert_utfstr(insertion_point, &replaced);
let mut new_cursor_pos =
insertion_point + replaced.len() + if back_into_trailing_quote { 1 } else { 0 };
if let Some(mut trailer) = trailer {
if is_variable_name {
let (tok, _) = get_token_extent(command_line, cursor_pos);
maybe_add_slash(&mut trailer, &result[tok.start..new_cursor_pos]);
}
if trailer != '/' {
if let Some(quote) = quote {
if unescaped_quote(command_line, insertion_point) != Some(quote) {
result.insert(new_cursor_pos, quote);
new_cursor_pos += 1;
}
}
}
if !have_trailer {
result.insert(new_cursor_pos, trailer);
}
new_cursor_pos += 1;
}
*inout_cursor_pos = new_cursor_pos;
result
}
fn reader_can_replace(s: &wstr, flags: CompleteFlags) -> bool {
if flags.contains(CompleteFlags::DONT_ESCAPE) {
return true;
}
!s.chars()
.any(|c| matches!(c, '$' | '*' | '?' | '(' | '{' | '}' | ')'))
}
impl<'a> Reader<'a> {
fn compute_and_apply_completions(&mut self, c: ReadlineCmd) {
assert_matches!(c, ReadlineCmd::Complete | ReadlineCmd::CompleteAndSearch);
assert!(
!get_tty_protocols_active(),
"should not be called with TTY protocols active"
);
let el = &self.command_line;
if is_backslashed(el.text(), el.position()) {
self.delete_char(true);
}
let el = &self.command_line;
let cmdsub_range = get_cmdsubst_extent(el.text(), el.position());
let position_in_cmdsub = el.position() - cmdsub_range.start;
let (mut token_range, _) =
get_token_extent(&el.text()[cmdsub_range.clone()], position_in_cmdsub);
let position_in_token = position_in_cmdsub - token_range.start;
if token_range.end > cmdsub_range.len() {
token_range.end = cmdsub_range.len();
}
token_range.start += cmdsub_range.start;
token_range.end += cmdsub_range.start;
let mut wc_expanded = WString::new();
match try_expand_wildcard(
self.parser,
el.text()[token_range.clone()].to_owned(),
position_in_token,
&mut wc_expanded,
) {
ExpandResultCode::error => {}
ExpandResultCode::overflow => {
self.flash(token_range);
return;
}
ExpandResultCode::wildcard_no_match => {}
ExpandResultCode::cancel => {
return;
}
ExpandResultCode::ok => {
self.rls_mut().completion_action = None;
self.push_edit(
EditableLineTag::Commandline,
Edit::new(token_range, wc_expanded),
);
return;
}
}
let cmdsub = &el.text()[cmdsub_range.start..token_range.end];
let (mut comp, _needs_load) = complete(
cmdsub,
CompletionRequestOptions::normal(),
&self.parser.context(),
);
let el = &self.command_line;
token_range.start = std::cmp::min(token_range.start, el.text().len());
token_range.end = std::cmp::min(token_range.end, el.text().len());
sort_and_prioritize(&mut comp, CompletionRequestOptions::default());
let el = &self.command_line;
self.cycle_command_line = el.text().to_owned();
self.cycle_cursor_pos = token_range.end;
let inserted_unique = self.handle_completions(token_range, comp);
self.rls_mut().completion_action = if inserted_unique {
Some(CompletionAction::InsertedUnique)
} else {
(!self.pager.is_empty()).then_some(CompletionAction::ShownAmbiguous)
};
if c == ReadlineCmd::CompleteAndSearch && !inserted_unique && !self.pager.is_empty() {
self.pager.set_search_field_shown(true);
self.select_completion_in_direction(SelectionMotion::Next, false);
}
}
fn try_insert(&mut self, c: Completion, tok: &wstr, token_range: Range<usize>) {
if !c.replaces_token() || reader_can_replace(tok, c.flags) {
self.completion_insert(
&c.completion,
token_range.end,
c.flags,
true,
);
}
}
fn handle_completions(&mut self, token_range: Range<usize>, mut comp: Vec<Completion>) -> bool {
let tok = self.command_line.text()[token_range.clone()].to_owned();
comp.retain({
let best_rank = comp.iter().map(|c| c.rank()).min().unwrap_or(u32::MAX);
move |c| {
assert!(c.rank() >= best_rank);
c.rank() == best_rank
}
});
let will_replace_token = comp.iter().all(|c| c.replaces_token());
comp.retain(|c| !c.replaces_token() || reader_can_replace(&tok, c.flags));
if !will_replace_token {
for c in &mut comp {
if c.replaces_token() {
c.flags |= CompleteFlags::SUPPRESS_PAGER_PREFIX;
}
}
}
let len = comp.len();
if len == 0 {
if token_range.is_empty() {
self.flash(0..self.command_line.len());
} else {
self.flash(token_range);
}
return false;
} else if len == 1 {
let c = std::mem::take(&mut comp[0]);
self.try_insert(c, &tok, token_range);
return true;
}
let mut use_prefix = false;
let mut common_prefix = L!("");
let all_matches_exact_or_prefix = comp.iter().all(|c| c.r#match.is_exact_or_prefix());
assert!(will_replace_token || all_matches_exact_or_prefix);
if all_matches_exact_or_prefix {
let mut flags = CompleteFlags::empty();
let mut prefix_is_partial_completion = false;
let mut first = true;
for c in &comp {
if c.flags.contains(CompleteFlags::SUPPRESS_PAGER_PREFIX) {
continue;
}
if first {
common_prefix = &c.completion;
flags = c.flags;
first = false;
} else {
let max = std::cmp::min(common_prefix.len(), c.completion.len());
let mut idx = 0;
while idx < max {
if common_prefix.as_char_slice()[idx] != c.completion.as_char_slice()[idx] {
break;
}
idx += 1;
}
common_prefix = common_prefix.slice_to(idx);
prefix_is_partial_completion = true;
if idx == 0 {
break;
}
}
}
use_prefix = common_prefix.len() > if will_replace_token { tok.len() } else { 0 };
assert!(!use_prefix || !common_prefix.is_empty());
if use_prefix {
if prefix_is_partial_completion {
flags |= CompleteFlags::NO_SPACE;
}
self.completion_insert(
common_prefix,
token_range.end,
flags,
false,
);
self.cycle_command_line = self.command_line.text().to_owned();
self.cycle_cursor_pos = self.command_line.position();
}
}
let prefix = if will_replace_token && !use_prefix {
Cow::Borrowed(L!(""))
} else {
let mut prefix = WString::new();
let full = if will_replace_token {
common_prefix.to_owned()
} else {
tok + common_prefix
};
if full.len() <= PREFIX_MAX_LEN {
prefix = full;
} else {
prefix.push(ELLIPSIS_CHAR);
let truncated = &full[full.len() - PREFIX_MAX_LEN..];
let (i, last_component) = truncated.split('/').enumerate().last().unwrap();
if i == 0 {
prefix.push_utfstr(&truncated);
} else {
prefix.push('/');
prefix.push_utfstr(last_component);
}
}
Cow::Owned(prefix)
};
if use_prefix {
let common_prefix_len = common_prefix.len();
for c in &mut comp {
if c.flags.contains(CompleteFlags::SUPPRESS_PAGER_PREFIX) {
continue;
}
c.flags &= !CompleteFlags::REPLACES_TOKEN;
c.completion.replace_range(0..common_prefix_len, L!(""));
}
}
self.pager.set_prefix(prefix, true);
self.pager.set_completions(&comp, true);
self.pager_selection_changed();
false
}
fn completion_insert(
&mut self,
val: &wstr,
token_end: usize,
flags: CompleteFlags,
is_unique: bool,
) {
let (elt, el) = self.active_edit_line();
if el.position() != token_end {
self.update_buff_pos(elt, Some(token_end));
}
let (_elt, el) = self.active_edit_line();
let mut cursor = el.position();
let new_command_line = completion_apply_to_command_line(
&OperationContext::background_interruptible(self.parser.vars()),
val,
flags,
el.text(),
&mut cursor,
false,
is_unique,
);
self.set_buffer_maintaining_pager(&new_command_line, cursor);
}
}
#[cfg(test)]
mod tests {
use super::{combine_command_and_autosuggestion, completion_apply_to_command_line};
use crate::complete::CompleteFlags;
use crate::operation_context::{no_cancel, OperationContext};
use crate::prelude::*;
use crate::tests::prelude::*;
#[test]
fn test_autosuggestion_combining() {
assert_eq!(
combine_command_and_autosuggestion(L!("alpha"), 0..5, L!("alphabeta")),
L!("alphabeta")
);
assert_eq!(
combine_command_and_autosuggestion(L!("alpha"), 0..5, L!("ALPHABETA")),
L!("ALPHABETA")
);
assert_eq!(
combine_command_and_autosuggestion(L!("alPha"), 0..5, L!("alphabeTa")),
L!("alPhabeTa")
);
assert_eq!(
combine_command_and_autosuggestion(L!("alpha"), 0..5, L!("ALPHAA")),
L!("ALPHAA")
);
assert_eq!(
combine_command_and_autosuggestion(L!("alpha"), 0..5, L!("ALPHA")),
L!("alpha")
);
assert_eq!(
combine_command_and_autosuggestion(L!("al\nbeta"), 0..2, L!("alpha")),
L!("alpha\nbeta").to_owned()
);
assert_eq!(
combine_command_and_autosuggestion(L!("alpha\nbe"), 6..8, L!("beta")),
L!("alpha\nbeta").to_owned()
);
assert_eq!(
combine_command_and_autosuggestion(L!("alpha\nbe\ngamma"), 6..8, L!("beta")),
L!("alpha\nbeta\ngamma").to_owned()
);
}
#[test]
fn test_completion_insertions() {
let parser = TestParser::new();
macro_rules! validate {
(
$line:expr, $completion:expr,
$flags:expr, $append_only:expr,
$expected:expr
) => {
let mut line = L!($line).to_owned();
let completion = L!($completion);
let mut expected = L!($expected).to_owned();
let in_cursor_pos = line.find(L!("^")).unwrap();
line.remove(in_cursor_pos);
let out_cursor_pos = expected.find(L!("^")).unwrap();
expected.remove(out_cursor_pos);
let mut cursor_pos = in_cursor_pos;
let result = completion_apply_to_command_line(
&OperationContext::test_only_foreground(
&parser,
parser.vars(),
Box::new(no_cancel),
),
completion,
$flags,
&line,
&mut cursor_pos,
$append_only,
false,
);
assert_eq!(result, expected);
assert_eq!(cursor_pos, out_cursor_pos);
};
}
validate!("foo^", "bar", CompleteFlags::default(), false, "foobar ^");
validate!(
"foo^ baz",
"bar",
CompleteFlags::default(),
false,
"foobar ^baz"
);
validate!(
"'foo^",
"bar",
CompleteFlags::default(),
false,
"'foobar' ^"
);
validate!(
"'foo'^",
"bar",
CompleteFlags::default(),
false,
"'foobar' ^"
);
validate!(
"'foo\\'^",
"bar",
CompleteFlags::default(),
false,
"'foo\\'bar' ^"
);
validate!(
"foo\\'^",
"bar",
CompleteFlags::default(),
false,
"foo\\'bar ^"
);
validate!("foo^", "bar", CompleteFlags::default(), true, "foobar ^");
validate!(
"foo^ baz",
"bar",
CompleteFlags::default(),
true,
"foobar ^baz"
);
validate!("'foo^", "bar", CompleteFlags::default(), true, "'foobar' ^");
validate!(
"'foo'^",
"bar",
CompleteFlags::default(),
true,
"'foo'bar ^"
);
validate!(
"'foo\\'^",
"bar",
CompleteFlags::default(),
true,
"'foo\\'bar' ^"
);
validate!(
"foo\\'^",
"bar",
CompleteFlags::default(),
true,
"foo\\'bar ^"
);
validate!("foo^", "bar", CompleteFlags::NO_SPACE, false, "foobar^");
validate!("'foo^", "bar", CompleteFlags::NO_SPACE, false, "'foobar^");
validate!("'foo'^", "bar", CompleteFlags::NO_SPACE, false, "'foobar'^");
validate!(
"'foo\\'^",
"bar",
CompleteFlags::NO_SPACE,
false,
"'foo\\'bar^"
);
validate!(
"foo\\'^",
"bar",
CompleteFlags::NO_SPACE,
false,
"foo\\'bar^"
);
validate!("foo^", "bar", CompleteFlags::REPLACES_TOKEN, false, "bar ^");
validate!(
"'foo^",
"bar",
CompleteFlags::REPLACES_TOKEN,
false,
"bar ^"
);
validate!(": (:^ ''", "", CompleteFlags::default(), false, ": (: ^''");
}
#[test]
fn test_try_apply_edit_to_autosuggestion() {
use super::try_apply_edit_to_autosuggestion;
use super::Autosuggestion;
use crate::editable_line::Edit;
macro_rules! validate {
(
$name:expr,
$autosuggestion:expr,
$command_line:expr,
$edit:expr,
$expected_autosuggestion:expr $(,)?
) => {
let mut autosuggestion = $autosuggestion;
let command_line = L!($command_line);
let edit = $edit;
let expected = $expected_autosuggestion;
let expect_success = expected.is_some();
assert_eq!(
try_apply_edit_to_autosuggestion(&mut autosuggestion, command_line, &edit),
expect_success,
"Test case '{}' failed: incorrect result",
$name
);
if expect_success {
assert_eq!(
autosuggestion,
expected.unwrap(),
"Test case '{}' failed: incorrect autosuggestion state",
$name
);
}
};
}
validate!(
"No autosuggestion",
Autosuggestion::default(),
"echo",
Edit::new(4..4, L!(" ").to_owned()),
None,
);
validate!(
"Matching edit",
Autosuggestion {
text: L!("echo hest").to_owned(),
search_string_range: 0..4,
icase_matched_codepoints: None,
is_whole_item_from_history: true,
},
"echo",
Edit::new(4..4, L!(" ").to_owned()),
Some(Autosuggestion {
text: L!("echo hest").to_owned(),
search_string_range: 0..5,
icase_matched_codepoints: None,
is_whole_item_from_history: true,
})
);
validate!(
"Non-matching edit",
Autosuggestion {
text: L!("echo hest").to_owned(),
search_string_range: 0..4,
icase_matched_codepoints: None,
is_whole_item_from_history: true,
},
"echo",
Edit::new(4..4, L!("f").to_owned()),
None,
);
validate!(
"Case-insensitive matching edit",
Autosuggestion {
text: L!("echo hest").to_owned(),
search_string_range: 0..4,
icase_matched_codepoints: Some(4),
is_whole_item_from_history: true,
},
"echo",
Edit::new(4..4, L!(" H").to_owned()),
Some(Autosuggestion {
text: L!("echo hest").to_owned(),
search_string_range: 0..6,
icase_matched_codepoints: Some(6),
is_whole_item_from_history: true,
})
);
validate!(
"Case-insensitive matching deletion",
Autosuggestion {
text: L!("Echo hest").to_owned(),
search_string_range: 0..4,
icase_matched_codepoints: Some(4),
is_whole_item_from_history: true,
},
"echo",
Edit::new(3..4, L!("").to_owned()),
Some(Autosuggestion {
text: L!("Echo hest").to_owned(),
search_string_range: 0..3,
icase_matched_codepoints: Some(3),
is_whole_item_from_history: true,
})
);
validate!(
"Lowercase mapping is only partially matched",
Autosuggestion {
text: L!("echo İnstall").to_owned(),
search_string_range: 0..6,
icase_matched_codepoints: Some(6),
is_whole_item_from_history: true,
},
"echo i",
Edit::new(6..6, L!("n").to_owned()),
None,
);
}
}