knave-expand 0.14.21

The reading, scoping and expansion stages of knave
Documentation
use std::ops::Range;

use super::Reader;
use super::cursor::Cursor;
use super::form::{Form, FormKind};
use super::string_literal::StringLiteralReader;
use crate::source::{SourceFile, Span};

pub struct LiteralForms {
    decoded: SourceFile,
    placement: Placement,
}

enum Placement {
    AtWrittenRanges(Vec<Range<usize>>),
    AtLiteral(Range<usize>),
}

impl LiteralForms {
    pub fn new(enclosing: &SourceFile, literal: Range<usize>, value: &str) -> Self {
        Self {
            decoded: SourceFile::new(
                enclosing.id().pinned(),
                enclosing.name().to_string(),
                value.to_string(),
            ),
            placement: Placement::of(enclosing.text(), literal, value),
        }
    }

    pub fn read_at(&self, character: usize) -> Option<(Form, usize)> {
        let text = self.decoded.text();
        let offset = text.char_indices().nth(character)?.0;
        let form = Reader::read_form_at(&self.decoded, offset)?;
        let end = text[..form.span().range().end].chars().count();
        Some((self.placed(&form), end))
    }

    fn placed(&self, form: &Form) -> Form {
        let kind = match form.kind() {
            FormKind::List(items) => {
                FormKind::List(items.iter().map(|item| self.placed(item)).collect())
            }
            other => other.clone(),
        };
        Form::new(kind, self.placement.span_of(form.span()))
    }
}

impl Placement {
    fn of(file_text: &str, literal: Range<usize>, value: &str) -> Self {
        let mut cursor = Cursor::starting_at(&file_text[..literal.end], literal.start);
        let (read_value, written_ranges) = StringLiteralReader::read_written_ranges(&mut cursor);
        if read_value == value {
            Self::AtWrittenRanges(written_ranges)
        } else {
            Self::AtLiteral(literal)
        }
    }

    fn span_of(&self, decoded: Span) -> Span {
        let range = decoded.range();
        let placed = match self {
            Self::AtWrittenRanges(written_ranges) => {
                written_ranges[range.start].start..written_ranges[range.end - 1].end
            }
            Self::AtLiteral(literal) => literal.clone(),
        };
        Span::new(decoded.source(), placed)
    }
}

#[cfg(test)]
mod tests {
    use super::LiteralForms;
    use crate::source::{SourceId, SourceMap};

    struct LiteralFixture {
        sources: SourceMap,
        file: SourceId,
        text: &'static str,
    }

    impl LiteralFixture {
        fn new(text: &'static str) -> Self {
            let mut sources = SourceMap::default();
            let file = sources.add("doc.knv", text);
            Self {
                sources,
                file,
                text,
            }
        }

        fn offset_of(&self, part: &str) -> usize {
            self.text.find(part).expect("the text holds it")
        }

        fn forms(&self, value: &str) -> LiteralForms {
            let literal = self.offset_of("\"")..self.text.rfind('"').expect("a closing quote") + 1;
            LiteralForms::new(self.sources.file(self.file), literal, value)
        }

        fn written_at(&self, value: &str, character: usize) -> String {
            let (form, _) = self.forms(value).read_at(character).expect("a form reads");
            self.text[form.span().range()].to_string()
        }
    }

    #[test]
    fn a_form_after_an_escape_points_at_its_written_text() {
        let fixture = LiteralFixture::new(r#"(doc "a \"b\" (+ 1 2) end")"#);
        assert_eq!(fixture.written_at("a \"b\" (+ 1 2) end", 6), "(+ 1 2)");
    }

    #[test]
    fn a_form_that_ends_in_a_wide_character_covers_all_of_it() {
        let fixture = LiteralFixture::new(r#"(doc "café")"#);
        assert_eq!(fixture.written_at("café", 0), "café");
    }

    #[test]
    fn a_form_that_ends_in_an_escape_covers_all_of_it() {
        let fixture = LiteralFixture::new(r#"(doc "\"b\"")"#);
        assert_eq!(fixture.written_at("\"b\"", 0), r#"\"b\""#);
    }

    #[test]
    fn a_read_form_is_pinned_to_the_source_of_its_literal() {
        let fixture = LiteralFixture::new(r#"(doc "(x)")"#);
        let (form, _) = fixture.forms("(x)").read_at(0).expect("a form reads");
        assert!(form.span().is_pinned());
        assert_eq!(form.span().source().index(), fixture.file.index());
    }

    #[test]
    fn a_character_counts_once_whatever_its_width() {
        let fixture = LiteralFixture::new(r#"(doc "\u{e9}é (x)")"#);
        assert_eq!(fixture.written_at("éé (x)", 3), "(x)");
    }

    #[test]
    fn an_item_of_a_list_points_at_its_own_text() {
        let fixture = LiteralFixture::new(r#"(doc "(alpha beta)")"#);
        let (form, _) = fixture
            .forms("(alpha beta)")
            .read_at(0)
            .expect("a form reads");
        let items = form.as_list().expect("a list");
        assert_eq!(&fixture.text[items[1].span().range()], "beta");
    }

    #[test]
    fn reading_returns_the_character_after_the_form() {
        let fixture = LiteralFixture::new(r#"(doc "(x) y")"#);
        let (_, end) = fixture.forms("(x) y").read_at(0).expect("a form reads");
        assert_eq!(end, 3);
    }

    #[test]
    fn a_position_past_the_end_reads_nothing() {
        let fixture = LiteralFixture::new(r#"(doc "(x)")"#);
        assert!(fixture.forms("(x)").read_at(9).is_none());
    }

    #[test]
    fn an_unbalanced_form_reads_nothing() {
        let fixture = LiteralFixture::new(r#"(doc "(x")"#);
        assert!(fixture.forms("(x").read_at(0).is_none());
    }

    #[test]
    fn a_value_that_the_literal_does_not_spell_points_at_the_whole_literal() {
        let fixture = LiteralFixture::new(r#"(doc "other")"#);
        assert_eq!(fixture.written_at("(x)", 0), r#""other""#);
    }
}