Port reader

This commit is contained in:
Johannes Altmanninger 2023-12-22 12:27:01 +01:00
parent 1093c636e5
commit 55fd43d86c
52 changed files with 6522 additions and 6185 deletions

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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<WString>,
pub set_cursor_marker: Option<WString>,
}
impl From<Replacer> for abbrs_replacer_t {
@ -219,23 +219,23 @@ impl From<Replacer> 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<usize>,
pub cursor: Option<usize>,
}
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<WString>) -> Self {
pub fn new(range: SourceRange, mut text: WString, set_cursor_marker: Option<WString>) -> 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<Replacer> {
with_abbrs(|set| set.r#match(token, position))
.into_iter()
.collect()
}
fn abbrs_match_ffi(token: &CxxWString, position: abbrs_position_t) -> Vec<abbrs_replacer_t> {
with_abbrs(|set| set.r#match(token.as_wstr(), position.into()))
.into_iter()

View file

@ -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<c_int> {
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<usize>,
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<usize>,
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<c_int> {
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
}

View file

@ -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

View file

@ -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.

View file

@ -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(

View file

@ -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())
}

View file

@ -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);

View file

@ -1906,6 +1906,21 @@ where
ScopeGuard::new((), restore_saved)
}
pub fn scoped_push_replacer_ctx<Context, Replacer, T>(
mut ctx: Context,
replacer: Replacer,
new_value: T,
) -> impl ScopeGuarding<Target = Context>
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<T: Send>() {}
pub const fn assert_sync<T: Sync>() {}

View file

@ -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);

View file

@ -116,6 +116,16 @@ pub struct EnvDyn {
inner: Box<dyn Environment + Send + Sync>,
}
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<dyn Environment + Send + Sync>) -> Self {
@ -389,8 +399,15 @@ impl Environment for EnvStack {
}
}
// TODO Remove Pin?
pub type EnvStackRef = Pin<Arc<EnvStack>>;
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! {

View file

@ -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<WString> {
ffi::get_history_variable_text_ffi(&history_session_id.to_ffi()).from_ffi()
}
/// Apply the pathvar behavior, splitting about colons.
pub fn colon_split<T: AsRef<wstr>>(val: &[T]) -> Vec<WString> {
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(),

View file

@ -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) {

View file

@ -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);
}

View file

@ -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.

View file

@ -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);

View file

@ -134,6 +134,8 @@ pub mod categories {
(screen, "screen", "Screen repaints");
(abbrs, "abbrs", "Abbreviation expansion");
(refcell, "refcell", "Refcell dynamic borrowing");
);
}

View file

@ -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<T, E> IsOkAnd for Result<T, E> {
type Type = T;
@ -38,6 +40,12 @@ impl<T, E> IsOkAnd for Result<T, E> {
Err(_) => false,
}
}
fn is_err_and(self, f: impl FnOnce(E) -> bool) -> bool {
match self {
Ok(_) => false,
Err(e) => f(e),
}
}
}
pub trait IsSorted {

View file

@ -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<HistoryItem>;
fn size(&self) -> usize;
fn clone(&self) -> Box<HistorySharedPtr>;
#[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<Self>,
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<EnvVar>) -> 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<Self> {
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<HistorySharedPtr> {

View file

@ -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) => {

View file

@ -93,6 +93,7 @@ mod print_help;
mod proc;
mod re;
mod reader;
mod reader_history_search;
mod redirection;
mod screen;
mod signal;

View file

@ -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,

View file

@ -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 {

File diff suppressed because it is too large Load diff

View file

@ -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<HistorySearch>,
/// The ordered list of matches. This may grow long.
matches: Vec<SearchMatch>,
/// A set of new items to skip, corresponding to matches_ and anything added in skip().
skips: HashSet<WString>,
/// 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<SourceRange> {
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<History>,
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<usize> {
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()
}
}

View file

@ -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(),

View file

@ -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)
}

View file

@ -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<_>>(),
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<usize>,
) -> Option<WString> {
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");
});

View file

@ -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.

View file

@ -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();

View file

@ -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<dyn Environment + Send + Sync>);
let history = History::with_name(L!("path_detection"));
history.clear();

View file

@ -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;

View file

@ -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.

View file

@ -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, ": (: ^''");
}

View file

@ -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<CppCallback>);
#[cxx_name = "iothread_perform_cantwait"]
fn iothread_perform_cant_wait_ffi(callback: &SharedPtr<CppCallback>);
}
}
extern "Rust" {
#[cxx_name = "debounce_t"]
type Debounce;
#[cxx_name = "perform"]
fn perform_ffi(&self, callback: &SharedPtr<CppCallback>) -> u64;
#[cxx_name = "perform_with_completion"]
fn perform_with_completion_ffi(
&self,
callback: &SharedPtr<CppCallback>,
completion: &SharedPtr<CppCallback>,
) -> u64;
#[cxx_name = "new_debounce_t"]
fn new_debounce_ffi(timeout_ms: u64) -> Box<Debounce>;
}
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<ffi::CppCallback>) {
let callback = callback.clone();
@ -130,7 +108,7 @@ fn iothread_perform_cant_wait_ffi(callback: &cxx::SharedPtr<ffi::CppCallback>) {
});
}
/// A [`ThreadPool`] or [`Debounce`] work request.
/// A [`ThreadPool`] work request.
type WorkItem = Box<dyn FnOnce() + 'static + Send>;
// A helper type to allow us to (temporarily) send an object to another thread.
@ -140,7 +118,7 @@ struct ForceSend<T>(T);
unsafe impl<T> Send for ForceSend<T> {}
#[allow(clippy::type_complexity)]
type DebounceCallback = ForceSend<Box<dyn FnOnce() + 'static>>;
type DebounceCallback = ForceSend<Box<dyn FnOnce(&mut ReaderData) + 'static>>;
/// 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<Debounce> {
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<ffi::CppCallback>) -> u64 {
let callback = callback.clone();
self.perform(move || {
callback.invoke();
})
.into()
}
fn perform_with_completion_ffi(
&self,
callback: &cxx::SharedPtr<ffi::CppCallback>,
completion: &cxx::SharedPtr<ffi::CppCallback>,
) -> 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<H, R, C>(&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();

View file

@ -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();
}

View file

@ -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() {

View file

@ -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<usize> {
pub fn ifind(haystack: &wstr, needle: &wstr, fuzzy: bool /* = false */) -> Option<usize> {
if needle.is_empty() {
return Some(0);
}
haystack
.as_char_slice()
.windows(needle.len())

View file

@ -1,523 +0,0 @@
// Functions used for implementing the commandline builtin.
#include "config.h" // IWYU pragma: keep
#include "commandline.h"
#include <algorithm>
#include <cerrno>
#include <cwchar>
#include <string>
#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<const parser_t *>(_parser);
auto &streams = *static_cast<io_streams_t *>(_streams);
auto argv = static_cast<const wchar_t **>(_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<readline_cmd_t>(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<unsigned long>(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<unsigned long>(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<long>(std::wcslen(current_buffer))));
commandline_set_buffer(current_buffer, static_cast<size_t>(new_pos));
} else {
size_t pos = current_cursor_pos - (begin - current_buffer);
streams.out()->append(format_string(L"%lu\n", static_cast<unsigned long>(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;
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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<rust::Box<HistorySharedPtr>> 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_; }

View file

@ -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

View file

@ -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{};
}

View file

@ -230,14 +230,14 @@ static void process_input(bool continuous_mode, bool verbose) {
std::vector<wchar_t> bind_chars;
std::fwprintf(stderr, L"Press a key:\n");
while (!check_exit_loop_maybe_warning(nullptr)) {
maybe_t<rust::Box<char_event_t>> evt{};
while (!check_exit_loop_maybe_warning()) {
maybe_t<rust::Box<CharEvent>> 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<char_event_t>::from_raw(evt_raw);
evt = rust::Box<CharEvent>::from_raw(evt_raw);
}
}
if (!evt || !(*evt)->is_char()) {

View file

@ -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<wcstring> {
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<wcstring> result;
auto expand_abbreviation_in_command = [](const wcstring &cmdline,
maybe_t<size_t> cursor_pos = {}) -> maybe_t<wcstring> {
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<highlight_spec_t> 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},

View file

@ -27,53 +27,6 @@ inline void iothread_perform_cantwait(std::function<void()> &&func) {
iothread_perform_cantwait(callback);
}
inline uint64_t debounce_perform(const debounce_t &debouncer, const std::function<void()> &func) {
std::shared_ptr<callback_t> callback = std::make_shared<callback_t>([=](const void *) {
func();
return nullptr;
});
return debouncer.perform(callback);
}
template <typename R>
inline void debounce_perform_with_completion(const debounce_t &debouncer, std::function<R()> &&func,
std::function<void(R)> &&completion) {
std::shared_ptr<callback_t> callback2 = std::make_shared<callback_t>([=](const void *r) {
assert(r != nullptr && "callback1 result was null!");
const R *result = (const R *)r;
completion(*result);
return nullptr;
});
std::shared_ptr<callback_t> callback1 = std::make_shared<callback_t>([=](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 <typename R>
inline void debounce_perform_with_completion(const debounce_t &debouncer, std::function<R()> &&func,
std::function<void(const R &)> &&completion) {
std::shared_ptr<callback_t> callback2 = std::make_shared<callback_t>([=](const void *r) {
assert(r != nullptr && "callback1 result was null!");
const R *result = (const R *)r;
completion(*result);
return nullptr;
});
std::shared_ptr<callback_t> callback1 = std::make_shared<callback_t>([=](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<void()> &func) {
std::shared_ptr<callback_t> callback = std::make_shared<callback_t>([=](const void *) {
func();

File diff suppressed because it is too large Load diff

View file

@ -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<char_event_t> 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<wcstring> 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<abbrs_replacement_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<source_range_t> selection{}; // visual selection, or none if none
maybe_t<rust::Box<HistorySharedPtr>>
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