shelly-shell 0.5.3

A Rust based Unix style shell with a typed and structured language syntax.

use reedline::Suggestion;

use crate::{
    language::{data::value::Value, interpreter::Interpreter, text::location::Location},
    runtime::readline::{Answer, Edit, Fault},
};

use super::{
    Editor, Result,
    values::{self, field, record, string},
};

impl Interpreter
{
    fn readline_edit(&self, edit: &Edit) -> Value
    {
        let (start, end) = edit.word();
        let span = record(
            self,
            "Span",
            vec![Value::Integer(start as i64), Value::Integer(end as i64)],
        );
        record(
            self,
            "Edit",
            vec![
                Value::from_string(edit.text.clone()),
                Value::Integer(edit.cursor as i64),
                Value::Integer(edit.revision as i64),
                Value::from_string(edit.text[..edit.cursor].into()),
                Value::from_string(edit.text[edit.cursor..].into()),
                span,
                Value::from_string(edit.text[start..edit.cursor].into()),
            ],
        )
    }
    pub(super) fn readline_callback(
        &mut self,
        location: &Location,
        editor: &Editor,
        hook: &str,
        edit: &Edit,
    ) -> Result<Answer>
    {
        if edit.text.len() > editor.limits.input
            || edit.cursor > edit.text.len()
            || !edit.text.is_char_boundary(edit.cursor)
        {
            return Err(Fault::new("LimitError", "Input exceeds input limits").into());
        }
        let callback = match hook
        {
            "complete" => &editor.complete,
            "validate" => &editor.validate,
            _ => &editor.highlight,
        };
        let input = self.readline_edit(edit);
        let value = self.readline_call(
            location,
            callback.clone(),
            vec![input],
            Some(&editor.limits),
        )?;
        match hook
        {
            "validate" =>
            {
                let kind = match values::enum_name(field(&value, "kind"))
                {
                    Some("Ready") => 0,
                    Some("More") => 1,
                    Some("Reject") => 2,
                    _ => return Err(Fault::new("CallbackError", "Invalid Validation").into()),
                };
                let message =
                    values::optional_string(field(&value, "message"))?.unwrap_or_default();
                if message.len() > 16384
                {
                    return Err(
                        Fault::new("CallbackError", "Validation message is too large").into(),
                    );
                }
                let span = if matches!(field(&value, "span").underlying(), Value::None)
                {
                    None
                }
                else
                {
                    Some(values::span(field(&value, "span"), &edit.text)?)
                };
                Ok(Answer::Validate {
                    kind,
                    message,
                    span,
                })
            }
            "complete" =>
            {
                let Value::Array(items) = value.underlying()
                else
                {
                    return Err(
                        Fault::new("CallbackError", "Completion must return an Array").into(),
                    );
                };
                if items.len() > editor.limits.completions
                {
                    return Err(Fault::new("CallbackError", "Too many completions").into());
                }
                let (start, end) = edit.word();
                let default = reedline::Span::new(start, end);
                let mut replacement = None;
                let mut suggestions = Vec::new();
                let mut bytes = 0;
                for item in items.iter()
                {
                    let plain = string(item);
                    let value =
                        plain
                            .or_else(|| string(field(item, "value")))
                            .ok_or_else(|| {
                                Fault::new("CallbackError", "Completion value must be String")
                            })?;
                    let span = if plain.is_some()
                        || matches!(field(item, "replace").underlying(), Value::None)
                    {
                        default
                    }
                    else
                    {
                        values::span(field(item, "replace"), &edit.text)?
                    };
                    if span.start > edit.cursor
                        || span.end < edit.cursor
                        || replacement.is_some_and(|previous| previous != span)
                    {
                        return Err(Fault::new(
                            "CallbackError",
                            "Completions must share a range containing cursor",
                        )
                        .into());
                    }
                    replacement = Some(span);
                    let display = values::optional_string(field(item, "display"))?;
                    let description = values::optional_string(field(item, "description"))?;
                    let display =
                        crate::runtime::readline::visible(display.as_deref().unwrap_or(value));
                    let description =
                        description.map(|text| crate::runtime::readline::visible(&text));
                    bytes +=
                        value.len() + display.len() + description.as_ref().map_or(0, String::len);
                    if bytes > editor.limits.completion_bytes
                    {
                        return Err(
                            Fault::new("CallbackError", "Completion bytes exceed limit").into()
                        );
                    }
                    let append = matches!(
                        field(item, "append_space").underlying(),
                        Value::Boolean(true)
                    ) && !edit.text[span.end..].starts_with(char::is_whitespace);
                    suggestions.push(Suggestion {
                        value: value.into(),
                        span,
                        display_override: Some(display),
                        description,
                        append_whitespace: append,
                        ..Suggestion::default()
                    });
                }
                Ok(Answer::Complete(suggestions))
            }
            _ =>
            {
                let Value::Array(items) = value.underlying()
                else
                {
                    return Err(
                        Fault::new("CallbackError", "Highlight must return an Array").into(),
                    );
                };
                if items.len() > editor.limits.spans
                {
                    return Err(Fault::new("CallbackError", "Too many highlight spans").into());
                }
                let mut end = 0;
                let mut spans = Vec::new();
                for item in items.iter()
                {
                    let span = values::span(field(item, "span"), &edit.text)?;
                    if span.start < end
                    {
                        return Err(Fault::new(
                            "CallbackError",
                            "Highlight spans overlap or are unsorted",
                        )
                        .into());
                    }
                    end = span.end;
                    spans.push((span, values::style(field(item, "style"))?));
                }
                Ok(Answer::Highlight(spans))
            }
        }
    }
}