knave 0.11.8

A Lisp programming language for humans and agents that infers types and ownership and compiles to readable Rust
Documentation
use std::env;
use std::io::{self, BufRead, IsTerminal, Write};
use std::rc::Rc;

use anstream::{AutoStream, ColorChoice};

use super::diagnostic_output::DiagnosticOutput;
use super::edited_lines::EditedLines;
use super::input_helper::InputHelper;
use super::interruption::Interruption;
use super::open_input::OpenInput;

pub enum EnteredText {
    Complete(String),
    Unfinished(String),
}

pub enum PromptLines {
    Piped(PipedLines<io::StdinLock<'static>>),
    Edited(Box<EditedLines>),
}

pub struct PipedLines<R> {
    input: R,
    prompt: Option<String>,
}

struct UnsupportedTerminal;

impl PromptLines {
    pub fn standard_input(notes: Rc<dyn DiagnosticOutput>) -> io::Result<Self> {
        let input = io::stdin();
        if !input.is_terminal() {
            return Ok(Self::Piped(PipedLines::new(input.lock(), None)));
        }
        let standard_output = AutoStream::choice(&io::stdout());
        if UnsupportedTerminal::is_current() {
            let prompt = PipedLines::<io::StdinLock>::prompt_for(standard_output);
            return Ok(Self::Piped(PipedLines::new(input.lock(), Some(prompt))));
        }
        Ok(Self::Edited(Box::new(EditedLines::new(
            standard_output,
            notes,
        )?)))
    }

    pub fn catch_interrupts(&self) -> Result<Interruption, ctrlc::Error> {
        match self {
            Self::Piped(lines) if !lines.reads_a_terminal() => Ok(Interruption::default()),
            Self::Piped(_) | Self::Edited(_) => Interruption::catch(),
        }
    }

    pub fn next_text(&mut self) -> io::Result<Option<EnteredText>> {
        match self {
            Self::Piped(lines) => lines.next_text(),
            Self::Edited(lines) => lines.next_text(),
        }
    }
}

impl<R: BufRead> PipedLines<R> {
    const PROMPT: &str = "> ";
    const CONTINUATION: &str = "  ";

    fn new(input: R, prompt: Option<String>) -> Self {
        Self { input, prompt }
    }

    fn reads_a_terminal(&self) -> bool {
        self.prompt.is_some()
    }

    pub fn next_text(&mut self) -> io::Result<Option<EnteredText>> {
        let mut text = String::new();
        self.show_prompt()?;
        while self.input.read_line(&mut text)? > 0 {
            if !OpenInput::awaits_more(&text) {
                return Ok(Some(EnteredText::Complete(text)));
            }
            self.show_continuation()?;
        }
        Ok((!text.is_empty()).then_some(EnteredText::Unfinished(text)))
    }

    fn prompt_for(standard_output: ColorChoice) -> String {
        match standard_output {
            ColorChoice::Never => Self::PROMPT.to_string(),
            _ => InputHelper::styled_prompt(Self::PROMPT),
        }
    }

    fn show_prompt(&self) -> io::Result<()> {
        match &self.prompt {
            Some(prompt) => Self::show(prompt),
            None => Ok(()),
        }
    }

    fn show_continuation(&self) -> io::Result<()> {
        match &self.prompt {
            Some(_) => Self::show(Self::CONTINUATION),
            None => Ok(()),
        }
    }

    fn show(text: &str) -> io::Result<()> {
        let mut output = io::stdout().lock();
        write!(output, "{text}")?;
        output.flush()
    }
}

impl UnsupportedTerminal {
    const VARIABLE: &str = "TERM";
    const NAMES: [&str; 3] = ["dumb", "cons25", "emacs"];

    fn is_current() -> bool {
        env::var(Self::VARIABLE).is_ok_and(|term| {
            Self::NAMES
                .iter()
                .any(|name| name.eq_ignore_ascii_case(&term))
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    struct LinesFixture;

    impl LinesFixture {
        fn texts(input: &str) -> Vec<String> {
            let mut lines = PipedLines::new(input.as_bytes(), None);
            let mut texts = Vec::new();
            while let Some(text) = lines.next_text().expect("a byte slice reads") {
                texts.push(Self::describe(text));
            }
            texts
        }

        fn describe(text: EnteredText) -> String {
            match text {
                EnteredText::Complete(text) => format!("complete {text:?}"),
                EnteredText::Unfinished(text) => format!("unfinished {text:?}"),
            }
        }
    }

    #[test]
    fn each_line_of_closed_forms_is_one_text() {
        assert_eq!(
            LinesFixture::texts("(+ 1 2)\n(+ 3 4) 5\n"),
            ["complete \"(+ 1 2)\\n\"", "complete \"(+ 3 4) 5\\n\""]
        );
    }

    #[test]
    fn an_unclosed_list_reads_the_next_line() {
        assert_eq!(
            LinesFixture::texts("(+ 1\n 2)\n"),
            ["complete \"(+ 1\\n 2)\\n\""]
        );
    }

    #[test]
    fn an_unclosed_string_reads_the_next_line() {
        assert_eq!(
            LinesFixture::texts("(println \"a\nb\")\n"),
            ["complete \"(println \\\"a\\nb\\\")\\n\""]
        );
    }

    #[test]
    fn a_parenthesis_inside_a_comment_reads_no_further() {
        assert_eq!(
            LinesFixture::texts("1 ; (\n2\n"),
            ["complete \"1 ; (\\n\"", "complete \"2\\n\""]
        );
    }

    #[test]
    fn an_extra_close_ends_the_text_at_once() {
        assert_eq!(
            LinesFixture::texts("(+ 1 2))\n(\n"),
            ["complete \"(+ 1 2))\\n\"", "unfinished \"(\\n\""]
        );
    }

    #[test]
    fn an_extra_close_beside_an_unclosed_list_ends_the_text_at_once() {
        assert_eq!(LinesFixture::texts(") (\n"), ["complete \") (\\n\""]);
    }

    #[test]
    fn the_end_of_input_inside_a_list_leaves_the_text_unfinished() {
        assert_eq!(LinesFixture::texts("(+ 1"), ["unfinished \"(+ 1\""]);
    }
}