Skip to main content

pfx_editor/
log.rs

1use std::collections::VecDeque;
2use std::path::{Path, PathBuf};
3
4use pfx_load::scene::{EditError, SceneError};
5
6pub const KEEP: usize = 500;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum Level {
10    Info,
11    Warn,
12    Error,
13}
14
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct Entry {
17    pub level: Level,
18    pub file: Option<PathBuf>,
19    pub line: Option<usize>,
20    pub text: String,
21}
22
23impl Entry {
24    pub fn place(&self, root: &Path) -> Option<String> {
25        let file = self.file.as_ref()?;
26        let name = crate::outline::relative(root, file);
27        Some(match self.line {
28            Some(line) => format!("{name}:{line}"),
29            None => name,
30        })
31    }
32}
33
34#[derive(Clone, Debug, Default, PartialEq, Eq)]
35pub struct Log {
36    pub entries: VecDeque<Entry>,
37}
38
39impl Log {
40    pub fn push(&mut self, entry: Entry) {
41        if self.entries.len() == KEEP {
42            self.entries.pop_front();
43        }
44        self.entries.push_back(entry);
45    }
46
47    pub fn info(&mut self, text: impl Into<String>) {
48        self.say(Level::Info, text);
49    }
50
51    pub fn warn(&mut self, text: impl Into<String>) {
52        self.say(Level::Warn, text);
53    }
54
55    pub fn error(&mut self, text: impl Into<String>) {
56        self.say(Level::Error, text);
57    }
58
59    fn say(&mut self, level: Level, text: impl Into<String>) {
60        self.push(Entry {
61            level,
62            file: None,
63            line: None,
64            text: text.into(),
65        });
66    }
67
68    fn see(&mut self, file: &Path, line: usize) {
69        self.push(Entry {
70            level: Level::Error,
71            file: Some(file.to_path_buf()),
72            line: Some(line),
73            text: "see here".to_string(),
74        });
75    }
76
77    pub fn scene_error(&mut self, error: &SceneError) {
78        self.push(Entry {
79            level: Level::Error,
80            file: Some(error.file.clone()),
81            line: error.line,
82            text: error.message.clone(),
83        });
84        for place in &error.related {
85            self.see(&place.file, place.line);
86        }
87    }
88
89    pub fn edit_error(&mut self, error: &EditError) {
90        self.push(Entry {
91            level: Level::Error,
92            file: Some(error.file.clone()),
93            line: error.line.map(|line| line as usize),
94            text: format!("{}: {}", error.key, error.message),
95        });
96        let root = pfx_scene::Project::root_of(&error.file);
97        for found in error.diagnostics.iter().filter(|found| found.is_error()) {
98            for place in &found.related {
99                self.see(&root.join(&place.file), place.line as usize);
100            }
101        }
102    }
103
104    pub fn last(&self) -> Option<&Entry> {
105        self.entries.back()
106    }
107
108    pub fn errors(&self) -> usize {
109        self.entries
110            .iter()
111            .filter(|entry| entry.level == Level::Error)
112            .count()
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn an_edit_refused_with_a_chain_logs_each_place_of_it() {
122        let mut log = Log::default();
123        let diagnostic = |file: &str, related: Vec<pfx_scene::Location>| pfx_scene::Diagnostic {
124            file: PathBuf::from(file),
125            line: 3,
126            column: 12,
127            end_line: 3,
128            end_column: 20,
129            severity: pfx_scene::Severity::Error,
130            code: pfx_scene::code::INCLUDE_CYCLE,
131            key: "include.0".to_string(),
132            message: "the scene includes itself".to_string(),
133            related,
134        };
135        let place = |file: &str| pfx_scene::Location {
136            file: PathBuf::from(file),
137            line: 3,
138            column: 12,
139        };
140        log.edit_error(&EditError {
141            file: PathBuf::from("/game/content/other.scene.toml"),
142            key: "include".to_string(),
143            line: Some(3),
144            code: pfx_scene::code::INCLUDE_CYCLE,
145            message: "the scene includes itself".to_string(),
146            diagnostics: vec![diagnostic(
147                "content/other.scene.toml",
148                vec![
149                    place("content/main.scene.toml"),
150                    place("content/other.scene.toml"),
151                ],
152            )],
153        });
154        let seen: Vec<(Option<PathBuf>, Option<usize>, &str)> = log
155            .entries
156            .iter()
157            .map(|entry| (entry.file.clone(), entry.line, entry.text.as_str()))
158            .collect();
159        assert_eq!(seen.len(), 3, "{seen:?}");
160        assert_eq!(seen[0].2, "include: the scene includes itself");
161        assert_eq!(seen[1].2, "see here");
162        assert_eq!(seen[1].1, Some(3));
163        assert!(
164            seen[1]
165                .0
166                .as_ref()
167                .unwrap()
168                .ends_with("content/main.scene.toml")
169        );
170        assert!(
171            seen[2]
172                .0
173                .as_ref()
174                .unwrap()
175                .ends_with("content/other.scene.toml")
176        );
177    }
178
179    #[test]
180    fn errors_keep_their_file_and_line_and_the_log_keeps_its_last_entries() {
181        let mut log = Log::default();
182        log.scene_error(&SceneError::new(
183            Path::new("/scenes/room.scene.toml"),
184            Some(14),
185            "unknown field 'colour'",
186        ));
187        let entry = log.last().unwrap();
188        assert_eq!(
189            entry.place(Path::new("/scenes/room.scene.toml")).unwrap(),
190            "room.scene.toml:14"
191        );
192        for index in 0..KEEP + 3 {
193            log.info(format!("line {index}"));
194        }
195        assert_eq!(log.entries.len(), KEEP);
196        assert_eq!(log.last().unwrap().text, format!("line {}", KEEP + 2));
197        assert_eq!(log.errors(), 0);
198    }
199}