mirror of
https://github.com/fish-shell/fish-shell.git
synced 2025-02-09 07:01:01 +08:00
501 lines
17 KiB
Rust
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
|
|
}
|