use super::prelude::*;
use super::read::TokenOutputMode;
use crate::{
    ast::{self, Kind, Leaf as _},
    builtins::Error,
    complete::Completion,
    err_fmt, err_str,
    expand::{ExpandFlags, ExpandResultCode, expand_string},
    input::{CharEvent, ReadlineCmd, input_function_get_code},
    operation_context::{OperationContext, no_cancel},
    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::{
        JumpDirection, JumpPrecision, commandline_get_state, commandline_set_buffer,
        commandline_set_search_field, reader_execute_readline_cmd, reader_jump,
        reader_showing_suggestion,
    },
    tokenizer::{TOK_ACCEPT_UNFINISHED, TokenType, Tokenizer},
};
use fish_common::{UnescapeFlags, UnescapeStringStyle, unescape_string};
use fish_wcstringutil::join_strings;
use std::ops::Range;

/// Which part of the comandbuffer are we operating on.
#[derive(Eq, PartialEq)]
enum TextScope {
    String,
    Job,
    Process,
    Token,
}

/// For text insertion, how should it be done.
#[derive(Eq, PartialEq)]
enum AppendMode {
    // replace current text
    Replace,
    // insert at cursor position
    Insert,
    // insert at cursor position, DWIM style.
    InsertSmart,
    // 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(
    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));
    }
}

// Prefix must be at the beginning of a process.
fn strip_dollar_prefixes(insert_mode: AppendMode, prefix: &wstr, insert: &wstr) -> Option<WString> {
    if insert_mode != AppendMode::InsertSmart {
        return None;
    }
    insert.find(L!("$ "))?; // Early return.
    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)
}

/// 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
/// and skipping non-string tokens
/// \param buffer the original command line buffer
/// \param cursor_pos the position of the cursor in the command line
#[allow(clippy::too_many_arguments)]
fn write_part(
    parser: &mut 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,
                    &mut OperationContext::foreground(
                        parser,
                        Box::new(no_cancel),
                        COMMANDLINE_TOKENS_MAX_EXPANSION,
                    ),
                    None,
                )
                .result
                {
                    ExpandResultCode::Error
                    | ExpandResultCode::Overflow
                    | ExpandResultCode::WildcardNoMatch => {
                        // Maybe hit expansion limit, forward the unexpanded string.
                        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);
    }
}

/// The commandline builtin. It is used for specifying a new value for the commandline.
pub fn commandline(
    parser: &mut 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' => {
                // 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,
            '\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 {
        // Check for invalid switch combinations.
        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);
            };
            // 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)
                && parser.is_repainting()
            {
                continue;
            }

            // Inserts the readline function at the back of the queue.
            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);
    }

    // Check for invalid switch combinations.
    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)
        // Special case - we allow to get/set cursor position relative to the process/job/token.
        && ((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 {
        // No tokens in insert mode just means we do nothing.
        return Err(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 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 => {
                // To-do: we can support it in command position.
                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);
    }

    // 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.
    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.borrow().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
            .borrow()
            .as_ref()
            .unwrap()
            .clone();
        current_buffer = &transient;
        current_cursor_pos = transient.len();
    } else if parser.interactive_initialized || is_interactive_session() {
        current_buffer = &rstate.text;
        current_cursor_pos = rstate.cursor_pos;
    } else {
        // There is no command line because we are not interactive.
        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, /*accept_incomplete=*/ 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)
}
