forc_util/
lib.rs

1//! Utility items shared between forc crates.
2use annotate_snippets::{
3    renderer::{AnsiColor, Style},
4    Annotation, AnnotationType, Renderer, Slice, Snippet, SourceAnnotation,
5};
6use anyhow::{bail, Context, Result};
7use forc_tracing::{println_action_green, println_error, println_red_err, println_yellow_err};
8use std::{
9    collections::{hash_map, HashSet},
10    fmt::Display,
11    fs::File,
12    hash::{Hash, Hasher},
13    path::{Path, PathBuf},
14    process::Termination,
15    str,
16};
17use sway_core::language::parsed::TreeType;
18use sway_error::{
19    diagnostic::{Diagnostic, Issue, Label, LabelType, Level, ToDiagnostic},
20    error::CompileError,
21    warning::CompileWarning,
22};
23use sway_types::{LineCol, LineColRange, SourceEngine, Span};
24use sway_utils::constants;
25
26pub mod bytecode;
27pub mod fs_locking;
28pub mod restricted;
29
30#[macro_use]
31pub mod cli;
32
33pub use ansiterm;
34pub use paste;
35pub use regex::Regex;
36pub use serial_test;
37
38pub const DEFAULT_OUTPUT_DIRECTORY: &str = "out";
39pub const DEFAULT_ERROR_EXIT_CODE: u8 = 1;
40pub const DEFAULT_SUCCESS_EXIT_CODE: u8 = 0;
41
42/// A result type for forc operations. This shouldn't be returned from entry points, instead return
43/// `ForcCliResult` to exit with correct exit code.
44pub type ForcResult<T, E = ForcError> = Result<T, E>;
45
46/// A wrapper around `ForcResult`. Designed to be returned from entry points as it handles
47/// error reporting and exits with correct exit code.
48#[derive(Debug)]
49pub struct ForcCliResult<T> {
50    result: ForcResult<T>,
51}
52
53/// A forc error type which is a wrapper around `anyhow::Error`. It enables propagation of custom
54/// exit code alongisde the original error.
55#[derive(Debug)]
56pub struct ForcError {
57    error: anyhow::Error,
58    exit_code: u8,
59}
60
61impl ForcError {
62    pub fn new(error: anyhow::Error, exit_code: u8) -> Self {
63        Self { error, exit_code }
64    }
65
66    /// Returns a `ForcError` with provided exit_code.
67    pub fn exit_code(self, exit_code: u8) -> Self {
68        Self {
69            error: self.error,
70            exit_code,
71        }
72    }
73}
74
75impl AsRef<anyhow::Error> for ForcError {
76    fn as_ref(&self) -> &anyhow::Error {
77        &self.error
78    }
79}
80
81impl From<&str> for ForcError {
82    fn from(value: &str) -> Self {
83        Self {
84            error: anyhow::anyhow!("{value}"),
85            exit_code: DEFAULT_ERROR_EXIT_CODE,
86        }
87    }
88}
89
90impl From<anyhow::Error> for ForcError {
91    fn from(value: anyhow::Error) -> Self {
92        Self {
93            error: value,
94            exit_code: DEFAULT_ERROR_EXIT_CODE,
95        }
96    }
97}
98
99impl From<std::io::Error> for ForcError {
100    fn from(value: std::io::Error) -> Self {
101        Self {
102            error: value.into(),
103            exit_code: DEFAULT_ERROR_EXIT_CODE,
104        }
105    }
106}
107
108impl Display for ForcError {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        self.error.fmt(f)
111    }
112}
113
114impl<T> Termination for ForcCliResult<T> {
115    fn report(self) -> std::process::ExitCode {
116        match self.result {
117            Ok(_) => DEFAULT_SUCCESS_EXIT_CODE.into(),
118            Err(e) => {
119                println_error(&format!("{}", e));
120                e.exit_code.into()
121            }
122        }
123    }
124}
125
126impl<T> From<ForcResult<T>> for ForcCliResult<T> {
127    fn from(value: ForcResult<T>) -> Self {
128        Self { result: value }
129    }
130}
131
132#[macro_export]
133macro_rules! forc_result_bail {
134    ($msg:literal $(,)?) => {
135        return $crate::ForcResult::Err(anyhow::anyhow!($msg).into())
136    };
137    ($err:expr $(,)?) => {
138        return $crate::ForcResult::Err(anyhow::anyhow!($err).into())
139    };
140    ($fmt:expr, $($arg:tt)*) => {
141        return $crate::ForcResult::Err(anyhow::anyhow!($fmt, $($arg)*).into())
142    };
143}
144
145#[cfg(feature = "tx")]
146pub mod tx_utils {
147
148    use anyhow::Result;
149    use clap::Args;
150    use fuels_core::{codec::ABIDecoder, types::param_types::ParamType};
151    use serde::{Deserialize, Serialize};
152    use std::collections::HashMap;
153    use sway_core::{asm_generation::ProgramABI, fuel_prelude::fuel_tx};
154
155    /// Added salt used to derive the contract ID.
156    #[derive(Debug, Args, Default, Deserialize, Serialize)]
157    pub struct Salt {
158        /// Added salt used to derive the contract ID.
159        ///
160        /// By default, this is
161        /// `0x0000000000000000000000000000000000000000000000000000000000000000`.
162        #[clap(long = "salt")]
163        pub salt: Option<fuel_tx::Salt>,
164    }
165
166    /// Format `Log` and `LogData` receipts.
167    pub fn format_log_receipts(
168        receipts: &[fuel_tx::Receipt],
169        pretty_print: bool,
170    ) -> Result<String> {
171        let mut receipt_to_json_array = serde_json::to_value(receipts)?;
172        for (rec_index, receipt) in receipts.iter().enumerate() {
173            let rec_value = receipt_to_json_array.get_mut(rec_index).ok_or_else(|| {
174                anyhow::anyhow!(
175                    "Serialized receipts does not contain {} th index",
176                    rec_index
177                )
178            })?;
179            match receipt {
180                fuel_tx::Receipt::LogData {
181                    data: Some(data), ..
182                } => {
183                    if let Some(v) = rec_value.pointer_mut("/LogData/data") {
184                        *v = hex::encode(data).into();
185                    }
186                }
187                fuel_tx::Receipt::ReturnData {
188                    data: Some(data), ..
189                } => {
190                    if let Some(v) = rec_value.pointer_mut("/ReturnData/data") {
191                        *v = hex::encode(data).into();
192                    }
193                }
194                _ => {}
195            }
196        }
197        if pretty_print {
198            Ok(serde_json::to_string_pretty(&receipt_to_json_array)?)
199        } else {
200            Ok(serde_json::to_string(&receipt_to_json_array)?)
201        }
202    }
203
204    /// A `LogData` decoded into a human readable format with its type information.
205    pub struct DecodedLog {
206        pub value: String,
207    }
208
209    pub fn decode_log_data(
210        log_id: &str,
211        log_data: &[u8],
212        program_abi: &ProgramABI,
213    ) -> anyhow::Result<DecodedLog> {
214        let program_abi = match program_abi {
215            ProgramABI::Fuel(fuel_abi) => Some(
216                fuel_abi_types::abi::unified_program::UnifiedProgramABI::from_counterpart(
217                    fuel_abi,
218                )?,
219            ),
220            _ => None,
221        }
222        .ok_or_else(|| anyhow::anyhow!("only fuelvm is supported for log decoding"))?;
223        // Create type lookup (id, TypeDeclaration)
224        let type_lookup = program_abi
225            .types
226            .iter()
227            .map(|decl| (decl.type_id, decl.clone()))
228            .collect::<HashMap<_, _>>();
229
230        let logged_type_lookup: HashMap<_, _> = program_abi
231            .logged_types
232            .iter()
233            .flatten()
234            .map(|logged_type| (logged_type.log_id.as_str(), logged_type.application.clone()))
235            .collect();
236
237        let type_application = logged_type_lookup
238            .get(&log_id)
239            .ok_or_else(|| anyhow::anyhow!("log id is missing"))?;
240
241        let abi_decoder = ABIDecoder::default();
242        let param_type = ParamType::try_from_type_application(type_application, &type_lookup)?;
243        let decoded_str = abi_decoder.decode_as_debug_str(&param_type, log_data)?;
244        let decoded_log = DecodedLog { value: decoded_str };
245
246        Ok(decoded_log)
247    }
248}
249
250pub fn find_file_name<'sc>(manifest_dir: &Path, entry_path: &'sc Path) -> Result<&'sc Path> {
251    let mut file_path = manifest_dir.to_path_buf();
252    file_path.pop();
253    let file_name = match entry_path.strip_prefix(file_path.clone()) {
254        Ok(o) => o,
255        Err(err) => bail!(err),
256    };
257    Ok(file_name)
258}
259
260pub fn lock_path(manifest_dir: &Path) -> PathBuf {
261    manifest_dir.join(constants::LOCK_FILE_NAME)
262}
263
264pub fn validate_project_name(name: &str) -> Result<()> {
265    restricted::is_valid_project_name_format(name)?;
266    validate_name(name, "project name")
267}
268
269// Using (https://github.com/rust-lang/cargo/blob/489b66f2e458404a10d7824194d3ded94bc1f4e4/src/cargo/util/toml/mod.rs +
270// https://github.com/rust-lang/cargo/blob/489b66f2e458404a10d7824194d3ded94bc1f4e4/src/cargo/ops/cargo_new.rs) for reference
271
272pub fn validate_name(name: &str, use_case: &str) -> Result<()> {
273    // if true returns formatted error
274    restricted::contains_invalid_char(name, use_case)?;
275
276    if restricted::is_keyword(name) {
277        bail!("the name `{name}` cannot be used as a {use_case}, it is a Sway keyword");
278    }
279    if restricted::is_conflicting_artifact_name(name) {
280        bail!(
281            "the name `{name}` cannot be used as a {use_case}, \
282            it conflicts with Forc's build directory names"
283        );
284    }
285    if name.to_lowercase() == "test" {
286        bail!(
287            "the name `test` cannot be used as a {use_case}, \
288            it conflicts with Sway's built-in test library"
289        );
290    }
291    if restricted::is_conflicting_suffix(name) {
292        bail!(
293            "the name `{name}` is part of Sway's standard library\n\
294            It is recommended to use a different name to avoid problems."
295        );
296    }
297    if restricted::is_windows_reserved(name) {
298        if cfg!(windows) {
299            bail!("cannot use name `{name}`, it is a reserved Windows filename");
300        } else {
301            bail!(
302                "the name `{name}` is a reserved Windows filename\n\
303                This package will not work on Windows platforms."
304            );
305        }
306    }
307    if restricted::is_non_ascii_name(name) {
308        bail!("the name `{name}` contains non-ASCII characters which are unsupported");
309    }
310    Ok(())
311}
312
313/// Simple function to convert kebab-case to snake_case.
314pub fn kebab_to_snake_case(s: &str) -> String {
315    s.replace('-', "_")
316}
317
318pub fn default_output_directory(manifest_dir: &Path) -> PathBuf {
319    manifest_dir.join(DEFAULT_OUTPUT_DIRECTORY)
320}
321
322/// Returns the user's `.forc` directory, `$HOME/.forc` by default.
323pub fn user_forc_directory() -> PathBuf {
324    dirs::home_dir()
325        .expect("unable to find the user home directory")
326        .join(constants::USER_FORC_DIRECTORY)
327}
328
329/// The location at which `forc` will checkout git repositories.
330pub fn git_checkouts_directory() -> PathBuf {
331    user_forc_directory().join("git").join("checkouts")
332}
333
334/// Given a path to a directory we wish to lock, produce a path for an associated lock file.
335///
336/// Note that the lock file itself is simply a placeholder for co-ordinating access. As a result,
337/// we want to create the lock file if it doesn't exist, but we can never reliably remove it
338/// without risking invalidation of an existing lock. As a result, we use a dedicated, hidden
339/// directory with a lock file named after the checkout path.
340///
341/// Note: This has nothing to do with `Forc.lock` files, rather this is about fd locks for
342/// coordinating access to particular paths (e.g. git checkout directories).
343fn fd_lock_path<X: AsRef<Path>>(path: X) -> PathBuf {
344    const LOCKS_DIR_NAME: &str = ".locks";
345    const LOCK_EXT: &str = "forc-lock";
346    let file_name = hash_path(path);
347    user_forc_directory()
348        .join(LOCKS_DIR_NAME)
349        .join(file_name)
350        .with_extension(LOCK_EXT)
351}
352
353/// Hash the path to produce a file-system friendly file name.
354/// Append the file stem for improved readability.
355fn hash_path<X: AsRef<Path>>(path: X) -> String {
356    let path = path.as_ref();
357    let mut hasher = hash_map::DefaultHasher::default();
358    path.hash(&mut hasher);
359    let hash = hasher.finish();
360    let file_name = match path.file_stem().and_then(|s| s.to_str()) {
361        None => format!("{hash:X}"),
362        Some(stem) => format!("{hash:X}-{stem}"),
363    };
364    file_name
365}
366
367/// Create an advisory lock over the given path.
368///
369/// See [fd_lock_path] for details.
370pub fn path_lock<X: AsRef<Path>>(path: X) -> Result<fd_lock::RwLock<File>> {
371    let lock_path = fd_lock_path(path);
372    let lock_dir = lock_path
373        .parent()
374        .expect("lock path has no parent directory");
375    std::fs::create_dir_all(lock_dir).context("failed to create forc advisory lock directory")?;
376    let lock_file = File::create(&lock_path).context("failed to create advisory lock file")?;
377    Ok(fd_lock::RwLock::new(lock_file))
378}
379
380pub fn program_type_str(ty: &TreeType) -> &'static str {
381    match ty {
382        TreeType::Script {} => "script",
383        TreeType::Contract {} => "contract",
384        TreeType::Predicate {} => "predicate",
385        TreeType::Library { .. } => "library",
386    }
387}
388
389pub fn print_compiling(ty: Option<&TreeType>, name: &str, src: &dyn std::fmt::Display) {
390    // NOTE: We can only print the program type if we can parse the program, so
391    // program type must be optional.
392    let ty = match ty {
393        Some(ty) => format!("{} ", program_type_str(ty)),
394        None => "".to_string(),
395    };
396    println_action_green(
397        "Compiling",
398        &format!("{ty}{} ({src})", ansiterm::Style::new().bold().paint(name)),
399    );
400}
401
402pub fn print_warnings(
403    source_engine: &SourceEngine,
404    terse_mode: bool,
405    proj_name: &str,
406    warnings: &[CompileWarning],
407    tree_type: &TreeType,
408) {
409    if warnings.is_empty() {
410        return;
411    }
412    let type_str = program_type_str(tree_type);
413
414    if !terse_mode {
415        warnings
416            .iter()
417            .for_each(|w| format_diagnostic(&w.to_diagnostic(source_engine)));
418    }
419
420    println_yellow_err(&format!(
421        "  Compiled {} {:?} with {} {}.",
422        type_str,
423        proj_name,
424        warnings.len(),
425        if warnings.len() > 1 {
426            "warnings"
427        } else {
428            "warning"
429        }
430    ));
431}
432
433pub fn print_on_failure(
434    source_engine: &SourceEngine,
435    terse_mode: bool,
436    warnings: &[CompileWarning],
437    errors: &[CompileError],
438    reverse_results: bool,
439) {
440    let e_len = errors.len();
441    let w_len = warnings.len();
442
443    if !terse_mode {
444        if reverse_results {
445            warnings
446                .iter()
447                .rev()
448                .for_each(|w| format_diagnostic(&w.to_diagnostic(source_engine)));
449            errors
450                .iter()
451                .rev()
452                .for_each(|e| format_diagnostic(&e.to_diagnostic(source_engine)));
453        } else {
454            warnings
455                .iter()
456                .for_each(|w| format_diagnostic(&w.to_diagnostic(source_engine)));
457            errors
458                .iter()
459                .for_each(|e| format_diagnostic(&e.to_diagnostic(source_engine)));
460        }
461    }
462
463    if e_len == 0 && w_len > 0 {
464        println_red_err(&format!(
465            "  Aborting. {} warning(s) treated as error(s).",
466            warnings.len()
467        ));
468    } else {
469        println_red_err(&format!(
470            "  Aborting due to {} {}.",
471            e_len,
472            if e_len > 1 { "errors" } else { "error" }
473        ));
474    }
475}
476
477/// Creates [Renderer] for printing warnings and errors.
478///
479/// To ensure the same styling of printed warnings and errors across all the tools,
480/// always use this function to create [Renderer]s,
481pub fn create_diagnostics_renderer() -> Renderer {
482    // For the diagnostic messages we use bold and bright colors.
483    // Note that for the summaries of warnings and errors we use
484    // their regular equivalents which are defined in `forc-tracing` package.
485    Renderer::styled()
486        .warning(
487            Style::new()
488                .bold()
489                .fg_color(Some(AnsiColor::BrightYellow.into())),
490        )
491        .error(
492            Style::new()
493                .bold()
494                .fg_color(Some(AnsiColor::BrightRed.into())),
495        )
496}
497
498pub fn format_diagnostic(diagnostic: &Diagnostic) {
499    /// Temporary switch for testing the feature.
500    /// Keep it false until we decide to fully support the diagnostic codes.
501    const SHOW_DIAGNOSTIC_CODE: bool = false;
502
503    if diagnostic.is_old_style() {
504        format_old_style_diagnostic(diagnostic.issue());
505        return;
506    }
507
508    let mut label = String::new();
509    get_title_label(diagnostic, &mut label);
510
511    let snippet_title = Some(Annotation {
512        label: Some(label.as_str()),
513        id: if SHOW_DIAGNOSTIC_CODE {
514            diagnostic.reason().map(|reason| reason.code())
515        } else {
516            None
517        },
518        annotation_type: diagnostic_level_to_annotation_type(diagnostic.level()),
519    });
520
521    let mut snippet_slices = Vec::<Slice<'_>>::new();
522
523    // We first display labels from the issue file...
524    if diagnostic.issue().is_in_source() {
525        snippet_slices.push(construct_slice(diagnostic.labels_in_issue_source()))
526    }
527
528    // ...and then all the remaining labels from the other files.
529    for source_path in diagnostic.related_sources(false) {
530        snippet_slices.push(construct_slice(diagnostic.labels_in_source(source_path)))
531    }
532
533    let mut snippet_footer = Vec::<Annotation<'_>>::new();
534    for help in diagnostic.help() {
535        snippet_footer.push(Annotation {
536            id: None,
537            label: Some(help),
538            annotation_type: AnnotationType::Help,
539        });
540    }
541
542    let snippet = Snippet {
543        title: snippet_title,
544        slices: snippet_slices,
545        footer: snippet_footer,
546    };
547
548    let renderer = create_diagnostics_renderer();
549    match diagnostic.level() {
550        Level::Info => tracing::info!("{}\n____\n", renderer.render(snippet)),
551        Level::Warning => tracing::warn!("{}\n____\n", renderer.render(snippet)),
552        Level::Error => tracing::error!("{}\n____\n", renderer.render(snippet)),
553    }
554
555    fn format_old_style_diagnostic(issue: &Issue) {
556        let annotation_type = label_type_to_annotation_type(issue.label_type());
557
558        let snippet_title = Some(Annotation {
559            label: if issue.is_in_source() {
560                None
561            } else {
562                Some(issue.text())
563            },
564            id: None,
565            annotation_type,
566        });
567
568        let mut snippet_slices = vec![];
569        if issue.is_in_source() {
570            let span = issue.span();
571            let input = span.input();
572            let mut start_pos = span.start();
573            let mut end_pos = span.end();
574            let LineColRange { mut start, end } = span.line_col_one_index();
575            let input = construct_window(&mut start, end, &mut start_pos, &mut end_pos, input);
576
577            let slice = Slice {
578                source: input,
579                line_start: start.line,
580                // Safe unwrap because the issue is in source, so the source path surely exists.
581                origin: Some(issue.source_path().unwrap().as_str()),
582                fold: false,
583                annotations: vec![SourceAnnotation {
584                    label: issue.text(),
585                    annotation_type,
586                    range: (start_pos, end_pos),
587                }],
588            };
589
590            snippet_slices.push(slice);
591        }
592
593        let snippet = Snippet {
594            title: snippet_title,
595            footer: vec![],
596            slices: snippet_slices,
597        };
598
599        let renderer = create_diagnostics_renderer();
600        tracing::error!("{}\n____\n", renderer.render(snippet));
601    }
602
603    fn get_title_label(diagnostics: &Diagnostic, label: &mut String) {
604        label.clear();
605        if let Some(reason) = diagnostics.reason() {
606            label.push_str(reason.description());
607        }
608    }
609
610    fn diagnostic_level_to_annotation_type(level: Level) -> AnnotationType {
611        match level {
612            Level::Info => AnnotationType::Info,
613            Level::Warning => AnnotationType::Warning,
614            Level::Error => AnnotationType::Error,
615        }
616    }
617}
618
619fn construct_slice(labels: Vec<&Label>) -> Slice {
620    debug_assert!(
621        !labels.is_empty(),
622        "To construct slices, at least one label must be provided."
623    );
624
625    debug_assert!(
626        labels.iter().all(|label| label.is_in_source()),
627        "Slices can be constructed only for labels that are related to a place in source code."
628    );
629
630    debug_assert!(
631        HashSet::<&str>::from_iter(labels.iter().map(|label| label.source_path().unwrap().as_str())).len() == 1,
632        "Slices can be constructed only for labels that are related to places in the same source code."
633    );
634
635    let source_file = labels[0].source_path().map(|path| path.as_str());
636    let source_code = labels[0].span().input();
637
638    // Joint span of the code snippet that covers all the labels.
639    let span = Span::join_all(labels.iter().map(|label| label.span().clone()));
640
641    let (source, line_start, shift_in_bytes) = construct_code_snippet(&span, source_code);
642
643    let mut annotations = vec![];
644
645    for message in labels {
646        annotations.push(SourceAnnotation {
647            label: message.text(),
648            annotation_type: label_type_to_annotation_type(message.label_type()),
649            range: get_annotation_range(message.span(), source_code, shift_in_bytes),
650        });
651    }
652
653    return Slice {
654        source,
655        line_start,
656        origin: source_file,
657        fold: true,
658        annotations,
659    };
660
661    fn get_annotation_range(
662        span: &Span,
663        source_code: &str,
664        shift_in_bytes: usize,
665    ) -> (usize, usize) {
666        let mut start_pos = span.start();
667        let mut end_pos = span.end();
668
669        let start_ix_bytes = start_pos - std::cmp::min(shift_in_bytes, start_pos);
670        let end_ix_bytes = end_pos - std::cmp::min(shift_in_bytes, end_pos);
671
672        // We want the start_pos and end_pos in terms of chars and not bytes, so translate.
673        start_pos = source_code[shift_in_bytes..(shift_in_bytes + start_ix_bytes)]
674            .chars()
675            .count();
676        end_pos = source_code[shift_in_bytes..(shift_in_bytes + end_ix_bytes)]
677            .chars()
678            .count();
679
680        (start_pos, end_pos)
681    }
682}
683
684fn label_type_to_annotation_type(label_type: LabelType) -> AnnotationType {
685    match label_type {
686        LabelType::Info => AnnotationType::Info,
687        LabelType::Help => AnnotationType::Help,
688        LabelType::Warning => AnnotationType::Warning,
689        LabelType::Error => AnnotationType::Error,
690    }
691}
692
693/// Given the overall span to be shown in the code snippet, determines how much of the input source
694/// to show in the snippet.
695///
696/// Returns the source to be shown, the line start, and the offset of the snippet in bytes relative
697/// to the beginning of the input code.
698///
699/// The library we use doesn't handle auto-windowing and line numbers, so we must manually
700/// calculate the line numbers and match them up with the input window. It is a bit fiddly.
701fn construct_code_snippet<'a>(span: &Span, input: &'a str) -> (&'a str, usize, usize) {
702    // how many lines to prepend or append to the highlighted region in the window
703    const NUM_LINES_BUFFER: usize = 2;
704
705    let LineColRange { start, end } = span.line_col_one_index();
706
707    let total_lines_in_input = input.chars().filter(|x| *x == '\n').count();
708    debug_assert!(end.line >= start.line);
709    let total_lines_of_highlight = end.line - start.line;
710    debug_assert!(total_lines_in_input >= total_lines_of_highlight);
711
712    let mut current_line = 0;
713    let mut lines_to_start_of_snippet = 0;
714    let mut calculated_start_ix = None;
715    let mut calculated_end_ix = None;
716    let mut pos = 0;
717    for character in input.chars() {
718        if character == '\n' {
719            current_line += 1
720        }
721
722        if current_line + NUM_LINES_BUFFER >= start.line && calculated_start_ix.is_none() {
723            calculated_start_ix = Some(pos);
724            lines_to_start_of_snippet = current_line;
725        }
726
727        if current_line >= end.line + NUM_LINES_BUFFER && calculated_end_ix.is_none() {
728            calculated_end_ix = Some(pos);
729        }
730
731        if calculated_start_ix.is_some() && calculated_end_ix.is_some() {
732            break;
733        }
734        pos += character.len_utf8();
735    }
736    let calculated_start_ix = calculated_start_ix.unwrap_or(0);
737    let calculated_end_ix = calculated_end_ix.unwrap_or(input.len());
738
739    (
740        &input[calculated_start_ix..calculated_end_ix],
741        lines_to_start_of_snippet,
742        calculated_start_ix,
743    )
744}
745
746// TODO: Remove once "old-style" diagnostic is fully replaced with new one and the backward
747//       compatibility is no longer needed.
748/// Given a start and an end position and an input, determine how much of a window to show in the
749/// error.
750/// Mutates the start and end indexes to be in line with the new slice length.
751///
752/// The library we use doesn't handle auto-windowing and line numbers, so we must manually
753/// calculate the line numbers and match them up with the input window. It is a bit fiddly.
754fn construct_window<'a>(
755    start: &mut LineCol,
756    end: LineCol,
757    start_ix: &mut usize,
758    end_ix: &mut usize,
759    input: &'a str,
760) -> &'a str {
761    // how many lines to prepend or append to the highlighted region in the window
762    const NUM_LINES_BUFFER: usize = 2;
763
764    let total_lines_in_input = input.chars().filter(|x| *x == '\n').count();
765    debug_assert!(end.line >= start.line);
766    let total_lines_of_highlight = end.line - start.line;
767    debug_assert!(total_lines_in_input >= total_lines_of_highlight);
768
769    let mut current_line = 1usize;
770
771    let mut chars = input.char_indices().map(|(char_offset, character)| {
772        let r = (current_line, char_offset);
773        if character == '\n' {
774            current_line += 1;
775        }
776        r
777    });
778
779    // Find the first char of the first line
780    let first_char = chars
781        .by_ref()
782        .find(|(current_line, _)| current_line + NUM_LINES_BUFFER >= start.line);
783
784    // Find the last char of the last line
785    let last_char = chars
786        .by_ref()
787        .find(|(current_line, _)| *current_line > end.line + NUM_LINES_BUFFER)
788        .map(|x| x.1);
789
790    // this releases the borrow of `current_line`
791    drop(chars);
792
793    let (first_char_line, first_char_offset, last_char_offset) = match (first_char, last_char) {
794        // has first and last
795        (Some((first_char_line, first_char_offset)), Some(last_char_offset)) => {
796            (first_char_line, first_char_offset, last_char_offset)
797        }
798        // has first and no last
799        (Some((first_char_line, first_char_offset)), None) => {
800            (first_char_line, first_char_offset, input.len())
801        }
802        // others
803        _ => (current_line, input.len(), input.len()),
804    };
805
806    // adjust indices to be inside the returned window
807    start.line = first_char_line;
808    *start_ix = start_ix.saturating_sub(first_char_offset);
809    *end_ix = end_ix.saturating_sub(first_char_offset);
810
811    &input[first_char_offset..last_char_offset]
812}
813
814#[test]
815fn ok_construct_window() {
816    fn t(
817        start_line: usize,
818        start_col: usize,
819        end_line: usize,
820        end_col: usize,
821        start_char: usize,
822        end_char: usize,
823        input: &str,
824    ) -> (usize, usize, &str) {
825        let mut s = LineCol {
826            line: start_line,
827            col: start_col,
828        };
829        let mut start = start_char;
830        let mut end = end_char;
831        let r = construct_window(
832            &mut s,
833            LineCol {
834                line: end_line,
835                col: end_col,
836            },
837            &mut start,
838            &mut end,
839            input,
840        );
841        (start, end, r)
842    }
843
844    // Invalid Empty file
845    assert_eq!(t(0, 0, 0, 0, 0, 0, ""), (0, 0, ""));
846
847    // Valid Empty File
848    assert_eq!(t(1, 1, 1, 1, 0, 0, ""), (0, 0, ""));
849
850    // One line, error after the last char
851    assert_eq!(t(1, 7, 1, 7, 6, 6, "script"), (6, 6, "script"));
852
853    //                       01 23 45 67 89 AB CD E
854    let eight_lines = "1\n2\n3\n4\n5\n6\n7\n8";
855
856    assert_eq!(t(1, 1, 1, 1, 0, 1, eight_lines), (0, 1, "1\n2\n3\n"));
857    assert_eq!(t(2, 1, 2, 1, 2, 3, eight_lines), (2, 3, "1\n2\n3\n4\n"));
858    assert_eq!(t(3, 1, 3, 1, 4, 5, eight_lines), (4, 5, "1\n2\n3\n4\n5\n"));
859    assert_eq!(t(4, 1, 4, 1, 6, 7, eight_lines), (4, 5, "2\n3\n4\n5\n6\n"));
860    assert_eq!(t(5, 1, 5, 1, 8, 9, eight_lines), (4, 5, "3\n4\n5\n6\n7\n"));
861    assert_eq!(t(6, 1, 6, 1, 10, 11, eight_lines), (4, 5, "4\n5\n6\n7\n8"));
862    assert_eq!(t(7, 1, 7, 1, 12, 13, eight_lines), (4, 5, "5\n6\n7\n8"));
863    assert_eq!(t(8, 1, 8, 1, 14, 15, eight_lines), (4, 5, "6\n7\n8"));
864
865    // Invalid lines
866    assert_eq!(t(9, 1, 9, 1, 14, 15, eight_lines), (2, 3, "7\n8"));
867    assert_eq!(t(10, 1, 10, 1, 14, 15, eight_lines), (0, 1, "8"));
868    assert_eq!(t(11, 1, 11, 1, 14, 15, eight_lines), (0, 0, ""));
869}