fish-shell/src/builtins/commandline.rs
2024-01-13 03:58:33 +01:00

501 lines
17 KiB
Rust

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;
/// 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
}