1use std::borrow::Cow;
2use std::io::Write;
3
4use ansiterm::{ANSIString, ANSIStrings};
5use itertools::Itertools;
6use strum::{EnumCount as _, IntoEnumIterator};
7use unicode_width::UnicodeWidthChar;
8
9use crate::fileset::{FileKind, FileStage};
10use crate::game::Game;
11use crate::report::errors::Errors;
12use crate::report::output_style::Styled;
13use crate::report::report_struct::pointer_indentation;
14use crate::report::{LogReportMetadata, LogReportPointers, OutputStyle, PointedMessage, Severity};
15
16const SPACES_PER_TAB: usize = 4;
18const MAX_IDLE_SPACE: usize = 16;
20
21pub fn log_report<O: Write + Send>(
23 errors: &Errors,
24 output: &mut O,
25 report: &LogReportMetadata,
26 pointers: &LogReportPointers,
27 additional: usize,
28) {
29 let indentation = pointer_indentation(pointers);
30 log_line_title(errors, output, report);
32
33 let iterator = pointers.iter();
35 let mut previous = None;
36 for pointer in iterator {
37 log_pointer(errors, output, previous, pointer, indentation, report.severity);
38 previous = Some(pointer);
39 }
40
41 if additional > 0 {
43 log_count(errors, output, indentation, additional);
44 }
45
46 if let Some(info) = &report.info {
48 log_line_info(errors, output, indentation, info);
49 }
50
51 if let Some(wiki) = &report.wiki {
53 log_line_wiki(errors, output, indentation, wiki);
54 }
55
56 _ = writeln!(output);
58}
59
60pub fn log_summary<O: Write + Send>(
61 output: &mut O,
62 styles: &OutputStyle,
63 reports: &Vec<(&LogReportMetadata, Cow<'_, LogReportPointers>, usize)>,
64) {
65 let mut counts = [0usize; Severity::COUNT];
66 for (metadata, _, additional) in reports {
67 counts[metadata.severity as usize] += 1 + additional;
68 }
69
70 let line = Severity::iter()
71 .rev()
72 .flat_map(|sev| {
73 [
74 styles.style(Styled::Tag(sev, true)).paint(format!("{sev}")),
75 styles.style(Styled::Tag(sev, false)).paint(format!(": {}", counts[sev as usize])),
76 styles.style(Styled::Default).paint(", "),
77 ]
78 })
79 .collect_vec();
80 _ = writeln!(output, "{}", ANSIStrings(&line[..line.len() - 1]));
81}
82
83fn log_pointer<O: Write + Send>(
84 errors: &Errors,
85 output: &mut O,
86 previous: Option<&PointedMessage>,
87 pointer: &PointedMessage,
88 indentation: usize,
89 severity: Severity,
90) {
91 if previous.is_none() || !previous.unwrap().loc.same_file(pointer.loc) {
92 log_line_file_location(errors, output, pointer, indentation);
94 }
95 if pointer.loc.line == 0 {
96 return;
99 }
100 if let Some(line) = errors.cache.get_line(pointer.loc) {
101 let (line, removed, spaces) = line_spacing(line);
102 log_line_from_source(errors, output, pointer, indentation, line, spaces);
103 log_line_carets(errors, output, pointer, indentation, line, removed, spaces, severity);
104 }
105}
106
107fn log_line_title<O: Write + Send>(errors: &Errors, output: &mut O, report: &LogReportMetadata) {
109 let line: &[ANSIString<'static>] = &[
110 errors
111 .styles
112 .style(Styled::Tag(report.severity, true))
113 .paint(format!("{}", report.severity)),
114 errors.styles.style(Styled::Tag(report.severity, false)).paint(format!("({})", report.key)),
115 errors.styles.style(Styled::Default).paint(": "),
116 errors.styles.style(Styled::ErrorMessage).paint(report.msg.clone()),
117 ];
118 _ = writeln!(output, "{}", ANSIStrings(line));
119}
120
121fn log_line_info<O: Write + Send>(errors: &Errors, output: &mut O, indentation: usize, info: &str) {
123 let line_info: &[ANSIString<'static>] = &[
124 errors.styles.style(Styled::Default).paint(format!("{:width$}", "", width = indentation)),
125 errors.styles.style(Styled::Default).paint(" "),
126 errors.styles.style(Styled::Location).paint("="),
127 errors.styles.style(Styled::Default).paint(" "),
128 errors.styles.style(Styled::InfoTag).paint("Info:"),
129 errors.styles.style(Styled::Default).paint(" "),
130 errors.styles.style(Styled::Info).paint(info.to_string()),
131 ];
132 _ = writeln!(output, "{}", ANSIStrings(line_info));
133}
134
135fn log_line_wiki<O: Write + Send>(errors: &Errors, output: &mut O, indentation: usize, wiki: &str) {
137 let line_info: &[ANSIString<'static>] = &[
138 errors.styles.style(Styled::Default).paint(format!("{:width$}", "", width = indentation)),
139 errors.styles.style(Styled::Default).paint(" "),
140 errors.styles.style(Styled::Location).paint("="),
141 errors.styles.style(Styled::Default).paint(" "),
142 errors.styles.style(Styled::InfoTag).paint("Wiki:"),
143 errors.styles.style(Styled::Default).paint(" "),
144 errors.styles.style(Styled::Info).paint(wiki.to_string()),
145 ];
146 _ = writeln!(output, "{}", ANSIStrings(line_info));
147}
148
149fn log_count<O: Write + Send>(errors: &Errors, output: &mut O, indentation: usize, count: usize) {
151 let line_count: &[ANSIString<'static>] = &[
152 errors.styles.style(Styled::Default).paint(format!("{:width$}", "", width = indentation)),
153 errors.styles.style(Styled::Location).paint("-->"),
154 errors.styles.style(Styled::Default).paint(" "),
155 errors.styles.style(Styled::Location).paint(format!("and {count} other locations")),
156 ];
157 _ = writeln!(output, "{}", ANSIStrings(line_count));
158}
159
160fn log_line_file_location<O: Write + Send>(
162 errors: &Errors,
163 output: &mut O,
164 pointer: &PointedMessage,
165 indentation: usize,
166) {
167 let line_filename: &[ANSIString<'static>] = &[
168 errors.styles.style(Styled::Default).paint(format!("{:width$}", "", width = indentation)),
169 errors.styles.style(Styled::Location).paint("-->"),
170 errors.styles.style(Styled::Default).paint(" "),
171 errors.styles.style(Styled::Location).paint(format!(
172 "[{}{}]",
173 kind_tag(errors, pointer.loc.kind),
174 stage_tag(pointer.loc.stage)
175 )),
176 errors.styles.style(Styled::Default).paint(" "),
177 errors
178 .styles
179 .style(Styled::Location)
180 .paint(format!("{}", pointer.loc.pathname().display())),
181 ];
182 _ = writeln!(output, "{}", ANSIStrings(line_filename));
183}
184
185fn log_line_from_source<O: Write + Send>(
187 errors: &Errors,
188 output: &mut O,
189 pointer: &PointedMessage,
190 indentation: usize,
191 line: &str,
192 spaces: usize,
193) {
194 let line_from_source: &[ANSIString<'static>] = &[
195 errors.styles.style(Styled::Location).paint(format!("{:indentation$}", pointer.loc.line,)),
196 errors.styles.style(Styled::Default).paint(" "),
197 errors.styles.style(Styled::Location).paint("|"),
198 errors.styles.style(Styled::Default).paint(" "),
199 errors.styles.style(Styled::SourceText).paint(format!("{:spaces$}{line}", "")),
200 ];
201 _ = writeln!(output, "{}", ANSIStrings(line_from_source));
202}
203
204#[allow(clippy::too_many_arguments)]
205fn log_line_carets<O: Write + Send>(
206 errors: &Errors,
207 output: &mut O,
208 pointer: &PointedMessage,
209 indentation: usize,
210 line: &str,
211 removed: usize,
212 spaces: usize,
213 severity: Severity,
214) {
215 if pointer.length == 0 {
216 return;
217 }
218
219 let mut spacing = String::new();
220 for c in line.chars().take((pointer.loc.column as usize).saturating_sub(removed + 1)) {
221 if c == '\t' {
223 spacing.push('\t');
224 } else {
225 for _ in 0..c.width().unwrap_or(0) {
226 spacing.push(' ');
227 }
228 }
229 }
230
231 let line_carets: &[ANSIString] = &[
233 errors.styles.style(Styled::Default).paint(format!("{:indentation$}", "")),
234 errors.styles.style(Styled::Default).paint(" "),
235 errors.styles.style(Styled::Location).paint("|"),
236 errors.styles.style(Styled::Default).paint(format!(
237 "{:width$}{spacing}",
238 "",
239 width = spaces + 1
240 )),
241 errors.styles.style(Styled::Tag(severity, true)).paint(format!(
242 "{:^^width$}",
243 "",
244 width = pointer.length
245 )),
246 errors.styles.style(Styled::Default).paint(" "),
247 errors
248 .styles
249 .style(Styled::Tag(severity, true))
250 .paint(pointer.msg.as_deref().map_or("", |_| "<-- ")),
251 errors
252 .styles
253 .style(Styled::Tag(severity, true))
254 .paint(pointer.msg.as_deref().unwrap_or("")),
255 ];
256 _ = writeln!(output, "{}", ANSIStrings(line_carets));
257}
258
259pub(crate) fn kind_tag<'a>(errors: &'a Errors<'a>, kind: FileKind) -> &'a str {
260 match kind {
261 FileKind::Internal => "Internal",
262 FileKind::Clausewitz => "Clausewitz",
263 FileKind::Jomini => "Jomini",
264 FileKind::Vanilla => match Game::game() {
265 #[cfg(feature = "ck3")]
266 Game::Ck3 => "CK3",
267 #[cfg(feature = "vic3")]
268 Game::Vic3 => "Vic3",
269 #[cfg(feature = "imperator")]
270 Game::Imperator => "Imperator",
271 #[cfg(feature = "eu5")]
272 Game::Eu5 => "EU5",
273 #[cfg(feature = "hoi4")]
274 Game::Hoi4 => "Hoi4",
275 },
276 FileKind::Dlc(idx) => &errors.loaded_dlcs_labels[idx as usize],
277 FileKind::LoadedMod(idx) => &errors.loaded_mods_labels[idx as usize],
278 FileKind::Mod => "MOD",
279 }
280}
281
282fn stage_tag(stage: FileStage) -> &'static str {
283 match stage {
284 #[cfg(feature = "eu5")]
285 FileStage::LoadingScreen => "(loadscreen)",
286 #[cfg(feature = "eu5")]
287 FileStage::MainMenu => "(menu)",
288 #[cfg(feature = "eu5")]
289 FileStage::InGame => "",
290 FileStage::NoStage => "",
291 }
292}
293
294fn line_spacing(line: &str) -> (&str, usize, usize) {
297 let mut remove = 0;
298 let mut spaces = 0;
299 for c in line.chars() {
300 if c == ' ' {
301 spaces += 1;
302 } else if c == '\t' {
303 spaces += SPACES_PER_TAB;
304 } else {
305 break;
306 }
307 remove += 1;
308 }
309 spaces = spaces.min(MAX_IDLE_SPACE);
310 (&line[remove..], remove, spaces)
311}