diff --git a/CMakeLists.txt b/CMakeLists.txt index d309111e6..fa686cc14 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,10 +97,6 @@ if(CMAKE_SYSTEM_NAME STREQUAL NetBSD) set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) endif() -# List of sources for builtin functions. -set(FISH_BUILTIN_SRCS - src/builtins/commandline.cpp -) # List of other sources. set(FISH_SRCS src/ast.cpp @@ -117,7 +113,6 @@ set(FISH_SRCS src/output.cpp src/parse_util.cpp src/path.cpp - src/reader.cpp src/rustffi.cpp src/wcstringutil.cpp src/wgetopt.cpp @@ -178,7 +173,7 @@ function(FISH_LINK_DEPS_AND_SIGN target) endfunction(FISH_LINK_DEPS_AND_SIGN) # Define libfish.a. -add_library(fishlib STATIC ${FISH_SRCS} ${FISH_BUILTIN_SRCS}) +add_library(fishlib STATIC ${FISH_SRCS}) target_sources(fishlib PRIVATE ${FISH_HEADERS}) target_link_libraries(fishlib fish-rust diff --git a/cmake/ConfigureChecks.cmake b/cmake/ConfigureChecks.cmake index 4887d4d3f..60ee0c90e 100644 --- a/cmake/ConfigureChecks.cmake +++ b/cmake/ConfigureChecks.cmake @@ -114,7 +114,6 @@ if (CMAKE_SYSTEM_NAME MATCHES "Linux|Android") endif() endif() -check_cxx_symbol_exists(ctermid_r stdio.h HAVE_CTERMID_R) check_struct_has_member("struct dirent" d_type dirent.h HAVE_STRUCT_DIRENT_D_TYPE LANGUAGE CXX) check_cxx_symbol_exists(dirfd "sys/types.h;dirent.h" HAVE_DIRFD) check_include_file_cxx(execinfo.h HAVE_EXECINFO_H) diff --git a/config_cmake.h.in b/config_cmake.h.in index 12f1eda9a..c73c2a39e 100644 --- a/config_cmake.h.in +++ b/config_cmake.h.in @@ -4,9 +4,6 @@ /* Define to 1 if compiled on WSL */ #cmakedefine WSL 1 -/* Define to 1 if you have the `ctermid_r' function. */ -#cmakedefine HAVE_CTERMID_R 1 - /* Define to 1 if C++11 thread_local is supported. */ #cmakedefine HAVE_CX11_THREAD_LOCAL 1 diff --git a/fish-rust/src/abbrs.rs b/fish-rust/src/abbrs.rs index 6224b09b5..41556bdf3 100644 --- a/fish-rust/src/abbrs.rs +++ b/fish-rust/src/abbrs.rs @@ -201,10 +201,10 @@ pub struct Replacer { pub replacement: WString, /// If true, treat 'replacement' as the name of a function. - is_function: bool, + pub is_function: bool, /// If set, the cursor should be moved to the first instance of this string in the expansion. - set_cursor_marker: Option, + pub set_cursor_marker: Option, } impl From for abbrs_replacer_t { @@ -219,23 +219,23 @@ impl From for abbrs_replacer_t { } } -struct Replacement { +pub struct Replacement { /// The original range of the token in the command line. - range: SourceRange, + pub range: SourceRange, /// The string to replace with. - text: WString, + pub text: WString, /// The new cursor location, or none to use the default. /// This is relative to the original range. - cursor: Option, + pub cursor: Option, } impl Replacement { /// Construct a replacement from a replacer. /// The \p range is the range of the text matched by the replacer in the command line. /// The text is passed in separately as it may be the output of the replacer's function. - fn new(range: SourceRange, mut text: WString, set_cursor_marker: Option) -> Self { + pub fn new(range: SourceRange, mut text: WString, set_cursor_marker: Option) -> Self { let mut cursor = None; if let Some(set_cursor_marker) = set_cursor_marker { let matched = text @@ -353,6 +353,12 @@ impl AbbreviationSet { /// \return the list of replacers for an input token, in priority order, using the global set. /// The \p position is given to describe where the token was found. +pub fn abbrs_match(token: &wstr, position: Position) -> Vec { + with_abbrs(|set| set.r#match(token, position)) + .into_iter() + .collect() +} + fn abbrs_match_ffi(token: &CxxWString, position: abbrs_position_t) -> Vec { with_abbrs(|set| set.r#match(token.as_wstr(), position.into())) .into_iter() diff --git a/fish-rust/src/builtins/commandline.rs b/fish-rust/src/builtins/commandline.rs index f9df0c827..f826ecda4 100644 --- a/fish-rust/src/builtins/commandline.rs +++ b/fish-rust/src/builtins/commandline.rs @@ -1,5 +1,500 @@ use super::prelude::*; +use crate::common::{unescape_string, UnescapeFlags, UnescapeStringStyle}; +use crate::input::input_function_get_code; +use crate::input_common::{CharEvent, ReadlineCmd}; +use crate::parse_constants::ParserTestErrorBits; +use crate::parse_util::{ + parse_util_detect_errors, parse_util_job_extent, parse_util_lineno, parse_util_process_extent, + parse_util_token_extent, +}; +use crate::proc::is_interactive_session; +use crate::reader::{ + commandline_get_state, commandline_set_buffer, reader_handle_command, reader_queue_ch, +}; +use crate::tokenizer::TokenType; +use crate::tokenizer::Tokenizer; +use crate::tokenizer::TOK_ACCEPT_UNFINISHED; +use crate::wchar::prelude::*; +use crate::wcstringutil::join_strings; +use crate::wgetopt::{wgetopter_t, wopt, woption, woption_argument_t}; +use std::ops::Range; -pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { - run_builtin_ffi(crate::ffi::builtin_commandline, parser, streams, args) +/// Which part of the comandbuffer are we operating on. +enum TextScope { + String, + Job, + Process, + Token, +} + +/// For text insertion, how should it be done. +enum AppendMode { + // replace current text + Replace, + // insert at cursor position + Insert, + // insert at end of current token/command/buffer + Append, +} + +/// Replace/append/insert the selection with/at/after the specified string. +/// +/// \param begin beginning of selection +/// \param end end of selection +/// \param insert the string to insert +/// \param append_mode can be one of REPLACE_MODE, INSERT_MODE or APPEND_MODE, affects the way the +/// test update is performed +/// \param buff the original command line buffer +/// \param cursor_pos the position of the cursor in the command line +fn replace_part( + range: Range, + insert: &wstr, + insert_mode: AppendMode, + buff: &wstr, + cursor_pos: usize, +) { + let mut out_pos = cursor_pos; + + let mut out = buff[..range.start].to_owned(); + + match insert_mode { + AppendMode::Replace => { + out.push_utfstr(insert); + out_pos = out.len(); + } + AppendMode::Append => { + out.push_utfstr(&buff[range.clone()]); + out.push_utfstr(insert); + } + AppendMode::Insert => { + let cursor = cursor_pos - range.start; + assert!(range.start <= cursor); + out.push_utfstr(&buff[range.start..cursor]); + out.push_utfstr(&insert); + out.push_utfstr(&buff[cursor..range.end]); + out_pos += insert.len(); + } + } + + out.push_utfstr(&buff[range.end..]); + commandline_set_buffer(out, Some(out_pos)); +} + +/// Output the specified selection. +/// +/// \param begin start of selection +/// \param end end of selection +/// \param cut_at_cursor whether printing should stop at the surrent cursor position +/// \param tokenize whether the string should be tokenized, printing one string token on every line +/// and skipping non-string tokens +/// \param buffer the original command line buffer +/// \param cursor_pos the position of the cursor in the command line +fn write_part( + range: Range, + cut_at_cursor: bool, + tokenize: bool, + buffer: &wstr, + cursor_pos: usize, + streams: &mut IoStreams, +) { + let pos = cursor_pos - range.start; + + if tokenize { + let mut out = WString::new(); + let buff = &buffer[range]; + let mut tok = Tokenizer::new(buff, TOK_ACCEPT_UNFINISHED); + while let Some(token) = tok.next() { + if cut_at_cursor && token.end() >= pos { + break; + } + + if token.type_ == TokenType::string { + let tmp = tok.text_of(&token); + let unescaped = + unescape_string(tmp, UnescapeStringStyle::Script(UnescapeFlags::INCOMPLETE)) + .unwrap(); + out.push_utfstr(&unescaped); + out.push('\n'); + } + } + + streams.out.append(out); + } else { + if cut_at_cursor { + streams.out.append(&buffer[range.start..range.start + pos]); + } else { + streams.out.append(&buffer[range]); + } + streams.out.push('\n'); + } +} + +/// The commandline builtin. It is used for specifying a new value for the commandline. +pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { + let rstate = commandline_get_state(); + + let mut buffer_part = None; + let mut cut_at_cursor = false; + let mut append_mode = None; + + let mut function_mode = false; + let mut selection_mode = false; + + let mut tokenize = false; + + let mut cursor_mode = false; + let mut selection_start_mode = false; + let mut selection_end_mode = false; + let mut line_mode = false; + let mut search_mode = false; + let mut paging_mode = false; + let mut paging_full_mode = false; + let mut is_valid = false; + + let mut range = 0..0; + let mut override_buffer = None; + + let ld = parser.libdata(); + + const short_options: &wstr = L!(":abijpctforhI:CBELSsP"); + let long_options: &[woption] = &[ + wopt(L!("append"), woption_argument_t::no_argument, 'a'), + wopt(L!("insert"), woption_argument_t::no_argument, 'i'), + wopt(L!("replace"), woption_argument_t::no_argument, 'r'), + wopt(L!("current-buffer"), woption_argument_t::no_argument, 'b'), + wopt(L!("current-job"), woption_argument_t::no_argument, 'j'), + wopt(L!("current-process"), woption_argument_t::no_argument, 'p'), + wopt( + L!("current-selection"), + woption_argument_t::no_argument, + 's', + ), + wopt(L!("current-token"), woption_argument_t::no_argument, 't'), + wopt(L!("cut-at-cursor"), woption_argument_t::no_argument, 'c'), + wopt(L!("function"), woption_argument_t::no_argument, 'f'), + wopt(L!("tokenize"), woption_argument_t::no_argument, 'o'), + wopt(L!("help"), woption_argument_t::no_argument, 'h'), + wopt(L!("input"), woption_argument_t::required_argument, 'I'), + wopt(L!("cursor"), woption_argument_t::no_argument, 'C'), + wopt(L!("selection-start"), woption_argument_t::no_argument, 'B'), + wopt(L!("selection-end"), woption_argument_t::no_argument, 'E'), + wopt(L!("line"), woption_argument_t::no_argument, 'L'), + wopt(L!("search-mode"), woption_argument_t::no_argument, 'S'), + wopt(L!("paging-mode"), woption_argument_t::no_argument, 'P'), + wopt(L!("paging-full-mode"), woption_argument_t::no_argument, 'F'), + wopt(L!("is-valid"), woption_argument_t::no_argument, '\x01'), + ]; + + let mut w = wgetopter_t::new(short_options, long_options, args); + let cmd = w.argv[0]; + while let Some(c) = w.wgetopt_long() { + match c { + 'a' => append_mode = Some(AppendMode::Append), + 'b' => buffer_part = Some(TextScope::String), + 'i' => append_mode = Some(AppendMode::Insert), + 'r' => append_mode = Some(AppendMode::Replace), + 'c' => cut_at_cursor = true, + 't' => buffer_part = Some(TextScope::Token), + 'j' => buffer_part = Some(TextScope::Job), + 'p' => buffer_part = Some(TextScope::Process), + 'f' => function_mode = true, + 'o' => tokenize = true, + 'I' => { + // A historical, undocumented feature. TODO: consider removing this. + override_buffer = Some(w.woptarg.unwrap().to_owned()); + } + 'C' => cursor_mode = true, + 'B' => selection_start_mode = true, + 'E' => selection_end_mode = true, + 'L' => line_mode = true, + 'S' => search_mode = true, + 's' => selection_mode = true, + 'P' => paging_mode = true, + 'F' => paging_full_mode = true, + '\x01' => is_valid = true, + 'h' => { + builtin_print_help(parser, streams, cmd); + return STATUS_CMD_OK; + } + ':' => { + builtin_missing_argument(parser, streams, cmd, w.argv[w.woptind - 1], true); + return STATUS_INVALID_ARGS; + } + '?' => { + builtin_unknown_option(parser, streams, cmd, w.argv[w.woptind - 1], true); + return STATUS_INVALID_ARGS; + } + _ => panic!(), + } + } + + let positional_args = w.argv.len() - w.woptind; + + if function_mode { + // Check for invalid switch combinations. + if buffer_part.is_some() + || cut_at_cursor + || append_mode.is_some() + || tokenize + || cursor_mode + || line_mode + || search_mode + || paging_mode + || selection_start_mode + || selection_end_mode + { + streams.err.append(wgettext_fmt!(BUILTIN_ERR_COMBO, cmd)); + builtin_print_error_trailer(parser, streams.err, cmd); + return STATUS_INVALID_ARGS; + } + + if positional_args == 0 { + builtin_missing_argument(parser, streams, cmd, L!("--function"), true); + return STATUS_INVALID_ARGS; + } + + type rl = ReadlineCmd; + for arg in &w.argv[w.woptind..] { + let Some(cmd) = input_function_get_code(arg) else { + streams + .err + .append(wgettext_fmt!("%ls: Unknown input function '%ls'", cmd, arg)); + builtin_print_error_trailer(parser, streams.err, cmd); + return STATUS_INVALID_ARGS; + }; + // Don't enqueue a repaint if we're currently in the middle of one, + // because that's an infinite loop. + if matches!(cmd, rl::RepaintMode | rl::ForceRepaint | rl::Repaint) { + if ld.pods.is_repaint { + continue; + } + } + + // HACK: Execute these right here and now so they can affect any insertions/changes + // made via bindings. The correct solution is to change all `commandline` + // insert/replace operations into readline functions with associated data, so that + // all queued `commandline` operations - including buffer modifications - are + // executed in order + match cmd { + rl::BeginUndoGroup | rl::EndUndoGroup => reader_handle_command(cmd), + _ => { + // Inserts the readline function at the back of the queue. + reader_queue_ch(CharEvent::from_readline(cmd)); + } + } + } + + return STATUS_CMD_OK; + } + + if selection_mode { + if let Some(selection) = rstate.selection { + streams.out.append(&rstate.text[selection]); + } + return STATUS_CMD_OK; + } + + // Check for invalid switch combinations. + if (selection_start_mode || selection_end_mode) && positional_args != 0 { + streams + .err + .append(wgettext_fmt!(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd)); + builtin_print_error_trailer(parser, streams.err, cmd); + return STATUS_INVALID_ARGS; + } + + if (search_mode || line_mode || cursor_mode || paging_mode) && positional_args > 1 { + streams + .err + .append(wgettext_fmt!(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd)); + builtin_print_error_trailer(parser, streams.err, cmd); + return STATUS_INVALID_ARGS; + } + + if (buffer_part.is_some() || tokenize || cut_at_cursor) + && (cursor_mode || line_mode || search_mode || paging_mode || paging_full_mode) + // Special case - we allow to get/set cursor position relative to the process/job/token. + && (buffer_part.is_none() || !cursor_mode) + { + streams.err.append(wgettext_fmt!(BUILTIN_ERR_COMBO, cmd)); + builtin_print_error_trailer(parser, streams.err, cmd); + return STATUS_INVALID_ARGS; + } + + if (tokenize || cut_at_cursor) && positional_args != 0 { + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_COMBO2, + cmd, + "--cut-at-cursor and --tokenize can not be used when setting the commandline" + )); + builtin_print_error_trailer(parser, streams.err, cmd); + return STATUS_INVALID_ARGS; + } + + if append_mode.is_some() && positional_args == 0 { + // No tokens in insert mode just means we do nothing. + return STATUS_CMD_ERROR; + } + + // Set default modes. + let append_mode = append_mode.unwrap_or(AppendMode::Replace); + + let buffer_part = buffer_part.unwrap_or(TextScope::String); + + if line_mode { + streams.out.append(sprintf!( + "%d\n", + parse_util_lineno(&rstate.text, rstate.cursor_pos) + )); + return STATUS_CMD_OK; + } + + if search_mode { + return if commandline_get_state().search_mode { + STATUS_CMD_OK + } else { + STATUS_CMD_ERROR + }; + } + + if paging_mode { + return if commandline_get_state().pager_mode { + STATUS_CMD_OK + } else { + STATUS_CMD_ERROR + }; + } + + if paging_full_mode { + let state = commandline_get_state(); + return if state.pager_mode && state.pager_fully_disclosed { + STATUS_CMD_OK + } else { + STATUS_CMD_ERROR + }; + } + + if selection_start_mode { + let Some(selection) = rstate.selection else { + return STATUS_CMD_ERROR; + }; + streams.out.append(sprintf!("%lu\n", selection.start)); + return STATUS_CMD_OK; + } + + if selection_end_mode { + let Some(selection) = rstate.selection else { + return STATUS_CMD_ERROR; + }; + streams.out.append(sprintf!("%lu\n", selection.end)); + return STATUS_CMD_OK; + } + + // At this point we have (nearly) exhausted the options which always operate on the true command + // line. Now we respect the possibility of a transient command line due to evaluating a wrapped + // completion. Don't do this in cursor_mode: it makes no sense to move the cursor based on a + // transient commandline. + let current_buffer; + let current_cursor_pos; + let transient; + if let Some(override_buffer) = &override_buffer { + current_buffer = override_buffer; + current_cursor_pos = current_buffer.len(); + } else if !ld.transient_commandlines.is_empty() && !cursor_mode { + transient = ld.transient_commandlines.last().unwrap().clone(); + current_buffer = &transient; + current_cursor_pos = transient.len(); + } else if rstate.initialized { + current_buffer = &rstate.text; + current_cursor_pos = rstate.cursor_pos; + } else { + // There is no command line, either because we are not interactive, or because we are + // interactive and are still reading init files (in which case we silently ignore this). + if !is_interactive_session() { + streams.err.append(cmd); + streams + .err + .append(L!(": Can not set commandline in non-interactive mode\n")); + builtin_print_error_trailer(parser, streams.err, cmd); + } + return STATUS_CMD_ERROR; + } + + if is_valid { + if current_buffer.is_empty() { + return Some(1); + } + let res = parse_util_detect_errors(current_buffer, None, /*accept_incomplete=*/ true); + return match res { + Ok(()) => STATUS_CMD_OK, + Err(err) => { + if err.contains(ParserTestErrorBits::INCOMPLETE) { + Some(2) + } else { + STATUS_CMD_ERROR + } + } + }; + } + + match buffer_part { + TextScope::String => { + range = 0..current_buffer.len(); + } + TextScope::Job => { + range = parse_util_job_extent(current_buffer, current_cursor_pos, None); + } + TextScope::Process => { + range = parse_util_process_extent(current_buffer, current_cursor_pos, None); + } + TextScope::Token => { + parse_util_token_extent(current_buffer, current_cursor_pos, &mut range, None); + } + } + + if cursor_mode { + if positional_args != 0 { + let arg = w.argv[w.woptind]; + let new_pos = match fish_wcstol(&arg[range.start..]) { + Err(_) => { + streams + .err + .append(wgettext_fmt!(BUILTIN_ERR_NOT_NUMBER, cmd, arg)); + builtin_print_error_trailer(parser, streams.err, cmd); + 0 + } + Ok(num) => num, + }; + + let new_pos = std::cmp::min(new_pos.max(0) as usize, current_buffer.len()); + commandline_set_buffer(current_buffer.to_owned(), Some(new_pos)); + } else { + streams.out.append(sprintf!("%lu\n", current_cursor_pos)); + } + return STATUS_CMD_OK; + } + + if positional_args == 0 { + write_part( + range, + cut_at_cursor, + tokenize, + current_buffer, + current_cursor_pos, + streams, + ); + } else if positional_args == 1 { + replace_part( + range, + args[w.woptind], + append_mode, + current_buffer, + current_cursor_pos, + ); + } else { + let sb = join_strings(&w.argv[w.woptind..], '\n'); + replace_part(range, &sb, append_mode, current_buffer, current_cursor_pos); + } + + STATUS_CMD_OK } diff --git a/fish-rust/src/builtins/complete.rs b/fish-rust/src/builtins/complete.rs index c240f5834..0214afc36 100644 --- a/fish-rust/src/builtins/complete.rs +++ b/fish-rust/src/builtins/complete.rs @@ -3,13 +3,13 @@ use crate::common::{ unescape_string, unescape_string_in_place, ScopeGuard, UnescapeFlags, UnescapeStringStyle, }; use crate::complete::{complete_add_wrapper, complete_remove_wrapper, CompletionRequestOptions}; -use crate::ffi; use crate::highlight::colorize; use crate::highlight::highlight_shell; use crate::nix::isatty; use crate::parse_constants::ParseErrorList; use crate::parse_util::parse_util_detect_errors_in_argument_list; use crate::parse_util::{parse_util_detect_errors, parse_util_token_extent}; +use crate::reader::{commandline_get_state, completion_apply_to_command_line}; use crate::wcstringutil::string_suffixes_string; use crate::{ common::str2wcstring, @@ -485,13 +485,14 @@ pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> let do_complete_param = match do_complete_param { None => { // No argument given, try to use the current commandline. - if !ffi::commandline_get_state_initialized_ffi() { + let commandline_state = commandline_get_state(); + if !commandline_state.initialized { // This corresponds to using 'complete -C' in non-interactive mode. // See #2361 . builtin_missing_argument(parser, streams, cmd, L!("-C"), true); return STATUS_INVALID_ARGS; } - ffi::commandline_get_state_text_ffi().from_ffi() + commandline_state.text } Some(param) => param, }; @@ -536,14 +537,13 @@ pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> // Make a fake commandline, and then apply the completion to it. let faux_cmdline = &do_complete_param[token.clone()]; let mut tmp_cursor = faux_cmdline.len(); - let mut faux_cmdline_with_completion = ffi::completion_apply_to_command_line( - &next.completion.to_ffi(), - unsafe { std::mem::transmute(next.flags) }, - &faux_cmdline.to_ffi(), + let mut faux_cmdline_with_completion = completion_apply_to_command_line( + &next.completion, + next.flags, + faux_cmdline, &mut tmp_cursor, false, - ) - .from_ffi(); + ); // completion_apply_to_command_line will append a space unless COMPLETE_NO_SPACE // is set. We don't want to set COMPLETE_NO_SPACE because that won't close diff --git a/fish-rust/src/builtins/history.rs b/fish-rust/src/builtins/history.rs index e954c5a7e..b9984057d 100644 --- a/fish-rust/src/builtins/history.rs +++ b/fish-rust/src/builtins/history.rs @@ -1,8 +1,8 @@ //! Implementation of the history builtin. -use crate::ffi::{self}; +use crate::history::in_private_mode; use crate::history::{self, history_session_id, History}; -use crate::history::{in_private_mode, HistorySharedPtr}; +use crate::reader::commandline_get_state; use super::prelude::*; @@ -243,17 +243,9 @@ pub fn history(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> // Use the default history if we have none (which happens if invoked non-interactively, e.g. // from webconfig.py. - let history = ffi::commandline_get_state_history_ffi(); - let history = if history.is_null() { - History::with_name(&history_session_id(parser.vars())) - } else { - { - *unsafe { - Box::from_raw(ffi::commandline_get_state_history_ffi() as *mut HistorySharedPtr) - } - } - .0 - }; + let history = commandline_get_state() + .history + .unwrap_or_else(|| History::with_name(&history_session_id(parser.vars()))); // If a history command hasn't already been specified via a flag check the first word. // Note that this can be simplified after we eliminate allowing subcommands as flags. diff --git a/fish-rust/src/builtins/read.rs b/fish-rust/src/builtins/read.rs index dfd346f0e..b571c3d12 100644 --- a/fish-rust/src/builtins/read.rs +++ b/fish-rust/src/builtins/read.rs @@ -14,8 +14,8 @@ use crate::env::EnvMode; use crate::env::Environment; use crate::env::READ_BYTE_LIMIT; use crate::env::{EnvVar, EnvVarFlags}; -use crate::ffi; use crate::nix::isatty; +use crate::reader::commandline_set_buffer; use crate::reader::ReaderConfig; use crate::reader::{reader_pop, reader_push, reader_readline}; use crate::tokenizer::Tokenizer; @@ -241,7 +241,7 @@ fn read_interactive( // Keep in-memory history only. reader_push(parser, L!(""), conf); - ffi::commandline_set_buffer_ffi(&commandline.to_ffi(), usize::MAX); + commandline_set_buffer(commandline.to_owned(), None); let mline = { let _interactive = scoped_push_replacer( diff --git a/fish-rust/src/builtins/shared.rs b/fish-rust/src/builtins/shared.rs index a8ee1ae83..8f2119028 100644 --- a/fish-rust/src/builtins/shared.rs +++ b/fish-rust/src/builtins/shared.rs @@ -1,13 +1,13 @@ use super::prelude::*; use crate::builtins::*; use crate::common::{escape, get_by_sorted_name, str2wcstring, Named}; -use crate::ffi; use crate::ffi::Repin; use crate::io::{IoChain, IoFd, OutputStream, OutputStreamFfi}; use crate::parse_constants::UNKNOWN_BUILTIN_ERR_MSG; use crate::parse_util::parse_util_argument_is_help; use crate::parser::{Block, BlockType, LoopStatus}; use crate::proc::{no_exec, ProcStatus}; +use crate::reader::reader_read; use crate::wchar::{wstr, WString, L}; use crate::wgetopt::{wgetopter_t, wopt, woption, woption_argument_t}; use cxx::CxxWString; @@ -951,11 +951,7 @@ fn builtin_breakpoint( } else { unsafe { &mut *streams.io_chain } }; - ffi::reader_read_ffi( - parser as *const Parser as *const autocxx::c_void, - autocxx::c_int(STDIN_FILENO), - &io_chain as *const _ as *const autocxx::c_void, - ); + reader_read(parser, STDIN_FILENO, io_chain); parser.pop_block(bpb); Some(parser.get_last_status()) } diff --git a/fish-rust/src/builtins/source.rs b/fish-rust/src/builtins/source.rs index 4f4c458f9..3d5267466 100644 --- a/fish-rust/src/builtins/source.rs +++ b/fish-rust/src/builtins/source.rs @@ -1,10 +1,10 @@ use crate::{ common::{escape, scoped_push_replacer, FilenameRef}, fds::{wopen_cloexec, AutoCloseFd}, - ffi::reader_read_ffi, io::IoChain, nix::isatty, parser::Block, + reader::reader_read, }; use libc::{c_int, O_RDONLY, S_IFMT, S_IFREG}; @@ -103,16 +103,15 @@ pub fn source(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> O parser.vars().set_argv(argv_list); let empty_io_chain = IoChain::new(); - let retval = reader_read_ffi( - parser as *const Parser as *const autocxx::c_void, - unsafe { std::mem::transmute(fd) }, + let mut retval = reader_read( + parser, + fd, if !streams.io_chain.is_null() { unsafe { &*streams.io_chain } } else { &empty_io_chain - } as *const _ as *const autocxx::c_void, + }, ); - let mut retval: c_int = unsafe { std::mem::transmute(retval) }; parser.pop_block(sb); diff --git a/fish-rust/src/common.rs b/fish-rust/src/common.rs index 7725e6e2f..ea771f35b 100644 --- a/fish-rust/src/common.rs +++ b/fish-rust/src/common.rs @@ -1906,6 +1906,21 @@ where ScopeGuard::new((), restore_saved) } +pub fn scoped_push_replacer_ctx( + mut ctx: Context, + replacer: Replacer, + new_value: T, +) -> impl ScopeGuarding +where + Replacer: Fn(&mut Context, T) -> T, +{ + let saved = replacer(&mut ctx, new_value); + let restore_saved = move |ctx: &mut Context| { + replacer(ctx, saved); + }; + ScopeGuard::new(ctx, restore_saved) +} + pub const fn assert_send() {} pub const fn assert_sync() {} diff --git a/fish-rust/src/complete.rs b/fish-rust/src/complete.rs index b04f68b73..712f49e84 100644 --- a/fish-rust/src/complete.rs +++ b/fish-rust/src/complete.rs @@ -173,7 +173,7 @@ impl Completion { pub fn new( completion: WString, description: WString, - r#match: StringFuzzyMatch, + r#match: StringFuzzyMatch, /* = exact_match */ flags: CompleteFlags, ) -> Self { let flags = resolve_auto_space(&completion, flags); diff --git a/fish-rust/src/env/environment.rs b/fish-rust/src/env/environment.rs index 89dfa73fb..7b33dee93 100644 --- a/fish-rust/src/env/environment.rs +++ b/fish-rust/src/env/environment.rs @@ -116,6 +116,16 @@ pub struct EnvDyn { inner: Box, } +pub trait AsEnvironment { + fn as_environment(&self) -> &(dyn Environment + Send + Sync); +} + +impl AsEnvironment for EnvDyn { + fn as_environment(&self) -> &(dyn Environment + Send + Sync) { + &*self.inner + } +} + impl EnvDyn { // Exposed for testing. pub fn new(inner: Box) -> Self { @@ -389,8 +399,15 @@ impl Environment for EnvStack { } } +// TODO Remove Pin? pub type EnvStackRef = Pin>; +impl AsEnvironment for EnvStackRef { + fn as_environment(&self) -> &(dyn Environment + Send + Sync) { + Pin::get_ref(Pin::as_ref(self)) + } +} + // A variable stack that only represents globals. // Do not push or pop from this. lazy_static! { diff --git a/fish-rust/src/env/environment_impl.rs b/fish-rust/src/env/environment_impl.rs index ec83ff011..ad6f3db9c 100644 --- a/fish-rust/src/env/environment_impl.rs +++ b/fish-rust/src/env/environment_impl.rs @@ -4,14 +4,14 @@ use crate::env::{ ELECTRIC_VARIABLES, PATH_ARRAY_SEP, }; use crate::env_universal_common::EnvUniversal; -use crate::ffi; use crate::flog::FLOG; use crate::global_safety::RelaxedAtomicBool; +use crate::history::{history_session_id_from_var, History}; use crate::kill::kill_entries; use crate::null_terminated_array::OwningNullTerminatedArray; +use crate::reader::{commandline_get_state, reader_status_count}; use crate::threads::{is_forked_child, is_main_thread}; use crate::wchar::prelude::*; -use crate::wchar_ffi::{WCharFromFFI, WCharToFFI}; use crate::wutil::fish_wcstol_radix; use lazy_static::lazy_static; @@ -41,11 +41,6 @@ pub fn uvars() -> MutexGuard<'static, EnvUniversal> { /// Whether we were launched with no_config; in this case setting a uvar instead sets a global. pub static UVAR_SCOPE_IS_GLOBAL: RelaxedAtomicBool = RelaxedAtomicBool::new(false); -/// Helper to get the history for a session ID. -fn get_history_var_text(history_session_id: &wstr) -> Vec { - ffi::get_history_variable_text_ffi(&history_session_id.to_ffi()).from_ffi() -} - /// Apply the pathvar behavior, splitting about colons. pub fn colon_split>(val: &[T]) -> Vec { let mut split_val = Vec::new(); @@ -362,15 +357,12 @@ impl EnvScopedImpl { if (!is_main_thread()) { return None; } - let fish_history_var = self - .getf(L!("fish_history"), EnvMode::default()) - .map(|v| v.as_string()); - let history_session_id = fish_history_var - .as_ref() - .map(WString::as_utfstr) - .unwrap_or(DFLT_FISH_HISTORY_SESSION_ID); - let vals = get_history_var_text(history_session_id); - return Some(EnvVar::new_from_name_vec("history"L, vals)); + let history = commandline_get_state().history.unwrap_or_else(|| { + let fish_history_var = self.getf(L!("fish_history"), EnvMode::default()); + let session_id = history_session_id_from_var(fish_history_var); + History::with_name(&session_id) + }); + return Some(EnvVar::new_from_name_vec("history"L, history.get_history())); } else if key == "fish_killring"L { Some(EnvVar::new_from_name_vec("fish_killring"L, kill_entries())) } else if key == "pipestatus"L { @@ -385,7 +377,7 @@ impl EnvScopedImpl { let js = &self.perproc_data.statuses; Some(EnvVar::new_from_name("status"L, js.status.to_wstring())) } else if key == "status_generation"L { - let status_generation = ffi::reader_status_count(); + let status_generation = reader_status_count(); Some(EnvVar::new_from_name( "status_generation"L, status_generation.to_wstring(), diff --git a/fish-rust/src/env_dispatch.rs b/fish-rust/src/env_dispatch.rs index bedc433ce..76a14acf5 100644 --- a/fish-rust/src/env_dispatch.rs +++ b/fish-rust/src/env_dispatch.rs @@ -8,10 +8,13 @@ use crate::function; use crate::input_common::{update_wait_on_escape_ms, update_wait_on_sequence_key_ms}; use crate::output::ColorSupport; use crate::proc::is_interactive_session; +use crate::reader::{ + reader_change_cursor_selection_mode, reader_change_history, reader_schedule_prompt_repaint, + reader_set_autosuggestion_enabled, +}; use crate::screen::screen_set_midnight_commander_hack; use crate::screen::LAYOUT_CACHE_SHARED; use crate::wchar::prelude::*; -use crate::wchar_ffi::WCharToFFI; use crate::wutil::fish_wcstoi; use std::borrow::Cow; use std::collections::HashMap; @@ -222,7 +225,7 @@ pub fn env_dispatch_var_change(key: &wstr, vars: &EnvStack) { fn handle_fish_term_change(vars: &EnvStack) { update_fish_color_support(vars); - crate::ffi::reader_schedule_prompt_repaint(); + reader_schedule_prompt_repaint(); } fn handle_change_ambiguous_width(vars: &EnvStack) { @@ -243,7 +246,7 @@ fn handle_term_size_change(vars: &EnvStack) { fn handle_fish_history_change(vars: &EnvStack) { let session_id = crate::history::history_session_id(vars); - crate::ffi::reader_change_history(&session_id.to_ffi()); + reader_change_history(&session_id); } fn handle_fish_cursor_selection_mode_change(vars: &EnvStack) { @@ -261,16 +264,11 @@ fn handle_fish_cursor_selection_mode_change(vars: &EnvStack) { CursorSelectionMode::Exclusive }; - let mode = mode as u8; - crate::ffi::reader_change_cursor_selection_mode(mode); + reader_change_cursor_selection_mode(mode); } fn handle_autosuggestion_change(vars: &EnvStack) { - // TODO: This was a call to reader_set_autosuggestion_enabled(vars) and - // reader::check_autosuggestion_enabled() should be private to the `reader` module. - crate::ffi::reader_set_autosuggestion_enabled_ffi(crate::reader::check_autosuggestion_enabled( - vars, - )); + reader_set_autosuggestion_enabled(vars); } fn handle_function_path_change(_: &EnvStack) { diff --git a/fish-rust/src/exec.rs b/fish-rust/src/exec.rs index 97210f8f0..475359ce6 100644 --- a/fish-rust/src/exec.rs +++ b/fish-rust/src/exec.rs @@ -16,7 +16,7 @@ use crate::env::{EnvMode, EnvStack, Environment, Statuses, READ_BYTE_LIMIT}; use crate::env_dispatch::use_posix_spawn; use crate::fds::make_fd_blocking; use crate::fds::{make_autoclose_pipes, open_cloexec, AutoCloseFd, AutoClosePipes, PIPE_ERROR}; -use crate::ffi::{self, wcstring_list_ffi_t}; +use crate::ffi::wcstring_list_ffi_t; use crate::flog::FLOGF; use crate::fork_exec::blocked_signals_for_job; use crate::fork_exec::postfork::{ @@ -40,7 +40,7 @@ use crate::proc::{ print_exit_warning_for_jobs, InternalProc, Job, JobGroupRef, ProcStatus, Process, ProcessType, TtyTransfer, INVALID_PID, }; -use crate::reader::reader_run_count; +use crate::reader::{reader_run_count, restore_term_mode}; use crate::redirection::{dup2_list_resolve_chain, Dup2List}; use crate::threads::{iothread_perform_cant_wait, is_forked_child}; use crate::timer::push_timer; @@ -444,7 +444,7 @@ fn launch_process_nofork(vars: &EnvStack, p: &Process) -> ! { let actual_cmd = wcs2zstring(&p.actual_cmd); // Ensure the terminal modes are what they were before we changed them. - ffi::restore_term_mode(); + restore_term_mode(); // Bounce to launch_process. This never returns. safe_launch_process(p, &actual_cmd, &argv, &*envp); } diff --git a/fish-rust/src/ffi.rs b/fish-rust/src/ffi.rs index f189b0ccd..e1ecf5d8b 100644 --- a/fish-rust/src/ffi.rs +++ b/fish-rust/src/ffi.rs @@ -35,28 +35,16 @@ include_cpp! { #include "tokenizer.h" #include "wutil.h" - #include "builtins/commandline.h" - safety!(unsafe_ffi) generate_pod!("wcharz_t") generate!("wcstring_list_ffi_t") generate!("set_inheriteds_ffi") - generate!("reader_init") - generate!("reader_run_count") - generate!("term_copy_modes") generate!("set_profiling_active") - generate!("reader_read_ffi") - generate!("fish_is_unwinding_for_exit") - generate!("restore_term_mode") - generate!("read_generation_count") generate!("set_flog_output_file_ffi") generate!("flog_setlinebuf_ffi") generate!("activate_flog_categories_by_pattern") - generate!("restore_term_foreground_process_group_for_exit") - - generate!("builtin_commandline") generate!("shell_modes_ffi") @@ -68,26 +56,9 @@ include_cpp! { generate!("rgb_color_t") generate_pod!("color24_t") - generate!("reader_status_count") - generate!("reader_write_title_ffi") - generate!("reader_push_ffi") - generate!("reader_readline_ffi") - generate!("reader_pop") - generate!("commandline_get_state_history_ffi") - generate!("commandline_set_buffer_ffi") - generate!("commandline_get_state_initialized_ffi") - generate!("commandline_get_state_text_ffi") - generate!("completion_apply_to_command_line") - - generate!("get_history_variable_text_ffi") generate_pod!("escape_string_style_t") - generate!("reader_schedule_prompt_repaint") - generate!("reader_reading_interrupted") - generate!("reader_change_history") - generate!("reader_change_cursor_selection_mode") - generate!("reader_set_autosuggestion_enabled_ffi") } /// Allow wcharz_t to be "into" wstr. diff --git a/fish-rust/src/fish.rs b/fish-rust/src/fish.rs index d449e2081..095c2b2c9 100644 --- a/fish-rust/src/fish.rs +++ b/fish-rust/src/fish.rs @@ -17,15 +17,14 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ -use autocxx::prelude::*; - use crate::{ ast::Ast, builtins::shared::{ BUILTIN_ERR_MISSING, BUILTIN_ERR_UNKNOWN, STATUS_CMD_OK, STATUS_CMD_UNKNOWN, }, common::{ - escape, exit_without_destructors, get_executable_path, save_term_foreground_process_group, + escape, exit_without_destructors, get_executable_path, + restore_term_foreground_process_group_for_exit, save_term_foreground_process_group, scoped_push_replacer, str2wcstring, wcs2string, PROFILING_ACTIVE, PROGRAM_NAME, }, env::Statuses, @@ -50,6 +49,7 @@ use crate::{ get_login, is_interactive_session, mark_login, mark_no_exec, proc_init, set_interactive_session, }, + reader::{reader_init, reader_read, restore_term_mode, term_copy_modes}, signal::{signal_clear_cancel, signal_unblock_all}, threads::{self, asan_maybe_exit}, topic_monitor, @@ -637,7 +637,7 @@ fn main() -> i32 { features::set_from_string(opts.features.as_utfstr()); proc_init(); crate::env::misc_init(); - ffi::reader_init(); + reader_init(); let parser = Parser::principal_parser(); parser.set_syncs_uvars(!opts.no_config); @@ -659,7 +659,7 @@ fn main() -> i32 { } // Re-read the terminal modes after config, it might have changed them. - ffi::term_copy_modes(); + term_copy_modes(); // Stomp the exit status of any initialization commands (issue #635). parser.set_last_statuses(Statuses::just(STATUS_CMD_OK.unwrap())); @@ -712,12 +712,7 @@ fn main() -> i32 { // above line should always exit return libc::EXIT_FAILURE; } - res = ffi::reader_read_ffi( - parser as *const Parser as *const autocxx::c_void, - c_int(libc::STDIN_FILENO), - &IoChain::new() as *const _ as *const autocxx::c_void, - ) - .into(); + res = reader_read(parser, libc::STDIN_FILENO, &IoChain::new()); } else { // C++ had not converted at this point, we must undo let n = wcs2string(&args[my_optind]); @@ -749,12 +744,7 @@ fn main() -> i32 { }, Some(Arc::new(rel_filename.to_owned())), ); - res = ffi::reader_read_ffi( - parser as *const Parser as *const autocxx::c_void, - c_int(f.as_raw_fd()), - &IoChain::new() as *const _ as *const autocxx::c_void, - ) - .into(); + res = reader_read(parser, f.as_raw_fd(), &IoChain::new()); if res != 0 { FLOGF!( warning, @@ -781,9 +771,9 @@ fn main() -> i32 { vec![exit_status.to_wstring()], ); - ffi::restore_term_mode(); + restore_term_mode(); // this is ported, but not adopted - ffi::restore_term_foreground_process_group_for_exit(); + restore_term_foreground_process_group_for_exit(); if let Some(profile_output) = opts.profile_output { let s = cstr_from_osstr(&profile_output); diff --git a/fish-rust/src/flog.rs b/fish-rust/src/flog.rs index 8cef1f063..60f8d0449 100644 --- a/fish-rust/src/flog.rs +++ b/fish-rust/src/flog.rs @@ -134,6 +134,8 @@ pub mod categories { (screen, "screen", "Screen repaints"); + (abbrs, "abbrs", "Abbreviation expansion"); + (refcell, "refcell", "Refcell dynamic borrowing"); ); } diff --git a/fish-rust/src/future.rs b/fish-rust/src/future.rs index 97b8d7e5c..7c0d5b609 100644 --- a/fish-rust/src/future.rs +++ b/fish-rust/src/future.rs @@ -28,6 +28,8 @@ pub trait IsOkAnd { type Error; #[allow(clippy::wrong_self_convention)] fn is_ok_and(self, s: impl FnOnce(Self::Type) -> bool) -> bool; + #[allow(clippy::wrong_self_convention)] + fn is_err_and(self, s: impl FnOnce(Self::Error) -> bool) -> bool; } impl IsOkAnd for Result { type Type = T; @@ -38,6 +40,12 @@ impl IsOkAnd for Result { Err(_) => false, } } + fn is_err_and(self, f: impl FnOnce(E) -> bool) -> bool { + match self { + Ok(_) => false, + Err(e) => f(e), + } + } } pub trait IsSorted { diff --git a/fish-rust/src/history.rs b/fish-rust/src/history.rs index bff37a5e3..7bf139a41 100644 --- a/fish-rust/src/history.rs +++ b/fish-rust/src/history.rs @@ -16,7 +16,7 @@ //! 5. The chaos_mode boolean can be set to true to do things like lower buffer sizes which can //! trigger race conditions. This is useful for testing. -use crate::{common::cstr2wcstring, wcstringutil::trim}; +use crate::{common::cstr2wcstring, env::EnvVar, wcstringutil::trim}; use std::{ borrow::Cow, collections::{BTreeMap, HashMap, HashSet, VecDeque}, @@ -46,7 +46,7 @@ use crate::{ str2wcstring, unescape_string, valid_var_name, wcs2zstring, write_loop, CancelChecker, UnescapeStringStyle, }, - env::{EnvDyn, EnvMode, EnvStack, EnvStackRefFFI, Environment}, + env::{AsEnvironment, EnvMode, EnvStack, EnvStackRefFFI, Environment}, expand::{expand_one, ExpandFlags}, fallback::fish_mkstemp_cloexec, fds::{wopen_cloexec, AutoCloseFd}, @@ -66,7 +66,7 @@ use crate::{ util::find_subslice, wchar::prelude::*, wchar_ext::WExt, - wchar_ffi::{AsWstr, WCharFromFFI, WCharToFFI}, + wchar_ffi::{WCharFromFFI, WCharToFFI}, wcstringutil::subsequence_in_string, wildcard::{wildcard_match, ANY_STRING}, wutil::{ @@ -129,12 +129,6 @@ mod history_ffi { Backward, } - pub enum HistoryPagerInvocation { - Anew, - Advance, - Refresh, - } - extern "Rust" { #[cxx_name = "history_save_all"] fn save_all(); @@ -205,13 +199,6 @@ mod history_ffi { fn item_at_index(&self, idx: usize) -> Box; fn size(&self) -> usize; fn clone(&self) -> Box; - #[cxx_name = "add_pending_with_file_detection"] - fn add_pending_with_file_detection_ffi( - &self, - s: &CxxWString, - vars: &EnvStackRefFFI, - persist_mode: PersistenceMode, - ); } extern "Rust" { @@ -1701,7 +1688,7 @@ impl History { pub fn add_pending_with_file_detection( self: Arc, s: &wstr, - vars: EnvDyn, + vars: impl AsEnvironment + Send + Sync + 'static, persist_mode: PersistenceMode, /*=disk*/ ) { // We use empty items as sentinels to indicate the end of history. @@ -1760,7 +1747,8 @@ impl History { drop(imp); iothread_perform(move || { // Don't hold the lock while we perform this file detection. - let validated_paths = expand_and_detect_paths(potential_paths, &vars); + let validated_paths = + expand_and_detect_paths(potential_paths, vars.as_environment()); let mut imp = self.imp(); imp.set_valid_file_paths(validated_paths, identifier); imp.enable_automatic_saving(); @@ -2096,10 +2084,13 @@ pub fn save_all() { /// Return the prefix for the files to be used for command and read history. pub fn history_session_id(vars: &dyn Environment) -> WString { - let Some(var) = vars.get(L!("fish_history")) else { + history_session_id_from_var(vars.get(L!("fish_history"))) +} + +pub fn history_session_id_from_var(history_name_var: Option) -> WString { + let Some(var) = history_name_var else { return DFLT_FISH_HISTORY_SESSION_ID.to_owned(); }; - let session_id = var.as_string(); if session_id.is_empty() || valid_var_name(&session_id) { session_id @@ -2335,18 +2326,6 @@ impl HistorySharedPtr { fn clone(&self) -> Box { Box::new(Self(Arc::clone(&self.0))) } - fn add_pending_with_file_detection_ffi( - &self, - s: &CxxWString, - vars: &EnvStackRefFFI, - persist_mode: PersistenceMode, - ) { - Arc::clone(&self.0).add_pending_with_file_detection( - s.as_wstr(), - vars.0.snapshot(), - persist_mode, - ) - } } fn history_with_name(name: &CxxWString) -> Box { diff --git a/fish-rust/src/input_common.rs b/fish-rust/src/input_common.rs index 0f568ddf5..14a46fd65 100644 --- a/fish-rust/src/input_common.rs +++ b/fish-rust/src/input_common.rs @@ -2,6 +2,7 @@ use crate::common::{is_windows_subsystem_for_linux, read_blocked}; use crate::env::{EnvStack, Environment}; use crate::fd_readable_set::FdReadableSet; use crate::flog::FLOG; +use crate::reader::reader_current_data; use crate::threads::{iothread_port, iothread_service_main}; use crate::universal_notifier::default_notifier; use crate::wchar::prelude::*; @@ -307,7 +308,7 @@ pub trait InputEventQueuer { } ReadbResult::IOPortNotified => { - iothread_service_main(); + iothread_service_main(reader_current_data().unwrap()); } ReadbResult::Byte(read_byte) => { diff --git a/fish-rust/src/lib.rs b/fish-rust/src/lib.rs index d8fef4378..2bb831b0e 100644 --- a/fish-rust/src/lib.rs +++ b/fish-rust/src/lib.rs @@ -93,6 +93,7 @@ mod print_help; mod proc; mod re; mod reader; +mod reader_history_search; mod redirection; mod screen; mod signal; diff --git a/fish-rust/src/operation_context.rs b/fish-rust/src/operation_context.rs index 37b11a788..2d72fe490 100644 --- a/fish-rust/src/operation_context.rs +++ b/fish-rust/src/operation_context.rs @@ -6,6 +6,8 @@ use crate::proc::JobGroupRef; use once_cell::sync::Lazy; use std::sync::Arc; +use crate::reader::read_generation_count; + /// A common helper which always returns false. pub fn no_cancel() -> bool { false @@ -146,7 +148,7 @@ impl<'a> OperationContext<'a> { pub fn get_bg_context(env: &EnvDyn, generation_count: u32) -> OperationContext { let cancel_checker = move || { // Cancel if the generation count changed. - generation_count != crate::ffi::read_generation_count() + generation_count != read_generation_count() }; OperationContext::background_with_cancel_checker( env, diff --git a/fish-rust/src/parse_util.rs b/fish-rust/src/parse_util.rs index 8289dca36..d6a3e494c 100644 --- a/fish-rust/src/parse_util.rs +++ b/fish-rust/src/parse_util.rs @@ -399,10 +399,10 @@ fn job_or_process_extent( { if tok_begin >= pos { finished = true; - result.start = tok_begin; + result.end = tok_begin; } else { // Statement at cursor might start after this token. - result.end = tok_begin + token.length(); + result.start = tok_begin + token.length(); out_tokens.as_mut().map(|tokens| tokens.clear()); } continue; // Do not add this to tokens @@ -490,7 +490,7 @@ pub fn parse_util_lineno(s: &wstr, offset: usize) -> usize { } let end = offset.min(s.len()); - s.chars().take(end).filter(|c| *c == '\n').count() + s.chars().take(end).filter(|c| *c == '\n').count() + 1 } /// Calculate the line number of the specified cursor position. @@ -570,6 +570,52 @@ pub fn parse_util_unescape_wildcards(s: &wstr) -> WString { result } +/// Return if the given string contains wildcard characters. +pub fn parse_util_contains_wildcards(s: &wstr) -> bool { + let unesc_qmark = !feature_test(FeatureFlag::qmark_noglob); + + let mut i = 0; + while i < s.len() { + let c = s.as_char_slice()[i]; + if c == '*' { + return true; + } else if unesc_qmark && c == '?' { + return true; + } else if c == '\\' { + if s.char_at(i + 1) == '*' { + i += 1; + } else if unesc_qmark && s.char_at(i + 1) == '?' { + i += 1; + } else if s.char_at(i + 1) == '\\' { + // Not a wildcard, but ensure the next iteration doesn't see this escaped backslash. + i += 1; + } + } + i += 1; + } + false +} + +/// Escape any wildcard characters in the given string. e.g. convert +/// "a*b" to "a\*b". +pub fn parse_util_escape_wildcards(s: &wstr) -> WString { + let mut result = WString::with_capacity(s.len()); + let unesc_qmark = !feature_test(FeatureFlag::qmark_noglob); + + for c in s.chars() { + if c == '*' { + result.push_str("\\*"); + } else if unesc_qmark && c == '?' { + result.push_str("\\?"); + } else if c == '\\' { + result.push_str("\\\\"); + } else { + result.push(c); + } + } + result +} + /// Checks if the specified string is a help option. #[widestrs] pub fn parse_util_argument_is_help(s: &wstr) -> bool { diff --git a/fish-rust/src/reader.rs b/fish-rust/src/reader.rs index 2cc1358b5..55664e08f 100644 --- a/fish-rust/src/reader.rs +++ b/fish-rust/src/reader.rs @@ -1,23 +1,254 @@ -use std::sync::atomic::AtomicI32; +//! Functions for reading data from stdin and passing to the parser. If stdin is a keyboard, it +//! supplies a killring, history, syntax highlighting, tab-completion and various other interactive +//! features. +//! +//! Internally the interactive mode functions rely in the functions of the input library to read +//! individual characters of input. +//! +//! Token search is handled incrementally. Actual searches are only done on when searching backwards, +//! since the previous results are saved. The last search position is remembered and a new search +//! continues from the last search position. All search results are saved in the list 'search_prev'. +//! When the user searches forward, i.e. presses Alt-down, the list is consulted for previous search +//! result, and subsequent backwards searches are also handled by consulting the list up until the +//! end of the list is reached, at which point regular searching will commence. -use crate::common::{escape_string, EscapeFlags, EscapeStringStyle}; -use crate::complete::{CompleteFlags, CompletionList}; -use crate::env::Environment; -use crate::expand::{expand_string, ExpandFlags, ExpandResultCode}; -use crate::ffi; -use crate::global_safety::RelaxedAtomicBool; -use crate::operation_context::OperationContext; -use crate::parser::Parser; -use crate::signal::signal_check_cancel; -use crate::wchar::prelude::*; -use crate::wchar_ffi::{WCharFromFFI, WCharToFFI}; -use crate::wcstringutil::count_preceding_backslashes; -use crate::wildcard::wildcard_has; -use cxx::{CxxWString, UniquePtr}; -use libc::SIGINT; +use libc::{ + c_char, c_int, c_void, EAGAIN, ECHO, EINTR, EIO, EISDIR, ENOTTY, EPERM, ESRCH, EWOULDBLOCK, + ICANON, ICRNL, IEXTEN, INLCR, IXOFF, IXON, ONLCR, OPOST, O_NONBLOCK, O_RDONLY, SIGINT, SIGTTIN, + STDIN_FILENO, STDOUT_FILENO, S_IFDIR, TCSANOW, VMIN, VQUIT, VSUSP, VTIME, _POSIX_VDISABLE, +}; +use once_cell::sync::Lazy; +use std::cell::UnsafeCell; +use std::io::BufReader; +use std::num::NonZeroUsize; +use std::ops::Range; use std::os::fd::RawFd; +use std::pin::Pin; +use std::rc::Rc; use std::sync::atomic::Ordering; +use std::sync::atomic::{AtomicI32, AtomicU32, AtomicU64, AtomicU8}; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::time::{Duration, Instant}; +use errno::{errno, Errno}; + +use crate::abbrs::abbrs_match; +use crate::ast::{self, Ast, Category, Traversal}; +use crate::builtins::shared::STATUS_CMD_OK; +use crate::color::RgbColor; +use crate::common::{ + escape, escape_string, exit_without_destructors, fish_reserved_codepoint, get_ellipsis_char, + get_obfuscation_read_char, redirect_tty_output, scoped_push_replacer, scoped_push_replacer_ctx, + shell_modes, shell_modes_mut, str2wcstring, wcs2string, write_loop, EscapeFlags, + EscapeStringStyle, ScopeGuard, PROGRAM_NAME, UTF8_BOM_WCHAR, +}; +use crate::compat::MB_CUR_MAX; +use crate::complete::{ + complete, complete_load, sort_and_prioritize, CompleteFlags, Completion, CompletionList, + CompletionRequestOptions, +}; +use crate::editable_line::{Edit, EditableLine}; +use crate::env::{EnvMode, Environment, Statuses}; +use crate::env_dispatch::term_supports_setting_title; +use crate::exec::exec_subshell; +use crate::expand::{expand_string, expand_tilde, ExpandFlags, ExpandResultCode}; +use crate::fallback::fish_wcwidth; +use crate::fd_readable_set::poll_fd_readable; +use crate::fds::{make_fd_blocking, wopen_cloexec, AutoCloseFd}; +use crate::flog::{FLOG, FLOGF}; +#[allow(unused_imports)] +use crate::future::{IsOkAnd, IsSomeAnd}; +use crate::global_safety::RelaxedAtomicBool; +use crate::highlight::{ + autosuggest_validate_from_history, highlight_shell, HighlightRole, HighlightSpec, +}; +use crate::history::{ + history_session_id, in_private_mode, History, HistorySearch, PersistenceMode, SearchDirection, + SearchType, +}; +use crate::input::init_input; +use crate::input::Inputter; +use crate::input_common::{CharEvent, CharInputStyle, ReadlineCmd}; +use crate::io::IoChain; +use crate::kill::{kill_add, kill_replace, kill_yank, kill_yank_rotate}; +use crate::operation_context::{get_bg_context, OperationContext}; +use crate::output::{parse_color, Outputter}; +use crate::pager::{PageRendering, Pager, SelectionMotion}; +use crate::parse_constants::SourceRange; +use crate::parse_constants::{ParseTreeFlags, ParserTestErrorBits}; +use crate::parse_tree::ParsedSource; +use crate::parse_util::{ + parse_util_cmdsubst_extent, parse_util_compute_indents, parse_util_contains_wildcards, + parse_util_detect_errors, parse_util_detect_errors_in_ast, parse_util_escape_string_with_quote, + parse_util_escape_wildcards, parse_util_get_line_from_offset, parse_util_get_offset, + parse_util_get_offset_from_line, parse_util_get_quote_type, parse_util_lineno, + parse_util_locate_cmdsubst_range, parse_util_token_extent, +}; +use crate::parser::{BlockType, EvalRes, Parser, ParserRef}; +use crate::proc::{ + have_proc_stat, hup_jobs, is_interactive_session, job_reap, jobs_requiring_warning_on_exit, + print_exit_warning_for_jobs, proc_update_jiffies, +}; +use crate::reader_history_search::{smartcase_flags, ReaderHistorySearch, SearchMode}; +use crate::screen::{screen_clear, screen_force_clear_to_end, Screen}; +use crate::signal::{ + signal_check_cancel, signal_clear_cancel, signal_reset_handlers, signal_set_handlers, + signal_set_handlers_once, +}; +use crate::termsize::{termsize_invalidate_tty, termsize_last, termsize_update}; +use crate::threads::{ + assert_is_background_thread, assert_is_main_thread, iothread_service_main_with_timeout, + Debounce, +}; +use crate::tokenizer::{ + tok_command, MoveWordStateMachine, MoveWordStyle, TokenType, Tokenizer, TOK_ACCEPT_UNFINISHED, + TOK_SHOW_COMMENTS, +}; +use crate::wchar::prelude::*; +use crate::wcstringutil::{ + count_preceding_backslashes, join_strings, string_prefixes_string, + string_prefixes_string_case_insensitive, StringFuzzyMatch, +}; +use crate::wildcard::wildcard_has; +use crate::wutil::{perror, write_to_fd}; +use crate::{abbrs, event, function, history}; + +/// A description of where fish is in the process of exiting. +#[repr(u8)] +enum ExitState { + /// fish is not exiting. + None, + /// fish intends to exit, and is running handlers like 'fish_exit'. + RunningHandlers, + /// fish is finished running handlers and no more fish script may be run. + FinishedHandlers, +} + +static EXIT_STATE: AtomicU8 = AtomicU8::new(ExitState::None as u8); + +/// Mode on startup, which we restore on exit. +static TERMINAL_MODE_ON_STARTUP: Lazy> = + Lazy::new(|| Mutex::new(unsafe { std::mem::zeroed() })); + +/// Mode we use to execute programs. +static TTY_MODES_FOR_EXTERNAL_CMDS: Lazy> = + Lazy::new(|| Mutex::new(unsafe { std::mem::zeroed() })); + +static RUN_COUNT: AtomicU64 = AtomicU64::new(0); + +static STATUS_COUNT: AtomicU64 = AtomicU64::new(0); + +/// This variable is set to a signal by the signal handler when ^C is pressed. +static INTERRUPTED: AtomicI32 = AtomicI32::new(0); + +/// If set, SIGHUP has been received. This latches to true. +/// This is set from a signal handler. +static SIGHUP_RECEIVED: RelaxedAtomicBool = RelaxedAtomicBool::new(false); + +/// A singleton snapshot of the reader state. This is updated when the reader changes. This is +/// factored out for thread-safety reasons: it may be fetched on a background thread. +fn commandline_state_snapshot() -> MutexGuard<'static, CommandlineState> { + static STATE: Mutex = Mutex::new(CommandlineState::new()); + STATE.lock().unwrap() +} + +/// Any time the contents of a buffer changes, we update the generation count. This allows for our +/// background threads to notice it and skip doing work that they would otherwise have to do. +static GENERATION: AtomicU32 = AtomicU32::new(0); + +/// Get the debouncer for autosuggestions and background highlighting. +fn debounce_autosuggestions() -> &'static Debounce { + const AUTOSUGGEST_TIMEOUT: Duration = Duration::from_millis(500); + static RES: once_cell::race::OnceBox = once_cell::race::OnceBox::new(); + RES.get_or_init(|| Box::new(Debounce::new(AUTOSUGGEST_TIMEOUT))) +} + +fn debounce_highlighting() -> &'static Debounce { + const HIGHLIGHT_TIMEOUT: Duration = Duration::from_millis(500); + static RES: once_cell::race::OnceBox = once_cell::race::OnceBox::new(); + RES.get_or_init(|| Box::new(Debounce::new(HIGHLIGHT_TIMEOUT))) +} + +fn debounce_history_pager() -> &'static Debounce { + const HISTORY_PAGER_TIMEOUT: Duration = Duration::from_millis(500); + static RES: once_cell::race::OnceBox = once_cell::race::OnceBox::new(); + RES.get_or_init(|| Box::new(Debounce::new(HISTORY_PAGER_TIMEOUT))) +} + +fn redirect_tty_after_sighup() { + // If we have received SIGHUP, redirect the tty to avoid a user script triggering SIGTTIN or + // SIGTTOU. + assert!(reader_received_sighup(), "SIGHUP not received"); + static TTY_REDIRECTED: RelaxedAtomicBool = RelaxedAtomicBool::new(false); + if !TTY_REDIRECTED.swap(true) { + redirect_tty_output(); + } +} + +/// The stack of current interactive reading contexts. +fn reader_data_stack() -> &'static mut Vec>> { + struct ReaderDataStack(UnsafeCell>>>); + // Safety: only used on main thread. + unsafe impl Sync for ReaderDataStack {} + + static mut READER_DATA_STACK: ReaderDataStack = ReaderDataStack(UnsafeCell::new(vec![])); + + assert_is_main_thread(); + unsafe { READER_DATA_STACK.0.get_mut() } +} + +/// Access the top level reader data. +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; + +/// Add a new reader to the reader stack. +/// \return a shared pointer to it. +fn reader_push_ret( + parser: &Parser, + history_name: &wstr, + conf: ReaderConfig, +) -> &'static mut ReaderData { + assert_is_main_thread(); + let hist = History::with_name(history_name); + hist.resolve_pending(); + let data = ReaderData::new(parser.shared(), hist, conf); + reader_data_stack().push(data); + let data = current_data().unwrap(); + data.command_line_changed(EditableLineTag::Commandline); + if reader_data_stack().len() == 1 { + reader_interactive_init(parser); + } + data.update_commandline_state(); + data +} + +/// Push a new reader environment controlled by \p conf, using the given history name. +/// If \p history_name is empty, then save history in-memory only; do not write it to disk. +pub fn reader_push(parser: &Parser, history_name: &wstr, conf: ReaderConfig) { + // Public variant which discards the return value. + reader_push_ret(parser, history_name, conf); +} + +/// Return to previous reader environment. +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(usize::try_from(termsize_last().width).unwrap()); + new_reader.update_commandline_state(); + } else { + reader_interactive_destroy(); + *commandline_state_snapshot() = CommandlineState::new(); + } +} + +/// Configuration that we provide to a reader. #[derive(Default)] pub struct ReaderConfig { /// Left prompt command, typically fish_prompt. @@ -54,37 +285,684 @@ pub struct ReaderConfig { pub inputfd: RawFd, } -pub fn reader_push(parser: &Parser, history_name: &wstr, conf: ReaderConfig) { - ffi::reader_push_ffi( - parser as *const Parser as *const autocxx::c_void, - &history_name.to_ffi(), - &conf as *const ReaderConfig as *const autocxx::c_void, - ); +/// Snapshotted state from the reader. +#[derive(Clone, Default)] +pub struct CommandlineState { + /// command line text, or empty if not interactive + pub text: WString, + /// position of the cursor, may be as large as text.size() + pub cursor_pos: usize, + /// visual selection, or none if none + pub selection: Option>, + /// current reader history, or null if not interactive + pub history: Option>, + /// pager is visible + pub pager_mode: bool, + /// pager already shows everything if possible + pub pager_fully_disclosed: bool, + /// pager is visible and search is active + pub search_mode: bool, + /// if false, the reader has not yet been entered + pub initialized: bool, } -pub fn reader_readline(nchars: i32) -> Option { - let mut line = L!("").to_ffi(); - if ffi::reader_readline_ffi(line.pin_mut(), autocxx::c_int(nchars)) { - Some(line.from_ffi()) - } else { - None +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_mode: false, + initialized: false, + } } } -pub fn reader_pop() { - ffi::reader_pop() +/// Strategy for determining how the selection behaves. +#[derive(Eq, PartialEq)] +pub enum CursorSelectionMode { + /// The character at/after the cursor is excluded. + /// This is most useful with a line cursor shape. + Exclusive, + /// The character at/after the cursor is included. + /// This is most useful with a block or underscore cursor shape. + Inclusive, } -pub fn reader_write_title(cmd: &wstr, parser: &Parser, reset_cursor_position: bool /*=true*/) { - ffi::reader_write_title_ffi( - &cmd.to_ffi(), - parser as *const Parser as *const autocxx::c_void, - reset_cursor_position, +/// A mode for calling the reader_kill function. +enum Kill { + /// In this mode, the new string is appended to the current contents of the kill buffer. + Append, + /// In this mode, the new string is prepended to the current contents of the kill buffer. + Prepend, +} + +#[derive(Clone, Copy, Eq, PartialEq)] +enum JumpDirection { + Forward, + Backward, +} + +#[derive(Clone, Copy, Eq, PartialEq)] +enum JumpPrecision { + Till, + To, +} + +/// readline_loop_state_t encapsulates the state used in a readline loop. +/// It is always stack allocated transient. This state should not be "publicly visible"; public +/// state should be in reader_data_t. +struct ReadlineLoopState { + /// The last command that was executed. + last_cmd: Option, + + /// If the last command was a yank, the length of yanking that occurred. + yank_len: usize, + + /// If the last "complete" readline command has inserted text into the command line. + complete_did_insert: bool, + + /// List of completions. + comp: Vec, + + /// Whether the loop has finished, due to reaching the character limit or through executing a + /// command. + finished: bool, + + /// Maximum number of characters to read. + nchars: Option, +} + +impl ReadlineLoopState { + fn new() -> Self { + Self { + last_cmd: None, + yank_len: 0, + complete_did_insert: true, + comp: vec![], + finished: false, + nchars: None, + } + } +} + +/// Data wrapping up the visual selection. +#[derive(Clone, Copy, Default, Eq, PartialEq)] +struct SelectionData { + /// The position of the cursor when selection was initiated. + begin: usize, + + /// The start and stop position of the current selection. + start: usize, + stop: usize, +} + +/// A value-type struct representing a layout that can be rendered. +/// The intent is that everything we send to the screen is encapsulated in this struct. +#[derive(Clone, Default)] +struct LayoutData { + /// Text of the command line. + text: WString, + + /// The colors. This has the same length as 'text'. + colors: Vec, + + /// Position of the cursor in the command line. + position: usize, + + /// Whether the cursor is focused on the pager or not. + focused_on_pager: bool, + + /// Visual selection of the command line, or none if none. + selection: Option, + + /// String containing the autosuggestion. + autosuggestion: WString, + + /// The matching range of the command line from a history search. If non-empty, then highlight + /// the range within the text. + history_search_range: Option, + + /// The result of evaluating the left, mode and right prompt commands. + /// That is, this the text of the prompts, not the commands to produce them. + left_prompt_buff: WString, + mode_prompt_buff: WString, + right_prompt_buff: WString, +} + +#[derive(Clone, Copy, Eq, PartialEq)] +enum EditableLineTag { + Commandline, + SearchField, +} + +/// A struct describing the state of the interactive reader. These states can be stacked, in case +/// reader_readline() calls are nested. This happens when the 'read' builtin is used. +pub struct ReaderData { + /// We could put the entire thing in an Rc but Rc::get_unchecked_mut is not yet stable. + /// This is sufficient for our use. + canary: Rc<()>, + /// Configuration for the reader. + conf: ReaderConfig, + /// The parser being used. + parser_ref: ParserRef, + /// String containing the whole current commandline. + command_line: EditableLine, + /// Whether the most recent modification to the command line was done by either history search + /// or a pager selection change. When this is true and another transient change is made, the + /// old transient change will be removed from the undo history. + command_line_has_transient_edit: bool, + /// The most recent layout data sent to the screen. + rendered_layout: LayoutData, + /// The current autosuggestion. + autosuggestion: Autosuggestion, + /// Current pager. + pager: Pager, + /// The output of the pager. + current_page_rendering: PageRendering, + /// When backspacing, we temporarily suppress autosuggestions. + suppress_autosuggestion: bool, + + /// HACK: A flag to reset the loop state from the outside. + reset_loop_state: bool, + + /// Whether this is the first prompt. + first_prompt: bool, + + /// The time when the last flash() completed + last_flash: Option, + + /// The representation of the current screen contents. + screen: Screen, + + /// The source of input events. + inputter: Inputter, + /// The history. + history: Arc, + /// The history search. + history_search: ReaderHistorySearch, + /// Whether the in-pager history search is active. + history_pager_active: bool, + /// The direction of the last successful history pager search. + history_pager_direction: SearchDirection, + /// The range in history covered by the history pager's current page. + history_pager_history_index_start: usize, + history_pager_history_index_end: usize, + + /// The cursor selection mode. + cursor_selection_mode: CursorSelectionMode, + + /// The selection data. If this is not none, then we have an active selection. + selection: Option, + + left_prompt_buff: WString, + mode_prompt_buff: WString, + /// The output of the last evaluation of the right prompt command. + right_prompt_buff: WString, + + /// When navigating the pager, we modify the command line. + /// This is the saved command line before modification. + cycle_command_line: WString, + cycle_cursor_pos: usize, + + /// If set, a key binding or the 'exit' command has asked us to exit our read loop. + exit_loop_requested: bool, + /// If this is true, exit reader even if there are running jobs. This happens if we press e.g. + /// ^D twice. + did_warn_for_bg_jobs: bool, + /// The current contents of the top item in the kill ring. + kill_item: WString, + + /// A flag which may be set to force re-execing all prompts and re-rendering. + /// This may come about when a color like $fish_color... has changed. + force_exec_prompt_and_repaint: bool, + + /// The target character of the last jump command. + last_jump_target: Option, + last_jump_direction: JumpDirection, + last_jump_precision: JumpPrecision, + + /// The text of the most recent asynchronous highlight and autosuggestion requests. + /// If these differs from the text of the command line, then we must kick off a new request. + in_flight_highlight_request: WString, + in_flight_autosuggest_request: WString, +} + +/// Read commands from \c fd until encountering EOF. +/// The fd is not closed. +pub fn reader_read(parser: &Parser, fd: RawFd, io: &IoChain) -> c_int { + // If reader_read is called recursively through the '.' builtin, we need to preserve + // is_interactive. This, and signal handler setup is handled by + // proc_push_interactive/proc_pop_interactive. + let mut interactive = false; + // This block is a hack to work around https://sourceware.org/bugzilla/show_bug.cgi?id=20632. + // See also, commit 396bf12. Without the need for this workaround we would just write: + // int inter = ((fd == STDIN_FILENO) && isatty(STDIN_FILENO)); + if fd == STDIN_FILENO { + let mut t: libc::termios = unsafe { std::mem::zeroed() }; + let a_tty = unsafe { libc::isatty(STDIN_FILENO) } != 0; + if a_tty { + interactive = true; + } else if unsafe { libc::tcgetattr(STDIN_FILENO, &mut t) } == -1 && errno().0 == EIO { + redirect_tty_output(); + interactive = true; + } + } + + let _interactive_push = scoped_push_replacer( + |new_value| std::mem::replace(&mut parser.libdata_mut().pods.is_interactive, new_value), + interactive, ); + signal_set_handlers_once(interactive); + + let res = if interactive { + read_i(parser) + } else { + read_ni(parser, fd, io) + }; + + // If the exit command was called in a script, only exit the script, not the program. + parser.libdata_mut().pods.exit_current_script = false; + + res } -/// This variable is set to a signal by the signal handler when ^C is pressed. -static INTERRUPTED: AtomicI32 = AtomicI32::new(0); +/// Read interactively. Read input from stdin while providing editing facilities. +fn read_i(parser: &Parser) -> i32 { + assert_is_main_thread(); + parser.assert_can_execute(); + let mut conf = ReaderConfig::default(); + conf.complete_ok = true; + conf.highlight_ok = true; + conf.syntax_check_ok = true; + conf.autosuggest_ok = check_autosuggestion_enabled(parser.vars()); + conf.expand_abbrev_ok = true; + conf.event = L!("fish_prompt"); + + 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 data = reader_push_ret(parser, &history_session_id(parser.vars()), conf); + data.import_history_if_necessary(); + + while !check_exit_loop_maybe_warning(Some(data)) { + RUN_COUNT.fetch_add(1, Ordering::Relaxed); + + let Some(command) = data.readline(None) else { + continue; + }; + + if command.is_empty() { + continue; + } + + data.update_buff_pos(EditableLineTag::Commandline, Some(0)); + data.command_line.clear(); + data.command_line_changed(EditableLineTag::Commandline); + 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); + } + + // If the command requested an exit, then process it now and clear it. + data.exit_loop_requested |= parser.libdata().pods.exit_current_script; + parser.libdata_mut().pods.exit_current_script = false; + + event::fire_generic(parser, L!("fish_postexec").to_owned(), vec![command]); + // Allow any pending history items to be returned in the history array. + data.history.resolve_pending(); + + let already_warned = data.did_warn_for_bg_jobs; + if check_exit_loop_maybe_warning(Some(data)) { + break; + } + if already_warned { + // We had previously warned the user and they ran another command. + // Reset the warning. + data.did_warn_for_bg_jobs = false; + } + + // Apply any command line update from this command or fish_postexec, etc. + // See #8807. + data.apply_commandline_state_changes(); + } + reader_pop(); + + // If we got SIGHUP, ensure the tty is redirected. + if reader_received_sighup() { + // If we are the top-level reader, then we translate SIGHUP into exit_forced. + redirect_tty_after_sighup(); + } + + // If we are the last reader, then kill remaining jobs before exiting. + if reader_data_stack().is_empty() { + // Send the exit event and then commit to not executing any more fish script. + 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()); + } + + 0 +} + +/// Read non-interactively. Read input from stdin without displaying the prompt, using syntax +/// highlighting. This is used for reading scripts and init files. +/// The file is not closed. +fn read_ni(parser: &Parser, fd: RawFd, io: &IoChain) -> i32 { + let mut buf: libc::stat = unsafe { std::mem::zeroed() }; + if unsafe { libc::fstat(fd, &mut buf) } == -1 { + let err = errno(); + FLOG!( + error, + wgettext_fmt!("Unable to read input file: %s", err.to_string()) + ); + return 1; + } + + /* FreeBSD allows read() on directories. Error explicitly in that case. */ + // XXX: This can be triggered spuriously, so we'll not do that for stdin. + // This can be seen e.g. with node's "spawn" api. + if fd != STDIN_FILENO && (buf.st_mode & S_IFDIR) != 0 { + FLOG!( + error, + wgettext_fmt!("Unable to read input file: %s", Errno(EISDIR).to_string()) + ); + return 1; + } + + // Read all data into a std::string. + let mut fd_contents = Vec::with_capacity(usize::try_from(buf.st_size).unwrap()); + loop { + let mut buff = [0_u8; 4096]; + let amt = unsafe { libc::read(fd, &mut buff[0] as *mut _ as *mut c_void, buff.len()) }; + if amt > 0 { + fd_contents.extend_from_slice(&buff[..usize::try_from(amt).unwrap()]); + } else if amt == 0 { + // EOF. + break; + } else { + assert!(amt == -1); + let err = errno(); + if err.0 == EINTR { + continue; + } else if err.0 == EAGAIN || err.0 == EWOULDBLOCK && make_fd_blocking(fd).is_ok() { + // We succeeded in making the fd blocking, keep going. + continue; + } else { + // Fatal error. + FLOG!( + error, + wgettext_fmt!("Unable to read input file: %s", err.to_string()) + ); + return 1; + } + } + } + + let mut s = str2wcstring(&fd_contents); + + // Eagerly deallocate to save memory. + drop(fd_contents); + + // Swallow a BOM (issue #1518). + if s.chars().next() == Some(UTF8_BOM_WCHAR) { + s.remove(0); + } + + // Parse into an ast and detect errors. + let mut errors = vec![]; + let ast = Ast::parse(&s, ParseTreeFlags::empty(), Some(&mut errors)); + let mut errored = ast.errored(); + if !errored { + errored = parse_util_detect_errors_in_ast(&ast, &s, Some(&mut errors)).is_err(); + } + if errored { + let sb = parser.get_backtrace(&s, &errors); + eprintf!("%ls", sb); + return 1; + } + + // Construct a parsed source ref. + // Be careful to transfer ownership, this could be a very large string. + let ps = Arc::new(ParsedSource::new(s, ast)); + parser.eval_parsed_source(&ps, io, None, BlockType::top); + 0 +} + +/// Initialize the reader. +pub fn reader_init() { + // Save the initial terminal mode. + let mut terminal_mode_on_startup = TERMINAL_MODE_ON_STARTUP.lock().unwrap(); + unsafe { libc::tcgetattr(STDIN_FILENO, &mut *terminal_mode_on_startup) }; + + // Set the mode used for program execution, initialized to the current mode. + let mut tty_modes_for_external_cmds = TTY_MODES_FOR_EXTERNAL_CMDS.lock().unwrap(); + *tty_modes_for_external_cmds = *terminal_mode_on_startup; + term_fix_external_modes(&mut tty_modes_for_external_cmds); + + // Disable flow control by default. + tty_modes_for_external_cmds.c_iflag &= !IXON; + tty_modes_for_external_cmds.c_iflag &= !IXOFF; + + // Set the mode used for the terminal, initialized to the current mode. + *shell_modes_mut() = *tty_modes_for_external_cmds; + + term_fix_modes(shell_modes_mut()); + + drop(terminal_mode_on_startup); + drop(tty_modes_for_external_cmds); + + // Set up our fixed terminal modes once, + // so we don't get flow control just because we inherited it. + if is_interactive_session() && unsafe { libc::getpgrp() == libc::tcgetpgrp(STDIN_FILENO) } { + term_donate(/*quiet=*/ true); + } +} + +/// Restore the term mode if we own the terminal and are interactive (#8705). +/// It's important we do this before restore_foreground_process_group, +/// otherwise we won't think we own the terminal. +pub fn restore_term_mode() { + if !is_interactive_session() || unsafe { libc::getpgrp() != libc::tcgetpgrp(STDIN_FILENO) } { + return; + } + + if unsafe { + libc::tcsetattr( + STDIN_FILENO, + TCSANOW, + &*TERMINAL_MODE_ON_STARTUP.lock().unwrap(), + ) == -1 + } && errno().0 == EIO + { + redirect_tty_output(); + } +} + +/// Change the history file for the current command reading context. +pub fn reader_change_history(name: &wstr) { + // We don't need to _change_ if we're not initialized yet. + 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) { + // We don't need to _change_ if we're not initialized yet. + if let Some(data) = current_data() { + data.cursor_selection_mode = selection_mode; + } +} + +fn check_autosuggestion_enabled(vars: &dyn Environment) -> bool { + vars.get(L!("fish_autosuggestion_enabled")) + .map(|v| v.as_string()) + .map(|v| v != L!("0")) + .unwrap_or(true) +} + +/// Enable or disable autosuggestions based on the associated variable. +pub fn reader_set_autosuggestion_enabled(vars: &dyn Environment) { + // We don't need to _change_ if we're not initialized yet. + let Some(data) = current_data() else { + return; + }; + let enable = check_autosuggestion_enabled(vars); + if data.conf.autosuggest_ok != enable { + data.conf.autosuggest_ok = enable; + data.force_exec_prompt_and_repaint = true; + data.inputter + .queue_char(CharEvent::from_readline(ReadlineCmd::Repaint)); + } +} + +/// Tell the reader that it needs to re-exec the prompt and repaint. +/// This may be called in response to e.g. a color variable change. +pub fn reader_schedule_prompt_repaint() { + assert_is_main_thread(); + let Some(data) = current_data() else { + return; + }; + if !data.force_exec_prompt_and_repaint { + data.force_exec_prompt_and_repaint = true; + data.inputter + .queue_char(CharEvent::from_readline(ReadlineCmd::Repaint)); + } +} + +pub fn reader_handle_command(cmd: ReadlineCmd) { + if let Some(data) = current_data() { + let mut rls = ReadlineLoopState::new(); + data.handle_readline_command(cmd, &mut rls); + } +} + +/// Enqueue an event to the back of the reader's input queue. +pub fn reader_queue_ch(ch: CharEvent) { + if let Some(data) = current_data() { + data.inputter.queue_char(ch); + } +} + +/// Return the value of the interrupted flag, which is set by the sigint handler, and clear it if it +/// was set. If the current reader is interruptible, call \c reader_exit(). +pub fn reader_reading_interrupted() -> i32 { + let res = reader_test_and_clear_interrupted(); + if res == 0 { + return 0; + } + if let Some(data) = current_data() { + if data.conf.exit_on_interrupt { + data.exit_loop_requested = true; + // We handled the interrupt ourselves, our caller doesn't need to handle it. + return 0; + } + } + res +} + +/// Read one line of input. Before calling this function, reader_push() must have been called in +/// order to set up a valid reader environment. If nchars > 0, return after reading that many +/// characters even if a full line has not yet been read. Note: the returned value may be longer +/// than nchars if a single keypress resulted in multiple characters being inserted into the +/// commandline. +pub fn reader_readline(nchars: i32) -> Option { + let nchars = usize::try_from(nchars).unwrap(); + let nchars = if nchars == 0 { + None + } else { + Some(NonZeroUsize::try_from(nchars).unwrap()) + }; + let data = current_data().unwrap(); + // Apply any outstanding commandline changes (#8633). + data.apply_commandline_state_changes(); + data.readline(nchars) +} + +/// Get the command line state. This may be fetched on a background thread. +pub fn commandline_get_state() -> CommandlineState { + commandline_state_snapshot().clone() +} + +/// Set the command line text and position. This may be called on a background thread; the reader +/// will pick it up when it is done executing. +pub fn commandline_set_buffer(text: WString, cursor_pos: Option) { + let mut state = commandline_state_snapshot(); + state.cursor_pos = std::cmp::min(cursor_pos.unwrap_or(usize::MAX), text.len()); + state.text = text; +} + +/// Return the current interactive reads loop count. Useful for determining how many commands have +/// been executed between invocations of code. +pub fn reader_run_count() -> u64 { + RUN_COUNT.load(Ordering::Relaxed) +} + +/// Returns the current "generation" of interactive status. Useful for determining whether the +/// previous command produced a status. +/// This is not incremented if the command being run produces no status, +/// (e.g. background job, or variable assignment). +pub fn reader_status_count() -> u64 { + STATUS_COUNT.load(Ordering::Relaxed) +} + +// Name of the variable that tells how long it took, in milliseconds, for the previous +// interactive command to complete. +const ENV_CMD_DURATION: &wstr = L!("CMD_DURATION"); + +/// Maximum length of prefix string when printing completion list. Longer prefixes will be +/// ellipsized. +const PREFIX_MAX_LEN: usize = 9; + +/// A simple prompt for reading shell commands that does not rely on fish specific commands, meaning +/// it will work even if fish is not installed. This is used by read_i. +const DEFAULT_PROMPT: &wstr = L!("echo -n \"$USER@$hostname $PWD \"'> '"); + +/// The name of the function that prints the fish prompt. +const LEFT_PROMPT_FUNCTION_NAME: &wstr = L!("fish_prompt"); + +/// The name of the function that prints the fish right prompt (RPROMPT). +const RIGHT_PROMPT_FUNCTION_NAME: &wstr = L!("fish_right_prompt"); + +/// The name of the function to use in place of the left prompt if we're in the debugger context. +const DEBUG_PROMPT_FUNCTION_NAME: &wstr = L!("fish_breakpoint_prompt"); + +/// The name of the function for getting the input mode indicator. +const MODE_PROMPT_FUNCTION_NAME: &wstr = L!("fish_mode_prompt"); + +/// The default title for the reader. This is used by reader_readline. +const DEFAULT_TITLE: &wstr = L!("echo (status current-command) \" \" $PWD"); + +/// The maximum number of characters to read from the keyboard without repainting. Note that this +/// readahead will only occur if new characters are available for reading, fish will never block for +/// more input without repainting. +const READAHEAD_MAX: usize = 256; + +/// Helper to get the generation count +pub fn read_generation_count() -> u32 { + GENERATION.load(Ordering::Relaxed) +} + +/// We try to ensure that syntax highlighting completes appropriately before executing what the user +/// typed. But we do not want it to block forever - e.g. it may hang on determining if an arbitrary +/// argument is a path. This is how long we'll wait (in milliseconds) before giving up and +/// performing a no-io syntax highlighting. See #7418, #5912. +const HIGHLIGHT_TIMEOUT_FOR_EXECUTION: Duration = Duration::from_millis(250); /// The readers interrupt signal handler. Cancels all currently running blocks. /// This is called from a signal handler! @@ -101,7 +979,7 @@ pub fn reader_reset_interrupted() { /// Return the value of the interrupted flag, which is set by the sigint handler, and clear it if it /// was set. In practice this will return 0 or SIGINT. -fn reader_test_and_clear_interrupted() -> i32 { +pub fn reader_test_and_clear_interrupted() -> i32 { let res = INTERRUPTED.load(Ordering::Relaxed); if res != 0 { INTERRUPTED.store(0, Ordering::Relaxed); @@ -109,10 +987,6 @@ fn reader_test_and_clear_interrupted() -> i32 { res } -/// If set, SIGHUP has been received. This latches to true. -/// This is set from a signal handler. -static SIGHUP_RECEIVED: RelaxedAtomicBool = RelaxedAtomicBool::new(false); - /// Mark that we encountered SIGHUP and must (soon) exit. This is invoked from a signal handler. pub fn reader_sighup() { // Beware, we may be in a signal handler. @@ -123,39 +997,3777 @@ fn reader_received_sighup() -> bool { SIGHUP_RECEIVED.load() } -#[repr(u8)] -pub enum CursorSelectionMode { - Exclusive = 0, - Inclusive = 1, +impl ReaderData { + fn new(parser: ParserRef, history: Arc, conf: ReaderConfig) -> Pin> { + let inputter = Inputter::new(parser.clone(), conf.inputfd); + Pin::new(Box::new(Self { + canary: Rc::new(()), + parser_ref: parser, + conf, + command_line: Default::default(), + command_line_has_transient_edit: false, + rendered_layout: Default::default(), + 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(), + screen: Screen::new(), + inputter, + history, + history_search: Default::default(), + history_pager_active: Default::default(), + history_pager_direction: SearchDirection::Forward, + history_pager_history_index_start: usize::MAX, + history_pager_history_index_end: usize::MAX, + cursor_selection_mode: CursorSelectionMode::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(), + })) + } + + fn is_navigating_pager_contents(&self) -> bool { + self.pager.is_navigating_contents() || self.history_pager_active + } + + 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, + } + } + + /// The line that is currently being edited. Typically the command line, but may be the search + /// field. + 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)) + } + + /// Return the variable set used for e.g. command duration. + fn vars(&self) -> &dyn Environment { + self.parser().vars() + } + + /// Access the parser. + fn parser(&self) -> &Parser { + &self.parser_ref + } + + /// Convenience cover over exec_count. + fn exec_count(&self) -> u64 { + self.parser().libdata().pods.exec_count + } + + /// Do what we need to do whenever our command line changes. + fn command_line_changed(&mut self, elt: EditableLineTag) { + assert_is_main_thread(); + match elt { + EditableLineTag::Commandline => { + // Update the gen count. + GENERATION.fetch_add(1, Ordering::Relaxed); + } + EditableLineTag::SearchField => { + if self.history_pager_active { + self.fill_history_pager( + HistoryPagerInvocation::Anew, + SearchDirection::Backward, + ); + return; + } + self.pager.refilter_completions(); + self.pager_selection_changed(); + } + } + // Ensure that the commandline builtin sees our new state. + self.update_commandline_state(); + } + + /// Reflect our current data in the command line state snapshot. + /// This is called before we run any fish script, so that the commandline builtin can see our + /// state. + fn update_commandline_state(&self) { + let mut snapshot = commandline_state_snapshot(); + 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; + snapshot.search_mode = self.history_search.active(); + snapshot.initialized = true; + } + + /// Apply any changes from the reader snapshot. This is called after running fish script, + /// incorporating changes from the commandline builtin. + fn apply_commandline_state_changes(&mut self) { + // Only the text and cursor position may be changed. + let state = commandline_get_state(); + if state.text != self.command_line.text() + || state.cursor_pos != self.command_line.position() + { + // The commandline builtin changed our contents. + self.clear_pager(); + self.set_buffer_maintaining_pager(&state.text, state.cursor_pos, false); + self.reset_loop_state = true; + } + } + + fn maybe_refilter_pager(&mut self, elt: EditableLineTag) { + match elt { + EditableLineTag::Commandline => (), + EditableLineTag::SearchField => self.command_line_changed(elt), + } + } + + /// Update the cursor position. + fn update_buff_pos(&mut self, elt: EditableLineTag, new_pos: Option) { + if let Some(pos) = new_pos { + self.edit_line_mut(elt).set_position(pos); + } + + if elt != EditableLineTag::Commandline { + return; + } + 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; + }; + 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; + } + } } -pub fn check_autosuggestion_enabled(vars: &dyn Environment) -> bool { - vars.get(L!("fish_autosuggestion_enabled")) - .map(|v| v.as_string()) - .map(|v| v != L!("0")) - .unwrap_or(true) +/// Given a command line and an autosuggestion, return the string that gets shown to the user. +/// Exposed for testing purposes only. +pub fn combine_command_and_autosuggestion(cmdline: &wstr, autosuggestion: &wstr) -> WString { + // We want to compute the full line, containing the command line and the autosuggestion They may + // disagree on whether characters are uppercase or lowercase Here we do something funny: if the + // last token of the command line contains any uppercase characters, we use its case. Otherwise + // we use the case of the autosuggestion. This is an idea from issue #335. + let mut full_line; + if autosuggestion.len() <= cmdline.len() || cmdline.is_empty() { + // No or useless autosuggestion, or no command line. + full_line = cmdline.to_owned(); + } else if string_prefixes_string(cmdline, autosuggestion) { + // No case disagreements, or no extra characters in the autosuggestion. + full_line = autosuggestion.to_owned(); + } else { + // We have an autosuggestion which is not a prefix of the command line, i.e. a case + // disagreement. Decide whose case we want to use. + let mut tok = 0..0; + parse_util_token_extent(cmdline, cmdline.len() - 1, &mut tok, None); + let last_token_contains_uppercase = cmdline[tok].chars().any(|c| c.is_uppercase()); + if !last_token_contains_uppercase { + // Use the autosuggestion's case. + full_line = autosuggestion.to_owned(); + } else { + // Use the command line case for its characters, then append the remaining characters in + // the autosuggestion. Note that we know that autosuggestion.size() > cmdline.size() due + // to the first test above. + full_line = cmdline.to_owned(); + full_line.push_utfstr(&autosuggestion[cmdline.len()..]); + } + } + full_line } -pub fn reader_reading_interrupted() -> i32 { - crate::ffi::reader_reading_interrupted().0 +impl ReaderData { + /// \return true if the command line has changed and repainting is needed. If \p colors is not + /// null, then also return true if the colors have changed. + fn is_repaint_needed(&self, mcolors: Option<&[HighlightSpec]>) -> bool { + // Note: this function is responsible for detecting all of the ways that the command line may + // change, by comparing it to what is present in rendered_layout. + // The pager is the problem child, it has its own update logic. + 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 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(focused_on_pager != last.focused_on_pager, "focus") + || check(self.command_line.position() != last.position, "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", + ) + } + + /// Generate a new layout data from the current state of the world. + /// If \p mcolors has a value, then apply it; otherwise extend existing colors. + 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!(result.text.len() == result.colors.len()); + result.position = if focused_on_pager { + self.pager.cursor_position() + } else { + self.command_line.position() + }; + result.selection = self.selection; + result.focused_on_pager = focused_on_pager; + 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 + } + + /// Generate a new layout data from the current state of the world, and paint with it. + /// If \p mcolors has a value, then apply it; otherwise extend existing colors. + fn layout_and_repaint(&mut self, reason: &wstr) { + self.rendered_layout = self.make_layout_data(); + self.paint_layout(reason); + } + + /// Paint the last rendered layout. + /// \p reason is used in FLOG to explain why. + fn paint_layout(&mut self, reason: &wstr) { + FLOGF!(reader_render, "Repainting from %ls", reason); + let data = &self.rendered_layout; + let cmd_line = &self.command_line; + + let full_line = if self.conf.in_silent_mode { + wstr::from_char_slice(&[get_obfuscation_read_char()]).repeat(cmd_line.len()) + } else { + // Combine the command and autosuggestion into one string. + combine_command_and_autosuggestion(cmd_line.text(), &self.autosuggestion.text) + }; + + // Copy the colors and extend them with autosuggestion color. + let mut colors = data.colors.clone(); + + // Highlight any history search. + if !self.conf.in_silent_mode && data.history_search_range.is_some() { + let mut end = data.history_search_range.unwrap().end(); + if colors.len() < end { + end = colors.len(); + } + + for color in &mut colors[data.history_search_range.unwrap().start()..end] { + color.background = HighlightRole::search_match; + } + } + + // Apply any selection. + if let Some(selection) = data.selection { + let selection_color = + HighlightSpec::with_fg_bg(HighlightRole::selection, HighlightRole::selection); + let end = std::cmp::min(selection.stop, colors.len()); + for color in &mut colors[selection.start..end] { + *color = selection_color; + } + } + + // Extend our colors with the autosuggestion. + colors.resize( + full_line.len(), + HighlightSpec::with_fg(HighlightRole::autosuggestion), + ); + + // Compute the indentation, then extend it with 0s for the autosuggestion. The autosuggestion + // always conceptually has an indent of 0. + let mut indents = parse_util_compute_indents(cmd_line.text()); + indents.resize(full_line.len(), 0); + + let screen = &mut self.screen; + let pager = &mut self.pager; + let current_page_rendering = &mut self.current_page_rendering; + let parser = &self.parser_ref; + screen.write( + // Prepend the mode prompt to the left prompt. + &(self.mode_prompt_buff.clone() + &self.left_prompt_buff[..]), + &self.right_prompt_buff, + &full_line, + cmd_line.len(), + &colors, + &indents, + data.position, + parser.vars(), + pager, + current_page_rendering, + data.focused_on_pager, + ); + } } -pub fn reader_schedule_prompt_repaint() { - crate::ffi::reader_schedule_prompt_repaint() +impl ReaderData { + /// Internal helper function for handling killing parts of text. + fn kill(&mut self, elt: EditableLineTag, range: Range, 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); + } + + /// Insert the characters of the string into the command line buffer and print them to the screen + /// using syntax highlighting, etc. + /// Returns true if the string changed. + fn insert_string(&mut self, elt: EditableLineTag, s: &wstr) { + if !s.is_empty() { + let history_search_active = self.history_search.active(); + let el = self.edit_line_mut(elt); + el.push_edit( + Edit::new(el.position()..el.position(), s.to_owned()), + /*allow_coalesce=*/ !history_search_active, + ); + } + + if elt == EditableLineTag::Commandline { + self.command_line_has_transient_edit = false; + self.suppress_autosuggestion = false; + } + self.maybe_refilter_pager(elt); + } + + /// Erase @length characters starting at @offset. + fn erase_substring(&mut self, elt: EditableLineTag, range: Range) { + self.push_edit(elt, Edit::new(range, L!("").to_owned())); + } + + /// Replace the text of length @length at @offset by @replacement. + fn replace_substring( + &mut self, + elt: EditableLineTag, + range: Range, + replacement: WString, + ) { + self.push_edit(elt, Edit::new(range, replacement)); + } + + fn push_edit(&mut self, elt: EditableLineTag, edit: Edit) { + self.edit_line_mut(elt) + .push_edit(edit, /*allow_coalesce=*/ false); + self.maybe_refilter_pager(elt); + } + + /// Insert the character into the command line buffer and print it to the screen using syntax + /// highlighting, etc. + fn insert_char(&mut self, elt: EditableLineTag, c: char) { + self.insert_string(elt, &WString::from_chars([c])); + } + + /// Set the specified string as the current buffer. + 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)); + } + + /// Undo the transient edit und update commandline accordingly. + fn clear_transient_edit(&mut self) { + if !self.command_line_has_transient_edit { + return; + } + self.command_line.undo(); + self.update_buff_pos(EditableLineTag::Commandline, None); + self.command_line_has_transient_edit = false; + } + + fn replace_current_token(&mut self, new_token: WString) { + // Find current token. + let (elt, el) = self.active_edit_line(); + let mut token_range = 0..0; + parse_util_token_extent(el.text(), el.position(), &mut token_range, None); + + self.replace_substring(elt, token_range, new_token); + } + + /// Apply the history search to the command line. + fn update_command_line_from_history_search(&mut self) { + let new_text = if self.history_search.is_at_end() { + self.history_search.search_string() + } else { + self.history_search.current_result() + } + .to_owned(); + if self.command_line_has_transient_edit { + self.command_line.undo(); + } + if self.history_search.by_token() { + self.replace_current_token(new_text); + } else { + assert!(self.history_search.by_line() || self.history_search.by_prefix()); + self.replace_substring( + EditableLineTag::Commandline, + 0..self.command_line.len(), + new_text, + ); + } + self.command_line_has_transient_edit = true; + self.update_buff_pos(EditableLineTag::Commandline, None); + } + + /// Remove the previous character in the character buffer and on the screen using syntax + /// highlighting, etc. + fn delete_char(&mut self, backward: bool /* = true */) { + 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; + } + + // Fake composed character sequences by continuing to delete until we delete a character of + // width at least 1. + let mut width; + loop { + pos -= 1; + width = fish_wcwidth(el.text().char_at(pos)); + if width != 0 || pos == 0 { + break; + } + } + self.erase_substring(elt, pos..pos_end); + self.update_buff_pos(elt, None); + self.suppress_autosuggestion = true; + } +} + +#[derive(Eq, PartialEq)] +enum MoveWordDir { + Left, + Right, +} + +impl ReaderData { + /// Move buffer position one word or erase one word. This function updates both the internal buffer + /// and the screen. It is used by M-left, M-right and ^W to do block movement or block erase. + /// + /// \param move_right true if moving right + /// \param erase Whether to erase the characters along the way or only move past them. + /// \param newv if the new kill item should be appended to the previous kill item or not. + fn move_word( + &mut self, + elt: EditableLineTag, + direction: MoveWordDir, + erase: bool, + style: MoveWordStyle, + newv: bool, + ) { + let move_right = direction == MoveWordDir::Right; + // Return if we are already at the edge. + let el = self.edit_line(elt); + let boundary = if move_right { el.len() } else { 0 }; + if el.position() == boundary { + return; + } + + // When moving left, a value of 1 means the character at index 0. + let mut state = MoveWordStateMachine::new(style); + let start_buff_pos = el.position(); + + let mut buff_pos = el.position(); + while buff_pos != boundary { + let idx = if move_right { buff_pos } else { buff_pos - 1 }; + let c = self.command_line.at(idx); + if !state.consume_char(c) { + break; + } + buff_pos = if move_right { + buff_pos + 1 + } else { + buff_pos - 1 + }; + } + + // Always consume at least one character. + if buff_pos == start_buff_pos { + buff_pos = if move_right { + buff_pos + 1 + } else { + buff_pos - 1 + }; + } + + // If we are moving left, buff_pos-1 is the index of the first character we do not delete + // (possibly -1). If we are moving right, then buff_pos is that index - possibly el->size(). + if erase { + // Don't autosuggest after a kill. + if elt == EditableLineTag::Commandline { + self.suppress_autosuggestion = true; + } + + if move_right { + self.kill(elt, start_buff_pos..buff_pos, Kill::Append, newv); + } else { + self.kill(elt, buff_pos..start_buff_pos, Kill::Prepend, newv); + } + } else { + self.update_buff_pos(elt, Some(buff_pos)); + } + } + + fn jump( + &mut self, + direction: JumpDirection, + precision: JumpPrecision, + elt: EditableLineTag, + target: char, + ) -> bool { + self.last_jump_target = Some(target); + self.last_jump_direction = direction; + self.last_jump_precision = precision; + + let el = self.edit_line(elt); + + match direction { + JumpDirection::Backward => { + let mut tmp_pos = el.position(); + + loop { + if tmp_pos == 0 { + return false; + } + tmp_pos -= 1; + if 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; + while tmp_pos < el.len() { + if 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; + } + return false; + } + } + } +} + +impl ReaderData { + /// Read a command to execute, respecting input bindings. + /// \return the command, or none if we were asked to cancel (e.g. SIGHUP). + fn readline(&mut self, nchars: Option) -> Option { + type rl = ReadlineCmd; + let mut rls = ReadlineLoopState::new(); + + // Suppress fish_trace during executing key bindings. + // This is simply to reduce noise. + let mut zelf = scoped_push_replacer_ctx( + self, + |zelf, new_value| { + std::mem::replace( + &mut zelf.parser().libdata_mut().pods.suppress_fish_trace, + new_value, + ) + }, + true, + ); + + // If nchars_or_0 is positive, then that's the maximum number of chars. Otherwise keep it at + // SIZE_MAX. + rls.nchars = nchars; + + // The command line before completion. + zelf.cycle_command_line.clear(); + zelf.cycle_cursor_pos = 0; + + zelf.history_search.reset(); + + // It may happen that a command we ran when job control was disabled nevertheless stole the tty + // from us. In that case when we read from our fd, it will trigger SIGTTIN. So just + // unconditionally reclaim the tty. See #9181. + unsafe { libc::tcsetpgrp(zelf.conf.inputfd, libc::getpgrp()) }; + + // Get the current terminal modes. These will be restored when the function returns. + let mut old_modes: libc::termios = unsafe { std::mem::zeroed() }; + if unsafe { libc::tcgetattr(zelf.conf.inputfd, &mut old_modes) } == -1 && errno().0 == EIO { + redirect_tty_output(); + } + + // Set the new modes. + if unsafe { libc::tcsetattr(zelf.conf.inputfd, TCSANOW, shell_modes()) } == -1 { + let err = errno().0; + if err == EIO { + redirect_tty_output(); + } + + // This check is required to work around certain issues with fish's approach to + // terminal control when launching interactive processes while in non-interactive + // mode. See #4178 for one such example. + if err != ENOTTY || is_interactive_session() { + perror("tcsetattr"); + } + } + + // HACK: Don't abandon line for the first prompt, because + // if we're started with the terminal it might not have settled, + // so the width is quite likely to be in flight. + // + // This means that `printf %s foo; fish` will overwrite the `foo`, + // but that's a smaller problem than having the omitted newline char + // appear constantly. + // + // I can't see a good way around this. + if !zelf.first_prompt { + zelf.screen + .reset_abandoning_line(usize::try_from(termsize_last().width).unwrap()); + } + zelf.first_prompt = false; + + if !zelf.conf.event.is_empty() { + event::fire_generic(zelf.parser(), zelf.conf.event.to_owned(), vec![]); + } + zelf.exec_prompt(); + + // A helper that kicks off syntax highlighting, autosuggestion computing, and repaints. + let color_suggest_repaint_now = |zelf: &mut Self| { + if zelf.conf.inputfd == STDIN_FILENO { + zelf.update_autosuggestion(); + zelf.super_highlight_me_plenty(); + } + if zelf.is_repaint_needed(None) { + zelf.layout_and_repaint(L!("toplevel")); + } + zelf.force_exec_prompt_and_repaint = false; + }; + + // Start out as initially dirty. + zelf.force_exec_prompt_and_repaint = true; + + while !rls.finished && !check_exit_loop_maybe_warning(Some(&mut zelf)) { + if zelf.reset_loop_state { + zelf.reset_loop_state = false; + rls.last_cmd = None; + rls.complete_did_insert = false; + } + // Perhaps update the termsize. This is cheap if it has not changed. + zelf.update_termsize(); + + // Repaint as needed. + color_suggest_repaint_now(&mut zelf); + + if rls + .nchars + .is_some_and(|nchars| usize::from(nchars) <= zelf.command_line.len()) + { + // We've already hit the specified character limit. + rls.finished = true; + break; + } + + let event_needing_handling = loop { + let event_needing_handling = zelf.read_normal_chars(&mut rls); + if event_needing_handling.is_some() { + break event_needing_handling; + } + if rls + .nchars + .is_some_and(|nchars| usize::from(nchars) <= zelf.command_line.len()) + { + break None; + } + }; + + // If we ran `exit` anywhere, exit. + zelf.exit_loop_requested = + zelf.exit_loop_requested || zelf.parser().libdata().pods.exit_current_script; + zelf.parser().libdata_mut().pods.exit_current_script = false; + if zelf.exit_loop_requested { + continue; + } + + let Some(event_needing_handling) = event_needing_handling else { + continue; + }; + if event_needing_handling.is_check_exit() { + continue; + } else if event_needing_handling.is_eof() { + reader_sighup(); + continue; + } + assert!( + event_needing_handling.is_char() || event_needing_handling.is_readline(), + "Should have a char or readline" + ); + + if !matches!(rls.last_cmd, Some(rl::Yank | rl::YankPop)) { + rls.yank_len = 0; + } + + if event_needing_handling.is_readline() { + let readline_cmd = event_needing_handling.get_readline(); + if readline_cmd == rl::Cancel && zelf.is_navigating_pager_contents() { + zelf.clear_transient_edit(); + } + + // Clear the pager if necessary. + let focused_on_search_field = + zelf.active_edit_line_tag() == EditableLineTag::SearchField; + if !zelf.history_search.active() + && command_ends_paging(readline_cmd, focused_on_search_field) + { + zelf.clear_pager(); + } + + zelf.handle_readline_command(readline_cmd, &mut rls); + + if zelf.history_search.active() && command_ends_history_search(readline_cmd) { + // "cancel" means to abort the whole thing, other ending commands mean to finish the + // search. + if readline_cmd == rl::Cancel { + // Go back to the search string by simply undoing the history-search edit. + zelf.clear_transient_edit(); + } + zelf.history_search.reset(); + zelf.command_line_has_transient_edit = false; + } + + rls.last_cmd = Some(readline_cmd); + } else { + // Ordinary char. + let c = event_needing_handling.get_char(); + if event_needing_handling.input_style == CharInputStyle::NotFirst + && zelf.active_edit_line().1.position() == 0 + { + // This character is skipped. + } else if !fish_reserved_codepoint(c) + && (c >= ' ' || c == '\n' || c == '\r') + && c != '\x7F' + { + // Regular character. + let (elt, _el) = zelf.active_edit_line(); + zelf.insert_char(elt, c); + + if elt == EditableLineTag::Commandline { + zelf.clear_pager(); + } + } else { + // This can happen if the user presses a control char we don't recognize. No + // reason to report this to the user unless they've enabled debugging output. + FLOG!(reader, wgettext_fmt!("Unknown key binding 0x%X", c)); + } + rls.last_cmd = None; + } + } + + // Redraw the command line. This is what ensures the autosuggestion is hidden, etc. after the + // user presses enter. + if zelf.is_repaint_needed(None) || zelf.conf.inputfd != STDIN_FILENO { + zelf.layout_and_repaint(L!("prepare to execute")); + } + + // Finish syntax highlighting (but do not wait forever). + if rls.finished { + zelf.finish_highlighting_before_exec(); + } + + // Emit a newline so that the output is on the line after the command. + // But do not emit a newline if the cursor has wrapped onto a new line all its own - see #6826. + if !zelf.screen.cursor_is_wrapped_to_own_line() { + let _ = write_to_fd(b"\n", STDOUT_FILENO); + } + + // HACK: If stdin isn't the same terminal as stdout, we just moved the cursor. + // For now, just reset it to the beginning of the line. + if zelf.conf.inputfd != STDIN_FILENO { + let _ = write_to_fd(b"\r", STDOUT_FILENO); + } + + // Ensure we have no pager contents when we exit. + if !zelf.pager.is_empty() { + // Clear to end of screen to erase the pager contents. + // TODO: this may fail if eos doesn't exist, in which case we should emit newlines. + screen_force_clear_to_end(); + zelf.clear_pager(); + } + + if EXIT_STATE.load(Ordering::Relaxed) != ExitState::FinishedHandlers as _ { + // The order of the two conditions below is important. Try to restore the mode + // in all cases, but only complain if interactive. + if unsafe { libc::tcsetattr(zelf.conf.inputfd, TCSANOW, &old_modes) } == -1 + && is_interactive_session() + { + if errno().0 == EIO { + redirect_tty_output(); + } + perror("tcsetattr"); // return to previous mode + } + Outputter::stdoutput() + .get_mut() + .set_color(RgbColor::RESET, RgbColor::RESET); + } + rls.finished.then(|| zelf.command_line.text().to_owned()) + } + + /// Run a sequence of commands from an input binding. + fn run_input_command_scripts(&mut self, cmds: &[WString]) { + let last_statuses = self.parser().vars().get_last_statuses(); + for cmd in cmds { + self.update_commandline_state(); + self.parser().eval(cmd, &IoChain::new()); + self.apply_commandline_state_changes(); + } + self.parser().set_last_statuses(last_statuses); + + // Restore tty to shell modes. + // Some input commands will take over the tty - see #2114 for an example where vim is invoked + // from a key binding. However we do NOT want to invoke term_donate(), because that will enable + // ECHO mode, causing a race between new input and restoring the mode (#7770). So we leave the + // tty alone, run the commands in shell mode, and then restore shell modes. + let mut res; + loop { + res = unsafe { libc::tcsetattr(STDIN_FILENO, TCSANOW, shell_modes_mut()) }; + if res >= 0 || errno().0 != EINTR { + break; + } + } + if res < 0 { + perror("tcsetattr"); + } + termsize_invalidate_tty(); + } + + /// Read normal characters, inserting them into the command line. + /// \return the next unhandled event. + fn read_normal_chars(&mut self, rls: &mut ReadlineLoopState) -> Option { + let mut event_needing_handling = None; + let limit = std::cmp::min( + rls.nchars.map_or(usize::MAX, |nchars| { + usize::from(nchars) - self.command_line.len() + }), + READAHEAD_MAX, + ); + + // We repaint our prompt if fstat reports the tty as having changed. + // But don't react to tty changes that we initiated, because of commands or + // on-variable events (e.g. for fish_bind_mode). See #3481. + let mut last_exec_count = self.exec_count(); + let mut accumulated_chars = WString::new(); + + let mut command_handler = { + // TODO Remove this hack. + let zelf = self as *mut Self; + move |cmds: &[WString]| { + // Safety: this is a pinned pointer. + let zelf = unsafe { &mut *zelf }; + zelf.run_input_command_scripts(cmds); + } + }; + + while accumulated_chars.len() < limit { + let evt = { + let allow_commands = accumulated_chars.is_empty(); + self.inputter + .read_char(allow_commands.then_some(&mut command_handler)) + }; + if !event_is_normal_char(&evt) || !poll_fd_readable(self.conf.inputfd) { + event_needing_handling = Some(evt); + break; + } else if evt.input_style == CharInputStyle::NotFirst + && accumulated_chars.is_empty() + && self.active_edit_line().1.position() == 0 + { + // The cursor is at the beginning and nothing is accumulated, so skip this character. + continue; + } else { + accumulated_chars.push(evt.get_char()); + } + + if last_exec_count != self.exec_count() { + last_exec_count = self.exec_count(); + self.screen.save_status(); + } + } + + if !accumulated_chars.is_empty() { + let (elt, _el) = self.active_edit_line(); + self.insert_string(elt, &accumulated_chars); + + // End paging upon inserting into the normal command line. + if elt == EditableLineTag::Commandline { + self.clear_pager(); + } + + // Since we handled a normal character, we don't have a last command. + rls.last_cmd = None; + } + + if last_exec_count != self.exec_count() { + #[allow(unused_assignments)] + { + last_exec_count = self.exec_count(); + } + self.screen.save_status(); + } + + event_needing_handling + } +} + +impl ReaderData { + fn handle_readline_command(&mut self, c: ReadlineCmd, rls: &mut ReadlineLoopState) { + type rl = ReadlineCmd; + match c { + rl::BeginningOfLine => { + // Go to beginning of line. + 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 => loop { + let position = { + let (_elt, el) = self.active_edit_line(); + let position = el.position(); + if position == el.len() { + self.accept_autosuggestion(true, false, MoveWordStyle::Punctuation); + break; + } + if el.text().char_at(position) == '\n' { + break; + } + position + }; + self.update_buff_pos(self.active_edit_line_tag(), Some(position + 1)); + }, + rl::BeginningOfBuffer => { + self.update_buff_pos(EditableLineTag::Commandline, Some(0)); + } + rl::EndOfBuffer => { + self.update_buff_pos(EditableLineTag::Commandline, Some(self.command_line.len())); + } + rl::CancelCommandline => { + if self.command_line.is_empty() { + return; + } + let outp = Outputter::stdoutput().get_mut(); + // Move cursor to the end of the line. + let end = self.command_line.len(); + self.update_buff_pos(EditableLineTag::Commandline, Some(end)); + self.autosuggestion.clear(); + // Repaint also changes the actual cursor position + if self.is_repaint_needed(None) { + self.layout_and_repaint(L!("cancel")); + } + + if let Some(fish_color_cancel) = self.vars().get(L!("fish_color_cancel")) { + outp.set_color( + parse_color(&fish_color_cancel, false), + parse_color(&fish_color_cancel, true), + ); + } + outp.write_wstr(L!("^C")); + outp.set_color(RgbColor::RESET, RgbColor::RESET); + + // We print a newline last so the prompt_sp hack doesn't get us. + outp.push(b'\n'); + + self.set_command_line_and_position( + EditableLineTag::Commandline, + L!("").to_owned(), + 0, + ); + self.screen + .reset_abandoning_line(usize::try_from(termsize_last().width).unwrap()); + + // Post fish_cancel. + event::fire_generic(self.parser(), L!("fish_cancel").to_owned(), vec![]); + } + rl::Cancel => { + // If we last inserted a completion, undo it. + // This doesn't apply if the completion was selected via the pager + // (in which case the last command is "execute" or similar, + // but never complete{,_and_search}) + // + // Also paging is already cancelled above. + if rls.complete_did_insert + && matches!(rls.last_cmd, Some(rl::Complete | rl::CompleteAndSearch)) + { + let (elt, el) = self.active_edit_line_mut(); + el.undo(); + self.update_buff_pos(elt, None); + } + } + rl::RepaintMode | rl::ForceRepaint | rl::Repaint => { + self.parser().libdata_mut().pods.is_repaint = true; + if c == rl::RepaintMode { + // Repaint the mode-prompt only if possible. + // This is an optimization basically exclusively for vi-mode, since the prompt + // may sometimes take a while but when switching the mode all we care about is the + // mode-prompt. + // + // Because some users set `fish_mode_prompt` to an empty function and display the mode + // elsewhere, we detect if the mode output is empty. + + // Don't go into an infinite loop of repainting. + // This can happen e.g. if a variable triggers a repaint, + // and the variable is set inside the prompt (#7324). + // builtin commandline will refuse to enqueue these. + self.exec_mode_prompt(); + if !self.mode_prompt_buff.is_empty() { + if self.is_repaint_needed(None) { + self.screen.reset_line(/*repaint_prompt=*/ true); + self.layout_and_repaint(L!("mode")); + } + self.parser().libdata_mut().pods.is_repaint = false; + return; + } + // Else we repaint as normal. + } + self.exec_prompt(); + self.screen.reset_line(/*repaint_prompt=*/ true); + self.layout_and_repaint(L!("readline")); + self.force_exec_prompt_and_repaint = false; + self.parser().libdata_mut().pods.is_repaint = false; + } + rl::Complete | rl::CompleteAndSearch => { + if !self.conf.complete_ok { + return; + } + if self.is_navigating_pager_contents() + || (!rls.comp.is_empty() + && !rls.complete_did_insert + && rls.last_cmd == Some(rl::Complete)) + { + // The user typed complete more than once in a row. If we are not yet fully + // disclosed, then become so; otherwise cycle through our available completions. + 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 { + // Either the user hit tab only once, or we had no visible completion list. + self.compute_and_apply_completions(c, rls); + } + } + rl::PagerToggleSearch => { + if self.history_pager_active { + self.fill_history_pager( + HistoryPagerInvocation::Advance, + SearchDirection::Forward, + ); + return; + } + if !self.pager.is_empty() { + // Toggle search, and begin navigating if we are now searching. + 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.kill(elt, range, Kill::Append, 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; // make sure we delete at least one character (see issue #580) + + // Delete until we hit a newline, or the beginning of the string. + while begin != 0 && text.as_char_slice()[begin] != '\n' { + begin -= 1; + } + + // If we landed on a newline, don't delete it. + if text.as_char_slice()[begin] == '\n' { + begin += 1; + } + assert!(end >= begin); + let len = std::cmp::max(end - begin, 1); + self.kill( + elt, + end - len..end, + Kill::Prepend, + rls.last_cmd != Some(rl::BackwardKillLine), + ); + } + rl::KillWholeLine | rl::KillInnerLine => { + // The first matches the emacs behavior here: "kills the entire line including + // the following newline". + // The second does not kill the following newline + let (elt, el) = self.active_edit_line(); + let text = el.text(); + let position = el.position(); + + // Back up to the character just past the previous newline, or go to the beginning + // of the command line. Note that if the position is on a newline, visually this + // looks like the cursor is at the end of a line. Therefore that newline is NOT the + // beginning of a line; this justifies the -1 check. + let mut begin = position + - text[..position] + .chars() + .rev() + .take_while(|&c| c != '\n') + .count(); + + // Push end forwards to just past the next newline, or just past the last char. + let mut end = position; + loop { + if end == text.len() { + if c == rl::KillWholeLine && begin > 0 { + // We are on the last line. Delete the newline in the beginning to clear + // this line. + 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.kill(elt, begin..end, Kill::Append, rls.last_cmd != Some(c)); + } + } + rl::Yank => { + let yank_str = kill_yank(); + self.insert_string(self.active_edit_line_tag(), &yank_str); + rls.yank_len = yank_str.len(); + } + rl::YankPop => { + if rls.yank_len != 0 { + let (elt, el) = self.active_edit_line(); + let yank_str = kill_yank_rotate(); + let new_yank_len = yank_str.len(); + self.replace_substring( + elt, + el.position() - rls.yank_len..el.position(), + yank_str, + ); + self.update_buff_pos(elt, None); + rls.yank_len = new_yank_len; + self.suppress_autosuggestion = true; + } + } + rl::BackwardDeleteChar => { + self.delete_char(true); + } + rl::Exit => { + // This is by definition a successful exit, override the status + self.parser() + .set_last_statuses(Statuses::just(STATUS_CMD_OK.unwrap())); + self.exit_loop_requested = true; + check_exit_loop_maybe_warning(Some(self)); + } + rl::DeleteOrExit | rl::DeleteChar => { + // Remove the current character in the character buffer and on the screen using + // syntax highlighting, etc. + let (_elt, el) = self.active_edit_line(); + if el.position() < el.len() { + self.delete_char(false); + } else if c == rl::DeleteOrExit && el.is_empty() { + // This is by definition a successful exit, override the status + self.parser() + .set_last_statuses(Statuses::just(STATUS_CMD_OK.unwrap())); + self.exit_loop_requested = true; + check_exit_loop_maybe_warning(Some(self)); + } + } + rl::Execute => { + if !self.handle_execute(rls) { + event::fire_generic( + self.parser(), + L!("fish_posterror").to_owned(), + vec![self.command_line.text().to_owned()], + ); + self.screen + .reset_abandoning_line(usize::try_from(termsize_last().width).unwrap()); + } + } + rl::HistoryPrefixSearchBackward + | rl::HistoryPrefixSearchForward + | rl::HistorySearchBackward + | rl::HistorySearchForward + | rl::HistoryTokenSearchBackward + | rl::HistoryTokenSearchForward => { + let mode = match c { + rl::HistoryTokenSearchBackward | rl::HistoryTokenSearchForward => { + SearchMode::Token + } + 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_end() { + let el = &self.command_line; + if mode == SearchMode::Token { + // Searching by token. + let mut token_range = 0..0; + parse_util_token_extent(el.text(), el.position(), &mut token_range, None); + self.history_search.reset_to_mode( + el.text()[token_range.clone()].to_owned(), + self.history.clone(), + SearchMode::Token, + token_range.start, + ); + } else { + // Searching by line. + self.history_search.reset_to_mode( + el.text().to_owned(), + self.history.clone(), + mode, + 0, + ); + + // Skip the autosuggestion in the history unless it was truncated. + let suggest = &self.autosuggestion.text; + if !suggest.is_empty() + && !self.screen.autosuggestion_is_truncated + && mode != SearchMode::Prefix + { + self.history_search.add_skip(suggest.clone()); + } + } + } + assert!(self.history_search.active()); + let dir = match c { + rl::HistorySearchBackward + | rl::HistoryTokenSearchBackward + | rl::HistoryPrefixSearchBackward => SearchDirection::Backward, + rl::HistorySearchForward + | rl::HistoryTokenSearchForward + | rl::HistoryPrefixSearchForward => SearchDirection::Forward, + _ => unreachable!(), + }; + let found = self.history_search.move_in_direction(dir); + + // Signal that we've found nothing + if !found { + self.flash(); + } + + if !found && !was_active_before { + self.history_search.reset(); + } else if found + || (dir == SearchDirection::Forward && self.history_search.is_at_end()) + { + self.update_command_line_from_history_search(); + } + } + rl::HistoryPager => { + if self.history_pager_active { + self.fill_history_pager( + HistoryPagerInvocation::Advance, + SearchDirection::Backward, + ); + return; + } + + // Record our cycle_command_line. + self.cycle_command_line = self.command_line.text().to_owned(); + self.cycle_cursor_pos = self.command_line.position(); + + self.history_pager_active = true; + self.history_pager_history_index_start = 0; + self.history_pager_history_index_end = 0; + // Update the pager data. + self.pager.set_search_field_shown(true); + self.pager.set_prefix( + if MB_CUR_MAX() > 1 { + L!("► ") + } else { + L!("> ") + }, + /*highlight=*/ false, + ); + // Update the search field, which triggers the actual history search. + let search_string = if !self.history_search.active() + || self.history_search.search_string().is_empty() + { + parse_util_escape_wildcards(self.command_line.text()) + } else { + // If we have an actual history search already going, reuse that term + // - this is if the user looks around a bit and decides to switch to the pager. + self.history_search.search_string().to_owned() + }; + self.insert_string(EditableLineTag::SearchField, &search_string); + } + rl::HistoryPagerDelete => { + if !self.history_pager_active { + self.inputter.function_set_status(false); + return; + } + self.inputter.function_set_status(true); + if let Some(completion) = + self.pager.selected_completion(&self.current_page_rendering) + { + self.history.remove(completion.completion.clone()); + self.history.save(); + self.fill_history_pager( + HistoryPagerInvocation::Refresh, + 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::ForwardChar | rl::ForwardSingleChar => { + let (elt, el) = self.active_edit_line(); + if self.is_navigating_pager_contents() { + self.select_completion_in_direction(SelectionMotion::East, false); + } else if el.position() != el.len() { + self.update_buff_pos(elt, Some(el.position() + 1)); + } else { + self.accept_autosuggestion( + /*full=*/ c != rl::ForwardSingleChar, + /*single=*/ c == rl::ForwardSingleChar, + MoveWordStyle::Punctuation, + ); + } + } + rl::BackwardKillWord | rl::BackwardKillPathComponent | rl::BackwardKillBigword => { + let style = match c { + rl::BackwardKillBigword => MoveWordStyle::Whitespace, + rl::BackwardKillPathComponent => MoveWordStyle::PathComponents, + rl::BackwardKillWord => MoveWordStyle::Punctuation, + _ => unreachable!(), + }; + // Is this the same killring item as the last kill? + let newv = !matches!( + rls.last_cmd, + Some( + rl::BackwardKillWord + | rl::BackwardKillPathComponent + | rl::BackwardKillBigword + ) + ); + self.move_word( + self.active_edit_line_tag(), + MoveWordDir::Left, + /*erase=*/ true, + style, + newv, + ) + } + rl::KillWord | rl::KillBigword => { + // The "bigword" functions differ only in that they move to the next whitespace, not + // punctuation. + let style = if c == rl::KillWord { + MoveWordStyle::Punctuation + } else { + MoveWordStyle::Whitespace + }; + self.move_word( + self.active_edit_line_tag(), + MoveWordDir::Right, + /*erase=*/ true, + style, + rls.last_cmd != Some(c), + ); + } + rl::BackwardWord | rl::BackwardBigword | rl::PrevdOrBackwardWord => { + if c == rl::PrevdOrBackwardWord && self.command_line.is_empty() { + let last_statuses = self.parser().vars().get_last_statuses(); + self.parser().eval(L!("prevd"), &IoChain::new()); + self.parser().set_last_statuses(last_statuses); + self.force_exec_prompt_and_repaint = true; + self.inputter + .queue_char(CharEvent::from_readline(ReadlineCmd::Repaint)); + return; + } + + let style = if c != rl::BackwardBigword { + MoveWordStyle::Punctuation + } else { + MoveWordStyle::Whitespace + }; + self.move_word( + self.active_edit_line_tag(), + MoveWordDir::Left, + /*erase=*/ false, + style, + false, + ); + } + rl::ForwardWord | rl::ForwardBigword | rl::NextdOrForwardWord => { + if c == rl::NextdOrForwardWord && self.command_line.is_empty() { + let last_statuses = self.parser().vars().get_last_statuses(); + self.parser().eval(L!("nextd"), &IoChain::new()); + self.parser().set_last_statuses(last_statuses); + self.force_exec_prompt_and_repaint = true; + self.inputter + .queue_char(CharEvent::from_readline(ReadlineCmd::Repaint)); + return; + } + + let style = if c != rl::ForwardBigword { + MoveWordStyle::Punctuation + } else { + MoveWordStyle::Whitespace + }; + let (elt, el) = self.active_edit_line(); + if el.position() != el.len() { + self.move_word(elt, MoveWordDir::Right, /*erase=*/ false, style, false); + } else { + self.accept_autosuggestion(false, false, style); + } + } + 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 up { + self.history_search.go_to_beginning(); + } else { + self.history_search.go_to_end(); + } + if self.history_search.active() { + self.update_command_line_from_history_search(); + } + } + } + rl::UpLine | rl::DownLine => { + if self.is_navigating_pager_contents() { + // We are already navigating pager contents. + let direction = if c == rl::DownLine { + // Down arrow is always south. + SelectionMotion::South + } else if self.selection_is_at_top() { + // Up arrow, but we are in the first column and first row. End navigation. + SelectionMotion::Deselect + } else { + // Up arrow, go north. + SelectionMotion::North + }; + + // Now do the selection. + self.select_completion_in_direction(direction, false); + } else if !self.pager.is_empty() { + // We pressed a direction with a non-empty pager, begin navigation. + self.select_completion_in_direction( + if c == rl::DownLine { + SelectionMotion::South + } else { + SelectionMotion::North + }, + false, + ); + } else { + // Not navigating the pager contents. + let (elt, el) = self.active_edit_line(); + let line_old = + i32::try_from(parse_util_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 = parse_util_lineno(el.text(), el.len()) - 1; + + if (0..=i32::try_from(line_count).unwrap()).contains(&line_new) { + let indents = parse_util_compute_indents(el.text()); + let base_pos_new = + parse_util_get_offset_from_line(el.text(), line_new).unwrap(); + let base_pos_old = + parse_util_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 = usize::try_from(indent_old).unwrap(); + let indent_new = usize::try_from(indent_new).unwrap(); + + let line_offset_old = el.position() - base_pos_old; + let total_offset_new = parse_util_get_offset( + el.text(), + line_new, + line_offset_old - 4 * (indent_new - indent_old), + ); + self.update_buff_pos(elt, total_offset_new); + } + } + } + rl::SuppressAutosuggestion => { + self.suppress_autosuggestion = true; + let success = !self.autosuggestion.is_empty(); + self.autosuggestion.clear(); + // Return true if we had a suggestion to clear. + self.inputter.function_set_status(success); + } + rl::AcceptAutosuggestion => { + self.accept_autosuggestion(true, false, MoveWordStyle::Punctuation); + } + rl::TransposeChars => { + let (elt, el) = self.active_edit_line(); + if el.len() < 2 { + return; + } + + // If the cursor is at the end, transpose the last two characters of the line. + if el.position() == el.len() { + self.update_buff_pos(elt, Some(el.position() - 1)); + } + + // Drag the character before the cursor forward over the character at the cursor, + // moving the cursor forward as well. + 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.set_command_line_and_position(elt, local_cmd, el.position() + 1); + } + } + rl::TransposeWords => { + let (elt, el) = self.active_edit_line(); + + // If we are not in a token, look for one ahead. + 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 = 0..0; + let mut prev_tok = 0..0; + parse_util_token_extent(text, el.position(), &mut tok, Some(&mut prev_tok)); + + // In case we didn't find a token at or after the cursor... + if tok.start == el.len() { + // ...retry beginning from the previous token. + let pos = prev_tok.end; + parse_util_token_extent(text, pos, &mut tok, Some(&mut prev_tok)); + } + + // Make sure we have two tokens. + 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..]; + + // Compose new command line with swapped tokens. + 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); + // Put cursor right after the second token. + 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(); + + // Check that the cursor is on a character + if buff_pos != el.len() { + let chr = el.text().as_char_slice()[buff_pos]; + + // Toggle the case of the current character + 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); + + // Restore the buffer position since replace_substring moves + // the buffer position ahead of the replaced text. + self.update_buff_pos(elt, Some(buff_pos)); + } + } + rl::TogglecaseSelection => { + let (elt, el) = self.active_edit_line(); + + // Check that we have an active selection and get the bounds. + if let Some(selection) = self.get_selection() { + let mut replacement = WString::new(); + + // Loop through the selected characters and toggle their case. + for pos in selection.clone() { + if pos >= el.len() { + break; + } + let chr = el.text().as_char_slice()[pos]; + + // Toggle the case of the current character. + 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); + + // Restore the buffer position since replace_substring moves + // the buffer position ahead of the replaced text. + self.update_buff_pos(elt, Some(buff_pos)); + } + } + rl::UpcaseWord | rl::DowncaseWord | rl::CapitalizeWord => { + let (elt, el) = self.active_edit_line(); + // For capitalize_word, whether we've capitalized a character so far. + let mut capitalized_first = false; + + // We apply the operation from the current location to the end of the word. + let pos = el.position(); + let init_pos = pos; + self.move_word( + elt, + MoveWordDir::Right, + false, + MoveWordStyle::Punctuation, + false, + ); + let (elt, el) = self.active_edit_line(); + let mut replacement = WString::new(); + while pos < el.position() { + let chr = el.text().as_char_slice()[pos]; + + // We always change the case; this decides whether we go uppercase (true) or + // lowercase (false). + let make_uppercase = if c == rl::CapitalizeWord { + !capitalized_first && chr.is_alphanumeric() + } else { + c == rl::UpcaseWord + }; + + // Apply the operation and then record what we did. + if make_uppercase { + replacement.extend(chr.to_uppercase()); + } else { + replacement.extend(chr.to_lowercase()); + }; + capitalized_first = capitalized_first || make_uppercase; + } + 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 = rls.last_cmd != Some(rl::KillSelection); + if let Some(selection) = self.get_selection() { + self.kill(EditableLineTag::Commandline, selection, Kill::Append, newv); + } + } + 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; + } + self.update_buff_pos(elt, Some(el.position() - 1)); + }; + 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; + } + self.update_buff_pos(elt, Some(el.position() + 1)); + }; + 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(); + let target = self.inputter.function_pop_arg(); + let success = self.jump(direction, precision, elt, target); + + self.inputter.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.jump( + self.last_jump_direction, + self.last_jump_precision, + elt, + target, + ); + } + + self.inputter.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.jump(dir, self.last_jump_precision, elt, last_target); + } + + self.last_jump_direction = original_dir; + + self.inputter.function_set_status(success); + } + rl::ExpandAbbr => { + if self.expand_abbreviation_at_cursor(1) { + self.inputter.function_set_status(true); + } else { + self.inputter.function_set_status(false); + } + } + rl::Undo | rl::Redo => { + let (elt, el) = self.active_edit_line_mut(); + let ok = if c == rl::Undo { el.undo() } else { el.redo() }; + if !ok { + self.flash(); + return; + } + if elt == EditableLineTag::Commandline { + self.clear_pager(); + } + self.update_buff_pos(elt, None); + self.maybe_refilter_pager(elt); + } + 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::DisableMouseTracking => { + let outp = Outputter::stdoutput().get_mut(); + outp.write_wstr(L!("\x1B[?1000l")); + } + rl::ClearScreenAndRepaint => { + self.parser().libdata_mut().pods.is_repaint = true; + let clear = screen_clear(); + if !clear.is_empty() { + // Clear the screen if we can. + // This is subtle: We first clear, draw the old prompt, + // and *then* reexecute the prompt and overdraw it. + // This removes the flicker, + // while keeping the prompt up-to-date. + let outp = Outputter::stdoutput().get_mut(); + outp.write_wstr(&clear); + self.screen.reset_line(/*repaint_prompt=*/ true); + self.layout_and_repaint(L!("readline")); + } + self.exec_prompt(); + self.screen.reset_line(/*repaint_prompt=*/ true); + self.layout_and_repaint(L!("readline")); + self.force_exec_prompt_and_repaint = false; + self.parser().libdata_mut().pods.is_repaint = false; + } + rl::SelfInsert | rl::SelfInsertNotFirst | rl::FuncAnd | rl::FuncOr => { + panic!("should have been handled by inputter_t::readch"); + } + _ => panic!("unhandled readline command {c:?}"), + } + } +} + +/// Returns true if the last token is a comment. +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 ReaderData { + // Handle readline_cmd_t::execute. This may mean inserting a newline if the command is + // unfinished. It may also set 'finished' and 'cmd' inside the rls. + // \return true on success, false if we got an error, in which case the caller should fire the + // error event. + fn handle_execute(&mut self, rls: &mut ReadlineLoopState) -> bool { + // Evaluate. If the current command is unfinished, or if the charater is escaped + // using a backslash, insert a newline. + // If the user hits return while navigating the pager, it only clears the pager. + if self.is_navigating_pager_contents() { + if self.history_pager_active && self.pager.selected_completion_idx.is_none() { + self.command_line.push_edit( + Edit::new( + 0..self.command_line.len(), + self.pager.search_field_line.text().to_owned(), + ), + /*allow_coalesce=*/ false, + ); + self.command_line + .set_position(self.pager.search_field_line.position()); + } + self.clear_pager(); + return true; + } + + // Delete any autosuggestion. + self.autosuggestion.clear(); + + // The user may have hit return with pager contents, but while not navigating them. + // Clear the pager in that event. + self.clear_pager(); + + // We only execute the command line. + let elt = EditableLineTag::Commandline; + let el = &mut self.command_line; + + // Allow backslash-escaped newlines. + let mut continue_on_next_line = false; + if el.position() >= el.len() { + // We're at the end of the text and not in a comment (issue #1225). + continue_on_next_line = + is_backslashed(el.text(), el.position()) && !text_ends_in_comment(el.text()); + } else { + // Allow mid line split if the following character is whitespace (issue #613). + if is_backslashed(el.text(), el.position()) + && el.text().as_char_slice()[el.position()].is_whitespace() + { + continue_on_next_line = true; + // Check if the end of the line is backslashed (issue #4467). + } else if is_backslashed(el.text(), el.len()) && !text_ends_in_comment(el.text()) { + // Move the cursor to the end of the line. + el.set_position(el.len()); + continue_on_next_line = true; + } + } + // If the conditions are met, insert a new line at the position of the cursor. + if continue_on_next_line { + self.insert_char(elt, '\n'); + return true; + } + + // Expand the command line in preparation for execution. + // to_exec is the command to execute; the command line itself has the command for history. + let test_res = self.expand_for_execute(); + if let Err(err) = test_res { + if err.contains(ParserTestErrorBits::ERROR) { + return false; + } else if err.contains(ParserTestErrorBits::INCOMPLETE) { + self.insert_char(elt, '\n'); + return true; + } + unreachable!(); + } + + self.add_to_history(); + rls.finished = true; + self.update_buff_pos(elt, Some(self.command_line.len())); + true + } + + // Expand abbreviations before execution. + // Replace the command line with any abbreviations as needed. + // \return the test result, which may be incomplete to insert a newline, or an error. + fn expand_for_execute(&mut self) -> Result<(), ParserTestErrorBits> { + // Expand abbreviations at the cursor. + // The first expansion is "user visible" and enters into history. + let el = &self.command_line; + + let mut test_res = Ok(()); + + // Syntax check before expanding abbreviations. We could consider relaxing this: a string may be + // syntactically invalid but become valid after expanding abbreviations. + if self.conf.syntax_check_ok { + test_res = reader_shell_test(self.parser(), el.text()); + if test_res.is_err_and(|err| err.contains(ParserTestErrorBits::ERROR)) { + return test_res; + } + } + + // Exec abbreviations at the cursor. + // Note we want to expand abbreviations even if incomplete. + if self.expand_abbreviation_at_cursor(0) { + // Trigger syntax highlighting as we are likely about to execute this command. + 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 + } + + // Ensure we have no pager contents. + fn clear_pager(&mut self) { + self.pager.clear(); + self.history_pager_active = false; + self.command_line_has_transient_edit = false; + } + + fn get_selection(&self) -> Option> { + let selection = self.selection?; + let start = selection.start; + let end = std::cmp::min(selection.stop, self.command_line.len()); + 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_some_and(|col| col != 0) + } + + /// Called to update the termsize, including $COLUMNS and $LINES, as necessary. + fn update_termsize(&mut self) { + termsize_update(self.parser()); + } + + /// Flash the screen. This function changes the color of the current line momentarily. + fn flash(&mut self) { + // Multiple flashes may be enqueued by keypress repeat events and can pile up to cause a + // significant delay in processing future input while all the flash() calls complete, as we + // effectively sleep for 100ms each go. See #8610. + let now = Instant::now(); + if self + .last_flash + .is_some_and(|last_flash| now.duration_since(last_flash) < Duration::from_millis(50)) + { + self.last_flash = Some(now); + return; + } + + let mut data = self.make_layout_data(); + + // Save off the colors and set the background. + let saved_colors = data.colors.clone(); + for i in 0..self.command_line.position() { + data.colors[i] = HighlightSpec::with_bg(HighlightRole::search_match); + } + self.rendered_layout = data.clone(); // need to copy the data since we will use it again. + self.paint_layout(L!("flash")); + + let _old_data = std::mem::take(&mut self.rendered_layout); + + std::thread::sleep(Duration::from_millis(100)); + + // Re-render with our saved data. + data.colors = saved_colors; + self.rendered_layout = data; + self.paint_layout(L!("unflash")); + + // Save the time we stopped flashing as the time of the most recent flash. We can't just + // increment the old `now` value because the sleep is non-deterministic. + self.last_flash = Some(Instant::now()); + } +} + +impl ReaderData { + /// Do what we need to do whenever our pager selection changes. + fn pager_selection_changed(&mut self) { + assert_is_main_thread(); + + let completion = self.pager.selected_completion(&self.current_page_rendering); + + // Update the cursor and command line. + let mut cursor_pos = self.cycle_cursor_pos; + + let new_cmd_line = match completion { + None => self.cycle_command_line.clone(), + Some(completion) => completion_apply_to_command_line( + &completion.completion, + completion.flags, + &self.cycle_command_line, + &mut cursor_pos, + false, + ), + }; + + // Only update if something changed, to avoid useless edits in the undo history. + if new_cmd_line != self.command_line.text() { + self.set_buffer_maintaining_pager(&new_cmd_line, cursor_pos, /*transient=*/ true); + } + } + + /// Sets the command line contents, without clearing the pager. + fn set_buffer_maintaining_pager( + &mut self, + b: &wstr, + mut pos: usize, + transient: bool, /* = false */ + ) { + let command_line_len = b.len(); + if transient { + if self.command_line_has_transient_edit { + self.command_line.undo(); + } + self.command_line_has_transient_edit = true; + } + self.replace_substring( + EditableLineTag::Commandline, + 0..self.command_line.len(), + b.to_owned(), + ); + self.command_line_changed(EditableLineTag::Commandline); + + // Don't set a position past the command line length. + if pos > command_line_len { + pos = command_line_len; + } + self.update_buff_pos(EditableLineTag::Commandline, Some(pos)); + + // Clear history search. + self.history_search.reset(); + } + + fn select_completion_in_direction( + &mut self, + dir: SelectionMotion, + force_selection_change: bool, /* = false */ + ) { + 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(); + } + } +} + +/// Restore terminal settings we care about, to prevent a broken shell. +fn term_fix_modes(modes: &mut libc::termios) { + modes.c_iflag &= !ICRNL; // disable mapping CR (\cM) to NL (\cJ) + modes.c_iflag &= !INLCR; // disable mapping NL (\cJ) to CR (\cM) + modes.c_lflag &= !ICANON; // turn off canonical mode + modes.c_lflag &= !ECHO; // turn off echo mode + modes.c_lflag &= !IEXTEN; // turn off handling of discard and lnext characters + modes.c_oflag |= OPOST; // turn on "implementation-defined post processing" - this often + // changes how line breaks work. + modes.c_oflag |= ONLCR; // "translate newline to carriage return-newline" - without + // you see staircase output. + + modes.c_cc[VMIN] = 1; + modes.c_cc[VTIME] = 0; + + // Prefer to use _POSIX_VDISABLE to disable control functions. + // This permits separately binding nul (typically control-space). + // POSIX calls out -1 as a special value which should be ignored. + let disabling_char = _POSIX_VDISABLE; + + // We ignore these anyway, so there is no need to sacrifice a character. + modes.c_cc[VSUSP] = disabling_char; + modes.c_cc[VQUIT] = disabling_char; +} + +fn term_fix_external_modes(modes: &mut libc::termios) { + // Turning off OPOST or ONLCR breaks output (staircase effect), we don't allow it. + // See #7133. + modes.c_oflag |= OPOST; + modes.c_oflag |= ONLCR; + // These cause other ridiculous behaviors like input not being shown. + modes.c_lflag |= ICANON; + modes.c_lflag |= IEXTEN; + modes.c_lflag |= ECHO; + modes.c_iflag |= ICRNL; + modes.c_iflag &= !INLCR; +} + +/// Give up control of terminal. +fn term_donate(quiet: bool /* = false */) { + while unsafe { + libc::tcsetattr( + STDIN_FILENO, + TCSANOW, + &*TTY_MODES_FOR_EXTERNAL_CMDS.lock().unwrap(), + ) + } == -1 + { + if errno().0 == EIO { + redirect_tty_output(); + } + if errno().0 != EINTR { + if !quiet { + FLOG!( + warning, + wgettext!("Could not set terminal mode for new job") + ); + perror("tcsetattr"); + } + break; + } + } +} + +/// Copy the (potentially changed) terminal modes and use them from now on. +pub fn term_copy_modes() { + let mut modes: libc::termios = unsafe { std::mem::zeroed() }; + unsafe { libc::tcgetattr(STDIN_FILENO, &mut modes) }; + let mut tty_modes_for_external_cmds = TTY_MODES_FOR_EXTERNAL_CMDS.lock().unwrap(); + *tty_modes_for_external_cmds = modes; + + // Copy flow control settings to shell modes. + if (tty_modes_for_external_cmds.c_iflag & IXON) != 0 { + shell_modes_mut().c_iflag |= IXON; + } else { + shell_modes_mut().c_iflag &= !IXON; + } + if (tty_modes_for_external_cmds.c_iflag & IXOFF) != 0 { + shell_modes_mut().c_iflag |= IXOFF; + } else { + shell_modes_mut().c_iflag &= !IXOFF; + } +} + +/// Grab control of terminal. +fn term_steal() { + term_copy_modes(); + while unsafe { libc::tcsetattr(STDIN_FILENO, TCSANOW, shell_modes()) } == -1 { + if errno().0 == EIO { + redirect_tty_output(); + } + if errno().0 != EINTR { + FLOG!(warning, wgettext!("Could not set terminal mode for shell")); + perror("tcsetattr"); + break; + } + } + + termsize_invalidate_tty(); +} + +// Ensure that fish owns the terminal, possibly waiting. If we cannot acquire the terminal, then +// report an error and exit. +fn acquire_tty_or_exit(shell_pgid: libc::pid_t) { + assert_is_main_thread(); + + // Check if we are in control of the terminal, so that we don't do semi-expensive things like + // reset signal handlers unless we really have to, which we often don't. + // Common case. + let mut owner = unsafe { libc::tcgetpgrp(STDIN_FILENO) }; + if owner == shell_pgid { + return; + } + + // In some strange cases the tty may be come preassigned to fish's pid, but not its pgroup. + // In that case we simply attempt to claim our own pgroup. + // See #7388. + if owner == unsafe { libc::getpid() } { + unsafe { libc::setpgid(owner, owner) }; + return; + } + + // Bummer, we are not in control of the terminal. Stop until parent has given us control of + // it. + // + // In theory, reseting signal handlers could cause us to miss signal deliveries. In + // practice, this code should only be run during startup, when we're not waiting for any + // signals. + signal_reset_handlers(); + let _restore_sigs = ScopeGuard::new((), |()| signal_set_handlers(true)); + + // Ok, signal handlers are taken out of the picture. Stop ourself in a loop until we are in + // control of the terminal. However, the call to signal(SIGTTIN) may silently not do + // anything if we are orphaned. + // + // As far as I can tell there's no really good way to detect that we are orphaned. One way + // is to just detect if the group leader exited, via kill(shell_pgid, 0). Another + // possibility is that read() from the tty fails with EIO - this is more reliable but it's + // harder, because it may succeed or block. So we loop for a while, trying those strategies. + // Eventually we just give up and assume we're orphaend. + for loop_count in 0.. { + owner = unsafe { libc::tcgetpgrp(STDIN_FILENO) }; + // 0 is a valid return code from `tcgetpgrp()` under at least FreeBSD and testing + // indicates that a subsequent call to `tcsetpgrp()` will succeed. 0 is the + // pid of the top-level kernel process, so I'm not sure if this means ownership + // of the terminal has gone back to the kernel (i.e. it's not owned) or if it is + // just an "invalid" pid for all intents and purposes. + if owner == 0 { + unsafe { libc::tcsetpgrp(STDIN_FILENO, shell_pgid) }; + // Since we expect the above to work, call `tcgetpgrp()` immediately to + // avoid a second pass through this loop. + owner = unsafe { libc::tcgetpgrp(STDIN_FILENO) }; + } + if owner == -1 && errno().0 == ENOTTY { + if !is_interactive_session() { + // It's OK if we're not able to take control of the terminal. We handle + // the fallout from this in a few other places. + break; + } + // No TTY, cannot be interactive? + redirect_tty_output(); + FLOG!( + warning, + wgettext!("No TTY for interactive shell (tcgetpgrp failed)") + ); + perror("setpgid"); + exit_without_destructors(1); + } + if owner == shell_pgid { + break; // success + } else { + if check_for_orphaned_process(loop_count, shell_pgid) { + // We're orphaned, so we just die. Another sad statistic. + let pid = unsafe { libc::getpid() }; + FLOG!(warning, wgettext_fmt!("I appear to be an orphaned process, so I am quitting politely. My pid is %d.", pid)); + exit_without_destructors(1); + } + + // Try stopping us. + let ret = unsafe { libc::killpg(shell_pgid, SIGTTIN) }; + if ret < 0 { + perror("killpg(shell_pgid, SIGTTIN)"); + exit_without_destructors(1); + } + } + } +} + +/// Initialize data for interactive use. +fn reader_interactive_init(parser: &Parser) { + assert_is_main_thread(); + + let mut shell_pgid = unsafe { libc::getpgrp() }; + let shell_pid = unsafe { libc::getpid() }; + + // Set up key bindings. + init_input(); + + // Ensure interactive signal handling is enabled. + signal_set_handlers_once(true); + + // Wait until we own the terminal. + acquire_tty_or_exit(shell_pgid); + + // If fish has no valid pgroup (possible with firejail, see #5295) or is interactive, + // ensure it owns the terminal. Also see #5909, #7060. + if shell_pgid == 0 || (is_interactive_session() && shell_pgid != shell_pid) { + shell_pgid = shell_pid; + if unsafe { libc::setpgid(shell_pgid, shell_pgid) } < 0 { + // If we're session leader setpgid returns EPERM. The other cases where we'd get EPERM + // don't apply as we passed our own pid. + // + // This should be harmless, so we ignore it. + if errno().0 != EPERM { + FLOG!( + error, + wgettext!("Failed to assign shell to its own process group") + ); + perror("setpgid"); + exit_without_destructors(1); + } + } + + // Take control of the terminal + if unsafe { libc::tcsetpgrp(STDIN_FILENO, shell_pgid) } == -1 { + if errno().0 == ENOTTY { + redirect_tty_output(); + } + FLOG!(error, wgettext!("Failed to take control of the terminal")); + perror("tcsetpgrp"); + exit_without_destructors(1); + } + + // Configure terminal attributes + if unsafe { libc::tcsetattr(STDIN_FILENO, TCSANOW, shell_modes_mut()) } == -1 { + if errno().0 == EIO { + redirect_tty_output(); + } + FLOG!(warning, wgettext!("Failed to set startup terminal mode!")); + perror("tcsetattr"); + } + } + + termsize_invalidate_tty(); + + // Provide value for `status current-command` + parser.libdata_mut().status_vars.command = L!("fish").to_owned(); + // Also provide a value for the deprecated fish 2.0 $_ variable + parser + .vars() + .set_one(L!("_"), EnvMode::GLOBAL, L!("fish").to_owned()); +} + +/// Destroy data for interactive use. +fn reader_interactive_destroy() { + Outputter::stdoutput() + .get_mut() + .set_color(RgbColor::RESET, RgbColor::RESET); } /// \return whether fish is currently unwinding the stack in preparation to exit. pub fn fish_is_unwinding_for_exit() -> bool { - crate::ffi::fish_is_unwinding_for_exit() + let exit_state = EXIT_STATE.load(Ordering::Relaxed); + let exit_state: ExitState = unsafe { std::mem::transmute(exit_state) }; + match exit_state { + ExitState::None => { + // Cancel if we got SIGHUP. + reader_received_sighup() + } + ExitState::RunningHandlers => { + // We intend to exit but we want to allow these handlers to run. + false + } + ExitState::FinishedHandlers => { + // Done running exit handlers, time to exit. + true + } + } } -pub fn reader_run_count() -> u64 { - crate::ffi::reader_run_count() +/// Write the title to the titlebar. This function is called just before a new application starts +/// executing and just after it finishes. +/// +/// \param cmd Command line string passed to \c fish_title if is defined. +/// \param parser The parser to use for autoloading fish_title. +/// \param reset_cursor_position If set, issue a \r so the line driver knows where we are +pub fn reader_write_title( + cmd: &wstr, + parser: &Parser, + reset_cursor_position: bool, /* = true */ +) { + if !term_supports_setting_title() { + return; + } + + let _noninteractive = scoped_push_replacer( + |new_value| std::mem::replace(&mut parser.libdata_mut().pods.is_interactive, new_value), + false, + ); + let _in_title = scoped_push_replacer( + |new_value| { + std::mem::replace( + &mut parser.libdata_mut().pods.suppress_fish_trace, + new_value, + ) + }, + true, + ); + + let mut fish_title_command = DEFAULT_TITLE.to_owned(); + if function::exists(L!("fish_title"), parser) { + fish_title_command = L!("fish_title").to_owned(); + if !cmd.is_empty() { + fish_title_command.push(' '); + fish_title_command.push_utfstr(&escape_string( + cmd, + EscapeStringStyle::Script(EscapeFlags::NO_QUOTED | EscapeFlags::NO_TILDE), + )); + } + } + + let mut lst = vec![]; + exec_subshell( + &fish_title_command, + parser, + Some(&mut lst), + /*apply_exit_status=*/ false, + ); + if !lst.is_empty() { + let mut title_line = L!("\x1B]0;").to_owned(); + for val in &lst { + title_line.push_utfstr(val); + } + title_line.push_str("\x07"); // BEL + let narrow = wcs2string(&title_line); + let _ = write_loop(&STDOUT_FILENO, &narrow); + } + + Outputter::stdoutput() + .get_mut() + .set_color(RgbColor::RESET, RgbColor::RESET); + if reset_cursor_position && !lst.is_empty() { + // Put the cursor back at the beginning of the line (issue #2453). + let _ = write_to_fd(b"\r", STDOUT_FILENO); + } } -/// When tab-completing with a wildcard, we expand the wildcard up to this many results. -/// If expansion would exceed this many results, beep and do nothing. -const TAB_COMPLETE_WILDCARD_MAX_EXPANSION: usize = 256; +impl ReaderData { + fn exec_mode_prompt(&mut self) { + self.mode_prompt_buff.clear(); + if function::exists(MODE_PROMPT_FUNCTION_NAME, self.parser()) { + let mut mode_indicator_list = vec![]; + exec_subshell( + MODE_PROMPT_FUNCTION_NAME, + self.parser(), + Some(&mut mode_indicator_list), + false, + ); + // We do not support multiple lines in the mode indicator, so just concatenate all of + // them. + for i in mode_indicator_list { + self.mode_prompt_buff.push_utfstr(&i); + } + } + } + + /// Reexecute the prompt command. The output is inserted into prompt_buff. + fn exec_prompt(&mut self) { + // Clear existing prompts. + self.left_prompt_buff.clear(); + self.right_prompt_buff.clear(); + + // Suppress fish_trace while in the prompt. + let mut zelf = scoped_push_replacer_ctx( + self, + |zelf, new_value| { + std::mem::replace( + &mut zelf.parser().libdata_mut().pods.suppress_fish_trace, + new_value, + ) + }, + true, + ); + + // Update the termsize now. + // This allows prompts to react to $COLUMNS. + zelf.update_termsize(); + + // If we have any prompts, they must be run non-interactively. + if !zelf.conf.left_prompt_cmd.is_empty() || !zelf.conf.right_prompt_cmd.is_empty() { + let mut zelf = scoped_push_replacer_ctx( + &mut zelf, + |zelf, new_value| { + std::mem::replace( + &mut zelf.parser().libdata_mut().pods.is_interactive, + new_value, + ) + }, + false, + ); + + zelf.exec_mode_prompt(); + + if !zelf.conf.left_prompt_cmd.is_empty() { + // Status is ignored. + let mut prompt_list = vec![]; + // Historic compatibility hack. + // If the left prompt function is deleted, then use a default prompt instead of + // producing an error. + let left_prompt_deleted = zelf.conf.left_prompt_cmd == LEFT_PROMPT_FUNCTION_NAME + && !function::exists(&zelf.conf.left_prompt_cmd, zelf.parser()); + exec_subshell( + if left_prompt_deleted { + DEFAULT_PROMPT + } else { + &zelf.conf.left_prompt_cmd + }, + zelf.parser(), + Some(&mut prompt_list), + /*apply_exit_status=*/ false, + ); + zelf.left_prompt_buff = join_strings(&prompt_list, '\n'); + } + + if !zelf.conf.right_prompt_cmd.is_empty() { + if function::exists(&zelf.conf.right_prompt_cmd, zelf.parser()) { + // Status is ignored. + let mut prompt_list = vec![]; + exec_subshell( + &zelf.conf.right_prompt_cmd, + zelf.parser(), + Some(&mut prompt_list), + /*apply_exit_status=*/ false, + ); + // Right prompt does not support multiple lines, so just concatenate all of them. + for i in prompt_list { + zelf.right_prompt_buff.push_utfstr(&i); + } + } + } + } + + // Write the screen title. Do not reset the cursor position: exec_prompt is called when there + // may still be output on the line from the previous command (#2499) and we need our PROMPT_SP + // hack to work. + reader_write_title(L!(""), zelf.parser(), false); + + // Some prompt may have requested an exit (#8033). + let exit_current_script = zelf.parser().libdata().pods.exit_current_script; + zelf.exit_loop_requested |= exit_current_script; + zelf.parser().libdata_mut().pods.exit_current_script = false; + } +} + +/// The result of an autosuggestion computation. +#[derive(Default)] +struct Autosuggestion { + // The text to use, as an extension of the command line. + text: WString, + + // The string which was searched for. + search_string: WString, + + // The list of completions which may need loading. + needs_load: Vec, + + // Whether the autosuggestion should be case insensitive. + // This is true for file-generated autosuggestions, but not for history. + icase: bool, +} + +impl Autosuggestion { + fn new(text: WString, search_string: WString, icase: bool) -> Self { + Self { + text, + search_string, + needs_load: vec![], + icase, + } + } + // Clear our contents. + fn clear(&mut self) { + self.text.clear(); + self.search_string.clear(); + } + + // \return whether we have empty text. + fn is_empty(&self) -> bool { + self.text.is_empty() + } +} + +// Returns a function that can be invoked (potentially +// on a background thread) to determine the autosuggestion +fn get_autosuggestion_performer( + parser: &Parser, + search_string: WString, + cursor_pos: usize, + history: Arc, +) -> impl FnOnce() -> Autosuggestion { + 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 = Autosuggestion::default(); + let ctx = get_bg_context(&vars, generation_count); + if ctx.check_cancel() { + return nothing; + } + + // Let's make sure we aren't using the empty string. + if search_string.is_empty() { + return nothing; + } + + // Search history for a matching item. + let mut searcher = + HistorySearch::new_with_type(history, search_string.to_owned(), SearchType::Prefix); + while !ctx.check_cancel() && searcher.go_to_next_match(SearchDirection::Backward) { + let item = searcher.current_item(); + + // Skip items with newlines because they make terrible autosuggestions. + if item.str().contains('\n') { + continue; + } + + if autosuggest_validate_from_history(item, &working_directory, &ctx) { + // The command autosuggestion was handled specially, so we're done. + // History items are case-sensitive, see #3978. + return Autosuggestion::new( + searcher.current_string().to_owned(), + search_string.to_owned(), + /*icase=*/ false, + ); + } + } + + // Maybe cancel here. + if ctx.check_cancel() { + return nothing; + } + + // Here we do something a little funny. If the line ends with a space, and the cursor is not + // at the end, don't use completion autosuggestions. It ends up being pretty weird seeing + // stuff get spammed on the right while you go back to edit a line + let last_char = search_string.chars().last().unwrap(); + let cursor_at_end = cursor_pos == search_string.len(); + if !cursor_at_end && last_char.is_whitespace() { + return nothing; + } + + // On the other hand, if the line ends with a quote, don't go dumping stuff after the quote. + if matches!(last_char, '\'' | '"') && cursor_at_end { + return nothing; + } + + // Try normal completions. + let complete_flags = CompletionRequestOptions::autosuggest(); + let (mut completions, needs_load) = complete(&search_string, complete_flags, &ctx); + + let mut result = Autosuggestion::default(); + result.search_string = search_string.to_owned(); + result.needs_load = needs_load; + result.icase = true; // normal completions are case-insensitive + if !completions.is_empty() { + sort_and_prioritize(&mut completions, complete_flags); + let comp = &completions[0]; + let mut cursor = cursor_pos; + result.text = completion_apply_to_command_line( + &comp.completion, + comp.flags, + &search_string, + &mut cursor, + /*append_only=*/ true, + ); + } + result + } +} + +impl ReaderData { + fn can_autosuggest(&self) -> bool { + // We autosuggest if suppress_autosuggestion is not set, if we're not doing a history search, + // and our command line contains a non-whitespace character. + let (elt, el) = self.active_edit_line(); + self.conf.autosuggest_ok + && !self.suppress_autosuggestion + && self.history_search.is_at_end() + && elt == EditableLineTag::Commandline + && el + .text() + .chars() + .any(|c| !matches!(c, ' ' | '\t' | '\r' | '\n' | '\x0B')) + } + + // Called after an autosuggestion has been computed on a background thread. + fn autosuggest_completed(&mut self, result: Autosuggestion) { + assert_is_main_thread(); + if result.search_string == self.in_flight_autosuggest_request { + self.in_flight_autosuggest_request.clear(); + } + if result.search_string != self.command_line.text() { + // This autosuggestion is stale. + return; + } + // Maybe load completions for commands discovered by this autosuggestion. + 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 %ls, restarting", + to_load + ); + loaded_new = true; + } + } + if loaded_new { + // We loaded new completions for this command. + // Re-do our autosuggestion. + self.update_autosuggestion(); + } else if !result.is_empty() + && self.can_autosuggest() + && string_prefixes_string_case_insensitive(&result.search_string, &result.text) + { + // Autosuggestion is active and the search term has not changed, so we're good to go. + self.autosuggestion = result; + if self.is_repaint_needed(None) { + self.layout_and_repaint(L!("autosuggest")); + } + } + } + + fn update_autosuggestion(&mut self) { + // If we can't autosuggest, just clear it. + if !self.can_autosuggest() { + self.in_flight_autosuggest_request.clear(); + self.autosuggestion.clear(); + return; + } + + // Check to see if our autosuggestion still applies; if so, don't recompute it. + // Since the autosuggestion computation is asynchronous, this avoids "flashing" as you type into + // the autosuggestion. + // This is also the main mechanism by which readline commands that don't change the command line + // text avoid recomputing the autosuggestion. + let el = &self.command_line; + if self.autosuggestion.text.len() > el.text().len() + && if self.autosuggestion.icase { + string_prefixes_string_case_insensitive(el.text(), &self.autosuggestion.text) + } else { + string_prefixes_string(el.text(), &self.autosuggestion.text) + } + { + return; + } + + // Do nothing if we've already kicked off this autosuggest request. + if el.text() == self.in_flight_autosuggest_request { + return; + } + self.in_flight_autosuggest_request = el.text().to_owned(); + + // Clear the autosuggestion and kick it off in the background. + FLOG!(reader_render, "Autosuggesting"); + self.autosuggestion.clear(); + let performer = get_autosuggestion_performer( + self.parser(), + el.text().to_owned(), + el.position(), + self.history.clone(), + ); + let canary = Rc::downgrade(&self.canary); + let completion = move |zelf: &mut Self, result| { + if canary.upgrade().is_none() { + return; + } + zelf.autosuggest_completed(result); + }; + debounce_autosuggestions().perform_with_completion(performer, completion); + } + + // Accept any autosuggestion by replacing the command line with it. If full is true, take the whole + // thing; if it's false, then respect the passed in style. + fn accept_autosuggestion( + &mut self, + full: bool, + single: bool, /* = false */ + style: MoveWordStyle, /* = Punctuation */ + ) { + if self.autosuggestion.is_empty() { + return; + } + // Accepting an autosuggestion clears the pager. + self.clear_pager(); + + // Accept the autosuggestion. + if full { + // Just take the whole thing. + self.replace_substring( + EditableLineTag::Commandline, + 0..self.command_line.len(), + self.autosuggestion.text.clone(), + ); + } else if single { + self.replace_substring( + EditableLineTag::Commandline, + self.command_line.len()..self.command_line.len(), + self.autosuggestion.text[self.command_line.len()..self.command_line.len() + 1] + .to_owned(), + ); + } else { + // Accept characters according to the specified style. + let mut state = MoveWordStateMachine::new(style); + let mut want = self.command_line.len(); + while want < self.autosuggestion.text.len() { + let wc = self.autosuggestion.text.as_char_slice()[want]; + if !state.consume_char(wc) { + break; + } + want += 1; + } + let have = self.command_line.len(); + self.replace_substring( + EditableLineTag::Commandline, + have..have, + self.autosuggestion.text[have..want].to_owned(), + ); + } + } +} + +#[derive(Default)] +struct HighlightResult { + colors: Vec, + text: WString, +} + +// Given text and whether IO is allowed, return a function that performs highlighting. The function +// may be invoked on a background thread. +fn get_highlight_performer( + parser: &Parser, + el: &EditableLine, + io_ok: bool, +) -> impl FnOnce() -> HighlightResult { + 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 ReaderData { + fn highlight_complete(&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")); + } + } + } + + /// Highlight the command line in a super, plentiful way. + fn super_highlight_me_plenty(&mut self) { + if !self.conf.highlight_ok { + return; + } + + // Do nothing if this text is already in flight. + 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, /*io_ok=*/ true); + let canary = Rc::downgrade(&self.canary); + let completion = move |zelf: &mut Self, result| { + if canary.upgrade().is_none() { + return; + } + zelf.highlight_complete(result); + }; + debounce_highlighting().perform_with_completion(highlight_performer, completion); + } + + /// Finish up any outstanding syntax highlighting, before execution. + /// This plays some tricks to not block on I/O for too long. + fn finish_highlighting_before_exec(&mut self) { + // Early-out if highlighting is not OK. + if !self.conf.highlight_ok { + return; + } + + // Decide if our current highlighting is OK. If not we will do a quick highlight without I/O. + let mut current_highlight_ok = false; + if self.in_flight_highlight_request.is_empty() { + // There is no in-flight highlight request. Two possibilities: + // 1: The user hit return after highlighting finished, so current highlighting is correct. + // 2: The user hit return before highlighting started, so current highlighting is stale. + // We can distinguish these based on what we last rendered. + current_highlight_ok = self.rendered_layout.text == self.command_line.text(); + } else if self.in_flight_highlight_request == self.command_line.text() { + // The user hit return while our in-flight highlight request was still processing the text. + // Wait for its completion to run, but not forever. + let mut now = Instant::now(); + let deadline = now + HIGHLIGHT_TIMEOUT_FOR_EXECUTION; + while now < deadline { + let timeout = deadline - now; + iothread_service_main_with_timeout(self, timeout); + + // Note iothread_service_main_with_timeout will reentrantly modify us, + // by invoking a completion. + if self.in_flight_highlight_request.is_empty() { + break; + } + now = Instant::now(); + } + + // If our in_flight_highlight_request is now empty, it means it completed and we highlighted + // successfully. + current_highlight_ok = self.in_flight_highlight_request.is_empty(); + } + + if !current_highlight_ok { + // We need to do a quick highlight without I/O. + let highlight_no_io = + get_highlight_performer(self.parser(), &self.command_line, /*io_ok=*/ false); + self.highlight_complete(highlight_no_io()); + } + } +} + +struct HistoryPagerResult { + matched_commands: Vec, + final_index: usize, + have_more_results: bool, +} + +#[derive(Eq, PartialEq)] +pub enum HistoryPagerInvocation { + Anew, + Advance, + Refresh, +} + +fn history_pager_search( + history: &Arc, + direction: SearchDirection, + history_index: usize, + search_string: &wstr, +) -> HistoryPagerResult { + // Limit the number of elements to half the screen like we do for completions + // Note that this is imperfect because we could have a multi-column layout. + // + // We can still push fish further upward in case the first entry is multiline, + // but that can't really be helped. + // (subtract 2 for the search line and the prompt) + let page_size = usize::try_from(std::cmp::max(termsize_last().height / 2 - 2, 12)).unwrap(); + + let mut completions = vec![]; + let mut search = HistorySearch::new_with( + history.clone(), + search_string.to_owned(), + history::SearchType::ContainsGlob, + smartcase_flags(search_string), + history_index, + ); + let mut next_match_found = search.go_to_next_match(direction); + if !next_match_found && !parse_util_contains_wildcards(search_string) { + // If there were no matches, and the user is not intending for + // wildcard search, try again with subsequence search. + search = HistorySearch::new_with( + history.clone(), + search_string.to_owned(), + history::SearchType::ContainsSubsequence, + smartcase_flags(search_string), + history_index, + ); + next_match_found = search.go_to_next_match(direction); + } + 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_COMMANDLINE + | CompleteFlags::DONT_ESCAPE + | CompleteFlags::DONT_SORT, + )); + + next_match_found = search.go_to_next_match(direction); + } + let last_index = search.current_index(); + if direction == SearchDirection::Forward { + completions.reverse(); + } + HistoryPagerResult { + matched_commands: completions, + final_index: last_index, + have_more_results: search.go_to_next_match(direction), + } +} + +impl ReaderData { + fn fill_history_pager( + &mut self, + why: HistoryPagerInvocation, + mut direction: SearchDirection, /* = Backward */ + ) { + let index; + let mut old_pager_index = None; + match why { + HistoryPagerInvocation::Anew => { + assert_eq!(direction, SearchDirection::Backward); + index = 0; + } + HistoryPagerInvocation::Advance => { + index = match direction { + SearchDirection::Forward => self.history_pager_history_index_start, + SearchDirection::Backward => self.history_pager_history_index_end, + _ => unreachable!(), + } + } + HistoryPagerInvocation::Refresh => { + // Redo the previous search previous direction. + direction = self.history_pager_direction; + index = self.history_pager_history_index_start; + old_pager_index = Some(self.pager.selected_completion_index()); + } + } + let search_term = self.pager.search_field_line.text().to_owned(); + let performer = { + let history = self.history.clone(); + let search_term = search_term.clone(); + move || history_pager_search(&history, direction, index, &search_term) + }; + let canary = Rc::downgrade(&self.canary); + let completion = move |zelf: &mut Self, result: HistoryPagerResult| { + if canary.upgrade().is_none() { + return; + } + if search_term != zelf.pager.search_field_line.text() { + return; // Stale request. + } + if result.matched_commands.is_empty() && why == HistoryPagerInvocation::Advance { + // No more matches, keep the existing ones and flash. + zelf.flash(); + return; + } + zelf.history_pager_direction = direction; + match direction { + SearchDirection::Forward => { + zelf.history_pager_history_index_start = result.final_index; + zelf.history_pager_history_index_end = index; + } + SearchDirection::Backward => { + zelf.history_pager_history_index_start = index; + zelf.history_pager_history_index_end = result.final_index; + } + _ => unreachable!(), + }; + zelf.pager.extra_progress_text = if result.have_more_results { + wgettext!("Search again for more results") + } else { + L!("") + } + .to_owned(); + zelf.pager.set_completions(&result.matched_commands, false); + if why == HistoryPagerInvocation::Refresh { + zelf.pager + .set_selected_completion_index(old_pager_index.unwrap()); + zelf.pager_selection_changed(); + } else { + zelf.select_completion_in_direction(SelectionMotion::Next, true); + } + zelf.super_highlight_me_plenty(); + zelf.layout_and_repaint(L!("history-pager")); + }; + let debouncer = debounce_history_pager(); + debouncer.perform_with_completion(performer, completion); + } +} + +/// Expand an abbreviation replacer, which may mean running its function. +/// \return the replacement, or none to skip it. This may run fish script! +fn expand_replacer( + range: SourceRange, + token: &wstr, + repl: &abbrs::Replacer, + parser: &Parser, +) -> Option { + if !repl.is_function { + // Literal replacement cannot fail. + FLOGF!( + abbrs, + "Expanded literal abbreviation <%ls> -> <%ls>", + 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 = scoped_push_replacer( + |new_value| std::mem::replace(&mut parser.libdata_mut().pods.is_interactive, new_value), + false, + ); + + let mut outputs = vec![]; + let ret = exec_subshell( + &cmd, + parser, + Some(&mut outputs), + /*apply_exit_status=*/ false, + ); + if ret != STATUS_CMD_OK.unwrap() { + return None; + } + let result = join_strings(&outputs, '\n'); + FLOGF!( + abbrs, + "Expanded function abbreviation <%ls> -> <%ls>", + token, + result + ); + + Some(abbrs::Replacement::new( + range, + result, + repl.set_cursor_marker.clone(), + )) +} + +// Extract all the token ranges in \p str, along with whether they are an undecorated command. +// Tokens containing command substitutions are skipped; this ensures tokens are non-overlapping. +struct PositionedToken { + range: SourceRange, + is_cmd: bool, +} + +fn extract_tokens(s: &wstr) -> Vec { + let ast_flags = ParseTreeFlags::CONTINUE_AFTER_ERROR + | ParseTreeFlags::ACCEPT_INCOMPLETE_TOKENS + | ParseTreeFlags::LEAVE_UNTERMINATED; + let ast = Ast::parse(s, ast_flags, None); + + // Helper to check if a node is the command portion of an undecorated statement. + let is_command = |node: &dyn ast::Node| { + let mut cursor = Some(node); + while let Some(cur) = cursor { + if let Some(stmt) = cur.as_decorated_statement() { + if stmt.opt_decoration.is_none() && node.pointer_eq(&stmt.command) { + return true; + } + } + cursor = cur.parent(); + } + false + }; + + let mut result = vec![]; + for node in Traversal::new(ast.top()) { + // We are only interested in leaf nodes with source. + if node.category() != Category::leaf { + continue; + } + let range = node.source_range(); + if range.length() == 0 { + continue; + } + + // If we have command subs, then we don't include this token; instead we recurse. + let mut has_cmd_subs = false; + let mut cmdsub_contents = L!(""); + let mut cmdsub_cursor = range.start(); + let mut cmdsub_start = 0; + let mut cmdsub_end = 0; + while parse_util_locate_cmdsubst_range( + s, + &mut cmdsub_cursor, + Some(&mut cmdsub_contents), + &mut cmdsub_start, + &mut cmdsub_end, + /*accept_incomplete=*/ true, + None, + None, + ) > 0 + { + if cmdsub_start >= range.end() { + break; + } + has_cmd_subs = true; + for mut t in extract_tokens(cmdsub_contents) { + // cmdsub_start is the open paren; the contents start one after it. + t.range.start += u32::try_from(cmdsub_start + 1).unwrap(); + result.push(t); + } + } + + if !has_cmd_subs { + // Common case of no command substitutions in this leaf node. + result.push(PositionedToken { + range, + is_cmd: is_command(node), + }) + } + } + + result +} + +/// Expand at most one abbreviation at the given cursor position, updating the position if the +/// abbreviation wants to move the cursor. Use the parser to run any abbreviations which want +/// function calls. \return none if no abbreviations were expanded, otherwise the resulting +/// replacement. +pub fn reader_expand_abbreviation_at_cursor( + cmdline: &wstr, + cursor_pos: usize, + parser: &Parser, +) -> Option { + // Find the token containing the cursor. Usually users edit from the end, so walk backwards. + let tokens = extract_tokens(cmdline); + let token = tokens + .into_iter() + .rev() + .find(|token| token.range.contains_inclusive(cursor_pos))?; + let range = token.range; + let position = if token.is_cmd { + abbrs::Position::Command + } else { + abbrs::Position::Anywhere + }; + + let token_str = &cmdline[Range::::from(range)]; + let replacers = abbrs_match(token_str, position); + for replacer in replacers { + if let Some(replacement) = expand_replacer(range, token_str, &replacer, parser) { + return Some(replacement); + } + } + None +} + +impl ReaderData { + /// Expand abbreviations at the current cursor position, minus the given cursor backtrack. This + /// may change the command line but does NOT repaint it. This is to allow the caller to coalesce + /// repaints. + 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 { + // Try expanding abbreviations. + self.update_commandline_state(); + 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 + } +} + +/// Indicates if the given command char ends paging. +fn command_ends_paging(c: ReadlineCmd, focused_on_search_field: bool) -> bool { + type rl = ReadlineCmd; + match c { + rl::HistoryPrefixSearchBackward + | rl::HistoryPrefixSearchForward + | rl::HistorySearchBackward + | rl::HistorySearchForward + | rl::HistoryTokenSearchBackward + | rl::HistoryTokenSearchForward + | rl::AcceptAutosuggestion + | rl::DeleteOrExit + | rl::CancelCommandline + | rl::Cancel => + // These commands always end paging. + { + true + } + rl::Complete + | rl::CompleteAndSearch + | rl::HistoryPager + | rl::BackwardChar + | rl::ForwardChar + | rl::ForwardSingleChar + | rl::UpLine + | rl::DownLine + | rl::Repaint + | rl::SuppressAutosuggestion + | rl::BeginningOfHistory + | rl::EndOfHistory => + // These commands never end paging. + { + false + } + rl::Execute => + // execute does end paging, but only executes if it was not paging. So it's handled + // specially. + { + false + } + rl::BeginningOfLine + | rl::EndOfLine + | rl::ForwardWord + | rl::BackwardWord + | rl::ForwardBigword + | rl::BackwardBigword + | rl::NextdOrForwardWord + | rl::PrevdOrBackwardWord + | rl::DeleteChar + | rl::BackwardDeleteChar + | rl::KillLine + | rl::Yank + | rl::YankPop + | rl::BackwardKillLine + | rl::KillWholeLine + | rl::KillInnerLine + | rl::KillWord + | rl::KillBigword + | rl::BackwardKillWord + | rl::BackwardKillPathComponent + | rl::BackwardKillBigword + | rl::SelfInsert + | rl::SelfInsertNotFirst + | rl::TransposeChars + | rl::TransposeWords + | rl::UpcaseWord + | rl::DowncaseWord + | rl::CapitalizeWord + | rl::BeginningOfBuffer + | rl::EndOfBuffer + | rl::Undo + | rl::Redo => + // These commands operate on the search field if that's where the focus is. + { + !focused_on_search_field + } + _ => false, + } +} + +/// Indicates if the given command ends the history search. +fn command_ends_history_search(c: ReadlineCmd) -> bool { + type rl = ReadlineCmd; + !matches!( + c, + rl::HistoryPrefixSearchBackward + | rl::HistoryPrefixSearchForward + | rl::HistorySearchBackward + | rl::HistorySearchForward + | rl::HistoryTokenSearchBackward + | rl::HistoryTokenSearchForward + | rl::BeginningOfHistory + | rl::EndOfHistory + | rl::Repaint + | rl::ForceRepaint + ) +} + +/// Return true if we believe ourselves to be orphaned. loop_count is how many times we've tried to +/// stop ourselves via SIGGTIN. +fn check_for_orphaned_process(loop_count: usize, shell_pgid: libc::pid_t) -> bool { + let mut we_think_we_are_orphaned = false; + // Try kill-0'ing the process whose pid corresponds to our process group ID. It's possible this + // will fail because we don't have permission to signal it. But more likely it will fail because + // it no longer exists, and we are orphaned. + if loop_count % 64 == 0 && unsafe { libc::kill(shell_pgid, 0) } < 0 && errno().0 == ESRCH { + we_think_we_are_orphaned = true; + } + + // Try reading from the tty; if we get EIO we are orphaned. This is sort of bad because it + // may block. + if !we_think_we_are_orphaned && loop_count % 128 == 0 { + extern "C" { + 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); + } + + // Open the tty. Presumably this is stdin, but maybe not? + let tty_fd = AutoCloseFd::new(unsafe { libc::open(tty, O_RDONLY | O_NONBLOCK) }); + if !tty_fd.is_valid() { + perror("open"); + exit_without_destructors(1); + } + + let mut tmp = 0 as libc::c_char; + if unsafe { + libc::read( + tty_fd.fd(), + &mut tmp as *mut libc::c_char as *mut libc::c_void, + 1, + ) + } < 0 + && errno().0 == EIO + { + we_think_we_are_orphaned = true; + } + } + + // Just give up if we've done it a lot times. + if loop_count > 4096 { + we_think_we_are_orphaned = true; + } + + we_think_we_are_orphaned +} + +/// Run the specified command with the correct terminal modes, and while taking care to perform job +/// notification, set the title, etc. +fn reader_run_command(parser: &Parser, cmd: &wstr) -> EvalRes { + let ft = tok_command(cmd); + + // Provide values for `status current-command` and `status current-commandline` + if !ft.is_empty() { + parser.libdata_mut().status_vars.command = ft.to_owned(); + parser.libdata_mut().status_vars.commandline = cmd.to_owned(); + // Also provide a value for the deprecated fish 2.0 $_ variable + parser + .vars() + .set_one(L!("_"), EnvMode::GLOBAL, ft.to_owned()); + } + + let outp = Outputter::stdoutput().get_mut(); + reader_write_title(cmd, parser, true); + outp.set_color(RgbColor::NORMAL, RgbColor::NORMAL); + term_donate(false); + + let time_before = Instant::now(); + let eval_res = parser.eval(cmd, &IoChain::new()); + job_reap(parser, true); + + // Update the execution duration iff a command is requested for execution + // issue - #4926 + if !ft.is_empty() { + let time_after = Instant::now(); + let duration = time_after.duration_since(time_before); + parser.vars().set_one( + ENV_CMD_DURATION, + EnvMode::UNEXPORT, + duration.as_millis().to_wstring(), + ); + } + + term_steal(); + + // Provide value for `status current-command` + parser.libdata_mut().status_vars.command = (*PROGRAM_NAME.get().unwrap()).to_owned(); + // Also provide a value for the deprecated fish 2.0 $_ variable + parser.vars().set_one( + L!("_"), + EnvMode::GLOBAL, + (*PROGRAM_NAME.get().unwrap()).to_owned(), + ); + // Provide value for `status current-commandline` + 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<(), ParserTestErrorBits> { + let mut errors = vec![]; + let res = parse_util_detect_errors(bstr, Some(&mut errors), /*accept_incomplete=*/ true); + + if res.is_err_and(|err| err.contains(ParserTestErrorBits::ERROR)) { + let mut error_desc = parser.get_backtrace(bstr, &errors); + + // Ensure we end with a newline. Also add an initial newline, because it's likely the user + // just hit enter and so there's junk on the current line. + if !error_desc.ends_with('\n') { + error_desc.push('\n'); + } + eprintf!("\n%s", error_desc); + reader_schedule_prompt_repaint(); + } + res +} + +impl ReaderData { + // Import history from older location (config path) if our current history is empty. + fn import_history_if_necessary(&mut self) { + if self.history.is_empty() { + self.history.populate_from_config_path(); + } + + // Import history from bash, etc. if our current history is still empty and is the default + // history. + if self.history.is_empty() && self.history.is_default() { + // Try opening a bash file. We make an effort to respect $HISTFILE; this isn't very complete + // (AFAIK it doesn't have to be exported), and to really get this right we ought to ask bash + // itself. But this is better than nothing. + 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 file = AutoCloseFd::new(wopen_cloexec(&path, O_RDONLY, 0)); + if !file.is_valid() { + return; + } + self.history.populate_from_bash(BufReader::new(file)); + } + } + + // Add the current command line contents to history. + fn add_to_history(&mut self) { + if self.conf.in_silent_mode { + return; + } + + // Historical behavior is to trim trailing spaces, unless escape (#7661). + let mut text = self.command_line.text().to_owned(); + while text.chars().last() == Some(' ') + && count_preceding_backslashes(&text, text.len() - 1) % 2 == 0 + { + text.pop(); + } + + // Remove ephemeral items - even if the text is empty. + self.history.remove_ephemeral_items(); + + if !text.is_empty() { + // Mark this item as ephemeral if there is a leading space (#615). + let mode = if text.as_char_slice()[0] == ' ' { + // Leading spaces are ephemeral (#615). + PersistenceMode::Ephemeral + } else if in_private_mode(self.vars()) { + PersistenceMode::Memory + } else { + PersistenceMode::Disk + }; + self.history.clone().add_pending_with_file_detection( + &text, + self.parser().variables.clone(), + mode, + ); + } + } +} + +/// Check if we have background jobs that we have not warned about. +/// If so, print a warning and return true. Otherwise return false. +impl ReaderData { + fn try_warn_on_background_jobs(&mut self) -> bool { + assert_is_main_thread(); + // Have we already warned? + if self.did_warn_for_bg_jobs { + return false; + } + // Are we the top-level reader? + if reader_data_stack().len() > 1 { + return false; + } + // Do we have background jobs? + let bg_jobs = jobs_requiring_warning_on_exit(self.parser()); + if bg_jobs.is_empty() { + return false; + } + // Print the warning! + print_exit_warning_for_jobs(&bg_jobs); + self.did_warn_for_bg_jobs = true; + true + } +} + +/// Check if we should exit the reader loop. +/// \return true if we should exit. +pub fn check_exit_loop_maybe_warning(data: Option<&mut ReaderData>) -> bool { + // sighup always forces exit. + if reader_received_sighup() { + return true; + } + + // Check if an exit is requested. + 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 +} /// Given that the user is tab-completing a token \p wc whose cursor is at \p pos in the token, /// try expanding it as a wildcard, populating \p result with the expanded string. @@ -186,6 +4798,10 @@ fn try_expand_wildcard( result.clear(); // Have a low limit on the number of matches, otherwise we will overwhelm the command line. + /// When tab-completing with a wildcard, we expand the wildcard up to this many results. + /// If expansion would exceed this many results, beep and do nothing. + const TAB_COMPLETE_WILDCARD_MAX_EXPANSION: usize = 256; + let ctx = OperationContext::background_with_cancel_checker( &*parser.variables, Box::new(|| signal_check_cancel() != 0), @@ -226,6 +4842,51 @@ fn try_expand_wildcard( ExpandResultCode::ok } +/// Test if the specified character in the specified string is backslashed. pos may be at the end of +/// the string, which indicates if there is a trailing backslash. +fn is_backslashed(s: &wstr, pos: usize) -> bool { + // note pos == str.size() is OK. + 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 { + 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 +} + +/// Apply a completion string. Exposed for testing only. +/// +/// Insert the string in the given command line at the given cursor position. The function checks if +/// the string is quoted or not and correctly escapes the string. +/// +/// \param val the string to insert +/// \param flags A union of all flags describing the completion to insert. See the completion_t +/// struct for more information on possible values. +/// \param command_line The command line into which we will insert +/// \param inout_cursor_pos On input, the location of the cursor within the command line. On output, +/// the new desired position. +/// \param append_only Whether we can only append to the command line, or also modify previous +/// characters. This is used to determine whether we go inside a trailing quote. +/// +/// \return The completed string pub fn completion_apply_to_command_line( val_str: &wstr, flags: CompleteFlags, @@ -233,112 +4894,498 @@ pub fn completion_apply_to_command_line( inout_cursor_pos: &mut usize, append_only: bool, ) -> WString { - ffi::completion_apply_to_command_line( - &val_str.to_ffi(), - flags.bits(), - &command_line.to_ffi(), - inout_cursor_pos, - append_only, - ) - .from_ffi() + let add_space = !flags.contains(CompleteFlags::NO_SPACE); + let do_replace = flags.contains(CompleteFlags::REPLACES_TOKEN); + let do_replace_commandline = flags.contains(CompleteFlags::REPLACES_COMMANDLINE); + let do_escape = !flags.contains(CompleteFlags::DONT_ESCAPE); + let no_tilde = flags.contains(CompleteFlags::DONT_ESCAPE_TILDES); + + let cursor_pos = *inout_cursor_pos; + let mut back_into_trailing_quote = false; + let have_space_after_token = command_line.char_at(cursor_pos) == ' '; + + if do_replace_commandline { + assert!(!do_escape, "unsupported completion flag"); + *inout_cursor_pos = val_str.len(); + return val_str.to_owned(); + } + + if do_replace { + let mut move_cursor; + let mut range = 0..0; + parse_util_token_extent(command_line, cursor_pos, &mut range, None); + + let mut sb = command_line[..range.start].to_owned(); + + if do_escape { + let escaped = escape_string( + val_str, + EscapeStringStyle::Script( + EscapeFlags::NO_QUOTED + | if no_tilde { + EscapeFlags::NO_TILDE + } else { + EscapeFlags::empty() + }, + ), + ); + sb.push_utfstr(&escaped); + move_cursor = escaped.len(); + } else { + sb.push_utfstr(val_str); + move_cursor = val_str.len(); + } + + if add_space { + if !have_space_after_token { + sb.push(' '); + } + 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 { + // We need to figure out whether the token we complete has unclosed quotes. Since the token + // may be inside a command substitutions we must first determine the extents of the + // innermost command substitution. + let cmdsub_range = parse_util_cmdsubst_extent(command_line, cursor_pos); + // Find the last quote in the token to complete. By parsing only the string inside any + // command substitution, we prevent the tokenizer from treating the entire command + // substitution as one token. + quote = parse_util_get_quote_type( + &command_line[cmdsub_range.clone()], + cursor_pos - cmdsub_range.start, + ); + + // If the token is reported as unquoted, but ends with a (unescaped) quote, and we can + // modify the command line, then delete the trailing quote so that we can insert within + // the quotes instead of after them. See issue #552. + if quote.is_none() && !append_only && cursor_pos > 0 { + // The entire token is reported as unquoted...see if the last character is an + // unescaped quote. + let trailing_quote = unescaped_quote(command_line, cursor_pos - 1); + if trailing_quote.is_some() { + quote = trailing_quote; + back_into_trailing_quote = true; + } + } + + parse_util_escape_string_with_quote(val_str, quote, no_tilde) + } else { + val_str.to_owned() + }; + + let mut insertion_point = cursor_pos; + if back_into_trailing_quote { + // Move the character back one so we enter the terminal quote. + insertion_point = insertion_point.checked_sub(1).unwrap(); + } + + // Perform the insertion and compute the new location. + 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 add_space { + if quote.is_some() && unescaped_quote(command_line, insertion_point) != quote { + // This is a quoted parameter, first print a quote. + result.insert(new_cursor_pos, quote.unwrap()); + new_cursor_pos += 1; + } + if !have_space_after_token { + result.insert(new_cursor_pos, ' '); + } + new_cursor_pos += 1; + } + *inout_cursor_pos = new_cursor_pos; + result +} + +/// Check if the specified string can be replaced by a case insensitive completion with the +/// specified flags. +/// +/// Advanced tokens like those containing {}-style expansion can not at the moment be replaced, +/// other than if the new token is already an exact replacement, e.g. if the COMPLETE_DONT_ESCAPE +/// flag is set. +fn reader_can_replace(s: &wstr, flags: CompleteFlags) -> bool { + if flags.contains(CompleteFlags::DONT_ESCAPE) { + return true; + } + + // Test characters that have a special meaning in any character position. + !s.chars() + .any(|c| matches!(c, '$' | '*' | '?' | '(' | '{' | '}' | ')')) +} + +/// Determine the best (lowest) match rank for a set of completions. +fn get_best_rank(comp: &[Completion]) -> u32 { + let mut best_rank = u32::MAX; + for c in comp { + best_rank = best_rank.min(c.rank()); + } + best_rank +} + +impl ReaderData { + /// Compute completions and update the pager and/or commandline as needed. + fn compute_and_apply_completions(&mut self, c: ReadlineCmd, rls: &mut ReadlineLoopState) { + assert!(matches!( + c, + ReadlineCmd::Complete | ReadlineCmd::CompleteAndSearch + )); + + // Remove a trailing backslash. This may trigger an extra repaint, but this is + // rare. + let el = &self.command_line; + if is_backslashed(el.text(), el.position()) { + self.delete_char(true); + } + + // Figure out the extent of the command substitution surrounding the cursor. + // This is because we only look at the current command substitution to form + // completions - stuff happening outside of it is not interesting. + let el = &self.command_line; + let cmdsub_range = parse_util_cmdsubst_extent(el.text(), el.position()); + let position_in_cmdsub = el.position() - cmdsub_range.start; + + // Figure out the extent of the token within the command substitution. Note we + // pass cmdsub_begin here, not buff. + let mut token_range = 0..0; + parse_util_token_extent( + &el.text()[cmdsub_range.clone()], + position_in_cmdsub, + &mut token_range, + None, + ); + let position_in_token = position_in_cmdsub - token_range.start; + + // Hack: the token may extend past the end of the command substitution, e.g. in + // (echo foo) the last token is 'foo)'. Don't let that happen. + 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; + + // Check if we have a wildcard within this string; if so we first attempt to expand the + // wildcard; if that succeeds we don't then apply user completions (#8593). + 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 => { + // This may come about if we exceeded the max number of matches. + // Return "success" to suppress normal completions. + self.flash(); + return; + } + ExpandResultCode::wildcard_no_match => {} + ExpandResultCode::cancel => { + // e.g. the user hit control-C. Suppress normal completions. + return; + } + ExpandResultCode::ok => { + rls.comp.clear(); + rls.complete_did_insert = false; + self.push_edit( + EditableLineTag::Commandline, + Edit::new(token_range, wc_expanded), + ); + return; + } + _ => unreachable!(), + } + + // Construct a copy of the string from the beginning of the command substitution + // up to the end of the token we're completing. + let cmdsub = &el.text()[cmdsub_range.start..token_range.end]; + + // Ensure that `commandline` inside the completions gets the current state. + self.update_commandline_state(); + + let (comp, _needs_load) = complete( + cmdsub, + CompletionRequestOptions::normal(), + &self.parser().context(), + ); + rls.comp = comp; + + // User-supplied completions may have changed the commandline - prevent buffer + // overflow. + token_range.start = std::cmp::min(token_range.start, el.text().len()); + token_range.end = std::cmp::min(token_range.end, el.text().len()); + + // Munge our completions. + sort_and_prioritize(&mut rls.comp, CompletionRequestOptions::default()); + + // Record our cycle_command_line. + self.cycle_command_line = el.text().to_owned(); + self.cycle_cursor_pos = token_range.end; + + rls.complete_did_insert = self.handle_completions(&rls.comp, token_range); + + // Show the search field if requested and if we printed a list of completions. + if c == ReadlineCmd::CompleteAndSearch && !rls.complete_did_insert && !self.pager.is_empty() + { + self.pager.set_search_field_shown(true); + self.select_completion_in_direction(SelectionMotion::Next, false); + } + } + + /// Handle the list of completions. This means the following: + /// + /// - If the list is empty, flash the terminal. + /// - If the list contains one element, write the whole element, and if the element does not end on + /// a '/', '@', ':', '.', ',', '-' or a '=', also write a trailing space. + /// - If the list contains multiple elements, insert their common prefix, if any and display + /// the list in the pager. Depending on terminal size and the length of the list, the pager + /// may either show less than a screenfull and exit or use an interactive pager to allow the + /// user to scroll through the completions. + /// + /// \param comp the list of completion strings + /// \param token_begin the position of the token to complete + /// \param token_end the position after the token to complete + /// + /// Return true if we inserted text into the command line, false if we did not. + fn handle_completions(&mut self, comp: &[Completion], token_range: Range) -> bool { + let mut done = false; + let mut success = false; + + let tok = self.command_line.text()[token_range.clone()].to_owned(); + + // Check trivial cases. + let len = comp.len(); + if len == 0 { + // No suitable completions found, flash screen and return. + self.flash(); + done = true; + } else if len == 1 { + // Exactly one suitable completion found - insert it. + let c = &comp[0]; + + // If this is a replacement completion, check that we know how to replace it, e.g. that + // the token doesn't contain evil operators like {}. + if !c.flags.contains(CompleteFlags::REPLACES_TOKEN) || reader_can_replace(&tok, c.flags) + { + self.completion_insert(&c.completion, token_range.end, c.flags); + } + done = true; + success = true; + } + + if done { + return success; + } + + let best_rank = get_best_rank(comp); + + // Determine whether we are going to replace the token or not. If any commands of the best + // rank do not require replacement, then ignore all those that want to use replacement. + let mut will_replace_token = true; + for c in comp { + if c.rank() <= best_rank && !c.flags.contains(CompleteFlags::REPLACES_TOKEN) { + will_replace_token = false; + break; + } + } + + // Decide which completions survived. There may be a lot of them; it would be nice if we could + // figure out how to avoid copying them here. + let mut surviving_completions = vec![]; + let mut all_matches_exact_or_prefix = true; + for c in comp { + // Ignore completions with a less suitable match rank than the best. + if c.rank() > best_rank { + continue; + } + + // Only use completions that match replace_token. + let completion_replaces_token = c.flags.contains(CompleteFlags::REPLACES_TOKEN); + if completion_replaces_token != will_replace_token { + continue; + } + + // Don't use completions that want to replace, if we cannot replace them. + if completion_replaces_token && !reader_can_replace(&tok, c.flags) { + continue; + } + + // This completion survived. + surviving_completions.push(c.clone()); + all_matches_exact_or_prefix = + all_matches_exact_or_prefix && c.r#match.is_exact_or_prefix(); + } + + if surviving_completions.len() == 1 { + // After sorting and stuff only one completion is left, use it. + // + // TODO: This happens when smartcase kicks in, e.g. + // the token is "cma" and the options are "cmake/" and "CMakeLists.txt" + // it would be nice if we could figure + // out how to use it more. + let c = &surviving_completions[0]; + + // If this is a replacement completion, check that we know how to replace it, e.g. that + // the token doesn't contain evil operators like {}. + if !c.flags.contains(CompleteFlags::REPLACES_TOKEN) || reader_can_replace(&tok, c.flags) + { + self.completion_insert(&c.completion, token_range.end, c.flags); + } + return true; + } + + let mut use_prefix = false; + let mut common_prefix = L!("").to_owned(); + if all_matches_exact_or_prefix { + // Try to find a common prefix to insert among the surviving completions. + let mut flags = CompleteFlags::empty(); + let mut prefix_is_partial_completion = false; + let mut first = true; + for c in &surviving_completions { + if first { + // First entry, use the whole string. + common_prefix = c.completion.clone(); + flags = c.flags; + first = false; + } else { + // Determine the shared prefix length. + 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; + } + + // idx is now the length of the new common prefix. + common_prefix.truncate(idx); + prefix_is_partial_completion = true; + + // Early out if we decide there's no common prefix. + if idx == 0 { + break; + } + } + } + + // Determine if we use the prefix. We use it if it's non-empty and it will actually make + // the command line longer. It may make the command line longer by virtue of not using + // REPLACE_TOKEN (so it always appends to the command line), or by virtue of replacing + // the token but being longer than it. + use_prefix = common_prefix.len() > if will_replace_token { tok.len() } else { 0 }; + assert!(!use_prefix || !common_prefix.is_empty()); + + if use_prefix { + // We got something. If more than one completion contributed, then it means we have + // a prefix; don't insert a space after it. + if prefix_is_partial_completion { + flags |= CompleteFlags::NO_SPACE; + } + self.completion_insert(&common_prefix, token_range.end, flags); + self.cycle_command_line = self.command_line.text().to_owned(); + self.cycle_cursor_pos = self.command_line.position(); + } + } + + if use_prefix { + for c in &mut surviving_completions { + c.flags &= !CompleteFlags::REPLACES_TOKEN; + c.completion.replace_range(0..common_prefix.len(), L!("")); + } + } + + // Print the completion list. + let mut prefix = WString::new(); + if will_replace_token || !all_matches_exact_or_prefix { + if use_prefix { + prefix.push_utfstr(&common_prefix); + } + } else if tok.len() + common_prefix.len() <= PREFIX_MAX_LEN { + prefix.push_utfstr(&tok); + prefix.push_utfstr(&common_prefix); + } else { + // Append just the end of the string. + prefix.push(get_ellipsis_char()); + let full = tok + &common_prefix[..]; + prefix.push_utfstr(&full[full.len() - PREFIX_MAX_LEN..]); + } + + // Update the pager data. + self.pager.set_prefix(&prefix, true); + self.pager.set_completions(&surviving_completions, true); + // Modify the command line to reflect the new pager. + self.pager_selection_changed(); + false + } + + /// Insert the string at the current cursor position. The function checks if the string is quoted or + /// not and correctly escapes the string. + /// + /// \param val the string to insert + /// \param token_end the position after the token to complete + /// \param flags A union of all flags describing the completion to insert. See the completion_t + /// struct for more information on possible values. + fn completion_insert(&mut self, val: &wstr, token_end: usize, flags: CompleteFlags) { + let (elt, el) = self.active_edit_line(); + + // Move the cursor to the end of the token. + 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( + val, + flags, + el.text(), + &mut cursor, + /*append_only=*/ false, + ); + self.set_buffer_maintaining_pager(&new_command_line, cursor, false); + } +} + +/// \return true if an event is a normal character that should be inserted into the buffer. +fn event_is_normal_char(evt: &CharEvent) -> bool { + if !evt.is_char() { + return false; + } + let c = evt.get_char(); + !fish_reserved_codepoint(c) && c > char::from(31) && c != char::from(127) } #[cxx::bridge] mod reader_ffi { - extern "C++" { - include!("operation_context.h"); - include!("env.h"); - include!("parser.h"); - #[cxx_name = "EnvDyn"] - type EnvDynFFI = crate::env::EnvDynFFI; - type Parser = crate::parser::Parser; - } extern "Rust" { - fn reader_reset_interrupted(); - fn reader_handle_sigint(); + #[cxx_name = "check_exit_loop_maybe_warning"] + fn check_exit_loop_maybe_warning_ffi() -> bool; fn reader_test_and_clear_interrupted() -> i32; - fn reader_sighup(); - fn reader_received_sighup() -> bool; - } - extern "Rust" { - #[cxx_name = "try_expand_wildcard"] - fn try_expand_wildcard_ffi( - parser: &Parser, - wc: &CxxWString, - position: usize, - result: &mut UniquePtr, - ) -> u8; - } - extern "Rust" { - type ReaderConfig; - #[cxx_name = "left_prompt_cmd"] - fn left_prompt_cmd_ffi(&self) -> UniquePtr; - #[cxx_name = "right_prompt_cmd"] - fn right_prompt_cmd_ffi(&self) -> UniquePtr; - #[cxx_name = "event"] - fn event_ffi(&self) -> UniquePtr; - #[cxx_name = "complete_ok"] - fn complete_ok_ffi(&self) -> bool; - #[cxx_name = "highlight_ok"] - fn highlight_ok_ffi(&self) -> bool; - #[cxx_name = "syntax_check_ok"] - fn syntax_check_ok_ffi(&self) -> bool; - #[cxx_name = "autosuggest_ok"] - fn autosuggest_ok_ffi(&self) -> bool; - #[cxx_name = "expand_abbrev_ok"] - fn expand_abbrev_ok_ffi(&self) -> bool; - #[cxx_name = "exit_on_interrupt"] - fn exit_on_interrupt_ffi(&self) -> bool; - #[cxx_name = "in_silent_mode"] - fn in_silent_mode_ffi(&self) -> bool; - #[cxx_name = "inputfd"] - fn inputfd_ffi(&self) -> i32; + fn reader_init(); + fn restore_term_mode(); + fn reader_schedule_prompt_repaint(); + fn reader_reading_interrupted() -> i32; + fn reader_reset_interrupted(); + #[cxx_name = "reader_current_data"] + fn reader_current_data_ffi() -> *mut u8; } } -impl ReaderConfig { - fn left_prompt_cmd_ffi(&self) -> UniquePtr { - self.left_prompt_cmd.to_ffi() - } - fn right_prompt_cmd_ffi(&self) -> UniquePtr { - self.right_prompt_cmd.to_ffi() - } - fn event_ffi(&self) -> UniquePtr { - self.event.to_ffi() - } - fn complete_ok_ffi(&self) -> bool { - self.complete_ok - } - fn highlight_ok_ffi(&self) -> bool { - self.highlight_ok - } - fn syntax_check_ok_ffi(&self) -> bool { - self.syntax_check_ok - } - fn autosuggest_ok_ffi(&self) -> bool { - self.autosuggest_ok - } - fn expand_abbrev_ok_ffi(&self) -> bool { - self.expand_abbrev_ok - } - fn exit_on_interrupt_ffi(&self) -> bool { - self.exit_on_interrupt - } - fn in_silent_mode_ffi(&self) -> bool { - self.in_silent_mode - } - fn inputfd_ffi(&self) -> i32 { - self.inputfd as _ - } +fn check_exit_loop_maybe_warning_ffi() -> bool { + check_exit_loop_maybe_warning(None) } -fn try_expand_wildcard_ffi( - parser: &Parser, - wc: &CxxWString, - position: usize, - result: &mut UniquePtr, -) -> u8 { - let mut rust_result = WString::new(); - let result_code = try_expand_wildcard(parser, wc.from_ffi(), position, &mut rust_result); - *result = rust_result.to_ffi(); - unsafe { std::mem::transmute(result_code) } + +fn reader_current_data_ffi() -> *mut u8 { + let data = current_data().unwrap(); + data as *mut ReaderData as *mut u8 } diff --git a/fish-rust/src/reader_history_search.rs b/fish-rust/src/reader_history_search.rs new file mode 100644 index 000000000..ada09960b --- /dev/null +++ b/fish-rust/src/reader_history_search.rs @@ -0,0 +1,272 @@ +//! Encapsulation of the reader's history search functionality. + +use crate::history::{self, History, HistorySearch, SearchDirection, SearchFlags, SearchType}; +use crate::parse_constants::SourceRange; +use crate::tokenizer::{TokenType, Tokenizer, TOK_ACCEPT_UNFINISHED}; +use crate::wchar::prelude::*; +use crate::wcstringutil::ifind; +use std::collections::HashSet; +use std::sync::Arc; + +// Make the search case-insensitive unless we have an uppercase character. +pub fn smartcase_flags(query: &wstr) -> history::SearchFlags { + if query == query.to_lowercase() { + history::SearchFlags::IGNORE_CASE + } else { + history::SearchFlags::default() + } +} + +struct SearchMatch { + /// The text of the match. + pub text: WString, + /// The offset of the current search string in this match. + offset: usize, +} + +impl SearchMatch { + fn new(text: WString, offset: usize) -> Self { + Self { text, offset } + } +} + +#[derive(Clone, Copy, Eq, Default, PartialEq)] +pub enum SearchMode { + #[default] + /// no search + Inactive, + /// searching by line + Line, + /// searching by prefix + Prefix, + /// searching by token + Token, +} + +/// Encapsulation of the reader's history search functionality. +#[derive(Default)] +pub struct ReaderHistorySearch { + /// The type of search performed. + mode: SearchMode, + + /// Our history search itself. + search: Option, + + /// The ordered list of matches. This may grow long. + matches: Vec, + + /// A set of new items to skip, corresponding to matches_ and anything added in skip(). + skips: HashSet, + + /// Index into our matches list. + match_index: usize, + + /// The offset of the current token in the command line. Only non-zero for a token search. + token_offset: usize, +} + +impl ReaderHistorySearch { + pub fn active(&self) -> bool { + self.mode != SearchMode::Inactive + } + pub fn by_token(&self) -> bool { + self.mode == SearchMode::Token + } + pub fn by_line(&self) -> bool { + self.mode == SearchMode::Line + } + pub fn by_prefix(&self) -> bool { + self.mode == SearchMode::Prefix + } + + /// Move the history search in the given direction \p dir. + pub fn move_in_direction(&mut self, dir: SearchDirection) -> bool { + if dir == SearchDirection::Forward { + self.move_forwards() + } else { + self.move_backwards() + } + } + + /// Go to the beginning (earliest) of the search. + pub fn go_to_beginning(&mut self) { + if self.matches.is_empty() { + return; + } + self.match_index = self.matches.len() - 1; + } + + /// Go to the end (most recent) of the search. + pub fn go_to_end(&mut self) { + self.match_index = 0; + } + + /// \return the current search result. + pub fn current_result(&self) -> &wstr { + &self.matches[self.match_index].text + } + + /// \return the string we are searching for. + pub fn search_string(&self) -> &wstr { + self.search().original_term() + } + + /// \return the range of the original search string in the new command line. + pub fn search_range_if_active(&self) -> Option { + if !self.active() || self.is_at_end() { + return None; + } + Some(SourceRange::new( + self.token_offset + self.matches[self.match_index].offset, + self.search_string().len(), + )) + } + + /// \return whether we are at the end (most recent) of our search. + pub fn is_at_end(&self) -> bool { + self.match_index == 0 + } + + // Add an item to skip. + // \return true if it was added, false if already present. + pub fn add_skip(&mut self, s: WString) -> bool { + self.skips.insert(s) + } + + /// Reset, beginning a new line or token mode search. + pub fn reset_to_mode( + &mut self, + text: WString, + hist: Arc, + mode: SearchMode, + token_offset: usize, + ) { + assert!( + mode != SearchMode::Inactive, + "mode cannot be inactive in this setter" + ); + self.skips = HashSet::from([text.clone()]); + self.matches = vec![SearchMatch::new(text.clone(), 0)]; + self.match_index = 0; + self.mode = mode; + self.token_offset = token_offset; + let flags = SearchFlags::NO_DEDUP | smartcase_flags(&text); + // We can skip dedup in history_search_t because we do it ourselves in skips_. + self.search = Some(HistorySearch::new_with( + hist, + text, + if self.by_prefix() { + SearchType::Prefix + } else { + SearchType::Contains + }, + flags, + 0, + )); + } + + /// Reset to inactive search. + pub fn reset(&mut self) { + self.matches.clear(); + self.skips.clear(); + self.match_index = 0; + self.mode = SearchMode::Inactive; + self.token_offset = 0; + self.search = None; + } + + /// Adds the given match if we haven't seen it before. + fn add_if_new(&mut self, search_match: SearchMatch) { + if self.add_skip(search_match.text.clone()) { + self.matches.push(search_match); + } + } + + /// Attempt to append matches from the current history item. + /// \return true if something was appended. + fn append_matches_from_search(&mut self) -> bool { + fn find(zelf: &ReaderHistorySearch, haystack: &wstr, needle: &wstr) -> Option { + if zelf.search().ignores_case() { + return ifind(haystack, needle, false); + } + haystack.find(needle) + } + let before = self.matches.len(); + let text = self.search().current_string(); + let needle = self.search_string(); + if matches!(self.mode, SearchMode::Line | SearchMode::Prefix) { + // FIXME: Previous versions asserted out if this wasn't true. + // This could be hit with a needle of "ö" and haystack of "echo Ö" + // I'm not sure why - this points to a bug in ifind (probably wrong locale?) + // However, because the user experience of having it crash is horrible, + // and the worst thing that can otherwise happen here is that a search is unsuccessful, + // we just check it instead. + if let Some(offset) = find(self, text, needle) { + self.add_if_new(SearchMatch::new(text.to_owned(), offset)); + } + } else if self.mode == SearchMode::Token { + let mut tok = Tokenizer::new(text, TOK_ACCEPT_UNFINISHED); + + let mut local_tokens = vec![]; + while let Some(token) = tok.next() { + if token.type_ != TokenType::string { + continue; + } + let text = tok.text_of(&token); + if let Some(offset) = find(self, text, needle) { + local_tokens.push(SearchMatch::new(text.to_owned(), offset)); + } + } + + // Make sure tokens are added in reverse order. See #5150 + for tok in local_tokens.into_iter().rev() { + self.add_if_new(tok); + } + } + self.matches.len() > before + } + + fn move_forwards(&mut self) -> bool { + // Try to move within our previously discovered matches. + if self.match_index > 0 { + self.match_index -= 1; + true + } else { + false + } + } + + fn move_backwards(&mut self) -> bool { + // Try to move backwards within our previously discovered matches. + if self.match_index + 1 < self.matches.len() { + self.match_index += 1; + return true; + } + + // Add more items from our search. + while self + .search_mut() + .go_to_next_match(SearchDirection::Backward) + { + if self.append_matches_from_search() { + self.match_index += 1; + assert!( + self.match_index < self.matches.len(), + "Should have found more matches" + ); + return true; + } + } + + // Here we failed to go backwards past the last history item. + false + } + + fn search(&self) -> &HistorySearch { + self.search.as_ref().unwrap() + } + + fn search_mut(&mut self) -> &mut HistorySearch { + self.search.as_mut().unwrap() + } +} diff --git a/fish-rust/src/screen.rs b/fish-rust/src/screen.rs index 005535669..74c143b7c 100644 --- a/fish-rust/src/screen.rs +++ b/fish-rust/src/screen.rs @@ -255,7 +255,7 @@ impl Screen { commandline: &wstr, explicit_len: usize, colors: &[HighlightSpec], - indent: &[usize], + indent: &[i32], cursor_pos: usize, vars: &dyn Environment, pager: &mut Pager, @@ -336,7 +336,7 @@ impl Screen { self.desired_append_char( effective_commandline.as_char_slice()[i], colors[i], - indent[i], + usize::try_from(indent[i]).unwrap(), first_line_prompt_space, usize::try_from(fish_wcwidth_visible( effective_commandline.as_char_slice()[i], @@ -1960,17 +1960,13 @@ impl Screen { cursor_is_within_pager: bool, ) { let vars = unsafe { Box::from_raw(vars as *mut EnvStackRef) }; - let mut my_indent = vec![]; - for n in indent.as_slice() { - my_indent.push(usize::try_from(*n).unwrap()); - } self.write( left_prompt.as_wstr(), right_prompt.as_wstr(), commandline.as_wstr(), explicit_len, &colors.0, - &my_indent, + indent.as_slice(), cursor_pos, vars.as_ref().as_ref().get_ref(), pager.get_mut(), diff --git a/fish-rust/src/termsize.rs b/fish-rust/src/termsize.rs index 87337451a..9eac421ad 100644 --- a/fish-rust/src/termsize.rs +++ b/fish-rust/src/termsize.rs @@ -280,7 +280,7 @@ pub fn handle_columns_lines_var_change(vars: &dyn Environment) { SHARED_CONTAINER.handle_columns_lines_var_change(vars); } -fn termsize_update(parser: &Parser) -> Termsize { +pub fn termsize_update(parser: &Parser) -> Termsize { SHARED_CONTAINER.updating(parser) } diff --git a/fish-rust/src/tests/abbrs.rs b/fish-rust/src/tests/abbrs.rs new file mode 100644 index 000000000..8e6a49473 --- /dev/null +++ b/fish-rust/src/tests/abbrs.rs @@ -0,0 +1,139 @@ +use crate::abbrs::{self, abbrs_get_set, abbrs_match, Abbreviation}; +use crate::complete::CompleteFlags; +use crate::editable_line::{apply_edit, Edit}; +use crate::highlight::HighlightSpec; +use crate::parser::Parser; +use crate::reader::{ + combine_command_and_autosuggestion, completion_apply_to_command_line, + reader_expand_abbreviation_at_cursor, +}; +use crate::wchar::prelude::*; + +crate::ffi_tests::add_test!("test_abbreviations", || { + { + let mut abbrs = abbrs_get_set(); + abbrs.add(Abbreviation::new( + L!("gc").to_owned(), + L!("gc").to_owned(), + L!("git checkout").to_owned(), + abbrs::Position::Command, + false, + )); + abbrs.add(Abbreviation::new( + L!("foo").to_owned(), + L!("foo").to_owned(), + L!("bar").to_owned(), + abbrs::Position::Command, + false, + )); + abbrs.add(Abbreviation::new( + L!("gx").to_owned(), + L!("gx").to_owned(), + L!("git checkout").to_owned(), + abbrs::Position::Command, + false, + )); + abbrs.add(Abbreviation::new( + L!("yin").to_owned(), + L!("yin").to_owned(), + L!("yang").to_owned(), + abbrs::Position::Anywhere, + false, + )); + } + + // Helper to expand an abbreviation, enforcing we have no more than one result. + macro_rules! abbr_expand_1 { + ($token:expr, $position:expr) => { + let result = abbrs_match(L!($token), $position); + assert_eq!(result, vec![]); + }; + ($token:expr, $position:expr, $expected:expr) => { + let result = abbrs_match(L!($token), $position); + assert_eq!( + result + .into_iter() + .map(|a| a.replacement) + .collect::>(), + vec![L!($expected).to_owned()] + ); + }; + } + + let cmd = abbrs::Position::Command; + abbr_expand_1!("", cmd); + abbr_expand_1!("nothing", cmd); + + abbr_expand_1!("gc", cmd, "git checkout"); + abbr_expand_1!("foo", cmd, "bar"); + + fn expand_abbreviation_in_command( + cmdline: &wstr, + cursor_pos: Option, + ) -> Option { + let replacement = reader_expand_abbreviation_at_cursor( + cmdline, + cursor_pos.unwrap_or(cmdline.len()), + Parser::principal_parser(), + )?; + let mut cmdline_expanded = cmdline.to_owned(); + let mut colors = vec![HighlightSpec::new(); cmdline.len()]; + apply_edit( + &mut cmdline_expanded, + &mut colors, + &Edit::new(replacement.range.into(), replacement.text), + ); + Some(cmdline_expanded) + } + + macro_rules! validate { + ($cmdline:expr, $cursor:expr) => {{ + let actual = expand_abbreviation_in_command(L!($cmdline), $cursor); + assert_eq!(actual, None); + }}; + ($cmdline:expr, $cursor:expr, $expected:expr) => {{ + let actual = expand_abbreviation_in_command(L!($cmdline), $cursor); + assert_eq!(actual, Some(L!($expected).to_owned())); + }}; + } + + validate!("just a command", Some(3)); + validate!("gc somebranch", Some(0), "git checkout somebranch"); + + validate!( + "gc somebranch", + Some("gc".chars().count()), + "git checkout somebranch" + ); + + // Space separation. + validate!( + "gx somebranch", + Some("gc".chars().count()), + "git checkout somebranch" + ); + + validate!( + "echo hi ; gc somebranch", + Some("echo hi ; g".chars().count()), + "echo hi ; git checkout somebranch" + ); + + validate!( + "echo (echo (echo (echo (gc ", + Some("echo (echo (echo (echo (gc".chars().count()), + "echo (echo (echo (echo (git checkout " + ); + + // If commands should be expanded. + validate!("if gc", None, "if git checkout"); + + // Others should not be. + validate!("of gc", None); + + // Others should not be. + validate!("command gc", None); + + // yin/yang expands everywhere. + validate!("command yin", None, "command yang"); +}); diff --git a/fish-rust/src/tests/debounce.rs b/fish-rust/src/tests/debounce.rs index aa748f75a..fa70a4e33 100644 --- a/fish-rust/src/tests/debounce.rs +++ b/fish-rust/src/tests/debounce.rs @@ -4,8 +4,11 @@ use std::sync::{ }; use std::time::Duration; +use crate::common::ScopeGuard; use crate::ffi_tests::add_test; use crate::global_safety::RelaxedAtomicBool; +use crate::parser::Parser; +use crate::reader::{reader_current_data, reader_pop, reader_push, ReaderConfig, ReaderData}; use crate::threads::{iothread_drain_all, iothread_service_main, Debounce}; use crate::wchar::prelude::*; @@ -43,7 +46,7 @@ add_test!("test_debounce", || { }; let completer = { let ctx = ctx.clone(); - move |idx: usize| { + move |_ctx: &mut ReaderData, idx: usize| { ctx.completion_ran[idx].store(true); } }; @@ -55,10 +58,13 @@ add_test!("test_debounce", || { ctx.cv.notify_all(); // Wait until the last completion is done. + reader_push(Parser::principal_parser(), L!(""), ReaderConfig::default()); + let _pop = ScopeGuard::new((), |()| reader_pop()); + let reader_data = reader_current_data().unwrap(); while !ctx.completion_ran.last().unwrap().load() { - iothread_service_main(); + iothread_service_main(reader_data); } - unsafe { iothread_drain_all() }; + unsafe { iothread_drain_all(reader_data) }; // Each perform() call may displace an existing queued operation. // Each operation waits until all are queued. diff --git a/fish-rust/src/tests/env_universal_common.rs b/fish-rust/src/tests/env_universal_common.rs index f0cc81157..1ffa46eb8 100644 --- a/fish-rust/src/tests/env_universal_common.rs +++ b/fish-rust/src/tests/env_universal_common.rs @@ -1,8 +1,11 @@ use crate::common::wcs2osstring; +use crate::common::ScopeGuard; use crate::env::{EnvVar, EnvVarFlags, VarTable}; use crate::env_universal_common::{CallbackDataList, EnvUniversal, UvarFormat}; use crate::ffi_tests::add_test; use crate::flog::FLOG; +use crate::parser::Parser; +use crate::reader::{reader_current_data, reader_pop, reader_push, ReaderConfig}; use crate::threads::{iothread_drain_all, iothread_perform}; use crate::wchar::prelude::*; use crate::wutil::file_id_for_path; @@ -37,11 +40,14 @@ add_test!("test_universal", || { let _ = std::fs::remove_dir_all("test/fish_uvars_test/"); std::fs::create_dir_all("test/fish_uvars_test/").unwrap(); + reader_push(Parser::principal_parser(), L!(""), ReaderConfig::default()); + let _pop = ScopeGuard::new((), |()| reader_pop()); + let threads = 1; for i in 0..threads { iothread_perform(move || test_universal_helper(i)); } - unsafe { iothread_drain_all() }; + unsafe { iothread_drain_all(reader_current_data().unwrap()) }; let mut uvars = EnvUniversal::new(); let mut callbacks = CallbackDataList::new(); diff --git a/fish-rust/src/tests/history.rs b/fish-rust/src/tests/history.rs index cff0ffa54..e083c389a 100644 --- a/fish-rust/src/tests/history.rs +++ b/fish-rust/src/tests/history.rs @@ -1,7 +1,7 @@ use crate::common::{ cstr2wcstring, is_windows_subsystem_for_linux, str2wcstring, wcs2osstring, wcs2string, }; -use crate::env::EnvDyn; +use crate::env::{EnvDyn, Environment}; use crate::fds::{wopen_cloexec, AutoCloseFd}; use crate::ffi_tests::add_test; use crate::history::{self, History, HistoryItem, HistorySearch, PathList, SearchDirection}; @@ -456,7 +456,7 @@ add_test!("test_history_path_detection", || { let mut test_vars = TestEnvironment::default(); test_vars.vars.insert(L!("PWD").to_owned(), tmpdir.clone()); test_vars.vars.insert(L!("HOME").to_owned(), tmpdir.clone()); - let vars = || EnvDyn::new(Box::new(test_vars.clone())); + let vars = || EnvDyn::new(Box::new(test_vars.clone()) as Box); let history = History::with_name(L!("path_detection")); history.clear(); diff --git a/fish-rust/src/tests/mod.rs b/fish-rust/src/tests/mod.rs index 22e4b357e..f33583e8a 100644 --- a/fish-rust/src/tests/mod.rs +++ b/fish-rust/src/tests/mod.rs @@ -1,5 +1,6 @@ use crate::wchar::prelude::*; +mod abbrs; #[cfg(test)] mod common; mod complete; @@ -18,6 +19,8 @@ mod pager; mod parse_util; mod parser; #[cfg(test)] +mod reader; +#[cfg(test)] mod redirection; mod screen; mod string_escape; diff --git a/fish-rust/src/tests/parser.rs b/fish-rust/src/tests/parser.rs index ed59e2395..f7f01e6da 100644 --- a/fish-rust/src/tests/parser.rs +++ b/fish-rust/src/tests/parser.rs @@ -1,5 +1,6 @@ use crate::ast::{self, Ast, List, Node, Traversal}; use crate::builtins::shared::{STATUS_CMD_OK, STATUS_UNMATCHED_WILDCARD}; +use crate::common::ScopeGuard; use crate::expand::ExpandFlags; use crate::io::{IoBufferfill, IoChain}; use crate::parse_constants::{ @@ -7,7 +8,9 @@ use crate::parse_constants::{ }; use crate::parse_util::{parse_util_detect_errors, parse_util_detect_errors_in_argument}; use crate::parser::Parser; -use crate::reader::reader_reset_interrupted; +use crate::reader::{ + reader_current_data, reader_pop, reader_push, reader_reset_interrupted, ReaderConfig, +}; use crate::signal::{signal_clear_cancel, signal_reset_handlers, signal_set_handlers}; use crate::tests::prelude::*; use crate::threads::{iothread_drain_all, iothread_perform}; @@ -681,11 +684,14 @@ fn test_1_cancellation(src: &wstr) { ); assert!(res.status.signal_exited() && res.status.signal_code() == SIGINT); unsafe { - iothread_drain_all(); + iothread_drain_all(reader_current_data().unwrap()); } } add_test!("test_cancellation", || { + reader_push(Parser::principal_parser(), L!(""), ReaderConfig::default()); + let _pop = ScopeGuard::new((), |()| reader_pop()); + println!("Testing Ctrl-C cancellation. If this hangs, that's a bug!"); // Enable fish's signal handling here. diff --git a/fish-rust/src/tests/reader.rs b/fish-rust/src/tests/reader.rs new file mode 100644 index 000000000..2b0450a91 --- /dev/null +++ b/fish-rust/src/tests/reader.rs @@ -0,0 +1,167 @@ +use crate::complete::CompleteFlags; +use crate::reader::{combine_command_and_autosuggestion, completion_apply_to_command_line}; +use crate::wchar::prelude::*; + +#[test] +fn test_autosuggestion_combining() { + assert_eq!( + combine_command_and_autosuggestion(L!("alpha"), L!("alphabeta")), + L!("alphabeta") + ); + + // When the last token contains no capital letters, we use the case of the autosuggestion. + assert_eq!( + combine_command_and_autosuggestion(L!("alpha"), L!("ALPHABETA")), + L!("ALPHABETA") + ); + + // When the last token contains capital letters, we use its case. + assert_eq!( + combine_command_and_autosuggestion(L!("alPha"), L!("alphabeTa")), + L!("alPhabeTa") + ); + + // If autosuggestion is not longer than input, use the input's case. + assert_eq!( + combine_command_and_autosuggestion(L!("alpha"), L!("ALPHAA")), + L!("ALPHAA") + ); + assert_eq!( + combine_command_and_autosuggestion(L!("alpha"), L!("ALPHA")), + L!("alpha") + ); +} + +#[test] +fn test_completion_insertions() { + macro_rules! validate { + ( + $line:expr, $completion:expr, + $flags:expr, $append_only:expr, + $expected:expr + ) => { + // line is given with a caret, which we use to represent the cursor position. Find it. + 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( + completion, + $flags, + &line, + &mut cursor_pos, + $append_only, + ); + assert_eq!(result, expected); + assert_eq!(cursor_pos, out_cursor_pos); + }; + } + + validate!("foo^", "bar", CompleteFlags::default(), false, "foobar ^"); + // An unambiguous completion of a token that is already trailed by a space character. + // After completing, the cursor moves on to the next token, suggesting to the user that the + // current token is finished. + 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 ^" + ); + + // Test append only. + 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 ^" + ); + + // See #6130 + validate!(": (:^ ''", "", CompleteFlags::default(), false, ": (: ^''"); +} diff --git a/fish-rust/src/threads.rs b/fish-rust/src/threads.rs index b5d4620e1..fd99ca5cd 100644 --- a/fish-rust/src/threads.rs +++ b/fish-rust/src/threads.rs @@ -2,6 +2,7 @@ //! ported directly from the cpp code so we can use rust threads instead of using pthreads. use crate::flog::{FloggableDebug, FLOG}; +use crate::reader::ReaderData; use once_cell::race::OnceBox; use std::num::NonZeroU64; use std::sync::atomic::{AtomicBool, Ordering}; @@ -73,47 +74,24 @@ mod ffi { extern "Rust" { fn iothread_port() -> i32; - fn iothread_service_main(); - #[cxx_name = "iothread_service_main_with_timeout"] - fn iothread_service_main_with_timeout_ffi(timeout_usec: u64); - #[cxx_name = "iothread_drain_all"] - fn iothread_drain_all_ffi(); + #[cxx_name = "iothread_service_main"] + fn iothread_service_main_ffi(ctx: *mut u8); #[cxx_name = "iothread_perform"] fn iothread_perform_ffi(callback: &SharedPtr); #[cxx_name = "iothread_perform_cantwait"] fn iothread_perform_cant_wait_ffi(callback: &SharedPtr); } +} - extern "Rust" { - #[cxx_name = "debounce_t"] - type Debounce; - - #[cxx_name = "perform"] - fn perform_ffi(&self, callback: &SharedPtr) -> u64; - #[cxx_name = "perform_with_completion"] - fn perform_with_completion_ffi( - &self, - callback: &SharedPtr, - completion: &SharedPtr, - ) -> u64; - - #[cxx_name = "new_debounce_t"] - fn new_debounce_ffi(timeout_ms: u64) -> Box; - } +fn iothread_service_main_ffi(ctx: *mut u8) { + let ctx = unsafe { &mut *(ctx as *mut ReaderData) }; + iothread_service_main(ctx); } pub use ffi::CppCallback; unsafe impl Send for ffi::CppCallback {} unsafe impl Sync for ffi::CppCallback {} -fn iothread_service_main_with_timeout_ffi(timeout_usec: u64) { - iothread_service_main_with_timeout(Duration::from_micros(timeout_usec)) -} - -fn iothread_drain_all_ffi() { - unsafe { iothread_drain_all() } -} - fn iothread_perform_ffi(callback: &cxx::SharedPtr) { let callback = callback.clone(); @@ -130,7 +108,7 @@ fn iothread_perform_cant_wait_ffi(callback: &cxx::SharedPtr) { }); } -/// A [`ThreadPool`] or [`Debounce`] work request. +/// A [`ThreadPool`] work request. type WorkItem = Box; // A helper type to allow us to (temporarily) send an object to another thread. @@ -140,7 +118,7 @@ struct ForceSend(T); unsafe impl Send for ForceSend {} #[allow(clippy::type_complexity)] -type DebounceCallback = ForceSend>; +type DebounceCallback = ForceSend>; /// The queue of [`WorkItem`]s to be executed on the main thread. This is read from in /// `iothread_service_main()`. @@ -561,13 +539,13 @@ pub fn iothread_port() -> i32 { NOTIFY_SIGNALLER.read_fd() } -pub fn iothread_service_main_with_timeout(timeout: Duration) { +pub fn iothread_service_main_with_timeout(ctx: &mut ReaderData, timeout: Duration) { if crate::fd_readable_set::is_fd_readable(iothread_port(), timeout.as_millis() as u64) { - iothread_service_main(); + iothread_service_main(ctx); } } -pub fn iothread_service_main() { +pub fn iothread_service_main(ctx: &mut ReaderData) { self::assert_is_main_thread(); // Note: the order here is important. We must consume events before handling requests, as @@ -578,12 +556,12 @@ pub fn iothread_service_main() { // Perform each completion in order. for callback in queue { - (callback.0)(); + (callback.0)(ctx); } } /// Does nasty polling via select() and marked as unsafe because it should only be used for testing. -pub unsafe fn iothread_drain_all() { +pub unsafe fn iothread_drain_all(ctx: &mut ReaderData) { while borrow_io_thread_pool() .shared .mutex @@ -592,7 +570,7 @@ pub unsafe fn iothread_drain_all() { .total_threads > 0 { - iothread_service_main_with_timeout(Duration::from_millis(1000)); + iothread_service_main_with_timeout(ctx, Duration::from_millis(1000)); } } @@ -626,10 +604,6 @@ struct DebounceData { start_time: Instant, } -fn new_debounce_ffi(timeout_ms: u64) -> Box { - Box::new(Debounce::new(Duration::from_millis(timeout_ms))) -} - impl Debounce { pub fn new(timeout: Duration) -> Self { Self { @@ -673,33 +647,7 @@ impl Debounce { /// /// The result is a token which is only of interest to the test suite. pub fn perform(&self, handler: impl FnOnce() + 'static + Send) -> NonZeroU64 { - self.perform_with_completion(handler, |_result| ()) - } - - fn perform_ffi(&self, callback: &cxx::SharedPtr) -> u64 { - let callback = callback.clone(); - - self.perform(move || { - callback.invoke(); - }) - .into() - } - - fn perform_with_completion_ffi( - &self, - callback: &cxx::SharedPtr, - completion: &cxx::SharedPtr, - ) -> u64 { - let callback = callback.clone(); - let completion = completion.clone(); - - self.perform_with_completion( - move || -> crate::ffi::void_ptr { callback.invoke().into() }, - move |result| { - completion.invoke_with_param(result.into()); - }, - ) - .into() + self.perform_with_completion(handler, |_ctx, _result| ()) } /// Enqueue `handler` to be performed on a background thread with [`Completion`] `completion` @@ -713,16 +661,16 @@ impl Debounce { pub fn perform_with_completion(&self, handler: H, completion: C) -> NonZeroU64 where H: FnOnce() -> R + 'static + Send, - C: FnOnce(R) + 'static, + C: FnOnce(&mut ReaderData, R) + 'static, R: 'static + Send, { assert_is_main_thread(); let completion_wrapper = ForceSend(completion); let work_item = Box::new(move || { let result = handler(); - let callback: DebounceCallback = ForceSend(Box::new(move || { + let callback: DebounceCallback = ForceSend(Box::new(move |ctx| { let completion = completion_wrapper; - (completion.0)(result); + (completion.0)(ctx, result); })); MAIN_THREAD_QUEUE.lock().unwrap().push(callback); NOTIFY_SIGNALLER.post(); diff --git a/fish-rust/src/tokenizer.rs b/fish-rust/src/tokenizer.rs index 294d8bcfc..92ab3e91f 100644 --- a/fish-rust/src/tokenizer.rs +++ b/fish-rust/src/tokenizer.rs @@ -300,6 +300,9 @@ impl Tok { pub fn set_length(&mut self, value: usize) { self.length = value.try_into().unwrap(); } + pub fn end(&self) -> usize { + self.offset() + self.length() + } pub fn set_error_offset_within_token(&mut self, value: usize) { self.error_offset_within_token = value.try_into().unwrap(); } diff --git a/fish-rust/src/wchar_ext.rs b/fish-rust/src/wchar_ext.rs index 7c0b2fd50..e4520b0bd 100644 --- a/fish-rust/src/wchar_ext.rs +++ b/fish-rust/src/wchar_ext.rs @@ -64,7 +64,7 @@ macro_rules! impl_to_wstring_unsigned { }; } -impl_to_wstring_unsigned!(u8, u16, u32, u64, usize); +impl_to_wstring_unsigned!(u8, u16, u32, u64, u128, usize); #[test] fn test_to_wstring() { diff --git a/fish-rust/src/wcstringutil.rs b/fish-rust/src/wcstringutil.rs index b29103e8b..536f503f6 100644 --- a/fish-rust/src/wcstringutil.rs +++ b/fish-rust/src/wcstringutil.rs @@ -61,7 +61,10 @@ pub fn subsequence_in_string(needle: &wstr, haystack: &wstr) -> bool { /// expanded to include symbolic characters (#3584). /// \return the offset of the first case-insensitive matching instance of `needle` within /// `haystack`, or `string::npos()` if no results were found. -pub fn ifind(haystack: &wstr, needle: &wstr, fuzzy: bool) -> Option { +pub fn ifind(haystack: &wstr, needle: &wstr, fuzzy: bool /* = false */) -> Option { + if needle.is_empty() { + return Some(0); + } haystack .as_char_slice() .windows(needle.len()) diff --git a/src/builtins/commandline.cpp b/src/builtins/commandline.cpp deleted file mode 100644 index a79a6dd09..000000000 --- a/src/builtins/commandline.cpp +++ /dev/null @@ -1,523 +0,0 @@ -// Functions used for implementing the commandline builtin. -#include "config.h" // IWYU pragma: keep - -#include "commandline.h" - -#include -#include -#include -#include - -#include "../builtin.h" -#include "../common.h" -#include "../fallback.h" // IWYU pragma: keep -#include "../io.h" -#include "../maybe.h" -#include "../parse_constants.h" -#include "../parse_util.h" -#include "../parser.h" -#include "../proc.h" -#include "../reader.h" -#include "../tokenizer.h" -#include "../wgetopt.h" -#include "../wutil.h" // IWYU pragma: keep -#include "builtins/shared.rs.h" -#include "input_ffi.rs.h" - -/// Which part of the comandbuffer are we operating on. -enum { - STRING_MODE = 1, // operate on entire buffer - JOB_MODE, // operate on job under cursor - PROCESS_MODE, // operate on process under cursor - TOKEN_MODE // operate on token under cursor -}; - -/// For text insertion, how should it be done. -enum { - REPLACE_MODE = 1, // replace current text - INSERT_MODE, // insert at cursor position - APPEND_MODE // insert at end of current token/command/buffer -}; - -/// Handle a single readline_cmd_t command out-of-band. -void reader_handle_command(readline_cmd_t cmd); - -/// Replace/append/insert the selection with/at/after the specified string. -/// -/// \param begin beginning of selection -/// \param end end of selection -/// \param insert the string to insert -/// \param append_mode can be one of REPLACE_MODE, INSERT_MODE or APPEND_MODE, affects the way the -/// test update is performed -/// \param buff the original command line buffer -/// \param cursor_pos the position of the cursor in the command line -static void replace_part(const wchar_t *begin, const wchar_t *end, const wchar_t *insert, - int append_mode, const wchar_t *buff, size_t cursor_pos) { - size_t out_pos = cursor_pos; - - wcstring out; - - out.append(buff, begin - buff); - - switch (append_mode) { - case REPLACE_MODE: { - out.append(insert); - out_pos = out.size(); - break; - } - case APPEND_MODE: { - out.append(begin, end - begin); - out.append(insert); - break; - } - case INSERT_MODE: { - long cursor = cursor_pos - (begin - buff); - out.append(begin, cursor); - out.append(insert); - out.append(begin + cursor, end - begin - cursor); - out_pos += std::wcslen(insert); - break; - } - default: { - DIE("unexpected append_mode"); - } - } - out.append(end); - commandline_set_buffer(out, out_pos); -} - -/// Output the specified selection. -/// -/// \param begin start of selection -/// \param end end of selection -/// \param cut_at_cursor whether printing should stop at the surrent cursor position -/// \param tokenize whether the string should be tokenized, printing one string token on every line -/// and skipping non-string tokens -/// \param buffer the original command line buffer -/// \param cursor_pos the position of the cursor in the command line -static void write_part(const wchar_t *begin, const wchar_t *end, int cut_at_cursor, int tokenize, - const wchar_t *buffer, size_t cursor_pos, io_streams_t &streams) { - size_t pos = cursor_pos - (begin - buffer); - - if (tokenize) { - // std::fwprintf( stderr, L"Subshell: %ls, end char %lc\n", buff, *end ); - wcstring out; - wcstring buff(begin, end - begin); - auto tok = new_tokenizer(buff.c_str(), TOK_ACCEPT_UNFINISHED); - while (auto token = tok->next()) { - if ((cut_at_cursor) && (token->offset + token->length >= pos)) break; - - if (token->type_ == token_type_t::string) { - wcstring tmp = *tok->text_of(*token); - auto maybe_unescaped = unescape_string(tmp.c_str(), tmp.size(), UNESCAPE_INCOMPLETE, - STRING_STYLE_SCRIPT); - assert(maybe_unescaped); - out.append(*maybe_unescaped); - out.push_back(L'\n'); - } - } - - streams.out()->append(out); - } else { - if (cut_at_cursor) { - streams.out()->append(wcstring{begin, pos}); - } else { - streams.out()->append(wcstring{begin, end}); - } - streams.out()->push(L'\n'); - } -} - -/// The commandline builtin. It is used for specifying a new value for the commandline. -int builtin_commandline(const void *_parser, void *_streams, void *_argv) { - const auto &parser = *static_cast(_parser); - auto &streams = *static_cast(_streams); - auto argv = static_cast(_argv); - const commandline_state_t rstate = commandline_get_state(); - const wchar_t *cmd = argv[0]; - int buffer_part = 0; - bool cut_at_cursor = false; - - int argc = builtin_count_args(argv); - int append_mode = 0; - - bool function_mode = false; - bool selection_mode = false; - - bool tokenize = false; - - bool cursor_mode = false; - bool selection_start_mode = false; - bool selection_end_mode = false; - bool line_mode = false; - bool search_mode = false; - bool paging_mode = false; - bool paging_full_mode = false; - bool is_valid = false; - const wchar_t *begin = nullptr, *end = nullptr; - const wchar_t *override_buffer = nullptr; - - const auto &ld = parser.libdata(); - - static const wchar_t *const short_options = L":abijpctforhI:CBELSsP"; - static const struct woption long_options[] = {{L"append", no_argument, 'a'}, - {L"insert", no_argument, 'i'}, - {L"replace", no_argument, 'r'}, - {L"current-buffer", no_argument, 'b'}, - {L"current-job", no_argument, 'j'}, - {L"current-process", no_argument, 'p'}, - {L"current-selection", no_argument, 's'}, - {L"current-token", no_argument, 't'}, - {L"cut-at-cursor", no_argument, 'c'}, - {L"function", no_argument, 'f'}, - {L"tokenize", no_argument, 'o'}, - {L"help", no_argument, 'h'}, - {L"input", required_argument, 'I'}, - {L"cursor", no_argument, 'C'}, - {L"selection-start", no_argument, 'B'}, - {L"selection-end", no_argument, 'E'}, - {L"line", no_argument, 'L'}, - {L"search-mode", no_argument, 'S'}, - {L"paging-mode", no_argument, 'P'}, - {L"paging-full-mode", no_argument, 'F'}, - {L"is-valid", no_argument, 1}, - {}}; - - int opt; - wgetopter_t w; - while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) { - switch (opt) { - case L'a': { - append_mode = APPEND_MODE; - break; - } - case L'b': { - buffer_part = STRING_MODE; - break; - } - case L'i': { - append_mode = INSERT_MODE; - break; - } - case L'r': { - append_mode = REPLACE_MODE; - break; - } - case 'c': { - cut_at_cursor = true; - break; - } - case 't': { - buffer_part = TOKEN_MODE; - break; - } - case 'j': { - buffer_part = JOB_MODE; - break; - } - case 'p': { - buffer_part = PROCESS_MODE; - break; - } - case 'f': { - function_mode = true; - break; - } - case 'o': { - tokenize = true; - break; - } - case 'I': { - // A historical, undocumented feature. TODO: consider removing this. - override_buffer = w.woptarg; - break; - } - case 'C': { - cursor_mode = true; - break; - } - case 'B': { - selection_start_mode = true; - break; - } - case 'E': { - selection_end_mode = true; - break; - } - case 'L': { - line_mode = true; - break; - } - case 'S': { - search_mode = true; - break; - } - case 's': { - selection_mode = true; - break; - } - case 'P': { - paging_mode = true; - break; - } - case 'F': { - paging_full_mode = true; - break; - } - case 1: { - is_valid = true; - break; - } - case 'h': { - builtin_print_help(parser, streams, cmd); - return STATUS_CMD_OK; - } - case ':': { - builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], true); - return STATUS_INVALID_ARGS; - } - case L'?': { - builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], true); - return STATUS_INVALID_ARGS; - } - default: { - DIE("unexpected retval from wgetopt_long"); - } - } - } - - if (function_mode) { - int i; - - // Check for invalid switch combinations. - if (buffer_part || cut_at_cursor || append_mode || tokenize || cursor_mode || line_mode || - search_mode || paging_mode || selection_start_mode || selection_end_mode) { - streams.err()->append(format_string(BUILTIN_ERR_COMBO, argv[0])); - builtin_print_error_trailer(parser, *streams.err(), cmd); - return STATUS_INVALID_ARGS; - } - - if (argc == w.woptind) { - builtin_missing_argument(parser, streams, cmd, argv[0], true); - return STATUS_INVALID_ARGS; - } - - using rl = readline_cmd_t; - for (i = w.woptind; i < argc; i++) { - int mci = input_function_get_code(argv[i]); - if (mci >= 0) { - readline_cmd_t mc = static_cast(mci); - // Don't enqueue a repaint if we're currently in the middle of one, - // because that's an infinite loop. - if (mc == rl::RepaintMode || mc == rl::ForceRepaint || mc == rl::Repaint) { - if (ld.is_repaint()) continue; - } - - // HACK: Execute these right here and now so they can affect any insertions/changes - // made via bindings. The correct solution is to change all `commandline` - // insert/replace operations into readline functions with associated data, so that - // all queued `commandline` operations - including buffer modifications - are - // executed in order - if (mc == rl::BeginUndoGroup || mc == rl::EndUndoGroup) { - reader_handle_command(mc); - } else { - // Inserts the readline function at the back of the queue. - reader_queue_ch(char_event_from_readline(mc)); - } - } else { - streams.err()->append( - format_string(_(L"%ls: Unknown input function '%ls'"), cmd, argv[i])); - builtin_print_error_trailer(parser, *streams.err(), cmd); - return STATUS_INVALID_ARGS; - } - } - - return STATUS_CMD_OK; - } - - if (selection_mode) { - if (rstate.selection) { - streams.out()->append( - {rstate.text.c_str() + rstate.selection->start, rstate.selection->length}); - } - return STATUS_CMD_OK; - } - - // Check for invalid switch combinations. - if ((selection_start_mode || selection_end_mode) && (argc - w.woptind)) { - streams.err()->append(format_string(BUILTIN_ERR_TOO_MANY_ARGUMENTS, argv[0])); - builtin_print_error_trailer(parser, *streams.err(), cmd); - return STATUS_INVALID_ARGS; - } - - if ((search_mode || line_mode || cursor_mode || paging_mode) && (argc - w.woptind > 1)) { - streams.err()->append(format_string(BUILTIN_ERR_TOO_MANY_ARGUMENTS, argv[0])); - builtin_print_error_trailer(parser, *streams.err(), cmd); - return STATUS_INVALID_ARGS; - } - - if ((buffer_part || tokenize || cut_at_cursor) && - (cursor_mode || line_mode || search_mode || paging_mode || paging_full_mode) && - // Special case - we allow to get/set cursor position relative to the process/job/token. - !(buffer_part && cursor_mode)) { - streams.err()->append(format_string(BUILTIN_ERR_COMBO, argv[0])); - builtin_print_error_trailer(parser, *streams.err(), cmd); - return STATUS_INVALID_ARGS; - } - - if ((tokenize || cut_at_cursor) && (argc - w.woptind)) { - streams.err()->append(format_string( - BUILTIN_ERR_COMBO2, cmd, - L"--cut-at-cursor and --tokenize can not be used when setting the commandline")); - builtin_print_error_trailer(parser, *streams.err(), cmd); - return STATUS_INVALID_ARGS; - } - - if (append_mode && !(argc - w.woptind)) { - // No tokens in insert mode just means we do nothing. - return STATUS_CMD_ERROR; - } - - // Set default modes. - if (!append_mode) { - append_mode = REPLACE_MODE; - } - - if (!buffer_part) { - buffer_part = STRING_MODE; - } - - if (line_mode) { - streams.out()->append( - format_string(L"%d\n", parse_util_lineno(rstate.text, rstate.cursor_pos))); - return STATUS_CMD_OK; - } - - if (search_mode) { - return commandline_get_state().search_mode ? 0 : 1; - } - - if (paging_mode) { - return commandline_get_state().pager_mode ? 0 : 1; - } - - if (paging_full_mode) { - auto state = commandline_get_state(); - return (state.pager_mode && state.pager_fully_disclosed) ? 0 : 1; - } - - if (selection_start_mode) { - if (!rstate.selection) { - return STATUS_CMD_ERROR; - } - source_offset_t start = rstate.selection->start; - streams.out()->append(format_string(L"%lu\n", static_cast(start))); - return STATUS_CMD_OK; - } - - if (selection_end_mode) { - if (!rstate.selection) { - return STATUS_CMD_ERROR; - } - source_offset_t end = rstate.selection->end(); - streams.out()->append(format_string(L"%lu\n", static_cast(end))); - return STATUS_CMD_OK; - } - - // At this point we have (nearly) exhausted the options which always operate on the true command - // line. Now we respect the possibility of a transient command line due to evaluating a wrapped - // completion. Don't do this in cursor_mode: it makes no sense to move the cursor based on a - // transient commandline. - const wchar_t *current_buffer = nullptr; - size_t current_cursor_pos{0}; - wcstring transient; - if (override_buffer) { - current_buffer = override_buffer; - current_cursor_pos = std::wcslen(current_buffer); - } else if (!ld.transient_commandlines_empty() && !cursor_mode) { - transient = *ld.transient_commandlines_back(); - current_buffer = transient.c_str(); - current_cursor_pos = transient.size(); - } else if (rstate.initialized) { - current_buffer = rstate.text.c_str(); - current_cursor_pos = rstate.cursor_pos; - } else { - // There is no command line, either because we are not interactive, or because we are - // interactive and are still reading init files (in which case we silently ignore this). - if (!is_interactive_session()) { - streams.err()->append(cmd); - streams.err()->append(L": Can not set commandline in non-interactive mode\n"); - builtin_print_error_trailer(parser, *streams.err(), cmd); - } - return STATUS_CMD_ERROR; - } - - if (is_valid) { - if (!*current_buffer) return 1; - parser_test_error_bits_t res = parse_util_detect_errors( - current_buffer, nullptr, true /* accept incomplete so we can tell the difference */); - if (res & PARSER_TEST_INCOMPLETE) { - return 2; - } - return res & PARSER_TEST_ERROR ? STATUS_CMD_ERROR : STATUS_CMD_OK; - } - - switch (buffer_part) { - case STRING_MODE: { - begin = current_buffer; - end = begin + std::wcslen(begin); - break; - } - case PROCESS_MODE: { - parse_util_process_extent(current_buffer, current_cursor_pos, &begin, &end, nullptr); - break; - } - case JOB_MODE: { - parse_util_job_extent(current_buffer, current_cursor_pos, &begin, &end); - break; - } - case TOKEN_MODE: { - parse_util_token_extent(current_buffer, current_cursor_pos, &begin, &end, nullptr, - nullptr); - break; - } - default: { - DIE("unexpected buffer_part"); - } - } - - if (cursor_mode) { - if (argc - w.woptind) { - long new_pos = fish_wcstol(argv[w.woptind]) + (begin - current_buffer); - if (errno) { - streams.err()->append(format_string(BUILTIN_ERR_NOT_NUMBER, cmd, argv[w.woptind])); - builtin_print_error_trailer(parser, *streams.err(), cmd); - } - - new_pos = - std::max(0L, std::min(new_pos, static_cast(std::wcslen(current_buffer)))); - commandline_set_buffer(current_buffer, static_cast(new_pos)); - } else { - size_t pos = current_cursor_pos - (begin - current_buffer); - streams.out()->append(format_string(L"%lu\n", static_cast(pos))); - } - return STATUS_CMD_OK; - } - - int arg_count = argc - w.woptind; - if (arg_count == 0) { - write_part(begin, end, cut_at_cursor, tokenize, current_buffer, current_cursor_pos, - streams); - } else if (arg_count == 1) { - replace_part(begin, end, argv[w.woptind], append_mode, current_buffer, current_cursor_pos); - } else { - wcstring sb = argv[w.woptind]; - for (int i = w.woptind + 1; i < argc; i++) { - sb.push_back(L'\n'); - sb.append(argv[i]); - } - replace_part(begin, end, sb.c_str(), append_mode, current_buffer, current_cursor_pos); - } - - return STATUS_CMD_OK; -} diff --git a/src/builtins/commandline.h b/src/builtins/commandline.h deleted file mode 100644 index 3ef03cf6f..000000000 --- a/src/builtins/commandline.h +++ /dev/null @@ -1,13 +0,0 @@ -// Prototypes for functions for executing builtin_commandline functions. -#ifndef FISH_BUILTIN_COMMANDLINE_H -#define FISH_BUILTIN_COMMANDLINE_H - -#include "../maybe.h" - -struct Parser; -using parser_t = Parser; -struct IoStreams; -using io_streams_t = IoStreams; - -int builtin_commandline(const void *parser, void *streams, void *argv); -#endif diff --git a/src/common.cpp b/src/common.cpp index f12c47ed2..8bc3eb890 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -757,22 +757,6 @@ void exit_without_destructors(int code) { _exit(code); } extern "C" void debug_thread_error(); -void save_term_foreground_process_group() { initial_fg_process_group = tcgetpgrp(STDIN_FILENO); } - -void restore_term_foreground_process_group_for_exit() { - // We wish to restore the tty to the initial owner. There's two ways this can go wrong: - // 1. We may steal the tty from someone else (#7060). - // 2. The call to tcsetpgrp may deliver SIGSTOP to us, and we will not exit. - // Hanging on exit seems worse, so ensure that SIGTTOU is ignored so we do not get SIGSTOP. - // Note initial_fg_process_group == 0 is possible with Linux pid namespaces. - // This is called during shutdown and from a signal handler. We don't bother to complain on - // failure because doing so is unlikely to be noticed. - if (initial_fg_process_group > 0 && initial_fg_process_group != getpgrp()) { - (void)signal(SIGTTOU, SIG_IGN); - (void)tcsetpgrp(STDIN_FILENO, initial_fg_process_group); - } -} - /// Test if the specified character is in a range that fish uses internally to store special tokens. /// /// NOTE: This is used when tokenizing the input. It is also used when reading input, before diff --git a/src/common.h b/src/common.h index 649392970..ef36e793e 100644 --- a/src/common.h +++ b/src/common.h @@ -485,10 +485,6 @@ wcstring escape_string(const wcstring &in, escape_flags_t flags = 0, using timepoint_t = double; timepoint_t timef(); -/// Save the value of tcgetpgrp so we can restore it on exit. -void save_term_foreground_process_group(); -void restore_term_foreground_process_group_for_exit(); - /// Determines if we are running under Microsoft's Windows Subsystem for Linux to work around /// some known limitations and/or bugs. /// See https://github.com/Microsoft/WSL/issues/423 and Microsoft/WSL#2997 diff --git a/src/env.cpp b/src/env.cpp index 1b2ecba7e..ff13f122d 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -222,31 +222,4 @@ void unsetenv_lock(const char *name) { } } -wcstring_list_ffi_t get_history_variable_text_ffi(const wcstring &fish_history_val) { - wcstring_list_ffi_t out{}; - maybe_t> history = commandline_get_state().history; - if (!history) { - // Effective duplication of history_session_id(). - wcstring session_id{}; - if (fish_history_val.empty()) { - // No session. - session_id.clear(); - } else if (!valid_var_name(fish_history_val)) { - session_id = L"fish"; - FLOGF(error, - _(L"History session ID '%ls' is not a valid variable name. " - L"Falling back to `%ls`."), - fish_history_val.c_str(), session_id.c_str()); - } else { - // Valid session. - session_id = fish_history_val; - } - history = history_with_name(session_id); - } - if (history) { - out = *(*history)->get_history(); - } - return out; -} - const EnvStackRef &env_stack_t::get_impl_ffi() const { return *impl_; } diff --git a/src/env.h b/src/env.h index f746ab26e..d1ecb02dc 100644 --- a/src/env.h +++ b/src/env.h @@ -321,10 +321,6 @@ void setenv_lock(const char *name, const char *value, int overwrite); void unsetenv_lock(const char *name); } -/// Populate the values in the "$history" variable. -/// fish_history_val is the value of the "$fish_history" variable, or "fish" if not set. -wcstring_list_ffi_t get_history_variable_text_ffi(const wcstring &fish_history_val); - void set_inheriteds_ffi(); #endif diff --git a/src/ffi_baggage.h b/src/ffi_baggage.h index 037acc4d8..3f0ce3d5f 100644 --- a/src/ffi_baggage.h +++ b/src/ffi_baggage.h @@ -1,5 +1,4 @@ #include "builtin.h" -#include "builtins/commandline.h" #include "event.h" #include "fds.h" #include "highlight.h" @@ -16,20 +15,11 @@ void mark_as_used(const parser_t& parser, env_stack_t& env_stack) { event_fire_generic(parser, {}); event_fire_generic(parser, {}, {}); expand_tilde(s, env_stack); - get_history_variable_text_ffi({}); highlight_spec_t{}; - reader_change_cursor_selection_mode(cursor_selection_mode_t::exclusive); - reader_change_history({}); - reader_read_ffi({}, {}, {}); - reader_schedule_prompt_repaint(); - reader_set_autosuggestion_enabled_ffi({}); - reader_status_count(); - restore_term_mode(); rgb_color_t{}; setenv_lock({}, {}, {}); set_inheriteds_ffi(); - term_copy_modes(); unsetenv_lock({}); - - builtin_commandline({}, {}, {}); + rgb_color_t::white(); + rgb_color_t{}; } diff --git a/src/fish_key_reader.cpp b/src/fish_key_reader.cpp index a555b829c..ace48f7d9 100644 --- a/src/fish_key_reader.cpp +++ b/src/fish_key_reader.cpp @@ -230,14 +230,14 @@ static void process_input(bool continuous_mode, bool verbose) { std::vector bind_chars; std::fwprintf(stderr, L"Press a key:\n"); - while (!check_exit_loop_maybe_warning(nullptr)) { - maybe_t> evt{}; + while (!check_exit_loop_maybe_warning()) { + maybe_t> evt{}; if (reader_test_and_clear_interrupted()) { evt = char_event_from_char(shell_modes.c_cc[VINTR]); } else { - char_event_t *evt_raw = queue->readch_timed_esc(); + CharEvent *evt_raw = queue->readch_timed_esc(); if (evt_raw) { - evt = rust::Box::from_raw(evt_raw); + evt = rust::Box::from_raw(evt_raw); } } if (!evt || !(*evt)->is_char()) { diff --git a/src/fish_tests.cpp b/src/fish_tests.cpp index ab6dcb57a..4652eb916 100644 --- a/src/fish_tests.cpp +++ b/src/fish_tests.cpp @@ -614,114 +614,6 @@ static void test_lru() { do_test(cache.size() == 0); } -// todo!("port this") -static void test_abbreviations() { - say(L"Testing abbreviations"); - { - auto abbrs = abbrs_get_set(); - abbrs->add(L"gc", L"gc", L"git checkout", abbrs_position_t::command, false); - abbrs->add(L"foo", L"foo", L"bar", abbrs_position_t::command, false); - abbrs->add(L"gx", L"gx", L"git checkout", abbrs_position_t::command, false); - abbrs->add(L"yin", L"yin", L"yang", abbrs_position_t::anywhere, false); - } - - // Helper to expand an abbreviation, enforcing we have no more than one result. - auto abbr_expand_1 = [](const wcstring &token, abbrs_position_t pos) -> maybe_t { - auto result = abbrs_match(token, pos); - if (result.size() > 1) { - err(L"abbreviation expansion for %ls returned more than 1 result", token.c_str()); - } - if (result.empty()) { - return none(); - } - return *result.front().replacement; - }; - - auto cmd = abbrs_position_t::command; - if (abbr_expand_1(L"", cmd)) err(L"Unexpected success with empty abbreviation"); - if (abbr_expand_1(L"nothing", cmd)) err(L"Unexpected success with missing abbreviation"); - - auto mresult = abbr_expand_1(L"gc", cmd); - if (!mresult) err(L"Unexpected failure with gc abbreviation"); - if (*mresult != L"git checkout") err(L"Wrong abbreviation result for gc"); - - mresult = abbr_expand_1(L"foo", cmd); - if (!mresult) err(L"Unexpected failure with foo abbreviation"); - if (*mresult != L"bar") err(L"Wrong abbreviation result for foo"); - - maybe_t result; - auto expand_abbreviation_in_command = [](const wcstring &cmdline, - maybe_t cursor_pos = {}) -> maybe_t { - if (auto replacement = reader_expand_abbreviation_at_cursor( - cmdline, cursor_pos.value_or(cmdline.size()), parser_principal_parser()->deref())) { - wcstring cmdline_expanded = cmdline; - std::vector colors{cmdline_expanded.size()}; - auto ffi_colors = new_highlight_spec_list(); - for (auto &c : colors) { - ffi_colors->push(c); - } - cmdline_expanded = *apply_edit( - cmdline_expanded, *ffi_colors, - new_edit(replacement->range.start, replacement->range.end(), *replacement->text)); - colors.clear(); - for (size_t i = 0; i < ffi_colors->size(); i++) { - colors.push_back(ffi_colors->at(i)); - } - return cmdline_expanded; - } - return none_t(); - }; - result = expand_abbreviation_in_command(L"just a command", 3); - if (result) err(L"Command wrongly expanded on line %ld", (long)__LINE__); - result = expand_abbreviation_in_command(L"gc somebranch", 0); - if (!result) err(L"Command not expanded on line %ld", (long)__LINE__); - - result = expand_abbreviation_in_command(L"gc somebranch", const_strlen(L"gc")); - if (!result) err(L"gc not expanded"); - if (result != L"git checkout somebranch") - err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result->c_str()); - - // Space separation. - result = expand_abbreviation_in_command(L"gx somebranch", const_strlen(L"gc")); - if (!result) err(L"gx not expanded"); - if (result != L"git checkout somebranch") - err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result->c_str()); - - result = - expand_abbreviation_in_command(L"echo hi ; gc somebranch", const_strlen(L"echo hi ; g")); - if (!result) err(L"gc not expanded on line %ld", (long)__LINE__); - if (result != L"echo hi ; git checkout somebranch") - err(L"gc incorrectly expanded on line %ld", (long)__LINE__); - - result = expand_abbreviation_in_command(L"echo (echo (echo (echo (gc ", - const_strlen(L"echo (echo (echo (echo (gc")); - if (!result) err(L"gc not expanded on line %ld", (long)__LINE__); - if (result != L"echo (echo (echo (echo (git checkout ") - err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result->c_str()); - - // If commands should be expanded. - result = expand_abbreviation_in_command(L"if gc"); - if (!result) err(L"gc not expanded on line %ld", (long)__LINE__); - if (result != L"if git checkout") - err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result->c_str()); - - // Others should not be. - result = expand_abbreviation_in_command(L"of gc"); - if (result) err(L"gc incorrectly expanded on line %ld", (long)__LINE__); - - // Others should not be. - result = expand_abbreviation_in_command(L"command gc"); - if (result) err(L"gc incorrectly expanded on line %ld", (long)__LINE__); - - // yin/yang expands everywhere. - result = expand_abbreviation_in_command(L"command yin"); - if (!result) err(L"gc not expanded on line %ld", (long)__LINE__); - if (result != L"command yang") { - err(L"command yin incorrectly expanded on line %ld to '%ls'", (long)__LINE__, - result->c_str()); - } -} - // todo!("already ported, delete this") /// Testing colors. static void test_colors() { @@ -740,79 +632,6 @@ static void test_colors() { } // todo!("port this") -static void test_1_completion(wcstring line, const wcstring &completion, complete_flags_t flags, - bool append_only, wcstring expected, long source_line) { - // str is given with a caret, which we use to represent the cursor position. Find it. - const size_t in_cursor_pos = line.find(L'^'); - do_test(in_cursor_pos != wcstring::npos); - line.erase(in_cursor_pos, 1); - - const size_t out_cursor_pos = expected.find(L'^'); - do_test(out_cursor_pos != wcstring::npos); - expected.erase(out_cursor_pos, 1); - - size_t cursor_pos = in_cursor_pos; - wcstring result = - completion_apply_to_command_line(completion, flags, line, &cursor_pos, append_only); - if (result != expected) { - std::fwprintf(stderr, L"line %ld: %ls + %ls -> [%ls], expected [%ls]\n", source_line, - line.c_str(), completion.c_str(), result.c_str(), expected.c_str()); - } - do_test(result == expected); - do_test(cursor_pos == out_cursor_pos); -} - -// todo!("port this") -static void test_completion_insertions() { -#define TEST_1_COMPLETION(a, b, c, d, e) test_1_completion(a, b, c, d, e, __LINE__) - say(L"Testing completion insertions"); - TEST_1_COMPLETION(L"foo^", L"bar", 0, false, L"foobar ^"); - // An unambiguous completion of a token that is already trailed by a space character. - // After completing, the cursor moves on to the next token, suggesting to the user that the - // current token is finished. - TEST_1_COMPLETION(L"foo^ baz", L"bar", 0, false, L"foobar ^baz"); - TEST_1_COMPLETION(L"'foo^", L"bar", 0, false, L"'foobar' ^"); - TEST_1_COMPLETION(L"'foo'^", L"bar", 0, false, L"'foobar' ^"); - TEST_1_COMPLETION(L"'foo\\'^", L"bar", 0, false, L"'foo\\'bar' ^"); - TEST_1_COMPLETION(L"foo\\'^", L"bar", 0, false, L"foo\\'bar ^"); - - // Test append only. - TEST_1_COMPLETION(L"foo^", L"bar", 0, true, L"foobar ^"); - TEST_1_COMPLETION(L"foo^ baz", L"bar", 0, true, L"foobar ^baz"); - TEST_1_COMPLETION(L"'foo^", L"bar", 0, true, L"'foobar' ^"); - TEST_1_COMPLETION(L"'foo'^", L"bar", 0, true, L"'foo'bar ^"); - TEST_1_COMPLETION(L"'foo\\'^", L"bar", 0, true, L"'foo\\'bar' ^"); - TEST_1_COMPLETION(L"foo\\'^", L"bar", 0, true, L"foo\\'bar ^"); - - TEST_1_COMPLETION(L"foo^", L"bar", COMPLETE_NO_SPACE, false, L"foobar^"); - TEST_1_COMPLETION(L"'foo^", L"bar", COMPLETE_NO_SPACE, false, L"'foobar^"); - TEST_1_COMPLETION(L"'foo'^", L"bar", COMPLETE_NO_SPACE, false, L"'foobar'^"); - TEST_1_COMPLETION(L"'foo\\'^", L"bar", COMPLETE_NO_SPACE, false, L"'foo\\'bar^"); - TEST_1_COMPLETION(L"foo\\'^", L"bar", COMPLETE_NO_SPACE, false, L"foo\\'bar^"); - - TEST_1_COMPLETION(L"foo^", L"bar", COMPLETE_REPLACES_TOKEN, false, L"bar ^"); - TEST_1_COMPLETION(L"'foo^", L"bar", COMPLETE_REPLACES_TOKEN, false, L"bar ^"); - - // See #6130 - TEST_1_COMPLETION(L": (:^ ''", L"", 0, false, L": (: ^''"); -} - -// todo!("port this") -static void test_autosuggestion_combining() { - say(L"Testing autosuggestion combining"); - do_test(combine_command_and_autosuggestion(L"alpha", L"alphabeta") == L"alphabeta"); - - // When the last token contains no capital letters, we use the case of the autosuggestion. - do_test(combine_command_and_autosuggestion(L"alpha", L"ALPHABETA") == L"ALPHABETA"); - - // When the last token contains capital letters, we use its case. - do_test(combine_command_and_autosuggestion(L"alPha", L"alphabeTa") == L"alPhabeTa"); - - // If autosuggestion is not longer than input, use the input's case. - do_test(combine_command_and_autosuggestion(L"alpha", L"ALPHAA") == L"ALPHAA"); - do_test(combine_command_and_autosuggestion(L"alpha", L"ALPHA") == L"alpha"); -} - /// Helper for test_timezone_env_vars(). long return_timezone_hour(time_t tstamp, const wchar_t *timezone) { env_stack_t vars{parser_principal_parser()->deref().vars_boxed()}; @@ -1013,15 +832,12 @@ static const test_t s_tests[]{ {TEST_GROUP("str_to_num"), test_str_to_num}, {TEST_GROUP("enum"), test_enum_set}, {TEST_GROUP("enum"), test_enum_array}, - {TEST_GROUP("autosuggestion"), test_autosuggestion_combining}, - {TEST_GROUP("test_abbreviations"), test_abbreviations}, {TEST_GROUP("convert"), test_convert}, {TEST_GROUP("convert"), test_convert_private_use}, {TEST_GROUP("convert_ascii"), test_convert_ascii}, {TEST_GROUP("iothread"), test_iothread}, {TEST_GROUP("lru"), test_lru}, {TEST_GROUP("colors"), test_colors}, - {TEST_GROUP("completion_insertions"), test_completion_insertions}, {TEST_GROUP("maybe"), test_maybe}, {TEST_GROUP("normalize"), test_normalize_path}, {TEST_GROUP("dirname"), test_dirname_basename}, diff --git a/src/iothread.h b/src/iothread.h index 18dd80c4f..c0b42b50e 100644 --- a/src/iothread.h +++ b/src/iothread.h @@ -27,53 +27,6 @@ inline void iothread_perform_cantwait(std::function &&func) { iothread_perform_cantwait(callback); } -inline uint64_t debounce_perform(const debounce_t &debouncer, const std::function &func) { - std::shared_ptr callback = std::make_shared([=](const void *) { - func(); - return nullptr; - }); - - return debouncer.perform(callback); -} - -template -inline void debounce_perform_with_completion(const debounce_t &debouncer, std::function &&func, - std::function &&completion) { - std::shared_ptr callback2 = std::make_shared([=](const void *r) { - assert(r != nullptr && "callback1 result was null!"); - const R *result = (const R *)r; - completion(*result); - return nullptr; - }); - - std::shared_ptr callback1 = std::make_shared([=](const void *) { - const R *result = new R(func()); - callback2->cleanups.push_back([result]() { delete result; }); - return (void *)result; - }); - - debouncer.perform_with_completion(callback1, callback2); -} - -template -inline void debounce_perform_with_completion(const debounce_t &debouncer, std::function &&func, - std::function &&completion) { - std::shared_ptr callback2 = std::make_shared([=](const void *r) { - assert(r != nullptr && "callback1 result was null!"); - const R *result = (const R *)r; - completion(*result); - return nullptr; - }); - - std::shared_ptr callback1 = std::make_shared([=](const void *) { - const R *result = new R(func()); - callback2->cleanups.push_back([result]() { delete result; }); - return (void *)result; - }); - - debouncer.perform_with_completion(callback1, callback2); -} - inline bool make_detached_pthread(const std::function &func) { std::shared_ptr callback = std::make_shared([=](const void *) { func(); diff --git a/src/reader.cpp b/src/reader.cpp deleted file mode 100644 index 2a8e70314..000000000 --- a/src/reader.cpp +++ /dev/null @@ -1,4754 +0,0 @@ -// Functions for reading data from stdin and passing to the parser. If stdin is a keyboard, it -// supplies a killring, history, syntax highlighting, tab-completion and various other interactive -// features. -// -// Internally the interactive mode functions rely in the functions of the input library to read -// individual characters of input. -// -// Token search is handled incrementally. Actual searches are only done on when searching backwards, -// since the previous results are saved. The last search position is remembered and a new search -// continues from the last search position. All search results are saved in the list 'search_prev'. -// When the user searches forward, i.e. presses Alt-down, the list is consulted for previous search -// result, and subsequent backwards searches are also handled by consulting the list up until the -// end of the list is reached, at which point regular searching will commence. -#include "config.h" - -#include -#include -#include - -#include "history.rs.h" -#ifdef HAVE_SIGINFO_H -#include -#endif -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "abbrs.h" -#include "ast.h" -#include "callback.h" -#include "color.h" -#include "common.h" -#include "complete.h" -#include "env.h" -#include "env_dispatch.rs.h" -#include "event.h" -#include "exec.h" -#include "expand.h" -#include "fallback.h" // IWYU pragma: keep -#include "fd_readable_set.rs.h" -#include "fds.h" -#include "flog.h" -#include "function.h" -#include "global_safety.h" -#include "highlight.h" -#include "history.h" -#include "input_ffi.rs.h" -#include "io.h" -#include "iothread.h" -#include "kill.rs.h" -#include "operation_context.h" -#include "output.h" -#include "pager.h" -#include "parse_constants.h" -#include "parse_tree.h" -#include "parse_util.h" -#include "parser.h" -#include "proc.h" -#include "reader.h" -#include "screen.h" -#include "signals.h" -#include "termsize.h" -#include "tokenizer.h" -#include "wcstringutil.h" -#include "wildcard.h" -#include "wutil.h" // IWYU pragma: keep - -// Name of the variable that tells how long it took, in milliseconds, for the previous -// interactive command to complete. -#define ENV_CMD_DURATION L"CMD_DURATION" - -/// Maximum length of prefix string when printing completion list. Longer prefixes will be -/// ellipsized. -#define PREFIX_MAX_LEN 9 - -/// A simple prompt for reading shell commands that does not rely on fish specific commands, meaning -/// it will work even if fish is not installed. This is used by read_i. -#define DEFAULT_PROMPT L"echo -n \"$USER@$hostname $PWD \"'> '" - -/// The name of the function that prints the fish prompt. -#define LEFT_PROMPT_FUNCTION_NAME L"fish_prompt" - -/// The name of the function that prints the fish right prompt (RPROMPT). -#define RIGHT_PROMPT_FUNCTION_NAME L"fish_right_prompt" - -/// The name of the function to use in place of the left prompt if we're in the debugger context. -#define DEBUG_PROMPT_FUNCTION_NAME L"fish_breakpoint_prompt" - -/// The name of the function for getting the input mode indicator. -#define MODE_PROMPT_FUNCTION_NAME L"fish_mode_prompt" - -/// The default title for the reader. This is used by reader_readline. -#define DEFAULT_TITLE L"echo (status current-command) \" \" $PWD" - -/// The maximum number of characters to read from the keyboard without repainting. Note that this -/// readahead will only occur if new characters are available for reading, fish will never block for -/// more input without repainting. -static constexpr size_t READAHEAD_MAX = 256; - -/// A mode for calling the reader_kill function. In this mode, the new string is appended to the -/// current contents of the kill buffer. -#define KILL_APPEND 0 - -/// A mode for calling the reader_kill function. In this mode, the new string is prepended to the -/// current contents of the kill buffer. -#define KILL_PREPEND 1 - -enum class jump_direction_t { forward, backward }; -enum class jump_precision_t { till, to }; - -/// A singleton snapshot of the reader state. This is updated when the reader changes. This is -/// factored out for thread-safety reasons: it may be fetched on a background thread. -static acquired_lock commandline_state_snapshot() { - // Deliberately leaked to avoid shutdown dtors. - static owning_lock *const s_state = new owning_lock(); - return s_state->acquire(); -} - -commandline_state_t commandline_get_state() { - auto s = commandline_state_snapshot(); - - commandline_state_t state{}; - state.text = s->text; - state.cursor_pos = s->cursor_pos; - state.selection = s->selection; - if (s->history) { - state.history = (*s->history)->clone(); - } - state.pager_mode = s->pager_mode; - state.pager_fully_disclosed = s->pager_fully_disclosed; - state.search_mode = s->search_mode; - state.initialized = s->initialized; - - return state; -} - -HistorySharedPtr *commandline_get_state_history_ffi() { - auto state = commandline_get_state(); - if (!state.history) return nullptr; - return state.history->into_raw(); -} - -bool commandline_get_state_initialized_ffi() { return commandline_get_state().initialized; } -wcstring commandline_get_state_text_ffi() { - // return std::make_unique(commandline_get_state().text); - return std::move(commandline_get_state().text); -} - -void commandline_set_buffer(wcstring text, size_t cursor_pos) { - auto state = commandline_state_snapshot(); - state->cursor_pos = std::min(cursor_pos, text.size()); - state->text = std::move(text); -} - -void commandline_set_buffer_ffi(const wcstring &text, size_t cursor_pos) { - commandline_set_buffer(text, cursor_pos); -} - -/// Any time the contents of a buffer changes, we update the generation count. This allows for our -/// background threads to notice it and skip doing work that they would otherwise have to do. -static std::atomic s_generation; - -/// Helper to get the generation count -uint32_t read_generation_count() { return s_generation.load(std::memory_order_relaxed); } - -/// We try to ensure that syntax highlighting completes appropriately before executing what the user -/// typed. But we do not want it to block forever - e.g. it may hang on determining if an arbitrary -/// argument is a path. This is how long we'll wait (in milliseconds) before giving up and -/// performing a no-io syntax highlighting. See #7418, #5912. -static constexpr long kHighlightTimeoutForExecutionMs = 250; - -/// Get the debouncer for autosuggestions and background highlighting. -/// These are deliberately leaked to avoid shutdown dtor registration. -static debounce_t &debounce_autosuggestions() { - const long kAutosuggestTimeoutMs = 500; - static auto res = new_debounce_t(kAutosuggestTimeoutMs); - return *res; -} - -static debounce_t &debounce_highlighting() { - const long kHighlightTimeoutMs = 500; - static auto res = new_debounce_t(kHighlightTimeoutMs); - return *res; -} - -static debounce_t &debounce_history_pager() { - const long kHistoryPagerTimeoutMs = 500; - static auto res = new_debounce_t(kHistoryPagerTimeoutMs); - return *res; -} - -// Make the search case-insensitive unless we have an uppercase character. -static history_search_flags_t smartcase_flags(const wcstring &query) { - return query == wcstolower(query) ? history_search_ignore_case : 0; -} - -namespace { - -/// Encapsulation of the reader's history search functionality. -class reader_history_search_t { - public: - enum mode_t { - inactive, // no search - line, // searching by line - prefix, // searching by prefix - token // searching by token - }; - - struct match_t { - /// The text of the match. - wcstring text; - /// The offset of the current search string in this match. - size_t offset; - }; - - private: - /// The type of search performed. - mode_t mode_{inactive}; - - /// Our history search itself. - maybe_t> search_; - - /// The ordered list of matches. This may grow long. - std::vector matches_; - - /// A set of new items to skip, corresponding to matches_ and anything added in skip(). - std::set skips_; - - /// Index into our matches list. - size_t match_index_{0}; - - /// The offset of the current token in the command line. Only non-zero for a token search. - size_t token_offset_{0}; - - /// Adds the given match if we haven't seen it before. - void add_if_new(match_t match) { - if (add_skip(match.text)) { - matches_.push_back(std::move(match)); - } - } - - /// Attempt to append matches from the current history item. - /// \return true if something was appended. - bool append_matches_from_search() { - auto find = [this](const wcstring &haystack, const wcstring &needle) { - if ((*search_)->ignores_case()) { - return ifind(haystack, needle); - } - return haystack.find(needle); - }; - const size_t before = matches_.size(); - auto text = *(*search_)->current_string(); - const wcstring &needle = search_string(); - if (mode_ == line || mode_ == prefix) { - size_t offset = find(text, needle); - // FIXME: Previous versions asserted out if this wasn't true. - // This could be hit with a needle of "ö" and haystack of "echo Ö" - // I'm not sure why - this points to a bug in ifind (probably wrong locale?) - // However, because the user experience of having it crash is horrible, - // and the worst thing that can otherwise happen here is that a search is unsuccessful, - // we just check it instead. - if (offset != wcstring::npos) { - add_if_new({std::move(text), offset}); - } - } else if (mode_ == token) { - auto tok = new_tokenizer(text.c_str(), TOK_ACCEPT_UNFINISHED); - - std::vector local_tokens; - while (auto token = tok->next()) { - if (token->type_ != token_type_t::string) continue; - wcstring text = *tok->text_of(*token); - size_t offset = find(text, needle); - if (offset != wcstring::npos) { - local_tokens.push_back({std::move(text), offset}); - } - } - - // Make sure tokens are added in reverse order. See #5150 - for (auto i = local_tokens.rbegin(); i != local_tokens.rend(); ++i) { - add_if_new(std::move(*i)); - } - } - return matches_.size() > before; - } - - bool move_forwards() { - // Try to move within our previously discovered matches. - if (match_index_ > 0) { - match_index_--; - return true; - } - return false; - } - - bool move_backwards() { - // Try to move backwards within our previously discovered matches. - if (match_index_ + 1 < matches_.size()) { - match_index_++; - return true; - } - - // Add more items from our search. - while ((*search_)->go_to_next_match(history_search_direction_t::Backward)) { - if (append_matches_from_search()) { - match_index_++; - assert(match_index_ < matches_.size() && "Should have found more matches"); - return true; - } - } - - // Here we failed to go backwards past the last history item. - return false; - } - - public: - reader_history_search_t() = default; - ~reader_history_search_t() = default; - - bool active() const { return mode_ != inactive; } - - bool by_token() const { return mode_ == token; } - - bool by_line() const { return mode_ == line; } - - bool by_prefix() const { return mode_ == prefix; } - - /// Move the history search in the given direction \p dir. - bool move_in_direction(history_search_direction_t dir) { - return dir == history_search_direction_t::Forward ? move_forwards() : move_backwards(); - } - - /// Go to the beginning (earliest) of the search. - void go_to_beginning() { - if (matches_.empty()) return; - match_index_ = matches_.size() - 1; - } - - /// Go to the end (most recent) of the search. - void go_to_end() { match_index_ = 0; } - - /// \return the current search result. - const match_t ¤t_result() const { - assert(match_index_ < matches_.size() && "Invalid match index"); - return matches_.at(match_index_); - } - - /// \return the string we are searching for. - const wcstring search_string() const { return *(*search_)->original_term(); } - - /// \return the range of the original search string in the new command line. - maybe_t search_range_if_active() const { - if (!active() || is_at_end()) { - return {}; - } - return {{static_cast(token_offset_ + current_result().offset), - static_cast(search_string().length())}}; - } - - /// \return whether we are at the end (most recent) of our search. - bool is_at_end() const { return match_index_ == 0; } - - // Add an item to skip. - // \return true if it was added, false if already present. - bool add_skip(const wcstring &str) { return skips_.insert(str).second; } - - /// Reset, beginning a new line or token mode search. - void reset_to_mode(const wcstring &text, const HistorySharedPtr &hist, mode_t mode, - size_t token_offset) { - assert(mode != inactive && "mode cannot be inactive in this setter"); - skips_ = {text}; - matches_ = {{text, 0}}; - match_index_ = 0; - mode_ = mode; - token_offset_ = token_offset; - history_search_flags_t flags = history_search_no_dedup | smartcase_flags(text); - // We can skip dedup in history_search_t because we do it ourselves in skips_. - search_ = rust_history_search_new( - hist, text.c_str(), - by_prefix() ? history_search_type_t::Prefix : history_search_type_t::Contains, flags, - 0); - } - - /// Reset to inactive search. - void reset() { - matches_.clear(); - skips_.clear(); - match_index_ = 0; - mode_ = inactive; - token_offset_ = 0; - search_ = maybe_t>{}; - } -}; - -/// The result of an autosuggestion computation. -struct autosuggestion_t { - // The text to use, as an extension of the command line. - wcstring text{}; - - // The string which was searched for. - wcstring search_string{}; - - // The list of completions which may need loading. - std::vector needs_load{}; - - // Whether the autosuggestion should be case insensitive. - // This is true for file-generated autosuggestions, but not for history. - bool icase{false}; - - // Clear our contents. - void clear() { - text.clear(); - search_string.clear(); - } - - // \return whether we have empty text. - bool empty() const { return text.empty(); } - - autosuggestion_t() = default; - autosuggestion_t(wcstring text, wcstring search_string, bool icase) - : text(std::move(text)), search_string(std::move(search_string)), icase(icase) {} -}; - -struct highlight_result_t { - std::vector colors; - wcstring text; -}; - -struct history_pager_result_t { - rust::Box matched_commands; - size_t final_index; - bool have_more_results; -}; - -/// readline_loop_state_t encapsulates the state used in a readline loop. -/// It is always stack allocated transient. This state should not be "publicly visible"; public -/// state should be in reader_data_t. -struct readline_loop_state_t { - /// The last command that was executed. - maybe_t last_cmd{}; - - /// If the last command was a yank, the length of yanking that occurred. - size_t yank_len{0}; - - /// If the last "complete" readline command has inserted text into the command line. - bool complete_did_insert{true}; - - /// List of completions. - rust::Box comp = new_completion_list(); - - /// Whether the loop has finished, due to reaching the character limit or through executing a - /// command. - bool finished{false}; - - /// Maximum number of characters to read. - size_t nchars{std::numeric_limits::max()}; -}; - -} // namespace - -/// Data wrapping up the visual selection. -namespace { -struct selection_data_t { - /// The position of the cursor when selection was initiated. - size_t begin{0}; - - /// The start and stop position of the current selection. - size_t start{0}; - size_t stop{0}; - - bool operator==(const selection_data_t &rhs) const { - return begin == rhs.begin && start == rhs.start && stop == rhs.stop; - } - - bool operator!=(const selection_data_t &rhs) const { return !(*this == rhs); } -}; - -/// A value-type struct representing a layout that can be rendered. -/// The intent is that everything we send to the screen is encapsulated in this struct. -struct layout_data_t { - /// Text of the command line. - wcstring text{}; - - /// The colors. This has the same length as 'text'. - std::vector colors{}; - - /// Position of the cursor in the command line. - size_t position{}; - - /// Whether the cursor is focused on the pager or not. - bool focused_on_pager{false}; - - /// Visual selection of the command line, or none if none. - maybe_t selection{}; - - /// String containing the autosuggestion. - wcstring autosuggestion{}; - - /// The matching range of the command line from a history search. If non-empty, then highlight - /// the range within the text. - maybe_t history_search_range{}; - - /// The result of evaluating the left, mode and right prompt commands. - /// That is, this the text of the prompts, not the commands to produce them. - wcstring left_prompt_buff{}; - wcstring mode_prompt_buff{}; - wcstring right_prompt_buff{}; -}; -} // namespace - -/// A struct describing the state of the interactive reader. These states can be stacked, in case -/// reader_readline() calls are nested. This happens when the 'read' builtin is used. -class reader_data_t : public std::enable_shared_from_this { - public: - /// Configuration for the reader. - reader_config_t conf; - /// The parser being used. - rust::Box parser_ref; - /// String containing the whole current commandline. - rust::Box command_line_box; - editable_line_t &command_line; - /// Whether the most recent modification to the command line was done by either history search - /// or a pager selection change. When this is true and another transient change is made, the - /// old transient change will be removed from the undo history. - bool command_line_has_transient_edit = false; - /// The most recent layout data sent to the screen. - layout_data_t rendered_layout; - /// The current autosuggestion. - autosuggestion_t autosuggestion; - /// Current pager. - rust::Box pager_box = new_pager(); - pager_t &pager = *pager_box; - /// The output of the pager. - rust::Box current_page_rendering = new_page_rendering(); - /// When backspacing, we temporarily suppress autosuggestions. - bool suppress_autosuggestion{false}; - - /// HACK: A flag to reset the loop state from the outside. - bool reset_loop_state{false}; - - /// Whether this is the first prompt. - bool first_prompt{true}; - - /// The time when the last flash() completed - std::chrono::time_point last_flash; - - /// The representation of the current screen contents. - rust::Box screen; - - /// The source of input events. - rust::Box inputter; - /// The history. - maybe_t> history{}; - /// The history search. - reader_history_search_t history_search{}; - /// Whether the in-pager history search is active. - bool history_pager_active{false}; - /// The direction of the last successful history pager search. - history_search_direction_t history_pager_direction{}; - /// The range in history covered by the history pager's current page. - size_t history_pager_history_index_start{static_cast(-1)}; - size_t history_pager_history_index_end{static_cast(-1)}; - - /// The cursor selection mode. - cursor_selection_mode_t cursor_selection_mode{cursor_selection_mode_t::exclusive}; - - /// The selection data. If this is not none, then we have an active selection. - maybe_t selection{}; - - wcstring left_prompt_buff; - wcstring mode_prompt_buff; - /// The output of the last evaluation of the right prompt command. - wcstring right_prompt_buff; - - /// When navigating the pager, we modify the command line. - /// This is the saved command line before modification. - wcstring cycle_command_line; - size_t cycle_cursor_pos{0}; - - /// If set, a key binding or the 'exit' command has asked us to exit our read loop. - bool exit_loop_requested{false}; - /// If this is true, exit reader even if there are running jobs. This happens if we press e.g. - /// ^D twice. - bool did_warn_for_bg_jobs{false}; - /// The current contents of the top item in the kill ring. - wcstring kill_item; - - /// A flag which may be set to force re-execing all prompts and re-rendering. - /// This may come about when a color like $fish_color... has changed. - bool force_exec_prompt_and_repaint{false}; - - /// The target character of the last jump command. - wchar_t last_jump_target{0}; - jump_direction_t last_jump_direction{jump_direction_t::forward}; - jump_precision_t last_jump_precision{jump_precision_t::to}; - - /// The text of the most recent asynchronous highlight and autosuggestion requests. - /// If these differs from the text of the command line, then we must kick off a new request. - wcstring in_flight_highlight_request; - wcstring in_flight_autosuggest_request; - - bool is_navigating_pager_contents() const { - return this->pager.is_navigating_contents() || history_pager_active; - } - - /// The line that is currently being edited. Typically the command line, but may be the search - /// field. - const editable_line_t *active_edit_line() const { - if (this->is_navigating_pager_contents() && this->pager.is_search_field_shown()) { - return this->pager.search_field_line(); - } - return &this->command_line; - } - - editable_line_t *active_edit_line() { - auto cthis = reinterpret_cast(this); - return const_cast(cthis->active_edit_line()); - } - - /// Do what we need to do whenever our command line changes. - void command_line_changed(const editable_line_t *el); - void maybe_refilter_pager(const editable_line_t *el); - using history_pager_invocation_t = HistoryPagerInvocation; - void fill_history_pager( - history_pager_invocation_t why, - history_search_direction_t direction = history_search_direction_t::Backward); - - /// Do what we need to do whenever our pager selection changes. - void pager_selection_changed(); - - /// Expand abbreviations at the current cursor position, minus cursor_backtrack. - bool expand_abbreviation_at_cursor(size_t cursor_backtrack); - - /// \return true if the command line has changed and repainting is needed. If \p colors is not - /// null, then also return true if the colors have changed. - using highlight_list_t = std::vector; - bool is_repaint_needed(const highlight_list_t *mcolors = nullptr) const; - - /// Generate a new layout data from the current state of the world. - /// If \p mcolors has a value, then apply it; otherwise extend existing colors. - layout_data_t make_layout_data() const; - - /// Generate a new layout data from the current state of the world, and paint with it. - /// If \p mcolors has a value, then apply it; otherwise extend existing colors. - void layout_and_repaint(const wchar_t *reason) { - this->rendered_layout = make_layout_data(); - paint_layout(reason); - } - - /// Paint the last rendered layout. - /// \p reason is used in FLOG to explain why. - void paint_layout(const wchar_t *reason); - - /// Return the variable set used for e.g. command duration. - // todo!("should return a reference") - env_stack_t vars() const { return env_stack_t{parser_ref->deref().vars_boxed()}; } - - /// Access the parser. - const parser_t &parser() const { return parser_ref->deref(); } - - /// Convenience cover over exec_count(). - uint64_t exec_count() const { return parser().libdata().exec_count(); } - - reader_data_t(rust::Box parser, HistorySharedPtr &hist, reader_config_t &&conf) - : conf(std::move(conf)), - parser_ref(std::move(parser)), - command_line_box(new_editable_line()), - command_line(*command_line_box), - screen(new_screen()), - inputter(make_inputter(*parser_ref, conf.in)), - history(hist.clone()) {} - - void update_buff_pos(editable_line_t *el, maybe_t new_pos = none_t()); - - void kill(editable_line_t *el, size_t begin_idx, size_t length, int mode, int newv); - /// Inserts a substring of str given by start, len at the cursor position. - void insert_string(editable_line_t *el, const wcstring &str); - /// Erase @length characters starting at @offset. - void erase_substring(editable_line_t *el, size_t offset, size_t length); - /// Replace the text of length @length at @offset by @replacement. - void replace_substring(editable_line_t *el, size_t offset, size_t length, wcstring replacement); - void push_edit(editable_line_t *el, rust::Box &&edit); - - /// Insert the character into the command line buffer and print it to the screen using syntax - /// highlighting, etc. - void insert_char(editable_line_t *el, wchar_t c) { insert_string(el, wcstring{c}); } - - /// Read a command to execute, respecting input bindings. - /// \return the command, or none if we were asked to cancel (e.g. SIGHUP). - maybe_t readline(int nchars); - - /// Reflect our current data in the command line state snapshot. - /// This is called before we run any fish script, so that the commandline builtin can see our - /// state. - void update_commandline_state() const; - - /// Apply any changes from the reader snapshot. This is called after running fish script, - /// incorporating changes from the commandline builtin. - void apply_commandline_state_changes(); - - /// Compute completions and update the pager and/or commandline as needed. - void compute_and_apply_completions(readline_cmd_t c, readline_loop_state_t &rls); - - void move_word(editable_line_t *el, bool move_right, bool erase, move_word_style_t style, - bool newv); - - void run_input_command_scripts(const std::vector &cmds); - maybe_t> read_normal_chars(readline_loop_state_t &rls); - void handle_readline_command(readline_cmd_t cmd, readline_loop_state_t &rls); - - // Handle readline_cmd_t::Execute. This may mean inserting a newline if the command is - // unfinished. It may also set 'finished' and 'cmd' inside the rls. - // \return true on success, false if we got an error, in which case the caller should fire the - // error event. - bool handle_execute(readline_loop_state_t &rls); - - // Add the current command line contents to history. - void add_to_history(); - - // Expand abbreviations before execution. - // Replace the command line with any abbreviations as needed. - // \return the test result, which may be incomplete to insert a newline, or an error. - parser_test_error_bits_t expand_for_execute(); - - void clear_pager(); - void select_completion_in_direction(selection_motion_t dir, - bool force_selection_change = false); - void flash(); - - maybe_t get_selection() const; - - void completion_insert(const wcstring &val, size_t token_end, complete_flags_t flags); - - bool can_autosuggest() const; - void autosuggest_completed(autosuggestion_t result); - void update_autosuggestion(); - void accept_autosuggestion(bool full, bool single = false, - move_word_style_t style = move_word_style_t::Punctuation); - void super_highlight_me_plenty(); - - /// Finish up any outstanding syntax highlighting, before execution. - /// This plays some tricks to not block on I/O for too long. - void finish_highlighting_before_exec(); - - void highlight_complete(highlight_result_t result); - void exec_mode_prompt(); - void exec_prompt(); - - bool jump(jump_direction_t dir, jump_precision_t precision, editable_line_t *el, - wchar_t target); - - bool handle_completions(const completion_list_t &comp, size_t token_begin, size_t token_end); - - void set_command_line_and_position(editable_line_t *el, wcstring &&new_str, size_t pos); - void clear_transient_edit(); - void replace_current_token(wcstring &&new_token); - void update_command_line_from_history_search(); - void set_buffer_maintaining_pager(const wcstring &b, size_t pos, bool transient = false); - void delete_char(bool backward = true); - - /// Called to update the termsize, including $COLUMNS and $LINES, as necessary. - void update_termsize() { termsize_update(parser()); } - - // Import history from older location (config path) if our current history is empty. - void import_history_if_necessary(); -}; - -// Prototypes for a bunch of functions defined later on. -static bool is_backslashed(const wcstring &str, size_t pos); -static wchar_t unescaped_quote(const wcstring &str, size_t pos); - -/// Mode on startup, which we restore on exit. -static struct termios terminal_mode_on_startup; - -/// Mode we use to execute programs. -static struct termios tty_modes_for_external_cmds; - -/// Restore terminal settings we care about, to prevent a broken shell. -static void term_fix_modes(struct termios *modes) { - modes->c_iflag &= ~ICRNL; // disable mapping CR (\cM) to NL (\cJ) - modes->c_iflag &= ~INLCR; // disable mapping NL (\cJ) to CR (\cM) - modes->c_lflag &= ~ICANON; // turn off canonical mode - modes->c_lflag &= ~ECHO; // turn off echo mode - modes->c_lflag &= ~IEXTEN; // turn off handling of discard and lnext characters - modes->c_oflag |= OPOST; // turn on "implementation-defined post processing" - this often - // changes how line breaks work. - modes->c_oflag |= ONLCR; // "translate newline to carriage return-newline" - without - // you see staircase output. - - modes->c_cc[VMIN] = 1; - modes->c_cc[VTIME] = 0; - - unsigned char disabling_char = '\0'; - // Prefer to use _POSIX_VDISABLE to disable control functions. - // This permits separately binding nul (typically control-space). - // POSIX calls out -1 as a special value which should be ignored. -#ifdef _POSIX_VDISABLE - if (_POSIX_VDISABLE != -1) disabling_char = _POSIX_VDISABLE; -#endif - - // We ignore these anyway, so there is no need to sacrifice a character. - modes->c_cc[VSUSP] = disabling_char; - modes->c_cc[VQUIT] = disabling_char; -} - -static void term_fix_external_modes(struct termios *modes) { - // Turning off OPOST or ONLCR breaks output (staircase effect), we don't allow it. - // See #7133. - modes->c_oflag |= OPOST; - modes->c_oflag |= ONLCR; - // These cause other ridiculous behaviors like input not being shown. - modes->c_lflag |= ICANON; - modes->c_lflag |= IEXTEN; - modes->c_lflag |= ECHO; - modes->c_iflag |= ICRNL; - modes->c_iflag &= ~INLCR; -} -/// A description of where fish is in the process of exiting. -enum class exit_state_t { - none, /// fish is not exiting. - running_handlers, /// fish intends to exit, and is running handlers like 'fish_exit'. - finished_handlers, /// fish is finished running handlers and no more fish script may be run. -}; -static relaxed_atomic_t s_exit_state{exit_state_t::none}; - -static void redirect_tty_after_sighup() { - // If we have received SIGHUP, redirect the tty to avoid a user script triggering SIGTTIN or - // SIGTTOU. - assert(reader_received_sighup() && "SIGHUP not received"); - static bool s_tty_redirected = false; - if (!s_tty_redirected) { - s_tty_redirected = true; - redirect_tty_output(); - } -} - -/// Give up control of terminal. -static void term_donate(bool quiet = false) { - while (tcsetattr(STDIN_FILENO, TCSANOW, &tty_modes_for_external_cmds) == -1) { - if (errno == EIO) redirect_tty_output(); - if (errno != EINTR) { - if (!quiet) { - FLOGF(warning, _(L"Could not set terminal mode for new job")); - wperror(L"tcsetattr"); - } - break; - } - } -} - -/// Copy the (potentially changed) terminal modes and use them from now on. -void term_copy_modes() { - struct termios modes; - tcgetattr(STDIN_FILENO, &modes); - std::memcpy(&tty_modes_for_external_cmds, &modes, sizeof tty_modes_for_external_cmds); - term_fix_external_modes(&tty_modes_for_external_cmds); - - // Copy flow control settings to shell modes. - if (tty_modes_for_external_cmds.c_iflag & IXON) { - shell_modes.c_iflag |= IXON; - } else { - shell_modes.c_iflag &= ~IXON; - } - if (tty_modes_for_external_cmds.c_iflag & IXOFF) { - shell_modes.c_iflag |= IXOFF; - } else { - shell_modes.c_iflag &= ~IXOFF; - } -} - -/// Grab control of terminal. -static void term_steal() { - term_copy_modes(); - while (tcsetattr(STDIN_FILENO, TCSANOW, &shell_modes) == -1) { - if (errno == EIO) redirect_tty_output(); - if (errno != EINTR) { - FLOGF(warning, _(L"Could not set terminal mode for shell")); - perror("tcsetattr"); - break; - } - } - - termsize_invalidate_tty(); -} - -bool fish_is_unwinding_for_exit() { - switch (s_exit_state) { - case exit_state_t::none: - // Cancel if we got SIGHUP. - return reader_received_sighup(); - case exit_state_t::running_handlers: - // We intend to exit but we want to allow these handlers to run. - return false; - case exit_state_t::finished_handlers: - // Done running exit handlers, time to exit. - return true; - } - DIE("Unreachable"); -} - -/// Given a command line and an autosuggestion, return the string that gets shown to the user. -wcstring combine_command_and_autosuggestion(const wcstring &cmdline, - const wcstring &autosuggestion) { - // We want to compute the full line, containing the command line and the autosuggestion They may - // disagree on whether characters are uppercase or lowercase Here we do something funny: if the - // last token of the command line contains any uppercase characters, we use its case. Otherwise - // we use the case of the autosuggestion. This is an idea from issue #335. - wcstring full_line; - if (autosuggestion.size() <= cmdline.size() || cmdline.empty()) { - // No or useless autosuggestion, or no command line. - full_line = cmdline; - } else if (string_prefixes_string(cmdline, autosuggestion)) { - // No case disagreements, or no extra characters in the autosuggestion. - full_line = autosuggestion; - } else { - // We have an autosuggestion which is not a prefix of the command line, i.e. a case - // disagreement. Decide whose case we want to use. - const wchar_t *begin = nullptr, *cmd = cmdline.c_str(); - parse_util_token_extent(cmd, cmdline.size() - 1, &begin, nullptr, nullptr, nullptr); - bool last_token_contains_uppercase = false; - if (begin) { - const wchar_t *end = begin + std::wcslen(begin); - last_token_contains_uppercase = (std::find_if(begin, end, iswupper) != end); - } - if (!last_token_contains_uppercase) { - // Use the autosuggestion's case. - full_line = autosuggestion; - } else { - // Use the command line case for its characters, then append the remaining characters in - // the autosuggestion. Note that we know that autosuggestion.size() > cmdline.size() due - // to the first test above. - full_line = cmdline; - full_line.append(autosuggestion, cmdline.size(), - autosuggestion.size() - cmdline.size()); - } - } - return full_line; -} - -/// Update the cursor position. -void reader_data_t::update_buff_pos(editable_line_t *el, maybe_t new_pos) { - if (new_pos.has_value()) { - el->set_position(*new_pos); - } - size_t buff_pos = el->position(); - if (el == &command_line && selection.has_value()) { - if (selection->begin <= buff_pos) { - selection->start = selection->begin; - selection->stop = - buff_pos + (cursor_selection_mode == cursor_selection_mode_t::inclusive ? 1 : 0); - } else { - selection->start = buff_pos; - selection->stop = selection->begin + - (cursor_selection_mode == cursor_selection_mode_t::inclusive ? 1 : 0); - } - } -} - -bool reader_data_t::is_repaint_needed(const std::vector *mcolors) const { - // Note: this function is responsible for detecting all of the ways that the command line may - // change, by comparing it to what is present in rendered_layout. - // The pager is the problem child, it has its own update logic. - auto check = [](bool val, const wchar_t *reason) { - if (val) FLOG(reader_render, L"repaint needed because", reason, L"change"); - return val; - }; - - bool focused_on_pager = active_edit_line() == pager.search_field_line(); - const layout_data_t &last = this->rendered_layout; - return check(force_exec_prompt_and_repaint, L"forced") || - check(*command_line.text() != last.text, L"text") || - check(mcolors && *mcolors != last.colors, L"highlight") || - check(selection != last.selection, L"selection") || - check(focused_on_pager != last.focused_on_pager, L"focus") || - check(command_line.position() != last.position, L"position") || - check(history_search.search_range_if_active() != last.history_search_range, - L"history search") || - check(autosuggestion.text != last.autosuggestion, L"autosuggestion") || - check(left_prompt_buff != last.left_prompt_buff, L"left_prompt") || - check(mode_prompt_buff != last.mode_prompt_buff, L"mode_prompt") || - check(right_prompt_buff != last.right_prompt_buff, L"right_prompt") || - check(pager.rendering_needs_update(*current_page_rendering), L"pager"); -} - -layout_data_t reader_data_t::make_layout_data() const { - layout_data_t result{}; - bool focused_on_pager = active_edit_line() == pager.search_field_line(); - result.text = *command_line.text(); - for (auto &color : editable_line_colors(command_line)) { - result.colors.push_back(color); - } - assert(result.text.size() == result.colors.size()); - result.position = focused_on_pager ? pager.cursor_position() : command_line.position(); - result.selection = selection; - result.focused_on_pager = (active_edit_line() == pager.search_field_line()); - result.history_search_range = history_search.search_range_if_active(); - result.autosuggestion = autosuggestion.text; - result.left_prompt_buff = left_prompt_buff; - result.mode_prompt_buff = mode_prompt_buff; - result.right_prompt_buff = right_prompt_buff; - return result; -} - -void reader_data_t::paint_layout(const wchar_t *reason) { - FLOGF(reader_render, L"Repainting from %ls", reason); - const layout_data_t &data = this->rendered_layout; - const editable_line_t *cmd_line = &command_line; - - wcstring full_line; - if (conf.in_silent_mode) { - full_line = wcstring(cmd_line->text()->length(), get_obfuscation_read_char()); - } else { - // Combine the command and autosuggestion into one string. - full_line = combine_command_and_autosuggestion(*cmd_line->text(), autosuggestion.text); - } - - // Copy the colors and extend them with autosuggestion color. - std::vector colors = data.colors; - - // Highlight any history search. - if (!conf.in_silent_mode && data.history_search_range) { - // std::min gets confused about types here. - size_t end = data.history_search_range->end(); - if (colors.size() < end) { - end = colors.size(); - } - - for (size_t i = data.history_search_range->start; i < end; i++) { - colors.at(i)->background = highlight_role_t::search_match; - } - } - - // Apply any selection. - if (data.selection.has_value()) { - highlight_spec_t selection_color = {highlight_role_t::selection, - highlight_role_t::selection}; - auto end = std::min(selection->stop, colors.size()); - for (size_t i = data.selection->start; i < end; i++) { - colors.at(i) = selection_color; - } - } - - // Extend our colors with the autosuggestion. - colors.resize(full_line.size(), highlight_role_t::autosuggestion); - - // Compute the indentation, then extend it with 0s for the autosuggestion. The autosuggestion - // always conceptually has an indent of 0. - std::vector indents = parse_util_compute_indents(*cmd_line->text()); - indents.resize(full_line.size(), 0); - - auto ffi_colors = new_highlight_spec_list(); - for (auto color : colors) ffi_colors->push(color); - // Prepend the mode prompt to the left prompt. - screen->write(mode_prompt_buff + left_prompt_buff, right_prompt_buff, full_line, - cmd_line->size(), *ffi_colors, indents, data.position, parser().vars_boxed(), - pager, *current_page_rendering, data.focused_on_pager); -} - -/// Internal helper function for handling killing parts of text. -void reader_data_t::kill(editable_line_t *el, size_t begin_idx, size_t length, int mode, int newv) { - wcstring text = *el->text(); - const wchar_t *begin = text.c_str() + begin_idx; - if (newv) { - kill_item = wcstring(begin, length); - kill_add(kill_item); - } else { - wcstring old = kill_item; - if (mode == KILL_APPEND) { - kill_item.append(begin, length); - } else { - kill_item = wcstring(begin, length); - kill_item.append(old); - } - - kill_replace(old, kill_item); - } - erase_substring(el, begin_idx, length); -} - -/// Make sure buffers are large enough to hold the current string length. -void reader_data_t::command_line_changed(const editable_line_t *el) { - ASSERT_IS_MAIN_THREAD(); - if (el == &this->command_line) { - // Update the gen count. - s_generation.store(1 + read_generation_count(), std::memory_order_relaxed); - } else if (el == this->pager.search_field_line()) { - if (history_pager_active) { - fill_history_pager(history_pager_invocation_t::Anew, - history_search_direction_t::Backward); - return; - } - this->pager.refilter_completions(); - this->pager_selection_changed(); - } - // Ensure that the commandline builtin sees our new state. - update_commandline_state(); -} - -void reader_data_t::maybe_refilter_pager(const editable_line_t *el) { - if (el == this->pager.search_field_line()) { - command_line_changed(el); - } -} - -static history_pager_result_t history_pager_search(const HistorySharedPtr &history, - history_search_direction_t direction, - size_t history_index, - const wcstring &search_string) { - // Limit the number of elements to half the screen like we do for completions - // Note that this is imperfect because we could have a multi-column layout. - // - // We can still push fish further upward in case the first entry is multiline, - // but that can't really be helped. - // (subtract 2 for the search line and the prompt) - size_t page_size = std::max(termsize_last().height / 2 - 2, (rust::isize)12); - - rust::Box completions = new_completion_list(); - - rust::Box search = - rust_history_search_new(history, search_string.c_str(), history_search_type_t::ContainsGlob, - smartcase_flags(search_string), history_index); - bool next_match_found = search->go_to_next_match(direction); - - if (!next_match_found && !parse_util_contains_wildcards(search_string)) { - // If there were no matches, and the user is not intending for - // wildcard search, try again with subsequence search. - search = rust_history_search_new(history, search_string.c_str(), - history_search_type_t::ContainsSubsequence, - smartcase_flags(search_string), history_index); - next_match_found = search->go_to_next_match(direction); - } - - while (completions->size() < page_size && next_match_found) { - const history_item_t &item = search->current_item(); - completions->push_back(*new_completion_with( - *item.str(), L"", - COMPLETE_REPLACES_COMMANDLINE | COMPLETE_DONT_ESCAPE | COMPLETE_DONT_SORT)); - - next_match_found = search->go_to_next_match(direction); - } - size_t last_index = search->current_index(); - if (direction == history_search_direction_t::Forward) { - completions->reverse(); - } - return {std::move(completions), last_index, search->go_to_next_match(direction)}; -} - -void reader_data_t::fill_history_pager(history_pager_invocation_t why, - history_search_direction_t direction) { - size_t index = -1; - maybe_t old_pager_index; - switch (why) { - case history_pager_invocation_t::Anew: - assert(direction == history_search_direction_t::Backward); - index = 0; - break; - case history_pager_invocation_t::Advance: - if (direction == history_search_direction_t::Forward) { - index = history_pager_history_index_start; - } else { - assert(direction == history_search_direction_t::Backward); - index = history_pager_history_index_end; - } - break; - case history_pager_invocation_t::Refresh: - // Redo the previous search previous direction. - direction = history_pager_direction; - index = history_pager_history_index_start; - old_pager_index = pager.selected_completion_index(); - break; - } - const wcstring search_term = *pager.search_field_line()->text(); - auto shared_this = this->shared_from_this(); - std::function func = [=]() { - return history_pager_search(**shared_this->history, direction, index, search_term); - }; - std::function completion = - [=](const history_pager_result_t &result) { - if (search_term != *shared_this->pager.search_field_line()->text()) - return; // Stale request. - if (result.matched_commands->empty() && why == history_pager_invocation_t::Advance) { - // No more matches, keep the existing ones and flash. - shared_this->flash(); - return; - } - history_pager_direction = direction; - if (direction == history_search_direction_t::Forward) { - shared_this->history_pager_history_index_start = result.final_index; - shared_this->history_pager_history_index_end = index; - } else { - shared_this->history_pager_history_index_start = index; - shared_this->history_pager_history_index_end = result.final_index; - } - shared_this->pager.set_extra_progress_text( - result.have_more_results ? _(L"Search again for more results") : L""); - shared_this->pager.set_completions(*result.matched_commands, false); - if (why == history_pager_invocation_t::Refresh) { - pager.set_selected_completion_index(*old_pager_index); - pager_selection_changed(); - } else { - shared_this->select_completion_in_direction(selection_motion_t::next, true); - } - shared_this->super_highlight_me_plenty(); - shared_this->layout_and_repaint(L"history-pager"); - }; - auto &debouncer = debounce_history_pager(); - debounce_perform_with_completion(debouncer, std::move(func), std::move(completion)); -} - -void reader_data_t::pager_selection_changed() { - ASSERT_IS_MAIN_THREAD(); - - const completion_t *completion = this->pager.selected_completion(*this->current_page_rendering); - - // Update the cursor and command line. - size_t cursor_pos = this->cycle_cursor_pos; - wcstring new_cmd_line; - - if (completion == nullptr) { - new_cmd_line = this->cycle_command_line; - } else { - new_cmd_line = - completion_apply_to_command_line(*completion->completion(), completion->flags(), - this->cycle_command_line, &cursor_pos, false); - } - - // Only update if something changed, to avoid useless edits in the undo history. - if (new_cmd_line != *command_line.text()) { - set_buffer_maintaining_pager(new_cmd_line, cursor_pos, true /* transient */); - } -} - -/// Expand an abbreviation replacer, which may mean running its function. -/// \return the replacement, or none to skip it. This may run fish script! -maybe_t expand_replacer(SourceRange range, const wcstring &token, - const abbrs_replacer_t &repl, const parser_t &parser) { - if (!repl.is_function) { - // Literal replacement cannot fail. - FLOGF(abbrs, L"Expanded literal abbreviation <%ls> -> <%ls>", token.c_str(), - (*repl.replacement).c_str()); - return abbrs_replacement_from(range, *repl.replacement, *repl.set_cursor_marker, - repl.has_cursor_marker); - } - - wcstring cmd = escape_string(*repl.replacement); - cmd.push_back(L' '); - cmd.append(escape_string(token)); - - // todo!("use scoped push") - bool is_interactive = parser.libdata_pods().is_interactive; - parser.libdata_pods_mut().is_interactive = false; - cleanup_t not_interactive{[&] { parser.libdata_pods_mut().is_interactive = is_interactive; }}; - - auto outputs = std::make_unique(); - int ret = exec_subshell(cmd, parser, outputs, false /* not apply_exit_status */); - if (ret != STATUS_CMD_OK) { - return none(); - } - wcstring result = join_strings(outputs->vals, L'\n'); - FLOGF(abbrs, L"Expanded function abbreviation <%ls> -> <%ls>", token.c_str(), result.c_str()); - return abbrs_replacement_from(range, result, *repl.set_cursor_marker, repl.has_cursor_marker); -} - -// Extract all the token ranges in \p str, along with whether they are an undecorated command. -// Tokens containing command substitutions are skipped; this ensures tokens are non-overlapping. -struct positioned_token_t { - source_range_t range; - bool is_cmd; -}; -static std::vector extract_tokens(const wcstring &str) { - using namespace ast; - - parse_tree_flags_t ast_flags = parse_flag_continue_after_error | - parse_flag_accept_incomplete_tokens | - parse_flag_leave_unterminated; - auto ast = ast_parse(str, ast_flags); - - // Helper to check if a node is the command portion of an undecorated statement. - auto is_command = [&](const ast::node_t &node) { - for (auto cursor = node.ptr(); cursor->has_value(); cursor = cursor->parent()) { - if (const auto *stmt = cursor->try_as_decorated_statement()) { - if (!stmt->has_opt_decoration() && node.pointer_eq(*stmt->command().ptr())) { - return true; - } - } - } - return false; - }; - - wcstring cmdsub_contents; - std::vector result; - for (auto tv = new_ast_traversal(*ast->top());;) { - auto node = tv->next(); - if (!node->has_value()) break; - // We are only interested in leaf nodes with source. - if (node->category() != category_t::leaf) continue; - source_range_t r = node->source_range(); - if (r.length == 0) continue; - - // If we have command subs, then we don't include this token; instead we recurse. - bool has_cmd_subs = false; - size_t cmdsub_cursor = r.start, cmdsub_start = 0, cmdsub_end = 0; - while (parse_util_locate_cmdsubst_range(str, &cmdsub_cursor, &cmdsub_contents, - &cmdsub_start, &cmdsub_end, - true /* accept incomplete */) > 0) { - if (cmdsub_start >= r.end()) { - break; - } - has_cmd_subs = true; - for (positioned_token_t t : extract_tokens(cmdsub_contents)) { - // cmdsub_start is the open paren; the contents start one after it. - t.range.start += static_cast(cmdsub_start + 1); - result.push_back(t); - } - } - - if (!has_cmd_subs) { - // Common case of no command substitutions in this leaf node. - result.push_back(positioned_token_t{r, is_command(*node)}); - } - } - return result; -} - -/// Expand abbreviations at the given cursor position. -/// \return the replacement. This does NOT inspect the current reader data. -maybe_t reader_expand_abbreviation_at_cursor(const wcstring &cmdline, - size_t cursor_pos, - const parser_t &parser) { - // Find the token containing the cursor. Usually users edit from the end, so walk backwards. - const auto tokens = extract_tokens(cmdline); - auto iter = std::find_if(tokens.rbegin(), tokens.rend(), [&](const positioned_token_t &t) { - return t.range.contains_inclusive(cursor_pos); - }); - if (iter == tokens.rend()) { - return none(); - } - source_range_t range = iter->range; - abbrs_position_t position = - iter->is_cmd ? abbrs_position_t::command : abbrs_position_t::anywhere; - - wcstring token_str = cmdline.substr(range.start, range.length); - auto replacers = abbrs_match(token_str, position); - for (const auto &replacer : replacers) { - if (auto replacement = expand_replacer(range, token_str, replacer, parser)) { - return replacement; - } - } - return none(); -} - -/// Expand abbreviations at the current cursor position, minus the given cursor backtrack. This may -/// change the command line but does NOT repaint it. This is to allow the caller to coalesce -/// repaints. -bool reader_data_t::expand_abbreviation_at_cursor(size_t cursor_backtrack) { - bool result = false; - editable_line_t *el = active_edit_line(); - - if (conf.expand_abbrev_ok && el == &command_line) { - // Try expanding abbreviations. - this->update_commandline_state(); - size_t cursor_pos = el->position() - std::min(el->position(), cursor_backtrack); - if (auto replacement = - reader_expand_abbreviation_at_cursor(*el->text(), cursor_pos, this->parser())) { - push_edit(el, new_edit(replacement->range.start, replacement->range.end(), - *replacement->text)); - if (replacement->has_cursor) { - update_buff_pos(el, replacement->cursor); - } else { - update_buff_pos(el, none()); - } - result = true; - } - } - return result; -} - -void reader_write_title_ffi(const wcstring &cmd, const void *parser, bool reset_cursor_position) { - reader_write_title(cmd, *static_cast(parser), reset_cursor_position); -} - -void reader_write_title(const wcstring &cmd, const parser_t &parser, bool reset_cursor_position) { - if (!term_supports_setting_title()) return; - - // todo!("use scoped push") - bool is_interactive = parser.libdata_pods().is_interactive; - parser.libdata_pods_mut().is_interactive = false; - cleanup_t noninteractive{[&] { parser.libdata_pods_mut().is_interactive = is_interactive; }}; - // todo!("use scoped push") - bool suppress_fish_trace = parser.libdata_pods().suppress_fish_trace; - parser.libdata_pods_mut().suppress_fish_trace = true; - cleanup_t in_title{ - [&] { parser.libdata_pods_mut().suppress_fish_trace = suppress_fish_trace; }}; - - wcstring fish_title_command = DEFAULT_TITLE; - if (function_exists(L"fish_title", parser)) { - fish_title_command = L"fish_title"; - if (!cmd.empty()) { - fish_title_command.append(L" "); - fish_title_command.append(escape_string(cmd, ESCAPE_NO_QUOTED | ESCAPE_NO_TILDE)); - } - } - - auto lst = std::make_unique(); - (void)exec_subshell(fish_title_command, parser, lst, false /* ignore exit status */); - if (!lst->empty()) { - wcstring title_line = L"\x1B]0;"; - for (const auto &i : lst->vals) { - title_line += i; - } - title_line += L"\a"; - std::string narrow = wcs2string(title_line); - ignore_result(write_loop(STDOUT_FILENO, narrow.data(), narrow.size())); - } - - stdoutput().set_color(rgb_color_t::reset(), rgb_color_t::reset()); - if (reset_cursor_position && !lst->empty()) { - // Put the cursor back at the beginning of the line (issue #2453). - ignore_result(write(STDOUT_FILENO, "\r", 1)); - } -} - -void reader_data_t::exec_mode_prompt() { - mode_prompt_buff.clear(); - if (function_exists(MODE_PROMPT_FUNCTION_NAME, parser())) { - auto mode_indicator_list = std::make_unique(); - exec_subshell(MODE_PROMPT_FUNCTION_NAME, parser(), mode_indicator_list, false); - // We do not support multiple lines in the mode indicator, so just concatenate all of - // them. - for (const auto &i : mode_indicator_list->vals) { - mode_prompt_buff += i; - } - } -} - -/// Reexecute the prompt command. The output is inserted into prompt_buff. -void reader_data_t::exec_prompt() { - // Clear existing prompts. - left_prompt_buff.clear(); - right_prompt_buff.clear(); - - // Suppress fish_trace while in the prompt. - // todo!("use scoped_push") - bool suppress_fish_trace = parser().libdata_pods().suppress_fish_trace; - parser().libdata_pods_mut().suppress_fish_trace = true; - cleanup_t in_prompt{ - [&] { parser().libdata_pods_mut().suppress_fish_trace = suppress_fish_trace; }}; - - // Update the termsize now. - // This allows prompts to react to $COLUMNS. - update_termsize(); - - // If we have any prompts, they must be run non-interactively. - if (!conf.left_prompt_cmd.empty() || !conf.right_prompt_cmd.empty()) { - // todo!("use scoped push") - bool is_interactive = parser().libdata_pods().is_interactive; - parser().libdata_pods_mut().is_interactive = false; - cleanup_t interactive{[&] { parser().libdata_pods_mut().is_interactive = is_interactive; }}; - - exec_mode_prompt(); - - if (!conf.left_prompt_cmd.empty()) { - // Status is ignored. - auto prompt_list = std::make_unique(); - // Historic compatibility hack. - // If the left prompt function is deleted, then use a default prompt instead of - // producing an error. - bool left_prompt_deleted = conf.left_prompt_cmd == LEFT_PROMPT_FUNCTION_NAME && - !function_exists(conf.left_prompt_cmd, parser()); - exec_subshell(left_prompt_deleted ? DEFAULT_PROMPT : conf.left_prompt_cmd, parser(), - prompt_list, false); - left_prompt_buff = join_strings(prompt_list->vals, L'\n'); - } - - if (!conf.right_prompt_cmd.empty()) { - if (function_exists(conf.right_prompt_cmd, parser())) { - // Status is ignored. - auto prompt_list = std::make_unique(); - exec_subshell(conf.right_prompt_cmd, parser(), prompt_list, false); - // Right prompt does not support multiple lines, so just concatenate all of them. - for (const auto &i : prompt_list->vals) { - right_prompt_buff += i; - } - } - } - } - - // Write the screen title. Do not reset the cursor position: exec_prompt is called when there - // may still be output on the line from the previous command (#2499) and we need our PROMPT_SP - // hack to work. - reader_write_title(L"", parser(), false); - - // Some prompt may have requested an exit (#8033). - this->exit_loop_requested |= parser().libdata_pods().exit_current_script; - parser().libdata_pods_mut().exit_current_script = false; -} - -void reader_init() { - // Save the initial terminal mode. - tcgetattr(STDIN_FILENO, &terminal_mode_on_startup); - - // Set the mode used for program execution, initialized to the current mode. - std::memcpy(&tty_modes_for_external_cmds, &terminal_mode_on_startup, - sizeof tty_modes_for_external_cmds); - term_fix_external_modes(&tty_modes_for_external_cmds); - - // Set the mode used for the terminal, initialized to the current mode. - std::memcpy(&shell_modes, &terminal_mode_on_startup, sizeof shell_modes); - - // Disable flow control by default. - tty_modes_for_external_cmds.c_iflag &= ~IXON; - tty_modes_for_external_cmds.c_iflag &= ~IXOFF; - shell_modes.c_iflag &= ~IXON; - shell_modes.c_iflag &= ~IXOFF; - - term_fix_modes(&shell_modes); - - // Set up our fixed terminal modes once, - // so we don't get flow control just because we inherited it. - if (is_interactive_session() && getpgrp() == tcgetpgrp(STDIN_FILENO)) { - term_donate(/* quiet */ true); - } -} - -/// Restore the term mode if we own the terminal and are interactive (#8705). -/// It's important we do this before restore_foreground_process_group, -/// otherwise we won't think we own the terminal. -void restore_term_mode() { - if (!is_interactive_session() || getpgrp() != tcgetpgrp(STDIN_FILENO)) return; - - if (tcsetattr(STDIN_FILENO, TCSANOW, &terminal_mode_on_startup) == -1 && errno == EIO) { - redirect_tty_output(); - } -} - -/// Indicates if the given command char ends paging. -static bool command_ends_paging(readline_cmd_t c, bool focused_on_search_field) { - using rl = readline_cmd_t; - switch (c) { - case rl::HistoryPrefixSearchBackward: - case rl::HistoryPrefixSearchForward: - case rl::HistorySearchBackward: - case rl::HistorySearchForward: - case rl::HistoryTokenSearchBackward: - case rl::HistoryTokenSearchForward: - case rl::AcceptAutosuggestion: - case rl::DeleteOrExit: - case rl::CancelCommandline: - case rl::Cancel: { - // These commands always end paging. - return true; - } - case rl::Complete: - case rl::CompleteAndSearch: - case rl::HistoryPager: - case rl::BackwardChar: - case rl::ForwardChar: - case rl::ForwardSingleChar: - case rl::UpLine: - case rl::DownLine: - case rl::Repaint: - case rl::SuppressAutosuggestion: - case rl::BeginningOfHistory: - case rl::EndOfHistory: { - // These commands never end paging. - return false; - } - case rl::Execute: { - // execute does end paging, but only executes if it was not paging. So it's handled - // specially. - return false; - } - case rl::BeginningOfLine: - case rl::EndOfLine: - case rl::ForwardWord: - case rl::BackwardWord: - case rl::ForwardBigword: - case rl::BackwardBigword: - case rl::NextdOrForwardWord: - case rl::PrevdOrBackwardWord: - case rl::DeleteChar: - case rl::BackwardDeleteChar: - case rl::KillLine: - case rl::Yank: - case rl::YankPop: - case rl::BackwardKillLine: - case rl::KillWholeLine: - case rl::KillInnerLine: - case rl::KillWord: - case rl::KillBigword: - case rl::BackwardKillWord: - case rl::BackwardKillPathComponent: - case rl::BackwardKillBigword: - case rl::SelfInsert: - case rl::SelfInsertNotFirst: - case rl::TransposeChars: - case rl::TransposeWords: - case rl::UpcaseWord: - case rl::DowncaseWord: - case rl::CapitalizeWord: - case rl::BeginningOfBuffer: - case rl::EndOfBuffer: - case rl::Undo: - case rl::Redo: - // These commands operate on the search field if that's where the focus is. - return !focused_on_search_field; - default: - return false; - } -} - -/// Indicates if the given command ends the history search. -static bool command_ends_history_search(readline_cmd_t c) { - switch (c) { - case readline_cmd_t::HistoryPrefixSearchBackward: - case readline_cmd_t::HistoryPrefixSearchForward: - case readline_cmd_t::HistorySearchBackward: - case readline_cmd_t::HistorySearchForward: - case readline_cmd_t::HistoryTokenSearchBackward: - case readline_cmd_t::HistoryTokenSearchForward: - case readline_cmd_t::BeginningOfHistory: - case readline_cmd_t::EndOfHistory: - case readline_cmd_t::Repaint: - case readline_cmd_t::ForceRepaint: - return false; - default: - return true; - } -} - -/// Remove the previous character in the character buffer and on the screen using syntax -/// highlighting, etc. -void reader_data_t::delete_char(bool backward) { - editable_line_t *el = active_edit_line(); - - size_t pos = el->position(); - if (!backward) { - pos++; - } - size_t pos_end = pos; - - if (el->position() == 0 && backward) return; - - // Fake composed character sequences by continuing to delete until we delete a character of - // width at least 1. - int width; - do { - pos--; - width = fish_wcwidth(el->text()->at(pos)); - } while (width == 0 && pos > 0); - erase_substring(el, pos, pos_end - pos); - update_buff_pos(el); - suppress_autosuggestion = true; -} - -/// Insert the characters of the string into the command line buffer and print them to the screen -/// using syntax highlighting, etc. -/// Returns true if the string changed. -void reader_data_t::insert_string(editable_line_t *el, const wcstring &str) { - if (!str.empty()) { - el->push_edit(new_edit(el->position(), el->position(), str), - !history_search.active() /* allow_coalesce */); - } - - if (el == &command_line) { - command_line_has_transient_edit = false; - suppress_autosuggestion = false; - } - maybe_refilter_pager(el); -} - -void reader_data_t::push_edit(editable_line_t *el, rust::Box &&edit) { - el->push_edit(std::move(edit), false /* allow_coalesce */); - maybe_refilter_pager(el); -} - -void reader_data_t::erase_substring(editable_line_t *el, size_t offset, size_t length) { - push_edit(el, new_edit(offset, offset + length, L"")); -} - -void reader_data_t::replace_substring(editable_line_t *el, size_t offset, size_t length, - wcstring replacement) { - push_edit(el, new_edit(offset, offset + length, std::move(replacement))); -} - -/// Insert the string in the given command line at the given cursor position. The function checks if -/// the string is quoted or not and correctly escapes the string. -/// -/// \param val the string to insert -/// \param flags A union of all flags describing the completion to insert. See the completion_t -/// struct for more information on possible values. -/// \param command_line The command line into which we will insert -/// \param inout_cursor_pos On input, the location of the cursor within the command line. On output, -/// the new desired position. -/// \param append_only Whether we can only append to the command line, or also modify previous -/// characters. This is used to determine whether we go inside a trailing quote. -/// -/// \return The completed string -wcstring completion_apply_to_command_line(const wcstring &val, complete_flags_t flags, - const wcstring &command_line, size_t *inout_cursor_pos, - bool append_only) { - bool add_space = !bool(flags & COMPLETE_NO_SPACE); - bool do_replace = bool(flags & COMPLETE_REPLACES_TOKEN); - bool do_replace_commandline = bool(flags & COMPLETE_REPLACES_COMMANDLINE); - bool do_escape = !bool(flags & COMPLETE_DONT_ESCAPE); - bool no_tilde = bool(flags & COMPLETE_DONT_ESCAPE_TILDES); - - const size_t cursor_pos = *inout_cursor_pos; - bool back_into_trailing_quote = false; - bool have_space_after_token = command_line[cursor_pos] == L' '; - - if (do_replace_commandline) { - assert(!do_escape && "unsupported completion flag"); - *inout_cursor_pos = val.size(); - return val; - } - - if (do_replace) { - size_t move_cursor; - const wchar_t *begin, *end; - - const wchar_t *buff = command_line.c_str(); - parse_util_token_extent(buff, cursor_pos, &begin, &end, nullptr, nullptr); - - wcstring sb(buff, begin - buff); - - if (do_escape) { - wcstring escaped = - escape_string(val, ESCAPE_NO_QUOTED | (no_tilde ? ESCAPE_NO_TILDE : 0)); - sb.append(escaped); - move_cursor = escaped.size(); - } else { - sb.append(val); - move_cursor = val.length(); - } - - if (add_space) { - if (!have_space_after_token) sb.append(L" "); - move_cursor += 1; - } - sb.append(end); - - size_t new_cursor_pos = (begin - buff) + move_cursor; - *inout_cursor_pos = new_cursor_pos; - return sb; - } - - wchar_t quote = L'\0'; - wcstring replaced; - if (do_escape) { - // We need to figure out whether the token we complete has unclosed quotes. Since the token - // may be inside a command substitutions we must first determine the extents of the - // innermost command substitution. - const wchar_t *cmdsub_begin, *cmdsub_end; - parse_util_cmdsubst_extent(command_line.c_str(), cursor_pos, &cmdsub_begin, &cmdsub_end); - size_t cmdsub_offset = cmdsub_begin - command_line.c_str(); - // Find the last quote in the token to complete. By parsing only the string inside any - // command substitution, we prevent the tokenizer from treating the entire command - // substitution as one token. - quote = parse_util_get_quote_type( - command_line.substr(cmdsub_offset, (cmdsub_end - cmdsub_begin)), - cursor_pos - cmdsub_offset); - - // If the token is reported as unquoted, but ends with a (unescaped) quote, and we can - // modify the command line, then delete the trailing quote so that we can insert within - // the quotes instead of after them. See issue #552. - if (quote == L'\0' && !append_only && cursor_pos > 0) { - // The entire token is reported as unquoted...see if the last character is an - // unescaped quote. - wchar_t trailing_quote = unescaped_quote(command_line, cursor_pos - 1); - if (trailing_quote != L'\0') { - quote = trailing_quote; - back_into_trailing_quote = true; - } - } - - replaced = parse_util_escape_string_with_quote(val, quote, no_tilde); - } else { - replaced = val; - } - - size_t insertion_point = cursor_pos; - if (back_into_trailing_quote) { - // Move the character back one so we enter the terminal quote. - assert(insertion_point > 0); - insertion_point--; - } - - // Perform the insertion and compute the new location. - wcstring result = command_line; - result.insert(insertion_point, replaced); - size_t new_cursor_pos = insertion_point + replaced.size() + (back_into_trailing_quote ? 1 : 0); - if (add_space) { - if (quote != L'\0' && unescaped_quote(command_line, insertion_point) != quote) { - // This is a quoted parameter, first print a quote. - result.insert(new_cursor_pos++, wcstring("e, 1)); - } - if (!have_space_after_token) result.insert(new_cursor_pos, L" "); - new_cursor_pos++; - } - *inout_cursor_pos = new_cursor_pos; - return result; -} - -/// Insert the string at the current cursor position. The function checks if the string is quoted or -/// not and correctly escapes the string. -/// -/// \param val the string to insert -/// \param token_end the position after the token to complete -/// \param flags A union of all flags describing the completion to insert. See the completion_t -/// struct for more information on possible values. -void reader_data_t::completion_insert(const wcstring &val, size_t token_end, - complete_flags_t flags) { - editable_line_t *el = active_edit_line(); - - // Move the cursor to the end of the token. - if (el->position() != token_end) update_buff_pos(el, token_end); - - size_t cursor = el->position(); - wcstring new_command_line = completion_apply_to_command_line(val, flags, *el->text(), &cursor, - false /* not append only */); - set_buffer_maintaining_pager(new_command_line, cursor); -} - -// Returns a function that can be invoked (potentially -// on a background thread) to determine the autosuggestion -static std::function get_autosuggestion_performer( - const parser_t &parser, const wcstring &search_string, size_t cursor_pos, - const HistorySharedPtr &history) { - const uint32_t generation_count = read_generation_count(); - // shared_ptr to work around std::function limitations - auto vars = std::make_shared>(parser.vars().snapshot()); - const wcstring working_directory = *parser.vars().get_pwd_slash(); - // TODO: suspicious use of 'history' here - // This is safe because histories are immortal, but perhaps - // this should use shared_ptr - const HistorySharedPtr *history_ptr = &history; - return [=]() -> autosuggestion_t { - ASSERT_IS_BACKGROUND_THREAD(); - autosuggestion_t nothing = {}; - auto ctxptr = get_bg_context(**vars, generation_count); - auto &ctx = *ctxptr; - if (ctx.check_cancel()) { - return nothing; - } - - // Let's make sure we aren't using the empty string. - if (search_string.empty()) { - return nothing; - } - - // Search history for a matching item. - rust::Box searcher = - rust_history_search_new(*history_ptr, search_string.c_str(), - history_search_type_t::Prefix, history_search_flags_t{}, 0); - while (!ctx.check_cancel() && - searcher->go_to_next_match(history_search_direction_t::Backward)) { - const history_item_t &item = searcher->current_item(); - - // Skip items with newlines because they make terrible autosuggestions. - if (item.str()->find(L'\n') != wcstring::npos) continue; - - if (autosuggest_validate_from_history(item, working_directory, ctx)) { - // The command autosuggestion was handled specially, so we're done. - // History items are case-sensitive, see #3978. - return autosuggestion_t{*searcher->current_string(), search_string, - false /* icase */}; - } - } - - // Maybe cancel here. - if (ctx.check_cancel()) return nothing; - - // Here we do something a little funny. If the line ends with a space, and the cursor is not - // at the end, don't use completion autosuggestions. It ends up being pretty weird seeing - // stuff get spammed on the right while you go back to edit a line - const wchar_t last_char = search_string.at(search_string.size() - 1); - const bool cursor_at_end = (cursor_pos == search_string.size()); - if (!cursor_at_end && iswspace(last_char)) return nothing; - - // On the other hand, if the line ends with a quote, don't go dumping stuff after the quote. - if (std::wcschr(L"'\"", last_char) && cursor_at_end) return nothing; - - // Try normal completions. - completion_request_options_t complete_flags = completion_request_options_autosuggest(); - auto needs_load = std::make_unique(); - auto completions_box = complete(search_string, complete_flags, ctx, needs_load); - completion_list_t &completions = *completions_box; - - autosuggestion_t result{}; - result.search_string = search_string; - result.needs_load = std::move(needs_load->vals); - result.icase = true; // normal completions are case-insensitive. - if (!completions.empty()) { - completions.sort_and_prioritize(complete_flags); - const completion_t &comp = completions.at(0); - size_t cursor = cursor_pos; - result.text = completion_apply_to_command_line( - *comp.completion(), comp.flags(), search_string, &cursor, true /* append only */); - } - return result; - }; -} - -bool reader_data_t::can_autosuggest() const { - // We autosuggest if suppress_autosuggestion is not set, if we're not doing a history search, - // and our command line contains a non-whitespace character. - const editable_line_t *el = active_edit_line(); - const wchar_t *whitespace = L" \t\r\n\v"; - return conf.autosuggest_ok && !suppress_autosuggestion && history_search.is_at_end() && - el == &command_line && el->text()->find_first_not_of(whitespace) != wcstring::npos; -} - -// Called after an autosuggestion has been computed on a background thread. -void reader_data_t::autosuggest_completed(autosuggestion_t result) { - ASSERT_IS_MAIN_THREAD(); - if (result.search_string == in_flight_autosuggest_request) { - in_flight_autosuggest_request.clear(); - } - if (result.search_string != *command_line.text()) { - // This autosuggestion is stale. - return; - } - // Maybe load completions for commands discovered by this autosuggestion. - bool loaded_new = false; - for (const wcstring &to_load : result.needs_load) { - if (complete_load(to_load, this->parser())) { - FLOGF(complete, "Autosuggest found new completions for %ls, restarting", - to_load.c_str()); - loaded_new = true; - } - } - if (loaded_new) { - // We loaded new completions for this command. - // Re-do our autosuggestion. - this->update_autosuggestion(); - } else if (!result.empty() && can_autosuggest() && - string_prefixes_string_case_insensitive(result.search_string, result.text)) { - // Autosuggestion is active and the search term has not changed, so we're good to go. - autosuggestion = std::move(result); - if (this->is_repaint_needed()) { - this->layout_and_repaint(L"autosuggest"); - } - } -} - -void reader_data_t::update_autosuggestion() { - // If we can't autosuggest, just clear it. - if (!can_autosuggest()) { - in_flight_autosuggest_request.clear(); - autosuggestion.clear(); - return; - } - - // Check to see if our autosuggestion still applies; if so, don't recompute it. - // Since the autosuggestion computation is asynchronous, this avoids "flashing" as you type into - // the autosuggestion. - // This is also the main mechanism by which readline commands that don't change the command line - // text avoid recomputing the autosuggestion. - const editable_line_t &el = command_line; - if (autosuggestion.text.size() > el.text()->size() && - (autosuggestion.icase - ? string_prefixes_string_case_insensitive(*el.text(), autosuggestion.text) - : string_prefixes_string(*el.text(), autosuggestion.text))) { - return; - } - - // Do nothing if we've already kicked off this autosuggest request. - if (*el.text() == in_flight_autosuggest_request) return; - in_flight_autosuggest_request = *el.text(); - - // Clear the autosuggestion and kick it off in the background. - FLOG(reader_render, L"Autosuggesting"); - autosuggestion.clear(); - std::function performer = - get_autosuggestion_performer(parser(), *el.text(), el.position(), **history); - auto shared_this = this->shared_from_this(); - std::function completion = [shared_this](autosuggestion_t result) { - shared_this->autosuggest_completed(std::move(result)); - }; - debounce_perform_with_completion(debounce_autosuggestions(), std::move(performer), - std::move(completion)); -} - -// Accept any autosuggestion by replacing the command line with it. If full is true, take the whole -// thing; if it's false, then respect the passed in style. -void reader_data_t::accept_autosuggestion(bool full, bool single, move_word_style_t style) { - if (!autosuggestion.empty()) { - // Accepting an autosuggestion clears the pager. - clear_pager(); - - // Accept the autosuggestion. - if (full) { - // Just take the whole thing. - replace_substring(&command_line, 0, command_line.size(), autosuggestion.text); - } else if (single) { - replace_substring(&command_line, command_line.size(), 0, - autosuggestion.text.substr(command_line.size(), 1)); - } else { - // Accept characters according to the specified style. - auto state = new_move_word_state_machine(style); - size_t want; - for (want = command_line.size(); want < autosuggestion.text.size(); want++) { - wchar_t wc = autosuggestion.text.at(want); - if (!state->consume_char(wc)) break; - } - size_t have = command_line.size(); - replace_substring(&command_line, command_line.size(), 0, - autosuggestion.text.substr(have, want - have)); - } - } -} - -// Ensure we have no pager contents. -void reader_data_t::clear_pager() { - pager.clear(); - history_pager_active = false; - command_line_has_transient_edit = false; -} - -void reader_data_t::select_completion_in_direction(selection_motion_t dir, - bool force_selection_change) { - bool selection_changed = - pager.select_next_completion_in_direction(dir, *current_page_rendering); - if (force_selection_change || selection_changed) { - pager_selection_changed(); - } -} - -/// Flash the screen. This function changes the color of the current line momentarily. -void reader_data_t::flash() { - // Multiple flashes may be enqueued by keypress repeat events and can pile up to cause a - // significant delay in processing future input while all the flash() calls complete, as we - // effectively sleep for 100ms each go. See #8610. - auto now = std::chrono::steady_clock::now(); - if ((now - last_flash) < std::chrono::milliseconds{50}) { - last_flash = now; - return; - } - - struct timespec pollint; - editable_line_t *el = &command_line; - layout_data_t data = make_layout_data(); - - // Save off the colors and set the background. - highlight_list_t saved_colors = data.colors; - for (size_t i = 0; i < el->position(); i++) { - data.colors.at(i) = highlight_spec_t::make_background(highlight_role_t::search_match); - } - this->rendered_layout = data; // need to copy the data since we will use it again. - paint_layout(L"flash"); - - layout_data_t old_data = std::move(rendered_layout); - - pollint.tv_sec = 0; - pollint.tv_nsec = 100 * 1000000; - nanosleep(&pollint, nullptr); - - // Re-render with our saved data. - data.colors = std::move(saved_colors); - this->rendered_layout = std::move(data); - paint_layout(L"unflash"); - - // Save the time we stopped flashing as the time of the most recent flash. We can't just - // increment the old `now` value because the sleep is non-deterministic. - last_flash = std::chrono::steady_clock::now(); -} - -maybe_t reader_data_t::get_selection() const { - if (!this->selection.has_value()) return none(); - size_t start = this->selection->start; - size_t len = - std::min(this->selection->stop, this->command_line.size()) - this->selection->start; - return source_range_t{static_cast(start), static_cast(len)}; -} - -/// Characters that may not be part of a token that is to be replaced by a case insensitive -/// completion. -const wchar_t *REPLACE_UNCLEAN = L"$*?({})"; - -/// Check if the specified string can be replaced by a case insensitive completion with the -/// specified flags. -/// -/// Advanced tokens like those containing {}-style expansion can not at the moment be replaced, -/// other than if the new token is already an exact replacement, e.g. if the COMPLETE_DONT_ESCAPE -/// flag is set. -static bool reader_can_replace(const wcstring &in, complete_flags_t flags) { - if (flags & COMPLETE_DONT_ESCAPE) { - return true; - } - - // Test characters that have a special meaning in any character position. - if (in.find_first_of(REPLACE_UNCLEAN) != wcstring::npos) return false; - - return true; -} - -/// Determine the best (lowest) match rank for a set of completions. -static uint32_t get_best_rank(const completion_list_t &comp) { - uint32_t best_rank = UINT32_MAX; - for (size_t i = 0; i < comp.size(); i++) { - auto &c = comp.at(i); - best_rank = std::min(best_rank, c.rank()); - } - return best_rank; -} - -/// Handle the list of completions. This means the following: -/// -/// - If the list is empty, flash the terminal. -/// - If the list contains one element, write the whole element, and if the element does not end on -/// a '/', '@', ':', '.', ',', '-' or a '=', also write a trailing space. -/// - If the list contains multiple elements, insert their common prefix, if any and display -/// the list in the pager. Depending on terminal size and the length of the list, the pager -/// may either show less than a screenfull and exit or use an interactive pager to allow the -/// user to scroll through the completions. -/// -/// \param comp the list of completion strings -/// \param token_begin the position of the token to complete -/// \param token_end the position after the token to complete -/// -/// Return true if we inserted text into the command line, false if we did not. -bool reader_data_t::handle_completions(const completion_list_t &comp, size_t token_begin, - size_t token_end) { - bool done = false; - bool success = false; - const editable_line_t *el = &command_line; - - const wcstring tok(*el->text(), token_begin, token_end - token_begin); - - // Check trivial cases. - size_t size = comp.size(); - if (size == 0) { - // No suitable completions found, flash screen and return. - flash(); - done = true; - } else if (size == 1) { - // Exactly one suitable completion found - insert it. - const completion_t &c = comp.at(0); - - // If this is a replacement completion, check that we know how to replace it, e.g. that - // the token doesn't contain evil operators like {}. - if (!(c.flags() & COMPLETE_REPLACES_TOKEN) || reader_can_replace(tok, c.flags())) { - completion_insert(*c.completion(), token_end, c.flags()); - } - done = true; - success = true; - } - - if (done) { - return success; - } - - auto best_rank = get_best_rank(comp); - - // Determine whether we are going to replace the token or not. If any commands of the best - // rank do not require replacement, then ignore all those that want to use replacement. - bool will_replace_token = true; - for (size_t i = 0; i < comp.size(); i++) { - const completion_t &el = comp.at(i); - if (el.rank() <= best_rank && !(el.flags() & COMPLETE_REPLACES_TOKEN)) { - will_replace_token = false; - break; - } - } - - // Decide which completions survived. There may be a lot of them; it would be nice if we could - // figure out how to avoid copying them here. - auto surviving_completions_box = new_completion_list(); - completion_list_t &surviving_completions = *surviving_completions_box; - bool all_matches_exact_or_prefix = true; - for (size_t i = 0; i < comp.size(); i++) { - const completion_t &el = comp.at(i); - // Ignore completions with a less suitable match rank than the best. - if (el.rank() > best_rank) continue; - - // Only use completions that match replace_token. - bool completion_replace_token = static_cast(el.flags() & COMPLETE_REPLACES_TOKEN); - if (completion_replace_token != will_replace_token) continue; - - // Don't use completions that want to replace, if we cannot replace them. - if (completion_replace_token && !reader_can_replace(tok, el.flags())) continue; - - // This completion survived. - surviving_completions.push_back(el); - all_matches_exact_or_prefix = all_matches_exact_or_prefix && el.match_is_exact_or_prefix(); - } - - if (surviving_completions.size() == 1) { - // After sorting and stuff only one completion is left, use it. - // - // TODO: This happens when smartcase kicks in, e.g. - // the token is "cma" and the options are "cmake/" and "CMakeLists.txt" - // it would be nice if we could figure - // out how to use it more. - const completion_t &c = surviving_completions.at(0); - - // If this is a replacement completion, check that we know how to replace it, e.g. that - // the token doesn't contain evil operators like {}. - if (!(c.flags() & COMPLETE_REPLACES_TOKEN) || reader_can_replace(tok, c.flags())) { - completion_insert(*c.completion(), token_end, c.flags()); - } - return true; - } - - bool use_prefix = false; - wcstring common_prefix; - if (all_matches_exact_or_prefix) { - // Try to find a common prefix to insert among the surviving completions. - complete_flags_t flags = 0; - bool prefix_is_partial_completion = false; - bool first = true; - for (size_t i = 0; i < surviving_completions.size(); i++) { - const completion_t &el = surviving_completions.at(i); - if (first) { - // First entry, use the whole string. - common_prefix = *el.completion(); - flags = el.flags(); - first = false; - } else { - // Determine the shared prefix length. - size_t idx, max = std::min(common_prefix.size(), el.completion()->size()); - - for (idx = 0; idx < max; idx++) { - if (common_prefix.at(idx) != el.completion()->at(idx)) break; - } - - // idx is now the length of the new common prefix. - common_prefix.resize(idx); - prefix_is_partial_completion = true; - - // Early out if we decide there's no common prefix. - if (idx == 0) break; - } - } - - // Determine if we use the prefix. We use it if it's non-empty and it will actually make - // the command line longer. It may make the command line longer by virtue of not using - // REPLACE_TOKEN (so it always appends to the command line), or by virtue of replacing - // the token but being longer than it. - use_prefix = common_prefix.size() > (will_replace_token ? tok.size() : 0); - assert(!use_prefix || !common_prefix.empty()); - - if (use_prefix) { - // We got something. If more than one completion contributed, then it means we have - // a prefix; don't insert a space after it. - if (prefix_is_partial_completion) flags |= COMPLETE_NO_SPACE; - completion_insert(common_prefix, token_end, flags); - cycle_command_line = *command_line.text(); - cycle_cursor_pos = command_line.position(); - } - } - - if (use_prefix) { - for (size_t i = 0; i < surviving_completions.size(); i++) { - completion_t &c = surviving_completions.at_mut(i); - c.set_flags(c.flags() & ~COMPLETE_REPLACES_TOKEN); - c.completion_erase(0, common_prefix.size()); - } - } - - // Print the completion list. - wcstring prefix; - if (will_replace_token || !all_matches_exact_or_prefix) { - if (use_prefix) prefix = std::move(common_prefix); - } else if (tok.size() + common_prefix.size() <= PREFIX_MAX_LEN) { - prefix = tok + common_prefix; - } else { - // Append just the end of the string. - prefix = wcstring{get_ellipsis_char()}; - prefix.append(tok + common_prefix, tok.size() + common_prefix.size() - PREFIX_MAX_LEN, - PREFIX_MAX_LEN); - } - - // Update the pager data. - pager.set_prefix(prefix, true); - pager.set_completions(surviving_completions, true); - // Modify the command line to reflect the new pager. - pager_selection_changed(); - return false; -} - -/// Return true if we believe ourselves to be orphaned. loop_count is how many times we've tried to -/// stop ourselves via SIGGTIN. -static bool check_for_orphaned_process(unsigned long loop_count, pid_t shell_pgid) { - bool we_think_we_are_orphaned = false; - // Try kill-0'ing the process whose pid corresponds to our process group ID. It's possible this - // will fail because we don't have permission to signal it. But more likely it will fail because - // it no longer exists, and we are orphaned. - if (loop_count % 64 == 0 && kill(shell_pgid, 0) < 0 && errno == ESRCH) { - we_think_we_are_orphaned = true; - } - - // Try reading from the tty; if we get EIO we are orphaned. This is sort of bad because it - // may block. - if (!we_think_we_are_orphaned && loop_count % 128 == 0) { -#ifdef HAVE_CTERMID_R - char buf[L_ctermid]; - char *tty = ctermid_r(buf); -#else - char *tty = ctermid(nullptr); -#endif - if (!tty) { - wperror(L"ctermid"); - exit_without_destructors(1); - } - - // Open the tty. Presumably this is stdin, but maybe not? - rust::Box tty_fd = new_autoclose_fd(open(tty, O_RDONLY | O_NONBLOCK)); - if (!tty_fd->valid()) { - wperror(L"open"); - exit_without_destructors(1); - } - - char tmp; - if (read(tty_fd->fd(), &tmp, 1) < 0 && errno == EIO) { - we_think_we_are_orphaned = true; - } - } - - // Just give up if we've done it a lot times. - if (loop_count > 4096) { - we_think_we_are_orphaned = true; - } - - return we_think_we_are_orphaned; -} - -// Ensure that fish owns the terminal, possibly waiting. If we cannot acquire the terminal, then -// report an error and exit. -static void acquire_tty_or_exit(pid_t shell_pgid) { - ASSERT_IS_MAIN_THREAD(); - - // Check if we are in control of the terminal, so that we don't do semi-expensive things like - // reset signal handlers unless we really have to, which we often don't. - // Common case. - pid_t owner = tcgetpgrp(STDIN_FILENO); - if (owner == shell_pgid) { - return; - } - - // In some strange cases the tty may be come preassigned to fish's pid, but not its pgroup. - // In that case we simply attempt to claim our own pgroup. - // See #7388. - if (owner == getpid()) { - (void)setpgid(owner, owner); - return; - } - - // Bummer, we are not in control of the terminal. Stop until parent has given us control of - // it. - // - // In theory, reseting signal handlers could cause us to miss signal deliveries. In - // practice, this code should only be run during startup, when we're not waiting for any - // signals. - signal_reset_handlers(); - cleanup_t restore_sigs([] { signal_set_handlers(true); }); - - // Ok, signal handlers are taken out of the picture. Stop ourself in a loop until we are in - // control of the terminal. However, the call to signal(SIGTTIN) may silently not do - // anything if we are orphaned. - // - // As far as I can tell there's no really good way to detect that we are orphaned. One way - // is to just detect if the group leader exited, via kill(shell_pgid, 0). Another - // possibility is that read() from the tty fails with EIO - this is more reliable but it's - // harder, because it may succeed or block. So we loop for a while, trying those strategies. - // Eventually we just give up and assume we're orphaend. - for (unsigned loop_count = 0;; loop_count++) { - owner = tcgetpgrp(STDIN_FILENO); - // 0 is a valid return code from `tcgetpgrp()` under at least FreeBSD and testing - // indicates that a subsequent call to `tcsetpgrp()` will succeed. 0 is the - // pid of the top-level kernel process, so I'm not sure if this means ownership - // of the terminal has gone back to the kernel (i.e. it's not owned) or if it is - // just an "invalid" pid for all intents and purposes. - if (owner == 0) { - tcsetpgrp(STDIN_FILENO, shell_pgid); - // Since we expect the above to work, call `tcgetpgrp()` immediately to - // avoid a second pass through this loop. - owner = tcgetpgrp(STDIN_FILENO); - } - if (owner == -1 && errno == ENOTTY) { - if (!is_interactive_session()) { - // It's OK if we're not able to take control of the terminal. We handle - // the fallout from this in a few other places. - break; - } - // No TTY, cannot be interactive? - redirect_tty_output(); - FLOGF(warning, _(L"No TTY for interactive shell (tcgetpgrp failed)")); - wperror(L"setpgid"); - exit_without_destructors(1); - } - if (owner == shell_pgid) { - break; // success - } else { - if (check_for_orphaned_process(loop_count, shell_pgid)) { - // We're orphaned, so we just die. Another sad statistic. - const wchar_t *fmt = - _(L"I appear to be an orphaned process, so I am quitting politely. My pid is " - L"%d."); - FLOGF(warning, fmt, static_cast(getpid())); - exit_without_destructors(1); - } - - // Try stopping us. - int ret = killpg(shell_pgid, SIGTTIN); - if (ret < 0) { - wperror(L"killpg(shell_pgid, SIGTTIN)"); - exit_without_destructors(1); - } - } - } -} - -/// Initialize data for interactive use. -static void reader_interactive_init(const parser_t &parser) { - ASSERT_IS_MAIN_THREAD(); - - pid_t shell_pgid = getpgrp(); - pid_t shell_pid = getpid(); - - // Set up key bindings. - init_input(); - - // Ensure interactive signal handling is enabled. - signal_set_handlers_once(true); - - // Wait until we own the terminal. - acquire_tty_or_exit(shell_pgid); - - // If fish has no valid pgroup (possible with firejail, see #5295) or is interactive, - // ensure it owns the terminal. Also see #5909, #7060. - if (shell_pgid == 0 || (is_interactive_session() && shell_pgid != shell_pid)) { - shell_pgid = shell_pid; - if (setpgid(shell_pgid, shell_pgid) < 0) { - // If we're session leader setpgid returns EPERM. The other cases where we'd get EPERM - // don't apply as we passed our own pid. - // - // This should be harmless, so we ignore it. - if (errno != EPERM) { - FLOG(error, _(L"Failed to assign shell to its own process group")); - wperror(L"setpgid"); - exit_without_destructors(1); - } - } - - // Take control of the terminal - if (tcsetpgrp(STDIN_FILENO, shell_pgid) == -1) { - if (errno == ENOTTY) { - redirect_tty_output(); - } - FLOG(error, _(L"Failed to take control of the terminal")); - wperror(L"tcsetpgrp"); - exit_without_destructors(1); - } - - // Configure terminal attributes - if (tcsetattr(STDIN_FILENO, TCSANOW, &shell_modes) == -1) { - if (errno == EIO) { - redirect_tty_output(); - } - FLOGF(warning, _(L"Failed to set startup terminal mode!")); - wperror(L"tcsetattr"); - } - } - - termsize_invalidate_tty(); - - // Provide value for `status current-command` - parser.libdata_mut().set_status_vars_command(L"fish"); - // Also provide a value for the deprecated fish 2.0 $_ variable - parser.vars().set_one(L"_", ENV_GLOBAL, L"fish"); -} - -/// Destroy data for interactive use. -static void reader_interactive_destroy() { - stdoutput().set_color(rgb_color_t::reset(), rgb_color_t::reset()); -} - -/// Set the specified string as the current buffer. -void reader_data_t::set_command_line_and_position(editable_line_t *el, wcstring &&new_str, - size_t pos) { - push_edit(el, new_edit(0, el->size(), std::move(new_str))); - el->set_position(pos); - update_buff_pos(el, pos); -} - -/// Undo the transient edit und update commandline accordingly. -void reader_data_t::clear_transient_edit() { - if (!command_line_has_transient_edit) { - return; - } - command_line.undo(); - update_buff_pos(&command_line); - command_line_has_transient_edit = false; -} - -void reader_data_t::replace_current_token(wcstring &&new_token) { - const wchar_t *begin, *end; - - // Find current token. - editable_line_t *el = active_edit_line(); - auto text = *el->text(); - const wchar_t *buff = text.c_str(); - parse_util_token_extent(buff, el->position(), &begin, &end, nullptr, nullptr); - - if (!begin || !end) return; - - size_t offset = begin - buff; - size_t length = end - begin; - replace_substring(el, offset, length, std::move(new_token)); -} - -/// Apply the history search to the command line. -void reader_data_t::update_command_line_from_history_search() { - wcstring new_text = history_search.is_at_end() ? history_search.search_string() - : history_search.current_result().text; - editable_line_t *el = active_edit_line(); - if (command_line_has_transient_edit) { - el->undo(); - } - if (history_search.by_token()) { - replace_current_token(std::move(new_text)); - } else { - assert(history_search.by_line() || history_search.by_prefix()); - replace_substring(&command_line, 0, command_line.size(), std::move(new_text)); - } - command_line_has_transient_edit = true; - assert(el == &command_line); - update_buff_pos(el); -} - -enum move_word_dir_t { MOVE_DIR_LEFT, MOVE_DIR_RIGHT }; - -/// Move buffer position one word or erase one word. This function updates both the internal buffer -/// and the screen. It is used by M-left, M-right and ^W to do block movement or block erase. -/// -/// \param move_right true if moving right -/// \param erase Whether to erase the characters along the way or only move past them. -/// \param newv if the new kill item should be appended to the previous kill item or not. -void reader_data_t::move_word(editable_line_t *el, bool move_right, bool erase, - move_word_style_t style, bool newv) { - // Return if we are already at the edge. - const size_t boundary = move_right ? el->size() : 0; - if (el->position() == boundary) return; - - // When moving left, a value of 1 means the character at index 0. - auto state = new_move_word_state_machine(style); - auto text = *el->text(); - const wchar_t *const command_line = text.c_str(); - const size_t start_buff_pos = el->position(); - - size_t buff_pos = el->position(); - while (buff_pos != boundary) { - size_t idx = (move_right ? buff_pos : buff_pos - 1); - wchar_t c = command_line[idx]; - if (!state->consume_char(c)) break; - buff_pos = (move_right ? buff_pos + 1 : buff_pos - 1); - } - - // Always consume at least one character. - if (buff_pos == start_buff_pos) buff_pos = (move_right ? buff_pos + 1 : buff_pos - 1); - - // If we are moving left, buff_pos-1 is the index of the first character we do not delete - // (possibly -1). If we are moving right, then buff_pos is that index - possibly el->size(). - if (erase) { - // Don't autosuggest after a kill. - if (el == &this->command_line) { - suppress_autosuggestion = true; - } - - if (move_right) { - kill(el, start_buff_pos, buff_pos - start_buff_pos, KILL_APPEND, newv); - } else { - kill(el, buff_pos, start_buff_pos - buff_pos, KILL_PREPEND, newv); - } - } else { - update_buff_pos(el, buff_pos); - } -} - -/// Sets the command line contents, without clearing the pager. -void reader_data_t::set_buffer_maintaining_pager(const wcstring &b, size_t pos, bool transient) { - size_t command_line_len = b.size(); - if (transient) { - if (command_line_has_transient_edit) { - command_line.undo(); - } - command_line_has_transient_edit = true; - } - replace_substring(&command_line, 0, command_line.size(), b); - command_line_changed(&command_line); - - // Don't set a position past the command line length. - if (pos > command_line_len) pos = command_line_len; //!OCLINT(parameter reassignment) - update_buff_pos(&command_line, pos); - - // Clear history search. - history_search.reset(); -} - -/// Run the specified command with the correct terminal modes, and while taking care to perform job -/// notification, set the title, etc. -static rust::Box reader_run_command(const parser_t &parser, const wcstring &cmd) { - wcstring ft = *tok_command(cmd); - - // Provide values for `status current-command` and `status current-commandline` - if (!ft.empty()) { - parser.libdata_mut().set_status_vars_command(ft); - parser.libdata_mut().set_status_vars_commandline(cmd); - // Also provide a value for the deprecated fish 2.0 $_ variable - parser.vars().set_one(L"_", ENV_GLOBAL, ft); - } - - outputter_t &outp = stdoutput(); - reader_write_title(cmd, parser); - outp.set_color(rgb_color_t::normal(), rgb_color_t::normal()); - term_donate(); - - timepoint_t time_before = timef(); - auto eval_res = parser.eval(cmd, *new_io_chain()); - job_reap(parser, true); - - // Update the execution duration iff a command is requested for execution - // issue - #4926 - if (!ft.empty()) { - timepoint_t time_after = timef(); - double duration = time_after - time_before; - long duration_ms = std::round(duration * 1000); - parser.vars().set_one(ENV_CMD_DURATION, ENV_UNEXPORT, to_string(duration_ms)); - } - - term_steal(); - - // Provide value for `status current-command` - parser.libdata_mut().set_status_vars_command(program_name); - // Also provide a value for the deprecated fish 2.0 $_ variable - parser.vars().set_one(L"_", ENV_GLOBAL, program_name); - // Provide value for `status current-commandline` - parser.libdata_mut().set_status_vars_commandline(L""); - - if (have_proc_stat()) { - proc_update_jiffies(parser); - } - - return eval_res; -} - -static parser_test_error_bits_t reader_shell_test(const parser_t &parser, const wcstring &bstr) { - auto errors = new_parse_error_list(); - parser_test_error_bits_t res = - parse_util_detect_errors(bstr, &*errors, true /* do accept incomplete */); - - if (res & PARSER_TEST_ERROR) { - wcstring error_desc = *parser.get_backtrace(bstr, *errors); - - // Ensure we end with a newline. Also add an initial newline, because it's likely the user - // just hit enter and so there's junk on the current line. - if (!string_suffixes_string(L"\n", error_desc)) { - error_desc.push_back(L'\n'); - } - std::fwprintf(stderr, L"\n%ls", error_desc.c_str()); - reader_schedule_prompt_repaint(); - } - return res; -} - -void reader_data_t::highlight_complete(highlight_result_t result) { - ASSERT_IS_MAIN_THREAD(); - in_flight_highlight_request.clear(); - if (result.text == *command_line.text()) { - assert(result.colors.size() == command_line.size()); - if (this->is_repaint_needed(&result.colors)) { - auto ffi_colors = new_highlight_spec_list(); - for (auto &c : result.colors) { - ffi_colors->push(c); - } - command_line.set_colors(*ffi_colors); - this->layout_and_repaint(L"highlight"); - } - } -} - -// Given text and whether IO is allowed, return a function that performs highlighting. The function -// may be invoked on a background thread. -static std::function get_highlight_performer(const parser_t &parser, - const editable_line_t *el, - bool io_ok) { - // shard_ptr to work around std::function requiring copyable types - auto vars = std::make_shared>(parser.vars().snapshot()); - uint32_t generation_count = read_generation_count(); - size_t position = el->position(); - wcstring text = *el->text(); - return [=]() -> highlight_result_t { - if (text.empty()) return {}; - auto ctx = get_bg_context(**vars, generation_count); - std::vector colors(text.size(), highlight_spec_t{}); - highlight_shell(text, colors, *ctx, io_ok, std::make_shared(position)); - return highlight_result_t{std::move(colors), text}; - }; -} - -/// Highlight the command line in a super, plentiful way. -void reader_data_t::super_highlight_me_plenty() { - if (!conf.highlight_ok) return; - - // Do nothing if this text is already in flight. - const editable_line_t *el = &command_line; - if (*el->text() == in_flight_highlight_request) return; - in_flight_highlight_request = *el->text(); - - FLOG(reader_render, L"Highlighting"); - std::function highlight_performer = - get_highlight_performer(parser(), el, true /* io_ok */); - auto shared_this = this->shared_from_this(); - std::function completion = [shared_this](highlight_result_t result) { - shared_this->highlight_complete(std::move(result)); - }; - debounce_perform_with_completion(debounce_highlighting(), std::move(highlight_performer), - std::move(completion)); -} - -void reader_data_t::finish_highlighting_before_exec() { - // Early-out if highlighting is not OK. - if (!conf.highlight_ok) return; - - // Decide if our current highlighting is OK. If not we will do a quick highlight without I/O. - bool current_highlight_ok = false; - if (in_flight_highlight_request.empty()) { - // There is no in-flight highlight request. Two possibilities: - // 1: The user hit return after highlighting finished, so current highlighting is correct. - // 2: The user hit return before highlighting started, so current highlighting is stale. - // We can distinguish these based on what we last rendered. - current_highlight_ok = (this->rendered_layout.text == *command_line.text()); - } else if (in_flight_highlight_request == *command_line.text()) { - // The user hit return while our in-flight highlight request was still processing the text. - // Wait for its completion to run, but not forever. - namespace sc = std::chrono; - auto now = sc::steady_clock::now(); - auto deadline = now + sc::milliseconds(kHighlightTimeoutForExecutionMs); - while (now < deadline) { - long timeout_usec = sc::duration_cast(deadline - now).count(); - iothread_service_main_with_timeout(timeout_usec); - - // Note iothread_service_main_with_timeout will reentrantly modify us, - // by invoking a completion. - if (in_flight_highlight_request.empty()) break; - now = sc::steady_clock::now(); - } - - // If our in_flight_highlight_request is now empty, it means it completed and we highlighted - // successfully. - current_highlight_ok = in_flight_highlight_request.empty(); - } - - if (!current_highlight_ok) { - // We need to do a quick highlight without I/O. - auto highlight_no_io = - get_highlight_performer(parser(), &command_line, false /* io not ok */); - this->highlight_complete(highlight_no_io()); - } -} - -/// The stack of current interactive reading contexts. -static std::vector> reader_data_stack; - -/// Access the top level reader data. -static reader_data_t *current_data_or_null() { - ASSERT_IS_MAIN_THREAD(); - return reader_data_stack.empty() ? nullptr : reader_data_stack.back().get(); -} - -static reader_data_t *current_data() { - ASSERT_IS_MAIN_THREAD(); - assert(!reader_data_stack.empty() && "no current reader"); - return reader_data_stack.back().get(); -} - -void reader_change_history(const wcstring &name) { - // We don't need to _change_ if we're not initialized yet. - reader_data_t *data = current_data_or_null(); - if (data && data->history) { - (*data->history)->save(); - data->history = history_with_name(name.c_str()); - commandline_state_snapshot()->history = (*data->history)->clone(); - } -} - -void reader_change_cursor_selection_mode(cursor_selection_mode_t selection_mode) { - // We don't need to _change_ if we're not initialized yet. - reader_data_t *data = current_data_or_null(); - if (data) { - data->cursor_selection_mode = selection_mode; - } -} - -void reader_change_cursor_selection_mode(uint8_t selection_mode) { - reader_change_cursor_selection_mode((cursor_selection_mode_t)selection_mode); -} - -static bool check_autosuggestion_enabled(const env_stack_t &vars) { - if (auto val = vars.get(L"fish_autosuggestion_enabled")) { - return val->as_string() != L"0"; - } - return true; -} - -void reader_set_autosuggestion_enabled(const env_stack_t &vars) { - // We don't need to _change_ if we're not initialized yet. - reader_data_t *data = current_data_or_null(); - if (data) { - bool enable = check_autosuggestion_enabled(vars); - if (data->conf.autosuggest_ok != enable) { - data->conf.autosuggest_ok = enable; - data->force_exec_prompt_and_repaint = true; - data->inputter->queue_readline(readline_cmd_t::Repaint); - } - } -} - -void reader_set_autosuggestion_enabled_ffi(bool enable) { - // We don't need to _change_ if we're not initialized yet. - reader_data_t *data = current_data_or_null(); - if (data) { - if (data->conf.autosuggest_ok != enable) { - data->conf.autosuggest_ok = enable; - data->force_exec_prompt_and_repaint = true; - data->inputter->queue_readline(readline_cmd_t::Repaint); - } - } -} - -/// Add a new reader to the reader stack. -/// \return a shared pointer to it. -static std::shared_ptr reader_push_ret(const parser_t &parser, - const wcstring &history_name, - reader_config_t &&conf) { - rust::Box hist = history_with_name(history_name.c_str()); - hist->resolve_pending(); // see #6892 - auto data = std::make_shared(parser.shared(), *hist, std::move(conf)); - reader_data_stack.push_back(data); - data->command_line_changed(&data->command_line); - if (reader_data_stack.size() == 1) { - reader_interactive_init(parser); - } - data->update_commandline_state(); - return data; -} - -/// Public variant which discards the return value. -void reader_push(const parser_t &parser, const wcstring &history_name, reader_config_t &&conf) { - (void)reader_push_ret(parser, history_name, std::move(conf)); -} - -void reader_push_ffi(const void *_parser, const wcstring &history_name, const void *_conf_ffi) { - const auto &parser = *static_cast(_parser); - const auto &conf_ffi = *static_cast(_conf_ffi); - reader_config_t conf; - conf.left_prompt_cmd = std::move(*conf_ffi.left_prompt_cmd()); - conf.right_prompt_cmd = std::move(*conf_ffi.right_prompt_cmd()); - conf.event = std::move(*conf_ffi.event()); - conf.complete_ok = conf_ffi.complete_ok(); - conf.highlight_ok = conf_ffi.highlight_ok(); - conf.syntax_check_ok = conf_ffi.syntax_check_ok(); - conf.autosuggest_ok = conf_ffi.autosuggest_ok(); - conf.expand_abbrev_ok = conf_ffi.expand_abbrev_ok(); - conf.exit_on_interrupt = conf_ffi.exit_on_interrupt(); - conf.in_silent_mode = conf_ffi.in_silent_mode(); - conf.in = conf_ffi.inputfd(); - reader_push(parser, history_name, std::move(conf)); -} - -void reader_pop() { - assert(!reader_data_stack.empty() && "empty stack in reader_data_stack"); - reader_data_stack.pop_back(); - reader_data_t *new_reader = current_data_or_null(); - if (new_reader == nullptr) { - reader_interactive_destroy(); - *commandline_state_snapshot() = commandline_state_t{}; - } else { - new_reader->screen->reset_abandoning_line(termsize_last().width); - new_reader->update_commandline_state(); - } -} - -void reader_data_t::import_history_if_necessary() { - // Import history from older location (config path) if our current history is empty. - if (history && (*history)->is_empty()) { - (*history)->populate_from_config_path(); - } - - // Import history from bash, etc. if our current history is still empty and is the default - // history. - if (history && (*history)->is_empty() && (*history)->is_default()) { - // Try opening a bash file. We make an effort to respect $HISTFILE; this isn't very complete - // (AFAIK it doesn't have to be exported), and to really get this right we ought to ask bash - // itself. But this is better than nothing. - const auto var = vars().get(L"HISTFILE"); - wcstring path = (var ? var->as_string() : L"~/.bash_history"); - expand_tilde(path, vars()); - (*history)->populate_from_bash(path.c_str()); - } -} - -/// Check if we have background jobs that we have not warned about. -/// If so, print a warning and return true. Otherwise return false. -static bool try_warn_on_background_jobs(reader_data_t *data) { - ASSERT_IS_MAIN_THREAD(); - // Have we already warned? - if (data->did_warn_for_bg_jobs) return false; - // Are we the top-level reader? - if (reader_data_stack.size() > 1) return false; - // Do we have background jobs? - auto bg_jobs = jobs_requiring_warning_on_exit(data->parser()); - if (bg_jobs->empty()) return false; - // Print the warning! - print_exit_warning_for_jobs(*bg_jobs); - data->did_warn_for_bg_jobs = true; - return true; -} - -/// Check if we should exit the reader loop. -/// \return true if we should exit. -bool check_exit_loop_maybe_warning(reader_data_t *data) { - // sighup always forces exit. - if (reader_received_sighup()) return true; - - // Check if an exit is requested. - if (data && data->exit_loop_requested) { - if (try_warn_on_background_jobs(data)) { - data->exit_loop_requested = false; - return false; - } - return true; - } - return false; -} - -static bool selection_is_at_top(const reader_data_t *data) { - const pager_t *pager = &data->pager; - size_t row = pager->get_selected_row(*data->current_page_rendering); - if (row != 0 && row != PAGER_SELECTION_NONE) return false; - - size_t col = pager->get_selected_column(*data->current_page_rendering); - return !(col != 0 && col != PAGER_SELECTION_NONE); -} - -void reader_data_t::update_commandline_state() const { - auto snapshot = commandline_state_snapshot(); - snapshot->text = *this->command_line.text(); - snapshot->cursor_pos = this->command_line.position(); - if (this->history) { - snapshot->history = (*this->history)->clone(); - } - snapshot->selection = this->get_selection(); - snapshot->pager_mode = !this->pager.empty(); - snapshot->pager_fully_disclosed = this->current_page_rendering->remaining_to_disclose() == 0; - snapshot->search_mode = this->history_search.active(); - snapshot->initialized = true; -} - -void reader_data_t::apply_commandline_state_changes() { - // Only the text and cursor position may be changed. - commandline_state_t state = commandline_get_state(); - if (state.text != *this->command_line.text() || - state.cursor_pos != this->command_line.position()) { - // The commandline builtin changed our contents. - this->clear_pager(); - this->set_buffer_maintaining_pager(state.text, state.cursor_pos); - this->reset_loop_state = true; - } -} - -void reader_data_t::compute_and_apply_completions(readline_cmd_t c, readline_loop_state_t &rls) { - assert((c == readline_cmd_t::Complete || c == readline_cmd_t::CompleteAndSearch) && - "Invalid command"); - editable_line_t *el = &command_line; - - // Remove a trailing backslash. This may trigger an extra repaint, but this is - // rare. - if (is_backslashed(*el->text(), el->position())) { - delete_char(); - } - - // Get the string; we have to do this after removing any trailing backslash. - auto text = *el->text(); - const wchar_t *const buff = text.c_str(); - - // Figure out the extent of the command substitution surrounding the cursor. - // This is because we only look at the current command substitution to form - // completions - stuff happening outside of it is not interesting. - const wchar_t *cmdsub_begin, *cmdsub_end; - parse_util_cmdsubst_extent(buff, el->position(), &cmdsub_begin, &cmdsub_end); - size_t position_in_cmdsub = el->position() - (cmdsub_begin - buff); - - // Figure out the extent of the token within the command substitution. Note we - // pass cmdsub_begin here, not buff. - const wchar_t *token_begin, *token_end; - parse_util_token_extent(cmdsub_begin, position_in_cmdsub, &token_begin, &token_end, nullptr, - nullptr); - size_t position_in_token = position_in_cmdsub - (token_begin - cmdsub_begin); - - // Hack: the token may extend past the end of the command substitution, e.g. in - // (echo foo) the last token is 'foo)'. Don't let that happen. - if (token_end > cmdsub_end) token_end = cmdsub_end; - - // Check if we have a wildcard within this string; if so we first attempt to expand the - // wildcard; if that succeeds we don't then apply user completions (#8593). - std::unique_ptr wc_expanded; - switch (static_cast(try_expand_wildcard( - this->parser(), wcstring(token_begin, token_end), position_in_token, wc_expanded))) { - case ExpandResultCode::error: - // This may come about if we exceeded the max number of matches. - // Return "success" to suppress normal completions. - flash(); - return; - case ExpandResultCode::wildcard_no_match: - break; - case ExpandResultCode::cancel: - // e.g. the user hit control-C. Suppress normal completions. - return; - case ExpandResultCode::ok: - rls.comp->clear(); - rls.complete_did_insert = false; - size_t tok_off = static_cast(token_begin - buff); - size_t tok_len = static_cast(token_end - token_begin); - push_edit(el, new_edit(tok_off, tok_off + tok_len, std::move(*wc_expanded))); - return; - } - - // Construct a copy of the string from the beginning of the command substitution - // up to the end of the token we're completing. - const wcstring buffcpy = wcstring(cmdsub_begin, token_end); - - // Ensure that `commandline` inside the completions gets the current state. - update_commandline_state(); - - std::unique_ptr needs_load = nullptr; - rls.comp = complete(buffcpy, completion_request_options_normal(), - *parser_context(parser_ref->deref()), needs_load); - - // User-supplied completions may have changed the commandline - prevent buffer - // overflow. - if (token_begin > buff + el->text()->size()) token_begin = buff + el->text()->size(); - if (token_end > buff + el->text()->size()) token_end = buff + el->text()->size(); - - // Munge our completions. - rls.comp->sort_and_prioritize(CompletionRequestOptions()); - - // Record our cycle_command_line. - cycle_command_line = *el->text(); - cycle_cursor_pos = token_end - buff; - - rls.complete_did_insert = handle_completions(*rls.comp, token_begin - buff, token_end - buff); - - // Show the search field if requested and if we printed a list of completions. - if (c == readline_cmd_t::CompleteAndSearch && !rls.complete_did_insert && !pager.empty()) { - pager.set_search_field_shown(true); - select_completion_in_direction(selection_motion_t::next); - } -} - -static relaxed_atomic_t run_count{0}; - -/// Returns the current interactive loop count -uint64_t reader_run_count() { return run_count; } - -static relaxed_atomic_t status_count{0}; - -/// Returns the current "generation" of interactive status. -/// This is not incremented if the command being run produces no status, -/// (e.g. background job, or variable assignment). -uint64_t reader_status_count() { return status_count; } - -/// Read interactively. Read input from stdin while providing editing facilities. -static int read_i(const parser_t &parser) { - ASSERT_IS_MAIN_THREAD(); - parser.assert_can_execute(); - reader_config_t conf; - conf.complete_ok = true; - conf.highlight_ok = true; - conf.syntax_check_ok = true; - conf.autosuggest_ok = check_autosuggestion_enabled(env_stack_t{parser.vars_boxed()}); - conf.expand_abbrev_ok = true; - conf.event = L"fish_prompt"; - - if (parser.is_breakpoint() && function_exists(DEBUG_PROMPT_FUNCTION_NAME, parser)) { - conf.left_prompt_cmd = DEBUG_PROMPT_FUNCTION_NAME; - conf.right_prompt_cmd = wcstring{}; - } else { - conf.left_prompt_cmd = LEFT_PROMPT_FUNCTION_NAME; - conf.right_prompt_cmd = RIGHT_PROMPT_FUNCTION_NAME; - } - - std::shared_ptr data = - reader_push_ret(parser, *history_session_id(parser.vars()), std::move(conf)); - data->import_history_if_necessary(); - - while (!check_exit_loop_maybe_warning(data.get())) { - ++run_count; - - if (maybe_t mcmd = data->readline(0)) { - const wcstring command = mcmd.acquire(); - if (command.empty()) { - continue; - } - - data->update_buff_pos(&data->command_line, 0); - data->command_line.clear(); - data->command_line_changed(&data->command_line); - event_fire_generic(parser, L"fish_preexec", {command}); - auto eval_res = reader_run_command(parser, command); - signal_clear_cancel(); - if (!eval_res->no_status()) { - ++status_count; - } - - // If the command requested an exit, then process it now and clear it. - data->exit_loop_requested |= parser.libdata_pods().exit_current_script; - parser.libdata_pods_mut().exit_current_script = false; - - event_fire_generic(parser, L"fish_postexec", {command}); - // Allow any pending history items to be returned in the history array. - if (data->history) { - (*data->history)->resolve_pending(); - } - - bool already_warned = data->did_warn_for_bg_jobs; - if (check_exit_loop_maybe_warning(data.get())) { - break; - } - if (already_warned) { - // We had previously warned the user and they ran another command. - // Reset the warning. - data->did_warn_for_bg_jobs = false; - } - - // Apply any command line update from this command or fish_postexec, etc. - // See #8807. - data->apply_commandline_state_changes(); - } - } - reader_pop(); - - // If we got SIGHUP, ensure the tty is redirected. - if (reader_received_sighup()) { - // If we are the top-level reader, then we translate SIGHUP into exit_forced. - redirect_tty_after_sighup(); - } - - // If we are the last reader, then kill remaining jobs before exiting. - if (reader_data_stack.empty()) { - // Send the exit event and then commit to not executing any more fish script. - s_exit_state = exit_state_t::running_handlers; - event_fire_generic(parser, L"fish_exit"); - s_exit_state = exit_state_t::finished_handlers; - hup_jobs(parser); - } - - return 0; -} - -/// Test if the specified character in the specified string is backslashed. pos may be at the end of -/// the string, which indicates if there is a trailing backslash. -static bool is_backslashed(const wcstring &str, size_t pos) { - // note pos == str.size() is OK. - if (pos > str.size()) return false; - - size_t count = 0, idx = pos; - while (idx--) { - if (str.at(idx) != L'\\') break; - count++; - } - - return (count % 2) == 1; -} - -static wchar_t unescaped_quote(const wcstring &str, size_t pos) { - wchar_t result = L'\0'; - if (pos < str.size()) { - wchar_t c = str.at(pos); - if ((c == L'\'' || c == L'"') && !is_backslashed(str, pos)) { - result = c; - } - } - return result; -} - -/// Returns true if the last token is a comment. -static bool text_ends_in_comment(const wcstring &text) { - auto tok = new_tokenizer(text.c_str(), TOK_ACCEPT_UNFINISHED | TOK_SHOW_COMMENTS); - bool is_comment = false; - while (auto token = tok->next()) { - is_comment = token->type_ == token_type_t::comment; - } - return is_comment; -} - -/// \return true if an event is a normal character that should be inserted into the buffer. -static bool event_is_normal_char(const char_event_t &evt) { - if (!evt.is_char()) return false; - auto c = evt.get_char(); - return !fish_reserved_codepoint(c) && c > 31 && c != 127; -} - -/// Run a sequence of commands from an input binding. -void reader_data_t::run_input_command_scripts(const std::vector &cmds) { - auto last_statuses = parser().vars().get_last_statuses(); - for (const wcstring &cmd : cmds) { - update_commandline_state(); - parser().eval(cmd, *new_io_chain()); - apply_commandline_state_changes(); - } - parser().set_last_statuses(*last_statuses); - - // Restore tty to shell modes. - // Some input commands will take over the tty - see #2114 for an example where vim is invoked - // from a key binding. However we do NOT want to invoke term_donate(), because that will enable - // ECHO mode, causing a race between new input and restoring the mode (#7770). So we leave the - // tty alone, run the commands in shell mode, and then restore shell modes. - int res; - do { - res = tcsetattr(STDIN_FILENO, TCSANOW, &shell_modes); - } while (res < 0 && errno == EINTR); - if (res < 0) { - wperror(L"tcsetattr"); - } - termsize_invalidate_tty(); -} - -/// Read normal characters, inserting them into the command line. -/// \return the next unhandled event. -maybe_t> reader_data_t::read_normal_chars(readline_loop_state_t &rls) { - maybe_t> event_needing_handling{}; - wcstring accumulated_chars; - size_t limit = std::min(rls.nchars - command_line.size(), READAHEAD_MAX); - shared_ptr normal_handler_ffi = - std::make_shared([this](const void *param) -> void * { - const auto *list = static_cast(param); - this->run_input_command_scripts(list->vals); - return nullptr; - }); - - // We repaint our prompt if fstat reports the tty as having changed. - // But don't react to tty changes that we initiated, because of commands or - // on-variable events (e.g. for fish_bind_mode). See #3481. - uint64_t last_exec_count = exec_count(); - while (accumulated_chars.size() < limit) { - bool allow_commands = (accumulated_chars.empty()); - auto evt = inputter->read_char(allow_commands ? normal_handler_ffi : nullptr); - if (!event_is_normal_char(*evt) || !poll_fd_readable(conf.in)) { - event_needing_handling = std::move(evt); - break; - } else if (evt->get_input_style() == char_input_style_t::NotFirst && - accumulated_chars.empty() && active_edit_line()->position() == 0) { - // The cursor is at the beginning and nothing is accumulated, so skip this character. - continue; - } else { - accumulated_chars.push_back(evt->get_char()); - } - - if (last_exec_count != exec_count()) { - last_exec_count = exec_count(); - screen->save_status(); - } - } - - if (!accumulated_chars.empty()) { - editable_line_t *el = active_edit_line(); - insert_string(el, accumulated_chars); - - // End paging upon inserting into the normal command line. - if (el == &command_line) { - clear_pager(); - } - - // Since we handled a normal character, we don't have a last command. - rls.last_cmd.reset(); - } - - if (last_exec_count != exec_count()) { - last_exec_count = exec_count(); - screen->save_status(); - } - - return event_needing_handling; -} - -/// Handle a readline command \p c, updating the state \p rls. -void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_state_t &rls) { - const auto &vars = this->vars(); - using rl = readline_cmd_t; - switch (c) { - // Go to beginning of line. - case rl::BeginningOfLine: { - editable_line_t *el = active_edit_line(); - while (el->position() > 0 && el->text()->at(el->position() - 1) != L'\n') { - update_buff_pos(el, el->position() - 1); - } - break; - } - case rl::EndOfLine: { - editable_line_t *el = active_edit_line(); - if (el->position() < el->size()) { - auto text = *el->text(); - const wchar_t *buff = text.c_str(); - while (buff[el->position()] && buff[el->position()] != L'\n') { - update_buff_pos(el, el->position() + 1); - } - } else { - accept_autosuggestion(true); - } - break; - } - case rl::BeginningOfBuffer: { - update_buff_pos(&command_line, 0); - break; - } - case rl::EndOfBuffer: { - update_buff_pos(&command_line, command_line.size()); - break; - } - case rl::CancelCommandline: { - if (!command_line.empty()) { - outputter_t &outp = stdoutput(); - // Move cursor to the end of the line. - update_buff_pos(&command_line, command_line.size()); - autosuggestion.clear(); - // Repaint also changes the actual cursor position - if (this->is_repaint_needed()) this->layout_and_repaint(L"cancel"); - - if (auto fish_color_cancel = vars.get(L"fish_color_cancel")) { - outp.set_color(parse_color(*fish_color_cancel, false), - parse_color(*fish_color_cancel, true)); - } - outp.writestr(L"^C"); - outp.set_color(rgb_color_t::reset(), rgb_color_t::reset()); - - // We print a newline last so the prompt_sp hack doesn't get us. - outp.push_back('\n'); - - set_command_line_and_position(&command_line, L"", 0); - screen->reset_abandoning_line(termsize_last().width - command_line.size()); - - // Post fish_cancel. - event_fire_generic(parser(), L"fish_cancel"); - } - break; - } - case rl::Cancel: { - // If we last inserted a completion, undo it. - // This doesn't apply if the completion was selected via the pager - // (in which case the last command is "execute" or similar, - // but never complete{,_and_search}) - // - // Also paging is already cancelled above. - if (rls.complete_did_insert && - (rls.last_cmd == rl::Complete || rls.last_cmd == rl::CompleteAndSearch)) { - editable_line_t *el = active_edit_line(); - el->undo(); - update_buff_pos(el); - } - break; - } - case rl::RepaintMode: { - // Repaint the mode-prompt only if possible. - // This is an optimization basically exclusively for vi-mode, since the prompt - // may sometimes take a while but when switching the mode all we care about is the - // mode-prompt. - // - // Because some users set `fish_mode_prompt` to an empty function and display the mode - // elsewhere, we detect if the mode output is empty. - - // Don't go into an infinite loop of repainting. - // This can happen e.g. if a variable triggers a repaint, - // and the variable is set inside the prompt (#7324). - // builtin commandline will refuse to enqueue these. - parser().libdata_pods_mut().is_repaint = true; - exec_mode_prompt(); - if (!mode_prompt_buff.empty()) { - if (this->is_repaint_needed()) { - screen->reset_line(true /* redraw prompt */); - this->layout_and_repaint(L"mode"); - } - parser().libdata_pods_mut().is_repaint = false; - break; - } - // Else we repaint as normal. - __fallthrough__ - } - case rl::ForceRepaint: - case rl::Repaint: { - parser().libdata_pods_mut().is_repaint = true; - exec_prompt(); - screen->reset_line(true /* redraw prompt */); - this->layout_and_repaint(L"readline"); - force_exec_prompt_and_repaint = false; - parser().libdata_pods_mut().is_repaint = false; - break; - } - case rl::Complete: - case rl::CompleteAndSearch: { - if (!conf.complete_ok) break; - if (is_navigating_pager_contents() || - (!rls.comp->empty() && !rls.complete_did_insert && rls.last_cmd == rl::Complete)) { - // The user typed complete more than once in a row. If we are not yet fully - // disclosed, then become so; otherwise cycle through our available completions. - if (current_page_rendering->remaining_to_disclose() > 0) { - pager.set_fully_disclosed(); - } else { - select_completion_in_direction(c == rl::Complete ? selection_motion_t::next - : selection_motion_t::prev); - } - } else { - // Either the user hit tab only once, or we had no visible completion list. - compute_and_apply_completions(c, rls); - } - break; - } - case rl::PagerToggleSearch: { - if (history_pager_active) { - fill_history_pager(history_pager_invocation_t::Advance, - history_search_direction_t::Forward); - break; - } - if (!pager.empty()) { - // Toggle search, and begin navigating if we are now searching. - bool sfs = pager.is_search_field_shown(); - pager.set_search_field_shown(!sfs); - pager.set_fully_disclosed(); - if (pager.is_search_field_shown() && !is_navigating_pager_contents()) { - select_completion_in_direction(selection_motion_t::south); - } - } - break; - } - case rl::KillLine: { - editable_line_t *el = active_edit_line(); - auto text = *el->text(); - const wchar_t *buff = text.c_str(); - const wchar_t *begin = &buff[el->position()]; - const wchar_t *end = begin; - - while (*end && *end != L'\n') end++; - - if (end == begin && *end) end++; - - size_t len = end - begin; - if (len) { - kill(el, begin - buff, len, KILL_APPEND, rls.last_cmd != rl::KillLine); - } - break; - } - case rl::BackwardKillLine: { - editable_line_t *el = active_edit_line(); - if (el->position() == 0) { - break; - } - auto text = *el->text(); - const wchar_t *buff = text.c_str(); - const wchar_t *end = &buff[el->position()]; - const wchar_t *begin = end; - - begin--; // make sure we delete at least one character (see issue #580) - - // Delete until we hit a newline, or the beginning of the string. - while (begin > buff && *begin != L'\n') begin--; - - // If we landed on a newline, don't delete it. - if (*begin == L'\n') begin++; - assert(end >= begin); - size_t len = std::max(end - begin, 1); - begin = end - len; - kill(el, begin - buff, len, KILL_PREPEND, rls.last_cmd != rl::BackwardKillLine); - break; - } - case rl::KillWholeLine: // We match the emacs behavior here: "kills the entire line - // including the following newline". - case rl::KillInnerLine: // Do not kill the following newline - { - editable_line_t *el = active_edit_line(); - auto text = *el->text(); - const wchar_t *buff = text.c_str(); - - // Back up to the character just past the previous newline, or go to the beginning - // of the command line. Note that if the position is on a newline, visually this - // looks like the cursor is at the end of a line. Therefore that newline is NOT the - // beginning of a line; this justifies the -1 check. - size_t begin = el->position(); - while (begin > 0 && buff[begin - 1] != L'\n') { - begin--; - } - - // Push end forwards to just past the next newline, or just past the last char. - size_t end = el->position(); - for (;; end++) { - if (buff[end] == L'\0') { - if (c == rl::KillWholeLine && begin > 0) { - // We are on the last line. Delete the newline in the beginning to clear - // this line. - begin--; - } - break; - } - if (buff[end] == L'\n') { - if (c == rl::KillWholeLine) { - end++; - } - break; - } - } - - assert(end >= begin); - - if (end > begin) { - kill(el, begin, end - begin, KILL_APPEND, rls.last_cmd != c); - } - break; - } - case rl::Yank: { - wcstring yank_str = std::move(*kill_yank()); - insert_string(active_edit_line(), yank_str); - rls.yank_len = yank_str.size(); - break; - } - case rl::YankPop: { - if (rls.yank_len) { - editable_line_t *el = active_edit_line(); - wcstring yank_str = std::move(*kill_yank_rotate()); - size_t new_yank_len = yank_str.size(); - replace_substring(el, el->position() - rls.yank_len, rls.yank_len, - std::move(yank_str)); - update_buff_pos(el); - rls.yank_len = new_yank_len; - suppress_autosuggestion = true; - } - break; - } - case rl::BackwardDeleteChar: { - delete_char(); - break; - } - case rl::Exit: { - // This is by definition a successful exit, override the status - parser().set_last_statuses(*statuses_just(STATUS_CMD_OK)); - exit_loop_requested = true; - check_exit_loop_maybe_warning(this); - break; - } - case rl::DeleteOrExit: - case rl::DeleteChar: { - // Remove the current character in the character buffer and on the screen using - // syntax highlighting, etc. - editable_line_t *el = active_edit_line(); - if (el->position() < el->size()) { - delete_char(false /* backward */); - } else if (c == rl::DeleteOrExit && el->empty()) { - // This is by definition a successful exit, override the status - parser().set_last_statuses(*statuses_just(STATUS_CMD_OK)); - exit_loop_requested = true; - check_exit_loop_maybe_warning(this); - } - break; - } - case rl::Execute: { - if (!this->handle_execute(rls)) { - event_fire_generic(parser(), L"fish_posterror", {*command_line.text()}); - screen->reset_abandoning_line(termsize_last().width); - } - break; - } - - case rl::HistoryPrefixSearchBackward: - case rl::HistoryPrefixSearchForward: - case rl::HistorySearchBackward: - case rl::HistorySearchForward: - case rl::HistoryTokenSearchBackward: - case rl::HistoryTokenSearchForward: { - reader_history_search_t::mode_t mode = - (c == rl::HistoryTokenSearchBackward || c == rl::HistoryTokenSearchForward) - ? reader_history_search_t::token - : (c == rl::HistoryPrefixSearchBackward || c == rl::HistoryPrefixSearchForward) - ? reader_history_search_t::prefix - : reader_history_search_t::line; - - bool was_active_before = history_search.active(); - - if (history_search.is_at_end()) { - const editable_line_t *el = &command_line; - if (mode == reader_history_search_t::token) { - // Searching by token. - const wchar_t *begin, *end; - auto text = *el->text(); - const wchar_t *buff = text.c_str(); - parse_util_token_extent(buff, el->position(), &begin, &end, nullptr, nullptr); - if (begin) { - wcstring token(begin, end); - history_search.reset_to_mode(token, **history, - reader_history_search_t::token, begin - buff); - } else { - // No current token, refuse to do a token search. - history_search.reset(); - } - } else { - // Searching by line. - history_search.reset_to_mode(*el->text(), **history, mode, 0); - - // Skip the autosuggestion in the history unless it was truncated. - const wcstring &suggest = autosuggestion.text; - if (!suggest.empty() && !screen->autosuggestion_is_truncated() && - mode != reader_history_search_t::prefix) { - history_search.add_skip(suggest); - } - } - } - if (history_search.active()) { - history_search_direction_t dir = - (c == rl::HistorySearchBackward || c == rl::HistoryTokenSearchBackward || - c == rl::HistoryPrefixSearchBackward) - ? history_search_direction_t::Backward - : history_search_direction_t::Forward; - bool found = history_search.move_in_direction(dir); - - // Signal that we've found nothing - if (!found) flash(); - - if (!found && !was_active_before) { - history_search.reset(); - break; - } - if (found || - (dir == history_search_direction_t::Forward && history_search.is_at_end())) { - update_command_line_from_history_search(); - } - } - break; - } - case rl::HistoryPager: { - if (history_pager_active) { - fill_history_pager(history_pager_invocation_t::Advance, - history_search_direction_t::Backward); - break; - } - - // Record our cycle_command_line. - cycle_command_line = *command_line.text(); - cycle_cursor_pos = command_line.position(); - - this->history_pager_active = true; - this->history_pager_history_index_start = 0; - this->history_pager_history_index_end = 0; - // Update the pager data. - pager.set_search_field_shown(true); - pager.set_prefix(MB_CUR_MAX > 1 ? L"► " : L"> ", false /* highlight */); - // Update the search field, which triggers the actual history search. - if (!history_search.active() || history_search.search_string().empty()) { - // Escape any wildcards the user may have in their input. - auto escaped_command_line = parse_util_escape_wildcards(*command_line.text()); - insert_string(pager.search_field_line(), escaped_command_line); - } else { - // If we have an actual history search already going, reuse that term - // - this is if the user looks around a bit and decides to switch to the pager. - insert_string(pager.search_field_line(), history_search.search_string()); - } - break; - } - case rl::HistoryPagerDelete: { - if (!history_pager_active) { - inputter->function_set_status(false); - break; - } - inputter->function_set_status(true); - if (auto completion = pager.selected_completion(*current_page_rendering)) { - (*history)->remove(*completion->completion()); - (*history)->save(); - fill_history_pager(history_pager_invocation_t::Refresh, - history_search_direction_t::Backward); - } - break; - } - case rl::BackwardChar: { - editable_line_t *el = active_edit_line(); - if (is_navigating_pager_contents()) { - select_completion_in_direction(selection_motion_t::west); - } else if (el->position() > 0) { - update_buff_pos(el, el->position() - 1); - } - break; - } - case rl::ForwardChar: { - editable_line_t *el = active_edit_line(); - if (is_navigating_pager_contents()) { - select_completion_in_direction(selection_motion_t::east); - } else if (el->position() < el->size()) { - update_buff_pos(el, el->position() + 1); - } else { - accept_autosuggestion(true); - } - break; - } - case rl::ForwardSingleChar: { - editable_line_t *el = active_edit_line(); - if (is_navigating_pager_contents()) { - select_completion_in_direction(selection_motion_t::east); - } else if (el->position() < el->size()) { - update_buff_pos(el, el->position() + 1); - } else { - accept_autosuggestion(false, true); - } - break; - } - case rl::BackwardKillWord: - case rl::BackwardKillPathComponent: - case rl::BackwardKillBigword: { - move_word_style_t style = - (c == rl::BackwardKillBigword ? move_word_style_t::Whitespace - : c == rl::BackwardKillPathComponent ? move_word_style_t::PathComponents - : move_word_style_t::Punctuation); - // Is this the same killring item as the last kill? - bool newv = (rls.last_cmd != rl::BackwardKillWord && - rls.last_cmd != rl::BackwardKillPathComponent && - rls.last_cmd != rl::BackwardKillBigword); - move_word(active_edit_line(), MOVE_DIR_LEFT, true /* erase */, style, newv); - break; - } - case rl::KillWord: - case rl::KillBigword: { - // The "bigword" functions differ only in that they move to the next whitespace, not - // punctuation. - auto move_style = (c == rl::KillWord) ? move_word_style_t::Punctuation - : move_word_style_t::Whitespace; - move_word(active_edit_line(), MOVE_DIR_RIGHT, true /* erase */, move_style, - rls.last_cmd != c /* same kill item if same movement */); - break; - } - case rl::BackwardWord: - case rl::BackwardBigword: - case rl::PrevdOrBackwardWord: { - if (c == rl::PrevdOrBackwardWord && command_line.empty()) { - auto last_statuses = parser().vars().get_last_statuses(); - (void)parser().eval(L"prevd", *new_io_chain()); - parser().set_last_statuses(*std::move(last_statuses)); - force_exec_prompt_and_repaint = true; - inputter->queue_readline(readline_cmd_t::Repaint); - break; - } - - auto move_style = (c != rl::BackwardBigword) ? move_word_style_t::Punctuation - : move_word_style_t::Whitespace; - move_word(active_edit_line(), MOVE_DIR_LEFT, false /* do not erase */, move_style, - false); - break; - } - case rl::ForwardWord: - case rl::ForwardBigword: - case rl::NextdOrForwardWord: { - if (c == rl::NextdOrForwardWord && command_line.empty()) { - auto last_statuses = parser().vars().get_last_statuses(); - (void)parser().eval(L"nextd", *new_io_chain()); - parser().set_last_statuses(*std::move(last_statuses)); - force_exec_prompt_and_repaint = true; - inputter->queue_readline(readline_cmd_t::Repaint); - break; - } - - auto move_style = (c != rl::ForwardBigword) ? move_word_style_t::Punctuation - : move_word_style_t::Whitespace; - editable_line_t *el = active_edit_line(); - if (el->position() < el->size()) { - move_word(el, MOVE_DIR_RIGHT, false /* do not erase */, move_style, false); - } else { - accept_autosuggestion(false, false, move_style); - } - break; - } - case rl::BeginningOfHistory: - case rl::EndOfHistory: { - bool up = (c == rl::BeginningOfHistory); - if (is_navigating_pager_contents()) { - select_completion_in_direction(up ? selection_motion_t::page_north - : selection_motion_t::page_south); - } else { - if (up) { - history_search.go_to_beginning(); - } else { - history_search.go_to_end(); - } - if (history_search.active()) { - update_command_line_from_history_search(); - } - } - break; - } - case rl::UpLine: - case rl::DownLine: { - if (is_navigating_pager_contents()) { - // We are already navigating pager contents. - selection_motion_t direction; - if (c == rl::DownLine) { - // Down arrow is always south. - direction = selection_motion_t::south; - } else if (selection_is_at_top(this)) { - // Up arrow, but we are in the first column and first row. End navigation. - direction = selection_motion_t::deselect; - } else { - // Up arrow, go north. - direction = selection_motion_t::north; - } - - // Now do the selection. - select_completion_in_direction(direction); - } else if (!pager.empty()) { - // We pressed a direction with a non-empty pager, begin navigation. - select_completion_in_direction(c == rl::DownLine ? selection_motion_t::south - : selection_motion_t::north); - } else { - // Not navigating the pager contents. - editable_line_t *el = active_edit_line(); - int line_old = parse_util_get_line_from_offset(*el->text(), el->position()); - int line_new; - - if (c == rl::UpLine) - line_new = line_old - 1; - else - line_new = line_old + 1; - - int line_count = parse_util_lineno(*el->text(), el->size()) - 1; - - if (line_new >= 0 && line_new <= line_count) { - auto indents = parse_util_compute_indents(*el->text()); - size_t base_pos_new = parse_util_get_offset_from_line(*el->text(), line_new); - size_t base_pos_old = parse_util_get_offset_from_line(*el->text(), line_old); - - assert(base_pos_new != static_cast(-1) && - base_pos_old != static_cast(-1)); - int indent_old = indents.at(std::min(indents.size() - 1, base_pos_old)); - int indent_new = indents.at(std::min(indents.size() - 1, base_pos_new)); - - size_t line_offset_old = el->position() - base_pos_old; - size_t total_offset_new = parse_util_get_offset( - *el->text(), line_new, line_offset_old - 4 * (indent_new - indent_old)); - update_buff_pos(el, total_offset_new); - } - } - break; - } - case rl::SuppressAutosuggestion: { - suppress_autosuggestion = true; - bool success = !autosuggestion.empty(); - autosuggestion.clear(); - // Return true if we had a suggestion to clear. - inputter->function_set_status(success); - break; - } - case rl::AcceptAutosuggestion: { - accept_autosuggestion(true); - break; - } - case rl::TransposeChars: { - editable_line_t *el = active_edit_line(); - if (el->size() < 2) { - break; - } - - // If the cursor is at the end, transpose the last two characters of the line. - if (el->position() == el->size()) { - update_buff_pos(el, el->position() - 1); - } - - // Drag the character before the cursor forward over the character at the cursor, - // moving the cursor forward as well. - if (el->position() > 0) { - wcstring local_cmd = *el->text(); - std::swap(local_cmd.at(el->position()), local_cmd.at(el->position() - 1)); - set_command_line_and_position(el, std::move(local_cmd), el->position() + 1); - } - break; - } - case rl::TransposeWords: { - editable_line_t *el = active_edit_line(); - size_t len = el->size(); - auto text = *el->text(); - const wchar_t *buff = text.c_str(); - const wchar_t *tok_begin, *tok_end, *prev_begin, *prev_end; - - // If we are not in a token, look for one ahead. - size_t buff_pos = el->position(); - while (buff_pos != len && !iswalnum(buff[buff_pos])) buff_pos++; - - update_buff_pos(el, buff_pos); - - parse_util_token_extent(buff, el->position(), &tok_begin, &tok_end, &prev_begin, - &prev_end); - - // In case we didn't find a token at or after the cursor... - if (tok_begin == &buff[len]) { - // ...retry beginning from the previous token. - size_t pos = prev_end - &buff[0]; - parse_util_token_extent(buff, pos, &tok_begin, &tok_end, &prev_begin, &prev_end); - } - - // Make sure we have two tokens. - if (prev_begin < prev_end && tok_begin < tok_end && tok_begin > prev_begin) { - const wcstring prev(prev_begin, prev_end - prev_begin); - const wcstring sep(prev_end, tok_begin - prev_end); - const wcstring tok(tok_begin, tok_end - tok_begin); - const wcstring trail(tok_end, &buff[len] - tok_end); - - // Compose new command line with swapped tokens. - wcstring new_buff(buff, prev_begin - buff); - new_buff.append(tok); - new_buff.append(sep); - new_buff.append(prev); - new_buff.append(trail); - // Put cursor right after the second token. - set_command_line_and_position(el, std::move(new_buff), tok_end - buff); - } - break; - } - case rl::TogglecaseChar: { - editable_line_t *el = active_edit_line(); - size_t buff_pos = el->position(); - - // Check that the cursor is on a character - if (buff_pos < el->size()) { - wchar_t chr = el->text()->at(buff_pos); - wcstring replacement; - - // Toggle the case of the current character - bool make_uppercase = iswlower(chr); - if (make_uppercase) { - chr = towupper(chr); - } else { - chr = std::tolower(chr); - } - - replacement.push_back(chr); - replace_substring(el, buff_pos, (size_t)1, std::move(replacement)); - - // Restore the buffer position since replace_substring moves - // the buffer position ahead of the replaced text. - update_buff_pos(el, buff_pos); - } - break; - } - case rl::TogglecaseSelection: { - editable_line_t *el = active_edit_line(); - - // Check that we have an active selection and get the bounds. - if (auto selection = this->get_selection()) { - size_t start = selection->start; - size_t len = selection->length; - - size_t buff_pos = el->position(); - wcstring replacement; - - // Loop through the selected characters and toggle their case. - for (size_t pos = start; pos < start + len && pos < el->size(); pos++) { - wchar_t chr = el->text()->at(pos); - - // Toggle the case of the current character. - bool make_uppercase = iswlower(chr); - if (make_uppercase) { - chr = towupper(chr); - } else { - chr = std::tolower(chr); - } - - replacement.push_back(chr); - } - - replace_substring(el, start, len, std::move(replacement)); - - // Restore the buffer position since replace_substring moves - // the buffer position ahead of the replaced text. - update_buff_pos(el, buff_pos); - } - break; - } - case rl::UpcaseWord: - case rl::DowncaseWord: - case rl::CapitalizeWord: { - editable_line_t *el = active_edit_line(); - // For capitalize_word, whether we've capitalized a character so far. - bool capitalized_first = false; - - // We apply the operation from the current location to the end of the word. - size_t pos = el->position(); - size_t init_pos = pos; - move_word(el, MOVE_DIR_RIGHT, false, move_word_style_t::Punctuation, false); - wcstring replacement; - for (; pos < el->position(); pos++) { - wchar_t chr = el->text()->at(pos); - - // We always change the case; this decides whether we go uppercase (true) or - // lowercase (false). - bool make_uppercase; - if (c == rl::CapitalizeWord) - make_uppercase = !capitalized_first && iswalnum(chr); - else - make_uppercase = (c == rl::UpcaseWord); - - // Apply the operation and then record what we did. - if (make_uppercase) - chr = towupper(chr); - else - chr = towlower(chr); - - replacement.push_back(chr); - capitalized_first = capitalized_first || make_uppercase; - } - replace_substring(el, init_pos, pos - init_pos, std::move(replacement)); - update_buff_pos(el); - break; - } - - case rl::BeginSelection: { - if (!selection) selection = selection_data_t{}; - size_t pos = command_line.position(); - selection->begin = pos; - selection->start = pos; - selection->stop = - pos + (cursor_selection_mode == cursor_selection_mode_t::inclusive ? 1 : 0); - break; - } - - case rl::EndSelection: { - selection.reset(); - break; - } - - case rl::SwapSelectionStartStop: { - if (!selection) break; - size_t tmp = selection->begin; - selection->begin = command_line.position(); - selection->start = command_line.position(); - editable_line_t *el = active_edit_line(); - update_buff_pos(el, tmp); - break; - } - - case rl::KillSelection: { - bool newv = (rls.last_cmd != rl::KillSelection); - if (auto selection = this->get_selection()) { - kill(&command_line, selection->start, selection->length, KILL_APPEND, newv); - } - break; - } - case rl::InsertLineOver: { - editable_line_t *el = active_edit_line(); - while (el->position() > 0 && el->text()->at(el->position() - 1) != L'\n') { - update_buff_pos(el, el->position() - 1); - } - insert_char(el, L'\n'); - update_buff_pos(el, el->position() - 1); - break; - } - case rl::InsertLineUnder: { - editable_line_t *el = active_edit_line(); - if (el->position() < el->size()) { - auto text = *el->text(); - const wchar_t *buff = text.c_str(); - while (buff[el->position()] && buff[el->position()] != L'\n') { - update_buff_pos(el, el->position() + 1); - } - } - insert_char(el, L'\n'); - break; - } - case rl::ForwardJump: - case rl::BackwardJump: - case rl::ForwardJumpTill: - case rl::BackwardJumpTill: { - auto direction = (c == rl::ForwardJump || c == rl::ForwardJumpTill) - ? jump_direction_t::forward - : jump_direction_t::backward; - auto precision = (c == rl::ForwardJump || c == rl::BackwardJump) - ? jump_precision_t::to - : jump_precision_t::till; - editable_line_t *el = active_edit_line(); - wchar_t target = inputter->function_pop_arg(); - bool success = jump(direction, precision, el, target); - - inputter->function_set_status(success); - break; - } - case rl::RepeatJump: { - editable_line_t *el = active_edit_line(); - bool success = false; - - if (last_jump_target) { - success = jump(last_jump_direction, last_jump_precision, el, last_jump_target); - } - - inputter->function_set_status(success); - break; - } - case rl::ReverseRepeatJump: { - editable_line_t *el = active_edit_line(); - bool success = false; - jump_direction_t original_dir, dir; - original_dir = last_jump_direction; - - if (last_jump_direction == jump_direction_t::forward) { - dir = jump_direction_t::backward; - } else { - dir = jump_direction_t::forward; - } - - if (last_jump_target) { - success = jump(dir, last_jump_precision, el, last_jump_target); - } - - last_jump_direction = original_dir; - - inputter->function_set_status(success); - break; - } - - case rl::ExpandAbbr: { - if (expand_abbreviation_at_cursor(1)) { - inputter->function_set_status(true); - } else { - inputter->function_set_status(false); - } - break; - } - case rl::Undo: - case rl::Redo: { - editable_line_t *el = active_edit_line(); - bool ok = (c == rl::Undo) ? el->undo() : el->redo(); - if (ok) { - if (el == &command_line) { - clear_pager(); - } - update_buff_pos(el); - maybe_refilter_pager(el); - } else { - flash(); - } - break; - } - case rl::BeginUndoGroup: { - editable_line_t *el = active_edit_line(); - el->begin_edit_group(); - break; - } - case rl::EndUndoGroup: { - editable_line_t *el = active_edit_line(); - el->end_edit_group(); - break; - } - case rl::DisableMouseTracking: { - outputter_t &outp = stdoutput(); - outp.writestr(L"\x1B[?1000l"); - break; - } - case rl::ClearScreenAndRepaint: { - parser().libdata_pods_mut().is_repaint = true; - auto clear = *screen_clear(); - if (!clear.empty()) { - // Clear the screen if we can. - // This is subtle: We first clear, draw the old prompt, - // and *then* reexecute the prompt and overdraw it. - // This removes the flicker, - // while keeping the prompt up-to-date. - outputter_t &outp = stdoutput(); - outp.writestr(clear.c_str()); - screen->reset_line(true /* redraw prompt */); - this->layout_and_repaint(L"readline"); - } - exec_prompt(); - screen->reset_line(true /* redraw prompt */); - this->layout_and_repaint(L"readline"); - force_exec_prompt_and_repaint = false; - parser().libdata_pods_mut().is_repaint = false; - break; - } - // Some commands should have been handled internally by inputter_t::readch(). - case rl::SelfInsert: - case rl::SelfInsertNotFirst: - case rl::FuncOr: - case rl::FuncAnd: { - DIE("should have been handled by inputter_t::readch"); - } - } -} - -void reader_data_t::add_to_history() { - if (!history || conf.in_silent_mode) { - return; - } - - // Historical behavior is to trim trailing spaces, unless escape (#7661). - wcstring text = *command_line.text(); - while (!text.empty() && text.back() == L' ' && - count_preceding_backslashes(text, text.size() - 1) % 2 == 0) { - text.pop_back(); - } - - // Remove ephemeral items - even if the text is empty. - (*history)->remove_ephemeral_items(); - - if (!text.empty()) { - // Mark this item as ephemeral if there is a leading space (#615). - history_persistence_mode_t mode; - if (text.front() == L' ') { - // Leading spaces are ephemeral (#615). - mode = history_persistence_mode_t::Ephemeral; - } else if (in_private_mode(this->vars().get_impl_ffi())) { - // Private mode means in-memory only. - mode = history_persistence_mode_t::Memory; - } else { - mode = history_persistence_mode_t::Disk; - } - (*history)->add_pending_with_file_detection(text, this->vars().get_impl_ffi(), mode); - } -} - -parser_test_error_bits_t reader_data_t::expand_for_execute() { - // Expand abbreviations at the cursor. - // The first expansion is "user visible" and enters into history. - editable_line_t *el = &command_line; - parser_test_error_bits_t test_res = 0; - - // Syntax check before expanding abbreviations. We could consider relaxing this: a string may be - // syntactically invalid but become valid after expanding abbreviations. - if (conf.syntax_check_ok) { - test_res = reader_shell_test(parser(), *el->text()); - if (test_res & PARSER_TEST_ERROR) return test_res; - } - - // Exec abbreviations at the cursor. - // Note we want to expand abbreviations even if incomplete. - if (expand_abbreviation_at_cursor(0)) { - // Trigger syntax highlighting as we are likely about to execute this command. - this->super_highlight_me_plenty(); - if (conf.syntax_check_ok) { - test_res = reader_shell_test(parser(), *el->text()); - } - } - return test_res; -} - -bool reader_data_t::handle_execute(readline_loop_state_t &rls) { - // Evaluate. If the current command is unfinished, or if the charater is escaped - // using a backslash, insert a newline. - // If the user hits return while navigating the pager, it only clears the pager. - if (is_navigating_pager_contents()) { - if (this->history_pager_active && - this->pager.selected_completion_index() == PAGER_SELECTION_NONE) { - command_line.push_edit( - new_edit(0, command_line.size(), *this->pager.search_field_line()->text()), - /* allow_coalesce */ false); - command_line.set_position(this->pager.search_field_line()->position()); - } - clear_pager(); - return true; - } - - // Delete any autosuggestion. - autosuggestion.clear(); - - // The user may have hit return with pager contents, but while not navigating them. - // Clear the pager in that event. - clear_pager(); - - // We only execute the command line. - editable_line_t *el = &command_line; - - // Allow backslash-escaped newlines. - bool continue_on_next_line = false; - if (el->position() >= el->size()) { - // We're at the end of the text and not in a comment (issue #1225). - continue_on_next_line = - is_backslashed(*el->text(), el->position()) && !text_ends_in_comment(*el->text()); - } else { - // Allow mid line split if the following character is whitespace (issue #613). - if (is_backslashed(*el->text(), el->position()) && - iswspace(el->text()->at(el->position()))) { - continue_on_next_line = true; - // Check if the end of the line is backslashed (issue #4467). - } else if (is_backslashed(*el->text(), el->size()) && !text_ends_in_comment(*el->text())) { - // Move the cursor to the end of the line. - el->set_position(el->size()); - continue_on_next_line = true; - } - } - // If the conditions are met, insert a new line at the position of the cursor. - if (continue_on_next_line) { - insert_char(el, L'\n'); - return true; - } - - // Expand the command line in preparation for execution. - // to_exec is the command to execute; the command line itself has the command for history. - parser_test_error_bits_t test_res = this->expand_for_execute(); - if (test_res & PARSER_TEST_ERROR) { - return false; - } else if (test_res & PARSER_TEST_INCOMPLETE) { - insert_char(el, L'\n'); - return true; - } - assert(test_res == 0); - - this->add_to_history(); - rls.finished = true; - update_buff_pos(&command_line, command_line.size()); - return true; -} - -maybe_t reader_data_t::readline(int nchars_or_0) { - using rl = readline_cmd_t; - readline_loop_state_t rls{}; - - // Suppress fish_trace during executing key bindings. - // This is simply to reduce noise. - scoped_push in_title(&parser().libdata_pods_mut().suppress_fish_trace, true); - - // If nchars_or_0 is positive, then that's the maximum number of chars. Otherwise keep it at - // SIZE_MAX. - if (nchars_or_0 > 0) { - rls.nchars = static_cast(nchars_or_0); - } - - // The command line before completion. - cycle_command_line.clear(); - cycle_cursor_pos = 0; - - history_search.reset(); - - // It may happen that a command we ran when job control was disabled nevertheless stole the tty - // from us. In that case when we read from our fd, it will trigger SIGTTIN. So just - // unconditionally reclaim the tty. See #9181. - (void)tcsetpgrp(conf.in, getpgrp()); - - // Get the current terminal modes. These will be restored when the function returns. - struct termios old_modes {}; - if (tcgetattr(conf.in, &old_modes) == -1 && errno == EIO) redirect_tty_output(); - - // Set the new modes. - if (tcsetattr(conf.in, TCSANOW, &shell_modes) == -1) { - int err = errno; - if (err == EIO) redirect_tty_output(); - - // This check is required to work around certain issues with fish's approach to - // terminal control when launching interactive processes while in non-interactive - // mode. See #4178 for one such example. - if (err != ENOTTY || is_interactive_session()) { - wperror(L"tcsetattr"); - } - } - - // HACK: Don't abandon line for the first prompt, because - // if we're started with the terminal it might not have settled, - // so the width is quite likely to be in flight. - // - // This means that `printf %s foo; fish` will overwrite the `foo`, - // but that's a smaller problem than having the omitted newline char - // appear constantly. - // - // I can't see a good way around this. - if (!first_prompt) { - screen->reset_abandoning_line(termsize_last().width); - } - first_prompt = false; - - if (!conf.event.empty()) { - event_fire_generic(parser(), conf.event); - } - exec_prompt(); - - /// A helper that kicks off syntax highlighting, autosuggestion computing, and repaints. - auto color_suggest_repaint_now = [this] { - if (conf.in == STDIN_FILENO) { - this->update_autosuggestion(); - this->super_highlight_me_plenty(); - } - if (this->is_repaint_needed()) this->layout_and_repaint(L"toplevel"); - this->force_exec_prompt_and_repaint = false; - }; - - // Start out as initially dirty. - force_exec_prompt_and_repaint = true; - - while (!rls.finished && !check_exit_loop_maybe_warning(this)) { - if (reset_loop_state) { - reset_loop_state = false; - rls.last_cmd = none(); - rls.complete_did_insert = false; - } - // Perhaps update the termsize. This is cheap if it has not changed. - update_termsize(); - - // Repaint as needed. - color_suggest_repaint_now(); - - if (rls.nchars <= command_line.size()) { - // We've already hit the specified character limit. - rls.finished = true; - break; - } - - maybe_t> maybe_event_needing_handling{}; - while (true) { - maybe_event_needing_handling = read_normal_chars(rls); - if (maybe_event_needing_handling.has_value()) break; - - if (rls.nchars <= command_line.size()) { - maybe_event_needing_handling.reset(); - break; - } - } - - // If we ran `exit` anywhere, exit. - exit_loop_requested |= parser().libdata_pods().exit_current_script; - parser().libdata_pods_mut().exit_current_script = false; - if (exit_loop_requested) continue; - - if (!maybe_event_needing_handling || (*maybe_event_needing_handling)->is_check_exit()) { - continue; - } else if ((*maybe_event_needing_handling)->is_eof()) { - reader_sighup(); - continue; - } - auto event_needing_handling = maybe_event_needing_handling.acquire(); - assert((event_needing_handling->is_char() || event_needing_handling->is_readline()) && - "Should have a char or readline"); - - if (rls.last_cmd != rl::Yank && rls.last_cmd != rl::YankPop) { - rls.yank_len = 0; - } - - if (event_needing_handling->is_readline()) { - readline_cmd_t readline_cmd = event_needing_handling->get_readline(); - if (readline_cmd == rl::Cancel && is_navigating_pager_contents()) { - clear_transient_edit(); - } - - // Clear the pager if necessary. - bool focused_on_search_field = (active_edit_line() == pager.search_field_line()); - if (!history_search.active() && - command_ends_paging(readline_cmd, focused_on_search_field)) { - clear_pager(); - } - - handle_readline_command(readline_cmd, rls); - - if (history_search.active() && command_ends_history_search(readline_cmd)) { - // "cancel" means to abort the whole thing, other ending commands mean to finish the - // search. - if (readline_cmd == rl::Cancel) { - // Go back to the search string by simply undoing the history-search edit. - clear_transient_edit(); - } - history_search.reset(); - command_line_has_transient_edit = false; - } - - rls.last_cmd = readline_cmd; - } else { - // Ordinary char. - wchar_t c = event_needing_handling->get_char(); - if (event_needing_handling->get_input_style() == char_input_style_t::NotFirst && - active_edit_line()->position() == 0) { - // This character is skipped. - } else if (!fish_reserved_codepoint(c) && (c >= L' ' || c == L'\n' || c == L'\r') && - c != 0x7F) { - // Regular character. - editable_line_t *el = active_edit_line(); - insert_char(active_edit_line(), c); - - // End paging upon inserting into the normal command line. - if (el == &command_line) { - clear_pager(); - } - } else { - // This can happen if the user presses a control char we don't recognize. No - // reason to report this to the user unless they've enabled debugging output. - FLOGF(reader, _(L"Unknown key binding 0x%X"), c); - } - rls.last_cmd = none(); - } - } - - // Redraw the command line. This is what ensures the autosuggestion is hidden, etc. after the - // user presses enter. - if (this->is_repaint_needed() || conf.in != STDIN_FILENO) - this->layout_and_repaint(L"prepare to execute"); - - // Finish syntax highlighting (but do not wait forever). - if (rls.finished) { - finish_highlighting_before_exec(); - } - - // Emit a newline so that the output is on the line after the command. - // But do not emit a newline if the cursor has wrapped onto a new line all its own - see #6826. - if (!screen->cursor_is_wrapped_to_own_line()) { - ignore_result(write(STDOUT_FILENO, "\n", 1)); - } - - // HACK: If stdin isn't the same terminal as stdout, we just moved the cursor. - // For now, just reset it to the beginning of the line. - if (conf.in != STDIN_FILENO) { - ignore_result(write(STDOUT_FILENO, "\r", 1)); - } - - // Ensure we have no pager contents when we exit. - if (!pager.empty()) { - // Clear to end of screen to erase the pager contents. - // TODO: this may fail if eos doesn't exist, in which case we should emit newlines. - screen_force_clear_to_end(); - clear_pager(); - } - - if (s_exit_state != exit_state_t::finished_handlers) { - // The order of the two conditions below is important. Try to restore the mode - // in all cases, but only complain if interactive. - if (tcsetattr(conf.in, TCSANOW, &old_modes) == -1 && is_interactive_session()) { - if (errno == EIO) redirect_tty_output(); - wperror(L"tcsetattr"); // return to previous mode - } - stdoutput().set_color(rgb_color_t::reset(), rgb_color_t::reset()); - } - return rls.finished ? maybe_t{command_line.text()} : none(); -} - -bool reader_data_t::jump(jump_direction_t dir, jump_precision_t precision, editable_line_t *el, - wchar_t target) { - bool success = false; - - last_jump_target = target; - last_jump_direction = dir; - last_jump_precision = precision; - - switch (dir) { - case jump_direction_t::backward: { - size_t tmp_pos = el->position(); - - while (tmp_pos--) { - if ((wchar_t)el->at(tmp_pos) == target) { - if (precision == jump_precision_t::till) { - tmp_pos = std::min(el->size() - 1, tmp_pos + 1); - } - update_buff_pos(el, tmp_pos); - success = true; - break; - } - } - break; - } - case jump_direction_t::forward: { - for (size_t tmp_pos = el->position() + 1; tmp_pos < el->size(); tmp_pos++) { - if ((wchar_t)el->at(tmp_pos) == target) { - if (precision == jump_precision_t::till && tmp_pos) { - tmp_pos--; - } - update_buff_pos(el, tmp_pos); - success = true; - break; - } - } - break; - } - } - - return success; -} - -maybe_t reader_readline(int nchars) { - auto *data = current_data(); - // Apply any outstanding commandline changes (#8633). - data->apply_commandline_state_changes(); - return data->readline(nchars); -} - -bool reader_readline_ffi(wcstring &line, int nchars) { - if (auto result = reader_readline(nchars)) { - line = std::move(*result); - return true; - } - return {}; -} - -int reader_reading_interrupted() { - int res = reader_test_and_clear_interrupted(); - reader_data_t *data = current_data_or_null(); - if (res && data && data->conf.exit_on_interrupt) { - data->exit_loop_requested = true; - // We handled the interrupt ourselves, our caller doesn't need to handle it. - return 0; - } - return res; -} - -void reader_schedule_prompt_repaint() { - ASSERT_IS_MAIN_THREAD(); - reader_data_t *data = current_data_or_null(); - if (data && !data->force_exec_prompt_and_repaint) { - data->force_exec_prompt_and_repaint = true; - data->inputter->queue_readline(readline_cmd_t::Repaint); - } -} - -void reader_handle_command(readline_cmd_t cmd) { - if (reader_data_t *data = current_data_or_null()) { - readline_loop_state_t rls{}; - data->handle_readline_command(cmd, rls); - } -} - -void reader_queue_ch(rust::Box ch) { - if (reader_data_t *data = current_data_or_null()) { - data->inputter->queue_char(std::move(ch)); - } -} - -/// Read non-interactively. Read input from stdin without displaying the prompt, using syntax -/// highlighting. This is used for reading scripts and init files. -/// The file is not closed. -static int read_ni(const parser_t &parser, int fd, const io_chain_t &io) { - struct stat buf {}; - if (fstat(fd, &buf) == -1) { - int err = errno; - FLOGF(error, _(L"Unable to read input file: %s"), strerror(err)); - return 1; - } - - /* FreeBSD allows read() on directories. Error explicitly in that case. */ - // XXX: This can be triggered spuriously, so we'll not do that for stdin. - // This can be seen e.g. with node's "spawn" api. - if (fd != STDIN_FILENO && buf.st_mode & S_IFDIR) { - FLOGF(error, _(L"Unable to read input file: %s"), strerror(EISDIR)); - return 1; - } - - // Read all data into a std::string. - std::string fd_contents; - fd_contents.reserve(buf.st_size); - for (;;) { - char buff[4096]; - ssize_t amt = read(fd, buff, sizeof buff); - if (amt > 0) { - fd_contents.append(buff, amt); - } else if (amt == 0) { - // EOF. - break; - } else { - assert(amt == -1); - int err = errno; - if (err == EINTR) { - continue; - } else if ((err == EAGAIN || err == EWOULDBLOCK) && make_fd_blocking(fd)) { - // We succeeded in making the fd blocking, keep going. - continue; - } else { - // Fatal error. - FLOGF(error, _(L"Unable to read input file: %s"), strerror(err)); - assert(false); - // Reset buffer on error. We won't evaluate incomplete files. - fd_contents.clear(); - return 1; - } - } - } - - wcstring str = str2wcstring(fd_contents); - - // Eagerly deallocate to save memory. - fd_contents.clear(); - fd_contents.shrink_to_fit(); - - // Swallow a BOM (issue #1518). - if (!str.empty() && str.at(0) == UTF8_BOM_WCHAR) { - str.erase(0, 1); - } - - // Parse into an ast and detect errors. - auto errors = new_parse_error_list(); - auto ast = ast_parse(str, parse_flag_none, &*errors); - bool errored = ast->errored(); - if (!errored) { - errored = parse_util_detect_errors(*ast, str, &*errors); - } - if (!errored) { - // Construct a parsed source ref. - // Be careful to transfer ownership, this could be a very large string. - auto ps = new_parsed_source_ref(str, *ast); - parser.eval_parsed_source(*ps, io); - return 0; - } else { - wcstring sb = *parser.get_backtrace(str, *errors); - std::fwprintf(stderr, L"%ls", sb.c_str()); - return 1; - } -} - -int reader_read_ffi(const void *_parser, int fd, const void *_io) { - const auto &parser = *static_cast(_parser); - const auto &io = *static_cast(_io); - return reader_read(parser, fd, io); -} - -int reader_read(const parser_t &parser, int fd, const io_chain_t &io) { - int res; - - // If reader_read is called recursively through the '.' builtin, we need to preserve - // is_interactive. This, and signal handler setup is handled by - // proc_push_interactive/proc_pop_interactive. - bool interactive = false; - // This block is a hack to work around https://sourceware.org/bugzilla/show_bug.cgi?id=20632. - // See also, commit 396bf12. Without the need for this workaround we would just write: - // int inter = ((fd == STDIN_FILENO) && isatty(STDIN_FILENO)); - if (fd == STDIN_FILENO) { - struct termios t; - int a_tty = isatty(STDIN_FILENO); - if (a_tty) { - interactive = true; - } else if (tcgetattr(STDIN_FILENO, &t) == -1 && errno == EIO) { - redirect_tty_output(); - interactive = true; - } - } - - scoped_push interactive_push{&parser.libdata_pods_mut().is_interactive, interactive}; - signal_set_handlers_once(interactive); - - res = interactive ? read_i(parser) : read_ni(parser, fd, io); - - // If the exit command was called in a script, only exit the script, not the program. - parser.libdata_pods_mut().exit_current_script = false; - - return res; -} diff --git a/src/reader.h b/src/reader.h index b1f8c1017..76f5d13a9 100644 --- a/src/reader.h +++ b/src/reader.h @@ -1,6 +1,3 @@ -// Prototypes for functions for reading data from stdin and passing to the parser. If stdin is a -// keyboard, it supplies a killring, history, syntax highlighting, tab-completion and various other -// features. #ifndef FISH_READER_H #define FISH_READER_H @@ -28,179 +25,4 @@ #include "editable_line.h" -int reader_read_ffi(const void *parser, int fd, const void *io_chain); -/// Read commands from \c fd until encountering EOF. -/// The fd is not closed. -int reader_read(const parser_t &parser, int fd, const io_chain_t &io); - -/// Initialize the reader. -void reader_init(); -void term_copy_modes(); - -/// Restore the term mode at startup. -void restore_term_mode(); - -/// Change the history file for the current command reading context. -void reader_change_history(const wcstring &name); - -/// Strategy for determining how the selection behaves. -enum class cursor_selection_mode_t : uint8_t { - /// The character at/after the cursor is excluded. - /// This is most useful with a line cursor shape. - exclusive, - /// The character at/after the cursor is included. - /// This is most useful with a block or underscore cursor shape. - inclusive, -}; - -#if INCLUDE_RUST_HEADERS -void reader_change_cursor_selection_mode(cursor_selection_mode_t selection_mode); -#else -void reader_change_cursor_selection_mode(uint8_t selection_mode); -#endif - -struct EnvDyn; -/// Enable or disable autosuggestions based on the associated variable. -void reader_set_autosuggestion_enabled(const env_stack_t &vars); -void reader_set_autosuggestion_enabled_ffi(bool enabled); - -/// Write the title to the titlebar. This function is called just before a new application starts -/// executing and just after it finishes. -/// -/// \param cmd Command line string passed to \c fish_title if is defined. -/// \param parser The parser to use for autoloading fish_title. -/// \param reset_cursor_position If set, issue a \r so the line driver knows where we are -void reader_write_title(const wcstring &cmd, const parser_t &parser, - bool reset_cursor_position = true); - -void reader_write_title_ffi(const wcstring &cmd, const void *parser, bool reset_cursor_position); - -/// Tell the reader that it needs to re-exec the prompt and repaint. -/// This may be called in response to e.g. a color variable change. -void reader_schedule_prompt_repaint(); - -/// Enqueue an event to the back of the reader's input queue. -struct CharEvent; -using char_event_t = CharEvent; -void reader_queue_ch(rust::Box ch); - -/// Return the value of the interrupted flag, which is set by the sigint handler, and clear it if it -/// was set. If the current reader is interruptible, call \c reader_exit(). -int reader_reading_interrupted(); - -/// Read one line of input. Before calling this function, reader_push() must have been called in -/// order to set up a valid reader environment. If nchars > 0, return after reading that many -/// characters even if a full line has not yet been read. Note: the returned value may be longer -/// than nchars if a single keypress resulted in multiple characters being inserted into the -/// commandline. -maybe_t reader_readline(int nchars); - -bool reader_readline_ffi(wcstring &line, int nchars); - -/// Configuration that we provide to a reader. -struct reader_config_t { - /// Left prompt command, typically fish_prompt. - wcstring left_prompt_cmd{}; - - /// Right prompt command, typically fish_right_prompt. - wcstring right_prompt_cmd{}; - - /// Name of the event to trigger once we're set up. - wcstring event{}; - - /// Whether tab completion is OK. - bool complete_ok{false}; - - /// Whether to perform syntax highlighting. - bool highlight_ok{false}; - - /// Whether to perform syntax checking before returning. - bool syntax_check_ok{false}; - - /// Whether to allow autosuggestions. - bool autosuggest_ok{false}; - - /// Whether to expand abbreviations. - bool expand_abbrev_ok{false}; - - /// Whether to exit on interrupt (^C). - bool exit_on_interrupt{false}; - - /// If set, do not show what is typed. - bool in_silent_mode{false}; - - /// The fd for stdin, default to actual stdin. - int in{0}; -}; - -class reader_data_t; -bool check_exit_loop_maybe_warning(reader_data_t *data); - -/// Push a new reader environment controlled by \p conf, using the given history name. -/// If \p history_name is empty, then save history in-memory only; do not write it to disk. -void reader_push(const parser_t &parser, const wcstring &history_name, reader_config_t &&conf); - -void reader_push_ffi(const void *parser, const wcstring &history_name, const void *conf); - -/// Return to previous reader environment. -void reader_pop(); - -/// \return whether fish is currently unwinding the stack in preparation to exit. -bool fish_is_unwinding_for_exit(); - -/// Given a command line and an autosuggestion, return the string that gets shown to the user. -/// Exposed for testing purposes only. -wcstring combine_command_and_autosuggestion(const wcstring &cmdline, - const wcstring &autosuggestion); - -/// Expand at most one abbreviation at the given cursor position, updating the position if the -/// abbreviation wants to move the cursor. Use the parser to run any abbreviations which want -/// function calls. \return none if no abbreviations were expanded, otherwise the resulting -/// replacement. -struct abbrs_replacement_t; -maybe_t reader_expand_abbreviation_at_cursor(const wcstring &cmdline, - size_t cursor_pos, - const parser_t &parser); - -/// Apply a completion string. Exposed for testing only. -wcstring completion_apply_to_command_line(const wcstring &val_str, complete_flags_t flags, - const wcstring &command_line, size_t *inout_cursor_pos, - bool append_only); - -/// Snapshotted state from the reader. -struct commandline_state_t { - wcstring text; // command line text, or empty if not interactive - size_t cursor_pos{0}; // position of the cursor, may be as large as text.size() - maybe_t selection{}; // visual selection, or none if none - maybe_t> - history{}; // current reader history, or null if not interactive - bool pager_mode{false}; // pager is visible - bool pager_fully_disclosed{false}; // pager already shows everything if possible - bool search_mode{false}; // pager is visible and search is active - bool initialized{false}; // if false, the reader has not yet been entered -}; - -/// Get the command line state. This may be fetched on a background thread. -commandline_state_t commandline_get_state(); - -HistorySharedPtr *commandline_get_state_history_ffi(); -bool commandline_get_state_initialized_ffi(); -wcstring commandline_get_state_text_ffi(); - -/// Set the command line text and position. This may be called on a background thread; the reader -/// will pick it up when it is done executing. -void commandline_set_buffer(wcstring text, size_t cursor_pos = -1); -void commandline_set_buffer_ffi(const wcstring &text, size_t cursor_pos); - -/// Return the current interactive reads loop count. Useful for determining how many commands have -/// been executed between invocations of code. -uint64_t reader_run_count(); - -/// Returns the current "generation" of interactive status. Useful for determining whether the -/// previous command produced a status. -uint64_t reader_status_count(); - -// For FFI -uint32_t read_generation_count(); - #endif