diff --git a/spec/errors/css_definition_expected_semicolon_2 b/spec/errors/css_definition_expected_semicolon_2 new file mode 100644 index 00000000..08f05bd3 --- /dev/null +++ b/spec/errors/css_definition_expected_semicolon_2 @@ -0,0 +1,18 @@ +component Main { + style root { + background: red + } +} +-------------------------------------------------------------------------------- +░ ERROR (CSS_DEFINITION_EXPECTED_SEMICOLON) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +I was expecting the semicolon of a CSS definition but I found "a space" instead: + + ┌ errors/css_definition_expected_semicolon_2:3:19 + ├──────────────────────────────────────────────── + 1│ component Main { + 2│ style root { + 3│ background: red + │ ⌃⌃⌃⌃ + 4│ } + 5│ } diff --git a/src/errorable.cr b/src/errorable.cr index fc36ef3f..35d6886e 100644 --- a/src/errorable.cr +++ b/src/errorable.cr @@ -23,7 +23,8 @@ module Mint class Error < Exception # Anything that can be a snippet. alias SnippetTarget = TypeChecker::Checkable | SnippetData | - Ast::Node | Parser | String + Ast::Node | Parser | String | + Tuple(Parser, Parser::Location) alias Element = Text | Bold | Code @@ -80,6 +81,18 @@ module Mint def snippet(value : SnippetTarget) target = case value + in Tuple(Parser, Parser::Location) + parser, position = + value + + min = + parser.input[position.offset]? == '\0' ? 0 : 1 + + SnippetData.new( + to: position.offset + [min, parser.word(position).to_s.size].max, + filename: parser.file.relative_path, + input: parser.file.contents, + from: position.offset) in Parser min = value.char == '\0' ? 0 : 1 diff --git a/src/parser.cr b/src/parser.cr index bb3a78fe..29506965 100644 --- a/src/parser.cr +++ b/src/parser.cr @@ -181,6 +181,18 @@ module Mint word end + # Returns the word (non whitespace sequence) a the position. + def word(position : Location) + current = position.offset + word = "" + + while (char = input[current]?) && !char.ascii_whitespace? + word += char + end + + word + end + # Returns whether or not the word is at the current position. def word?(word) : Bool word.chars.each_with_index.all? do |char, i| @@ -411,5 +423,31 @@ module Mint end end end + + # Returns the last non whitespace position. + def last_non_whitespace_position + position, line, column = + @position.to_tuple + + while input[position - 1]?.try(&.ascii_whitespace?) + case previous_char + when '\n' + current = position - 2 + column = 0_i64 + line -= 1 + + while input[current]? && (input[current] != '\n') + current -= 1 + column += 1 + end + else + column -= 1 + end + + position -= 1 + end + + Location.new(offset: position, line: line, column: column) + end end end diff --git a/src/parsers/css_definition.cr b/src/parsers/css_definition.cr index ed7d60d7..bde9c620 100644 --- a/src/parsers/css_definition.cr +++ b/src/parsers/css_definition.cr @@ -9,7 +9,7 @@ module Mint value = many(parse_whitespace: false) { - string_literal || interpolation || raw { char.in_set?("^;{\0\"") } + string_literal || interpolation || raw { char.in_set?("^;}{\0\"") } }.map do |item| if item.is_a?(Ast::StringLiteral) && (raw = static_value(item)) %("#{raw}") @@ -19,8 +19,11 @@ module Mint end next error :css_definition_expected_semicolon do - expected "the semicolon of a CSS definition", word - snippet self + position = + last_non_whitespace_position + + expected "the semicolon of a CSS definition", word(position) + snippet({self, position}) end unless char! ';' Ast::CssDefinition.new( diff --git a/src/utils/terminal_snippet.cr b/src/utils/terminal_snippet.cr index 499c2973..71e64972 100644 --- a/src/utils/terminal_snippet.cr +++ b/src/utils/terminal_snippet.cr @@ -165,7 +165,8 @@ module Mint line.highlight(from, to) "#{gutter} #{a}\n#{" " * gutter_width}│ #{b}" - elsif from == input.size && line.offset + line.contents.size == from + elsif (from == input.size && line.offset + line.contents.size == from) || + (line.offset + line.contents.size == (to - 1)) "#{gutter} #{line.contents}\n#{" " * gutter_width}│ #{" " * line.contents.size}⌃⌃⌃⌃" end || "#{gutter} #{line.contents}" end