Skip to main content

pfx_editor_toml/
table.rs

1use std::collections::BTreeMap;
2use std::hash::{DefaultHasher, Hash, Hasher};
3use std::ops::Range;
4
5use crate::lex::{Kind, Lexed};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct Table {
9    pub path: Vec<String>,
10    pub index: Option<usize>,
11    pub header: Range<usize>,
12}
13
14impl Table {
15    pub fn same(&self, other: &Table) -> bool {
16        self.path == other.path && self.index == other.index
17    }
18}
19
20fn header(text: &str) -> Option<(Vec<String>, bool)> {
21    let (inner, array) = match text.strip_prefix("[[").and_then(|t| t.strip_suffix("]]")) {
22        Some(inner) => (inner, true),
23        None => (text.strip_prefix('[')?.strip_suffix(']')?, false),
24    };
25    let keys = toml_edit::Key::parse(inner.trim()).ok()?;
26    Some((
27        keys.iter().map(|key| key.get().to_string()).collect(),
28        array,
29    ))
30}
31
32type Entry = Option<(Vec<String>, Option<usize>)>;
33
34fn headers(lexed: &Lexed) -> Vec<Range<usize>> {
35    lexed
36        .lines()
37        .iter()
38        .filter_map(|line| {
39            let token = line
40                .tokens
41                .iter()
42                .find(|token| token.kind == Kind::Header)?;
43            Some(token.range.start + line.start..token.range.end + line.start)
44        })
45        .collect()
46}
47
48fn entries(text: &str, headers: &[Range<usize>]) -> Vec<Entry> {
49    let mut counts: BTreeMap<Vec<String>, usize> = BTreeMap::new();
50    headers
51        .iter()
52        .map(|range| {
53            let (path, array) = header(&text[range.clone()])?;
54            counts.retain(|known, _| !(known.len() > path.len() && known.starts_with(&path)));
55            let index = array.then(|| {
56                let count = counts.entry(path.clone()).or_default();
57                *count += 1;
58                *count - 1
59            });
60            Some((path, index))
61        })
62        .collect()
63}
64
65#[derive(Debug, Default)]
66pub struct TableCache {
67    key: Option<u64>,
68    entries: Vec<Entry>,
69    parsed: u64,
70}
71
72impl TableCache {
73    pub fn at(&mut self, lexed: &Lexed, byte: usize) -> Option<Table> {
74        let text = lexed.text();
75        let headers = headers(lexed);
76        let mut hasher = DefaultHasher::new();
77        for range in &headers {
78            text[range.clone()].hash(&mut hasher);
79        }
80        let key = hasher.finish();
81        if self.key != Some(key) {
82            self.entries = entries(text, &headers);
83            self.key = Some(key);
84            self.parsed += 1;
85        }
86        let line = lexed.line_of(byte.min(text.len()));
87        let end = lexed
88            .lines()
89            .get(line + 1)
90            .map_or(usize::MAX, |next| next.start);
91        let before = headers.partition_point(|range| range.start < end);
92        (0..before).rev().find_map(|index| {
93            let (path, at) = self.entries[index].clone()?;
94            Some(Table {
95                path,
96                index: at,
97                header: headers[index].clone(),
98            })
99        })
100    }
101
102    pub fn parsed(&self) -> u64 {
103        self.parsed
104    }
105}
106
107pub fn tables(lexed: &Lexed) -> Vec<Table> {
108    let headers = headers(lexed);
109    entries(lexed.text(), &headers)
110        .into_iter()
111        .zip(headers)
112        .filter_map(|(entry, header)| {
113            let (path, index) = entry?;
114            Some(Table {
115                path,
116                index,
117                header,
118            })
119        })
120        .collect()
121}
122
123pub fn table_at(lexed: &Lexed, byte: usize) -> Option<Table> {
124    TableCache::default().at(lexed, byte)
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    const TEXT: &str = "top = 1\n[[object]]\nname = \"a\"\n[[object]]\nname = \"b\"\n[[object.part]]\nx = 1\n[[object]]\nname = \"c\"\n[[object.part]]\nx = 2\n[materials.\"red one\"]\nshine = 0.5\n";
132
133    fn at(needle: &str) -> Option<Table> {
134        table_at(&Lexed::new(TEXT), TEXT.find(needle).unwrap())
135    }
136
137    #[test]
138    fn each_header_names_its_table_and_its_place_in_an_array() {
139        let found = tables(&Lexed::new(TEXT));
140        let summary: Vec<(String, Option<usize>)> = found
141            .iter()
142            .map(|table| (table.path.join("."), table.index))
143            .collect();
144        assert_eq!(
145            summary,
146            vec![
147                ("object".into(), Some(0)),
148                ("object".into(), Some(1)),
149                ("object.part".into(), Some(0)),
150                ("object".into(), Some(2)),
151                ("object.part".into(), Some(0)),
152                ("materials.red one".into(), None),
153            ]
154        );
155        assert_eq!(&TEXT[found[5].header.clone()], "[materials.\"red one\"]");
156    }
157
158    #[test]
159    fn a_byte_belongs_to_the_table_above_it() {
160        assert_eq!(at("top"), None);
161        assert_eq!(at("name = \"c\"").unwrap().index, Some(2));
162        assert_eq!(at("x = 2").unwrap().path, vec!["object", "part"]);
163        let header = TEXT.find("[[object]]\nname = \"b\"").unwrap();
164        assert_eq!(
165            table_at(&Lexed::new(TEXT), header + 3).unwrap().index,
166            Some(1)
167        );
168        assert_eq!(table_at(&Lexed::new(TEXT), header).unwrap().index, Some(1));
169        assert_eq!(at("shine").unwrap().path, vec!["materials", "red one"]);
170        assert!(at("shine").unwrap().same(&at("shine").unwrap()));
171    }
172
173    #[test]
174    fn the_cache_parses_headers_again_only_when_one_changes() {
175        let mut cache = TableCache::default();
176        let typed = TEXT.replace("x = 2", "x = 22");
177        let renamed = TEXT.replace("[materials.\"red one\"]", "[materials.blue]");
178        for (text, needle) in [(TEXT, "x = 2"), (&typed, "x = 22"), (&renamed, "shine")] {
179            let lexed = Lexed::new(text);
180            let at = text.find(needle).unwrap();
181            assert_eq!(cache.at(&lexed, at), table_at(&lexed, at));
182        }
183        assert_eq!(cache.parsed(), 2);
184    }
185}