use crate::report::ReportText::{BinOp, Concat, Module, Region, Value}; use roc_module::symbol::{Interns, ModuleId, Symbol}; use roc_problem::can::PrecedenceProblem::BothNonAssociative; use roc_problem::can::Problem; use roc_types::pretty_print::content_to_string; use roc_types::subs::{Content, Subs}; use roc_types::types::{write_error_type, ErrorType}; use std::path::PathBuf; use std::fmt; use ven_pretty::{BoxAllocator, DocAllocator, DocBuilder, Render, RenderAnnotated}; /// A textual report. pub struct Report { pub filename: PathBuf, pub text: ReportText, } pub struct Palette<'a> { pub primary: &'a str, pub code_block: &'a str, pub variable: &'a str, pub type_variable: &'a str, pub structure: &'a str, pub alias: &'a str, pub error: &'a str, pub line_number: &'a str, pub gutter_bar: &'a str, pub module_name: &'a str, pub binop: &'a str, } pub const TEST_PALETTE: Palette = Palette { primary: WHITE_CODE, code_block: WHITE_CODE, variable: BLUE_CODE, type_variable: YELLOW_CODE, structure: GREEN_CODE, alias: YELLOW_CODE, error: RED_CODE, line_number: CYAN_CODE, gutter_bar: MAGENTA_CODE, module_name: GREEN_CODE, binop: GREEN_CODE, }; pub fn can_problem(filename: PathBuf, problem: Problem) -> Report { let mut texts = Vec::new(); match problem { Problem::UnusedDef(symbol, region) => { texts.push(Value(symbol)); texts.push(plain_text(" is not used anywhere in your code.")); texts.push(Region(region)); texts.push(plain_text("If you didn't intend on using ")); texts.push(Value(symbol)); texts.push(plain_text( " then remove it so future readers of your code don't wonder why it is there.", )); } Problem::UnusedImport(module_id, region) => { texts.push(plain_text("Nothing from ")); texts.push(Module(module_id)); texts.push(plain_text(" is used in this module.")); texts.push(Region(region)); texts.push(plain_text("Since ")); texts.push(Module(module_id)); texts.push(plain_text(" isn't used, you don't need to import it.")); } Problem::UnusedArgument(closure_symbol, argument_symbol, region) => { texts.push(Value(closure_symbol)); texts.push(plain_text(" doesn't use ")); texts.push(Value(argument_symbol)); texts.push(plain_text(".")); texts.push(Region(region)); texts.push(plain_text("If you don't need ")); texts.push(Value(argument_symbol)); texts.push(plain_text( ", then you can just remove it. However, if you really do need ", )); texts.push(Value(argument_symbol)); texts.push(plain_text(" as an argument of ")); texts.push(Value(closure_symbol)); texts.push(plain_text(", prefix it with an underscore, like this: \"_")); texts.push(Value(argument_symbol)); texts.push(plain_text("\". Adding an underscore at the start of a variable name is a way of saying that the variable is not used.")); } Problem::PrecedenceProblem(BothNonAssociative(region, left_bin_op, right_bin_op)) => { if left_bin_op.value == right_bin_op.value { texts.push(plain_text("Using more than one ")); texts.push(BinOp(left_bin_op.value)); texts.push(plain_text( " like this requires parentheses, to clarify how things should be grouped.", )) } else { texts.push(plain_text("Using ")); texts.push(BinOp(left_bin_op.value)); texts.push(plain_text(" and ")); texts.push(BinOp(right_bin_op.value)); texts.push(plain_text( " together requires parentheses, to clarify how they should be grouped.", )) } texts.push(Region(region)); } Problem::UnsupportedPattern(_pattern_type, _region) => { panic!("TODO implement unsupported pattern report") } Problem::ShadowingInAnnotation { original_region, shadow, } => { // v-- just to satisfy clippy let _a = original_region; let _b = shadow; panic!("TODO implement shadow report"); } Problem::RuntimeError(_runtime_error) => { panic!("TODO implement run time error report"); } }; Report { filename, text: Concat(texts), } } #[derive(Debug, Clone)] pub enum ReportText { /// A value. Render it qualified unless it was defined in the current module. Value(Symbol), /// A module, Module(ModuleId), /// A type. Render it using roc_types::pretty_print for now, but maybe /// do something fancier later. Type(Content), ErrorType(ErrorType), /// Plain text Plain(Box), /// Emphasized text (might be bold, italics, a different color, etc) EmText(Box), /// A global tag rendered as code (e.g. a monospace font, or with backticks around it). GlobalTag(Box), /// A private tag rendered as code (e.g. a monospace font, or with backticks around it). PrivateTag(Symbol), /// A record field name rendered as code (e.g. a monospace font, or with backticks around it). RecordField(Box), /// A language keyword like `if`, rendered as code (e.g. a monospace font, or with backticks around it). Keyword(Box), /// A region in the original source Region(roc_region::all::Region), /// A URL, which should be rendered as a hyperlink. Url(Box), /// The documentation for this symbol. Docs(Symbol), BinOp(roc_parse::operator::BinOp), /// Many ReportText that should be concatenated together. Concat(Vec), /// Many ReportText that each get separate lines Stack(Vec), Indent(usize, Box), } pub fn plain_text(str: &str) -> ReportText { ReportText::Plain(Box::from(str)) } pub fn em_text(str: &str) -> ReportText { ReportText::EmText(Box::from(str)) } pub fn private_tag_text(symbol: Symbol) -> ReportText { ReportText::PrivateTag(symbol) } pub fn global_tag_text(str: &str) -> ReportText { ReportText::GlobalTag(Box::from(str)) } pub fn record_field_text(str: &str) -> ReportText { ReportText::RecordField(Box::from(str)) } pub fn keyword_text(str: &str) -> ReportText { ReportText::Keyword(Box::from(str)) } pub fn url(str: &str) -> ReportText { ReportText::Url(Box::from(str)) } pub const RED_CODE: &str = "\u{001b}[31m"; pub const WHITE_CODE: &str = "\u{001b}[37m"; pub const BLUE_CODE: &str = "\u{001b}[34m"; pub const YELLOW_CODE: &str = "\u{001b}[33m"; pub const GREEN_CODE: &str = "\u{001b}[42m"; pub const CYAN_CODE: &str = "\u{001b}[36m"; pub const MAGENTA_CODE: &str = "\u{001b}[35m"; pub const BOLD_CODE: &str = "\u{001b}[1m"; pub const UNDERLINE_CODE: &str = "\u{001b}[4m"; pub const RESET_CODE: &str = "\u{001b}[0m"; pub struct CiWrite { style_stack: Vec, upstream: W, } impl CiWrite { pub fn new(upstream: W) -> CiWrite { CiWrite { style_stack: vec![], upstream, } } } pub struct ColorWrite<'a, W> { style_stack: Vec, palette: &'a Palette<'a>, upstream: W, } impl<'a, W> ColorWrite<'a, W> { pub fn new(palette: &'a Palette, upstream: W) -> ColorWrite<'a, W> { ColorWrite { style_stack: vec![], palette, upstream, } } } #[derive(Copy, Clone)] pub enum Annotation { Emphasized, Url, Keyword, GlobalTag, PrivateTag, RecordField, TypeVariable, Alias, Structure, Symbol, BinOp, Error, GutterBar, LineNumber, PlainText, CodeBlock, Module, } impl Render for CiWrite where W: fmt::Write, { type Error = fmt::Error; fn write_str(&mut self, s: &str) -> Result { self.write_str_all(s).map(|_| s.len()) } fn write_str_all(&mut self, s: &str) -> fmt::Result { self.upstream.write_str(s) } } impl RenderAnnotated for CiWrite where W: fmt::Write, { fn push_annotation(&mut self, annotation: &Annotation) -> Result<(), Self::Error> { use Annotation::*; match annotation { Emphasized => { self.write_str("*")?; } Url => { self.write_str("<")?; } GlobalTag | PrivateTag | RecordField | Keyword => { self.write_str("`")?; } CodeBlock | PlainText | LineNumber | Error | GutterBar | TypeVariable | Alias | Module | Structure | Symbol | BinOp => {} } self.style_stack.push(*annotation); Ok(()) } fn pop_annotation(&mut self) -> Result<(), Self::Error> { use Annotation::*; match self.style_stack.pop() { None => {} Some(annotation) => match annotation { Emphasized => { self.write_str("*")?; } Url => { self.write_str(">")?; } GlobalTag | PrivateTag | RecordField | Keyword => { self.write_str("`")?; } CodeBlock | PlainText | LineNumber | Error | GutterBar | TypeVariable | Alias | Module | Structure | Symbol | BinOp => {} }, } Ok(()) } } impl<'a, W> Render for ColorWrite<'a, W> where W: fmt::Write, { type Error = fmt::Error; fn write_str(&mut self, s: &str) -> Result { self.write_str_all(s).map(|_| s.len()) } fn write_str_all(&mut self, s: &str) -> fmt::Result { self.upstream.write_str(s) } } impl<'a, W> RenderAnnotated for ColorWrite<'a, W> where W: fmt::Write, { fn push_annotation(&mut self, annotation: &Annotation) -> Result<(), Self::Error> { use Annotation::*; match annotation { Emphasized => { self.write_str(BOLD_CODE)?; } Url => { self.write_str(UNDERLINE_CODE)?; } PlainText => { self.write_str(self.palette.primary)?; } CodeBlock => { self.write_str(self.palette.code_block)?; } TypeVariable => { self.write_str(self.palette.type_variable)?; } Alias => { self.write_str(self.palette.alias)?; } BinOp => { self.write_str(self.palette.alias)?; } Symbol => { self.write_str(self.palette.variable)?; } GutterBar => { self.write_str(self.palette.gutter_bar)?; } Error => { self.write_str(self.palette.error)?; } LineNumber => { self.write_str(self.palette.line_number)?; } Structure => { self.write_str(self.palette.structure)?; } Module => { self.write_str(self.palette.module_name)?; } GlobalTag | PrivateTag | RecordField | Keyword => { self.write_str("`")?; } } self.style_stack.push(*annotation); Ok(()) } fn pop_annotation(&mut self) -> Result<(), Self::Error> { use Annotation::*; match self.style_stack.pop() { None => {} Some(annotation) => match annotation { Emphasized | Url | TypeVariable | Alias | Symbol | BinOp | Error | GutterBar | Structure | CodeBlock | PlainText | LineNumber | Module => { self.write_str(RESET_CODE)?; } GlobalTag | PrivateTag | RecordField | Keyword => { self.write_str("`")?; } }, } Ok(()) } } impl ReportText { /// Render to CI console output, where no colors are available. pub fn render_ci( self, buf: &mut String, subs: &mut Subs, home: ModuleId, src_lines: &[&str], interns: &Interns, ) { let alloc = BoxAllocator; let err_msg = ""; self.pretty::<_>(&alloc, subs, home, src_lines, interns) .1 .render_raw(70, &mut CiWrite::new(buf)) .expect(err_msg); } /// Render to a color terminal using ANSI escape sequences pub fn render_color_terminal( self, buf: &mut String, subs: &mut Subs, home: ModuleId, src_lines: &[&str], interns: &Interns, palette: &Palette, ) { let alloc = BoxAllocator; let err_msg = ""; self.pretty::<_>(&alloc, subs, home, src_lines, interns) .1 .render_raw(70, &mut ColorWrite::new(palette, buf)) .expect(err_msg); } /// General idea: this function puts all the characters in. Any styling (emphasis, colors, /// monospace font, etc) is done in the CiWrite and ColorWrite `RenderAnnotated` instances. pub fn pretty<'b, D>( self, alloc: &'b D, subs: &mut Subs, home: ModuleId, src_lines: &'b [&'b str], interns: &Interns, ) -> DocBuilder<'b, D, Annotation> where D: DocAllocator<'b, Annotation>, D::Doc: Clone, { use ReportText::*; match self { Plain(string) => alloc .text(format!("{}", string)) .annotate(Annotation::PlainText), EmText(string) => alloc .text(format!("{}", string)) .annotate(Annotation::Emphasized), Url(url) => alloc.text(format!("{}", url)).annotate(Annotation::Url), Keyword(string) => alloc .text(format!("{}", string)) .annotate(Annotation::Keyword), GlobalTag(string) => alloc .text(format!("{}", string)) .annotate(Annotation::GlobalTag), RecordField(string) => alloc .text(format!(".{}", string)) .annotate(Annotation::RecordField), PrivateTag(symbol) => { if symbol.module_id() == home { // Render it unqualified if it's in the current module. alloc .text(format!("{}", symbol.ident_string(interns))) .annotate(Annotation::PrivateTag) } else { alloc .text(format!( "{}.{}", symbol.module_string(interns), symbol.ident_string(interns), )) .annotate(Annotation::PrivateTag) } } Value(symbol) => { if symbol.module_id() == home { // Render it unqualified if it's in the current module. alloc .text(format!("{}", symbol.ident_string(interns))) .annotate(Annotation::Symbol) } else { alloc .text(format!( "{}.{}", symbol.module_string(interns), symbol.ident_string(interns), )) .annotate(Annotation::Symbol) } } Module(module_id) => alloc .text(format!("{}", interns.module_name(module_id))) .annotate(Annotation::Module), Type(content) => match content { Content::FlexVar(_) | Content::RigidVar(_) => alloc .text(content_to_string(content, subs, home, interns)) .annotate(Annotation::TypeVariable), Content::Structure(_) => alloc .text(content_to_string(content, subs, home, interns)) .annotate(Annotation::Structure), Content::Alias(_, _, _) => alloc .text(content_to_string(content, subs, home, interns)) .annotate(Annotation::Alias), Content::Error => alloc.text(content_to_string(content, subs, home, interns)), }, ErrorType(error_type) => alloc .nil() .append(alloc.hardline()) .append( alloc .text(write_error_type(home, interns, error_type)) .indent(4), ) .append(alloc.hardline()), Indent(n, nested) => { let rest = nested.pretty(alloc, subs, home, src_lines, interns); alloc.nil().append(rest).indent(n) } Docs(_) => { panic!("TODO implment docs"); } Concat(report_texts) => alloc.concat( report_texts .into_iter() .map(|rep| rep.pretty(alloc, subs, home, src_lines, interns)), ), Stack(report_texts) => alloc.intersperse( report_texts .into_iter() .map(|rep| (rep.pretty(alloc, subs, home, src_lines, interns))), alloc.hardline(), ), BinOp(bin_op) => alloc.text(bin_op.to_string()).annotate(Annotation::BinOp), Region(region) => { let max_line_number_length = (region.end_line + 1).to_string().len(); let indent = 2; let body = if region.start_line == region.end_line { let i = region.start_line; let line_number_string = (i + 1).to_string(); let line_number = line_number_string; let this_line_number_length = line_number.len(); let line = src_lines[i as usize]; let rest_of_line = if line.trim().is_empty() { alloc.nil() } else { alloc .nil() .append(alloc.text(line).indent(2)) .annotate(Annotation::CodeBlock) }; let source_line = alloc .line() .append( alloc .text(" ".repeat(max_line_number_length - this_line_number_length)), ) .append(alloc.text(line_number).annotate(Annotation::LineNumber)) .append(alloc.text(" ┆").annotate(Annotation::GutterBar)) .append(rest_of_line); let highlight_line = alloc .line() .append(alloc.text(" ".repeat(max_line_number_length))) .append(alloc.text(" ┆").annotate(Annotation::GutterBar)) .append( alloc .text(" ".repeat(region.start_col as usize)) .indent(indent), ) .append( alloc .text("^".repeat((region.end_col - region.start_col) as usize)) .annotate(Annotation::Error), ); source_line.append(highlight_line) } else { let mut result = alloc.nil(); for i in region.start_line..=region.end_line { let line_number_string = (i + 1).to_string(); let line_number = line_number_string; let this_line_number_length = line_number.len(); let line = src_lines[i as usize]; let rest_of_line = if !line.trim().is_empty() { alloc .text(line) .annotate(Annotation::CodeBlock) .indent(indent) } else { alloc.nil() }; let source_line = alloc .line() .append(alloc.text( " ".repeat(max_line_number_length - this_line_number_length), )) .append(alloc.text(line_number).annotate(Annotation::LineNumber)) .append(alloc.text(" ┆").annotate(Annotation::GutterBar)) .append(alloc.text(">").annotate(Annotation::Error)) .append(rest_of_line); result = result.append(source_line); } result }; alloc .nil() .append(alloc.line()) .append(body) .append(alloc.line()) .append(alloc.line()) } } } }