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