use super::prelude::*;
use super::read::TokenOutputMode;
use crate::{
ast::{self, Kind, Leaf as _},
builtins::error::Error,
complete::Completion,
err_fmt, err_str,
expand::{expand_string, ExpandFlags, ExpandResultCode},
input::input_function_get_code,
input_common::{CharEvent, ReadlineCmd},
operation_context::{no_cancel, OperationContext},
parse_constants::ParseTreeFlags,
parse_util::{
detect_parse_errors, get_job_extent, get_offset_from_line, get_process_extent,
get_token_extent, lineno,
},
prelude::*,
proc::is_interactive_session,
reader::{
commandline_get_state, commandline_set_buffer, commandline_set_search_field,
reader_execute_readline_cmd, reader_jump, reader_showing_suggestion, JumpDirection,
JumpPrecision,
},
tokenizer::{TokenType, Tokenizer, TOK_ACCEPT_UNFINISHED},
};
use fish_common::{unescape_string, UnescapeFlags, UnescapeStringStyle};
use fish_wcstringutil::join_strings;
use std::ops::Range;
#[derive(Eq, PartialEq)]
enum TextScope {
String,
Job,
Process,
Token,
}
#[derive(Eq, PartialEq)]
enum AppendMode {
Replace,
Insert,
InsertSmart,
Append,
}
fn replace_part(
parser: &Parser,
range: Range<usize>,
insert: &wstr,
insert_mode: AppendMode,
buff: &wstr,
cursor_pos: usize,
search_field_mode: bool,
) {
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 | AppendMode::InsertSmart => {
assert!(cursor_pos >= range.start);
assert!(cursor_pos <= range.end);
let insert = strip_dollar_prefixes(insert_mode, &buff[range.start..cursor_pos], insert)
.map(Cow::Owned)
.unwrap_or(Cow::Borrowed(insert));
out.push_utfstr(&buff[range.start..cursor_pos]);
out.push_utfstr(&insert);
out.push_utfstr(&buff[cursor_pos..range.end]);
out_pos += insert.len();
}
}
out.push_utfstr(&buff[range.end..]);
if search_field_mode {
commandline_set_search_field(parser, out, Some(out_pos));
} else {
commandline_set_buffer(parser, Some(out), Some(out_pos));
}
}
fn strip_dollar_prefixes(insert_mode: AppendMode, prefix: &wstr, insert: &wstr) -> Option<WString> {
if insert_mode != AppendMode::InsertSmart {
return None;
}
insert.find(L!("$ "))?; let source = prefix.to_owned() + insert;
let flags = ParseTreeFlags {
accept_incomplete_tokens: true,
leave_unterminated: true,
..Default::default()
};
let ast = ast::parse(&source, flags, None);
let mut stripped = WString::new();
let mut have = prefix.len();
for node in ast.walk() {
let Kind::DecoratedStatement(ds) = node.kind() else {
continue;
};
let Some(range) = ds.command.range() else {
continue;
};
let pos = range.start();
if pos < prefix.len() {
continue;
}
if (pos == 0 || source.as_char_slice()[pos - 1] == '\n')
&& source.as_char_slice()[pos] == '$'
&& source.char_at(pos + 1) == ' '
{
stripped.push_utfstr(&source[have..pos]);
have = pos + "$ ".len();
}
}
stripped.push_utfstr(&source[have..]);
Some(stripped)
}
#[allow(clippy::too_many_arguments)]
fn write_part(
parser: &Parser,
range: Range<usize>,
range_is_single_token: bool,
cut_at_cursor: bool,
token_mode: Option<TokenOutputMode>,
buffer: &wstr,
cursor_pos: usize,
streams: &mut IoStreams,
) {
let pos = cursor_pos - range.start;
let Some(token_mode) = token_mode else {
let range = if cut_at_cursor {
range.start..range.start + pos
} else {
range
};
streams.out.appendln(&buffer[range]);
return;
};
let mut args = vec![];
let mut add_token = |token_text: &wstr| {
match token_mode {
TokenOutputMode::Expanded => {
const COMMANDLINE_TOKENS_MAX_EXPANSION: usize = 512;
match expand_string(
token_text.to_owned(),
&mut args,
ExpandFlags::SKIP_CMDSUBST,
&OperationContext::foreground(
parser,
Box::new(no_cancel),
COMMANDLINE_TOKENS_MAX_EXPANSION,
),
None,
)
.result
{
ExpandResultCode::error
| ExpandResultCode::overflow
| ExpandResultCode::wildcard_no_match => {
args.push(Completion::from_completion(token_text.to_owned()));
}
ExpandResultCode::cancel => {}
ExpandResultCode::ok => (),
}
}
TokenOutputMode::Raw => {
args.push(Completion::from_completion(token_text.to_owned()));
}
TokenOutputMode::Unescaped => {
let unescaped = unescape_string(
token_text,
UnescapeStringStyle::Script(UnescapeFlags::INCOMPLETE),
)
.unwrap();
args.push(Completion::from_completion(unescaped));
}
}
};
let buff = &buffer[range];
if range_is_single_token {
add_token(buff);
} else {
let mut tok = Tokenizer::new(buff, TOK_ACCEPT_UNFINISHED);
let mut in_redirection = false;
while let Some(token) = tok.next() {
if cut_at_cursor && token.end() >= pos {
break;
}
let is_redirection_target = in_redirection;
in_redirection = token.type_ == TokenType::Redirect;
if is_redirection_target && token.type_ == TokenType::String {
continue;
}
if token.type_ != TokenType::String {
continue;
}
let token_text = tok.text_of(&token);
add_token(token_text);
}
}
for arg in args {
streams.out.appendln(&arg.completion);
}
}
pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> BuiltinResult {
let rstate = commandline_get_state(true);
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 token_mode = None;
let mut cursor_mode = false;
let mut selection_start_mode = false;
let mut selection_end_mode = false;
let mut line_mode = false;
let mut column_mode = false;
let mut search_mode = false;
let mut paging_mode = false;
let mut paging_full_mode = false;
let mut search_field_mode = false;
let mut is_valid = false;
let mut showing_suggestion = false;
let mut override_buffer = None;
let mut forward_jump = false;
let mut backward_jump = false;
let mut forward_jump_till = false;
let mut backward_jump_till = false;
let mut jump_target = None;
let short_options = L!("abijpctfxorhI:CBELSsP");
let long_options: &[WOption] = &[
wopt(L!("append"), ArgType::NoArgument, 'a'),
wopt(L!("insert"), ArgType::NoArgument, 'i'),
wopt(L!("insert-smart"), ArgType::NoArgument, '\x06'),
wopt(L!("replace"), ArgType::NoArgument, 'r'),
wopt(L!("current-buffer"), ArgType::NoArgument, 'b'),
wopt(L!("current-job"), ArgType::NoArgument, 'j'),
wopt(L!("current-process"), ArgType::NoArgument, 'p'),
wopt(L!("current-selection"), ArgType::NoArgument, 's'),
wopt(L!("current-token"), ArgType::NoArgument, 't'),
wopt(L!("cut-at-cursor"), ArgType::NoArgument, 'c'),
wopt(L!("function"), ArgType::NoArgument, 'f'),
wopt(L!("tokens-expanded"), ArgType::NoArgument, 'x'),
wopt(L!("tokens-raw"), ArgType::NoArgument, '\x02'),
wopt(L!("tokenize"), ArgType::NoArgument, 'o'),
wopt(L!("help"), ArgType::NoArgument, 'h'),
wopt(L!("input"), ArgType::RequiredArgument, 'I'),
wopt(L!("cursor"), ArgType::NoArgument, 'C'),
wopt(L!("selection-start"), ArgType::NoArgument, 'B'),
wopt(L!("selection-end"), ArgType::NoArgument, 'E'),
wopt(L!("line"), ArgType::NoArgument, 'L'),
wopt(L!("column"), ArgType::NoArgument, '\x05'),
wopt(L!("search-mode"), ArgType::NoArgument, 'S'),
wopt(L!("paging-mode"), ArgType::NoArgument, 'P'),
wopt(L!("paging-full-mode"), ArgType::NoArgument, 'F'),
wopt(L!("search-field"), ArgType::NoArgument, '\x03'),
wopt(L!("is-valid"), ArgType::NoArgument, '\x01'),
wopt(L!("showing-suggestion"), ArgType::NoArgument, '\x04'),
wopt(L!("forward-jump"), ArgType::RequiredArgument, '\x07'),
wopt(L!("backward-jump"), ArgType::RequiredArgument, '\x08'),
wopt(L!("forward-jump-till"), ArgType::RequiredArgument, '\x09'),
wopt(L!("backward-jump-till"), ArgType::RequiredArgument, '\x0a'),
];
let mut w = WGetopter::new(short_options, long_options, args);
let cmd = w.argv[0];
while let Some(c) = w.next_opt() {
match c {
'a' => append_mode = Some(AppendMode::Append),
'b' => buffer_part = Some(TextScope::String),
'i' => append_mode = Some(AppendMode::Insert),
'\x06' => append_mode = Some(AppendMode::InsertSmart),
'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,
'x' | '\x02' | 'o' => {
if token_mode.is_some() {
err_fmt!(
Error::INVALID_OPT_COMBO_WITH_CTX,
wgettext!("--tokens options are mutually exclusive")
)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
token_mode = Some(match c {
'x' => TokenOutputMode::Expanded,
'\x02' => TokenOutputMode::Raw,
'o' => TokenOutputMode::Unescaped,
_ => unreachable!(),
});
}
'I' => {
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,
'\x05' => column_mode = true,
'S' => search_mode = true,
's' => selection_mode = true,
'P' => paging_mode = true,
'F' => paging_full_mode = true,
'\x03' => search_field_mode = true,
'\x01' => is_valid = true,
'\x04' => showing_suggestion = true,
'\x07' => {
forward_jump = true;
jump_target = Some(w.woptarg.unwrap().to_owned());
}
'\x08' => {
backward_jump = true;
jump_target = Some(w.woptarg.unwrap().to_owned());
}
'\x09' => {
forward_jump_till = true;
jump_target = Some(w.woptarg.unwrap().to_owned());
}
'\x0a' => {
backward_jump_till = true;
jump_target = Some(w.woptarg.unwrap().to_owned());
}
'h' => {
builtin_print_help(parser, streams, cmd);
return Ok(SUCCESS);
}
':' => {
builtin_missing_argument(
parser,
streams,
cmd,
None,
w.argv[w.wopt_index - 1],
true,
);
return Err(STATUS_INVALID_ARGS);
}
';' => {
builtin_unexpected_argument(parser, streams, cmd, w.argv[w.wopt_index - 1], true);
return Err(STATUS_INVALID_ARGS);
}
'?' => {
builtin_unknown_option(parser, streams, cmd, w.argv[w.wopt_index - 1], true);
return Err(STATUS_INVALID_ARGS);
}
_ => panic!(),
}
}
if forward_jump || forward_jump_till || backward_jump || backward_jump_till {
let direction = if forward_jump || forward_jump_till {
JumpDirection::Forward
} else {
JumpDirection::Backward
};
let precision = if forward_jump || backward_jump {
JumpPrecision::To
} else {
JumpPrecision::Till
};
let target = jump_target.unwrap();
let Some(target) = target.chars().next() else {
return Err(STATUS_INVALID_ARGS);
};
return if reader_jump(direction, precision, target) {
Ok(SUCCESS)
} else {
Err(STATUS_CMD_ERROR)
};
}
let positional_args = w.argv.len() - w.wopt_index;
if function_mode {
if buffer_part.is_some()
|| cut_at_cursor
|| append_mode.is_some()
|| token_mode.is_some()
|| cursor_mode
|| line_mode
|| column_mode
|| search_mode
|| paging_mode
|| selection_start_mode
|| selection_end_mode
{
err_str!(Error::INVALID_OPT_COMBO)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if positional_args == 0 {
builtin_missing_argument(parser, streams, cmd, None, L!("--function"), true);
return Err(STATUS_INVALID_ARGS);
}
type RL = ReadlineCmd;
for arg in &w.argv[w.wopt_index..] {
let Some(cmd) = input_function_get_code(arg) else {
err_fmt!("Unknown input function '%s'", arg)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
};
if matches!(cmd, RL::RepaintMode | RL::ForceRepaint | RL::Repaint)
&& parser.is_repainting()
{
continue;
}
reader_execute_readline_cmd(parser, CharEvent::from_readline(cmd));
}
return Ok(SUCCESS);
}
if selection_mode {
if let Some(selection) = rstate.selection {
streams.out.append(&rstate.text[selection]);
}
return Ok(SUCCESS);
}
if (selection_start_mode || selection_end_mode) && positional_args != 0 {
err_str!(Error::TOO_MANY_ARGUMENTS)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if (search_mode || line_mode || column_mode || cursor_mode || paging_mode)
&& positional_args > 1
{
err_str!(Error::TOO_MANY_ARGUMENTS)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if (buffer_part.is_some() || token_mode.is_some() || cut_at_cursor || search_field_mode)
&& (cursor_mode || line_mode||column_mode || search_mode || paging_mode || paging_full_mode)
&& ((buffer_part.is_none() && !search_field_mode) || !cursor_mode)
{
err_str!(Error::INVALID_OPT_COMBO)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if (token_mode.is_some() || cut_at_cursor) && positional_args != 0 {
err_fmt!(
Error::INVALID_OPT_COMBO_WITH_CTX,
"--cut-at-cursor and token options can not be used when setting the commandline"
)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if search_field_mode && (buffer_part.is_some() || token_mode.is_some()) {
err_str!(Error::INVALID_OPT_COMBO)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if append_mode.is_some() && positional_args == 0 {
return Err(STATUS_CMD_ERROR);
}
let append_mode = append_mode.unwrap_or(AppendMode::Replace);
let buffer_part = buffer_part.unwrap_or(TextScope::String);
if append_mode == AppendMode::InsertSmart {
if search_field_mode {
err_fmt!(Error::COMBO_EXCLUSIVE, "--insert-smart", "--search-field")
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
match buffer_part {
TextScope::String | TextScope::Job | TextScope::Process => (),
TextScope::Token => {
err_fmt!(Error::COMBO_EXCLUSIVE, "--insert-smart", "--current-token")
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
}
if line_mode || column_mode {
if positional_args != 0 {
let arg = w.argv[w.wopt_index];
let new_coord = match fish_wcstol(arg) {
Err(_) => {
err_fmt!(Error::NOT_NUMBER, arg)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
0
}
Ok(num) => num - 1,
};
let Ok(new_coord) = usize::try_from(new_coord) else {
err_str!("line/column index starts at 1")
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
};
let new_pos = if line_mode {
let Some(offset) =
get_offset_from_line(&rstate.text, i32::try_from(new_coord).unwrap())
else {
err_fmt!("there is no line %s", arg)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
};
offset
} else {
let line_index =
i32::try_from(lineno(&rstate.text, rstate.cursor_pos)).unwrap() - 1;
let line_offset =
get_offset_from_line(&rstate.text, line_index).unwrap_or_default();
let next_line_offset =
get_offset_from_line(&rstate.text, line_index + 1).unwrap_or(rstate.text.len());
if line_offset + new_coord > next_line_offset {
err_fmt!("column %s exceeds line length", arg)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
line_offset + new_coord
};
commandline_set_buffer(parser, None, Some(new_pos));
} else {
streams.out.append(&sprintf!(
"%d\n",
if line_mode {
lineno(&rstate.text, rstate.cursor_pos)
} else {
rstate.cursor_pos + 1
- get_offset_from_line(
&rstate.text,
i32::try_from(lineno(&rstate.text, rstate.cursor_pos) - 1).unwrap(),
)
.unwrap_or_default()
}
));
}
return Ok(SUCCESS);
}
if search_mode {
return if rstate.search_mode {
Ok(SUCCESS)
} else {
Err(STATUS_CMD_ERROR)
};
}
if paging_mode {
return if rstate.pager_mode {
Ok(SUCCESS)
} else {
Err(STATUS_CMD_ERROR)
};
}
if paging_full_mode {
return if rstate.pager_mode && rstate.pager_fully_disclosed {
Ok(SUCCESS)
} else {
Err(STATUS_CMD_ERROR)
};
}
if selection_start_mode {
let Some(selection) = rstate.selection else {
return Err(STATUS_CMD_ERROR);
};
streams.out.append(&sprintf!("%u\n", selection.start));
return Ok(SUCCESS);
}
if selection_end_mode {
let Some(selection) = rstate.selection else {
return Err(STATUS_CMD_ERROR);
};
streams.out.append(&sprintf!("%u\n", selection.end));
return Ok(SUCCESS);
}
let current_buffer;
let current_cursor_pos;
let transient;
if search_field_mode {
let Some((search_field_text, cursor_pos)) = rstate.search_field else {
return Err(STATUS_CMD_ERROR);
};
transient = search_field_text;
current_buffer = &transient;
current_cursor_pos = cursor_pos;
} else if let Some(override_buffer) = &override_buffer {
current_buffer = override_buffer;
current_cursor_pos = current_buffer.len();
} else if parser.libdata().transient_commandline.is_some() {
if cursor_mode && positional_args != 0 {
err_str!("setting cursor while evaluating 'complete --arguments' is not yet supported")
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
transient = parser.libdata().transient_commandline.clone().unwrap();
current_buffer = &transient;
current_cursor_pos = transient.len();
} else if parser.interactive_initialized.load() || is_interactive_session() {
current_buffer = &rstate.text;
current_cursor_pos = rstate.cursor_pos;
} else {
err_str!("Can not set commandline in non-interactive mode")
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
if is_valid {
if current_buffer.is_empty() {
return Err(STATUS_CMD_ERROR);
}
let res = detect_parse_errors(current_buffer, None, true);
return match res {
Ok(()) => Ok(SUCCESS),
Err(p) if p.incomplete => Err(STATUS_INVALID_ARGS),
Err(_) => Err(STATUS_CMD_ERROR),
};
}
if showing_suggestion {
if reader_showing_suggestion(parser) {
return Ok(SUCCESS);
}
return Err(STATUS_CMD_ERROR);
}
let range;
if search_field_mode {
range = 0..current_buffer.len();
} else {
match buffer_part {
TextScope::String => {
range = 0..current_buffer.len();
}
TextScope::Job => {
range = get_job_extent(current_buffer, current_cursor_pos, None);
}
TextScope::Process => {
range = get_process_extent(current_buffer, current_cursor_pos, None);
}
TextScope::Token => {
(range, _) = get_token_extent(current_buffer, current_cursor_pos);
}
}
}
if cursor_mode {
if positional_args != 0 {
let arg = w.argv[w.wopt_index];
let new_pos = match fish_wcstol(arg) {
Err(_) => {
err_fmt!(Error::NOT_NUMBER, arg)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
0
}
Ok(num) => num,
};
let new_pos = std::cmp::min(
range
.start
.saturating_add_signed(isize::try_from(new_pos).unwrap()),
current_buffer.len(),
);
commandline_set_buffer(parser, None, Some(new_pos));
} else {
streams
.out
.append(&sprintf!("%u\n", current_cursor_pos - range.start));
}
return Ok(SUCCESS);
}
if positional_args == 0 {
write_part(
parser,
range,
buffer_part == TextScope::Token,
cut_at_cursor,
token_mode,
current_buffer,
current_cursor_pos,
streams,
);
} else {
let replacement = if positional_args == 1 {
Cow::Borrowed(args[w.wopt_index])
} else {
Cow::Owned(join_strings(&w.argv[w.wopt_index..], '\n'))
};
replace_part(
parser,
range,
&replacement,
append_mode,
current_buffer,
current_cursor_pos,
search_field_mode,
);
}
Ok(SUCCESS)
}