From db5ec613fc534dd67a59e6f0923c53681fb9432f Mon Sep 17 00:00:00 2001 From: tompng Date: Fri, 1 Nov 2024 20:03:32 +0900 Subject: [PATCH] Colorize using Prism --- irb.gemspec | 1 + lib/irb/color.rb | 357 ++++++++++++++++++++++------------------- lib/irb/context.rb | 2 +- test/irb/test_color.rb | 55 ++----- 4 files changed, 208 insertions(+), 207 deletions(-) diff --git a/irb.gemspec b/irb.gemspec index b29002f59..a024210e7 100644 --- a/irb.gemspec +++ b/irb.gemspec @@ -41,6 +41,7 @@ Gem::Specification.new do |spec| spec.required_ruby_version = Gem::Requirement.new(">= 2.7") + spec.add_dependency "prism", ">= 1.0" spec.add_dependency "reline", ">= 0.4.2" spec.add_dependency "rdoc", ">= 4.0.0" end diff --git a/lib/irb/color.rb b/lib/irb/color.rb index fca942b28..872e6b6af 100644 --- a/lib/irb/color.rb +++ b/lib/irb/color.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true require 'reline' -require 'ripper' +require 'prism' require_relative 'ruby-lex' module IRB # :nodoc: @@ -18,64 +18,96 @@ module Color CYAN = 36 WHITE = 37 - TOKEN_KEYWORDS = { - on_kw: ['nil', 'self', 'true', 'false', '__FILE__', '__LINE__', '__ENCODING__'], - on_const: ['ENV'], - } - private_constant :TOKEN_KEYWORDS - - # A constant of all-bit 1 to match any Ripper's state in #dispatch_seq - ALL = -1 - private_constant :ALL - - begin - # Following pry's colors where possible, but sometimes having a compromise like making - # backtick and regexp as red (string's color, because they're sharing tokens). - TOKEN_SEQ_EXPRS = { - on_CHAR: [[BLUE, BOLD], ALL], - on_backtick: [[RED, BOLD], ALL], - on_comment: [[BLUE, BOLD], ALL], - on_const: [[BLUE, BOLD, UNDERLINE], ALL], - on_embexpr_beg: [[RED], ALL], - on_embexpr_end: [[RED], ALL], - on_embvar: [[RED], ALL], - on_float: [[MAGENTA, BOLD], ALL], - on_gvar: [[GREEN, BOLD], ALL], - on_heredoc_beg: [[RED], ALL], - on_heredoc_end: [[RED], ALL], - on_ident: [[BLUE, BOLD], Ripper::EXPR_ENDFN], - on_imaginary: [[BLUE, BOLD], ALL], - on_int: [[BLUE, BOLD], ALL], - on_kw: [[GREEN], ALL], - on_label: [[MAGENTA], ALL], - on_label_end: [[RED, BOLD], ALL], - on_qsymbols_beg: [[RED, BOLD], ALL], - on_qwords_beg: [[RED, BOLD], ALL], - on_rational: [[BLUE, BOLD], ALL], - on_regexp_beg: [[RED, BOLD], ALL], - on_regexp_end: [[RED, BOLD], ALL], - on_symbeg: [[YELLOW], ALL], - on_symbols_beg: [[RED, BOLD], ALL], - on_tstring_beg: [[RED, BOLD], ALL], - on_tstring_content: [[RED], ALL], - on_tstring_end: [[RED, BOLD], ALL], - on_words_beg: [[RED, BOLD], ALL], - on_parse_error: [[RED, REVERSE], ALL], - compile_error: [[RED, REVERSE], ALL], - on_assign_error: [[RED, REVERSE], ALL], - on_alias_error: [[RED, REVERSE], ALL], - on_class_name_error:[[RED, REVERSE], ALL], - on_param_error: [[RED, REVERSE], ALL], - on___end__: [[GREEN], ALL], - } - rescue NameError - # Give up highlighting Ripper-incompatible older Ruby - TOKEN_SEQ_EXPRS = {} + # Following pry's colors where possible + TOKEN_SEQS = { + KEYWORD_NIL: [CYAN, BOLD], + KEYWORD_SELF: [CYAN, BOLD], + KEYWORD_TRUE: [CYAN, BOLD], + KEYWORD_FALSE: [CYAN, BOLD], + KEYWORD___FILE__: [CYAN, BOLD], + KEYWORD___LINE__: [CYAN, BOLD], + KEYWORD___ENCODING__: [CYAN, BOLD], + CHARACTER_LITERAL: [BLUE, BOLD], + BACKTICK: [RED, BOLD], + COMMENT: [BLUE, BOLD], + EMBDOC_BEGIN: [BLUE, BOLD], + EMBDOC_LINE: [BLUE, BOLD], + EMBDOC_END: [BLUE, BOLD], + CONSTANT: [BLUE, BOLD, UNDERLINE], + EMBEXPR_BEGIN: [RED], + EMBEXPR_END: [RED], + EMBVAR: [RED], + FLOAT: [MAGENTA, BOLD], + GLOBAL_VARIABLE: [GREEN, BOLD], + HEREDOC_START: [RED], + HEREDOC_END: [RED], + FLOAT_IMAGINARY: [BLUE, BOLD], + INTEGER_IMAGINARY: [BLUE, BOLD], + FLOAT_RATIONAL_IMAGINARY: [BLUE, BOLD], + INTEGER_RATIONAL_IMAGINARY: [BLUE, BOLD], + INTEGER: [BLUE, BOLD], + INTEGER_RATIONAL: [BLUE, BOLD], + FLOAT_RATIONAL: [BLUE, BOLD], + KEYWORD_END: [GREEN], + KEYWORD_CLASS: [GREEN], + KEYWORD_MODULE: [GREEN], + KEYWORD_IF: [GREEN], + KEYWORD_IF_MODIFIER: [GREEN], + KEYWORD_UNLESS_MODIFIER: [GREEN], + KEYWORD_WHILE_MODIFIER: [GREEN], + KEYWORD_UNTIL_MODIFIER: [GREEN], + KEYWORD_THEN: [GREEN], + KEYWORD_UNLESS: [GREEN], + KEYWORD_ELSE: [GREEN], + KEYWORD_WHILE: [GREEN], + KEYWORD_UNTIL: [GREEN], + KEYWORD_CASE: [GREEN], + KEYWORD_WHEN: [GREEN], + KEYWORD_IN: [GREEN], + KEYWORD_DEF: [GREEN], + KEYWORD_DO: [GREEN], + KEYWORD_DO_LOOP: [GREEN], + KEYWORD_FOR: [GREEN], + KEYWORD_BEGIN: [GREEN], + KEYWORD_RESCUE: [GREEN], + KEYWORD_ENSURE: [GREEN], + KEYWORD_ALIAS: [GREEN], + KEYWORD_UNDEF: [GREEN], + KEYWORD_BEGIN_UPCASE: [GREEN], + KEYWORD_END_UPCASE: [GREEN], + KEYWORD_YIELD: [GREEN], + KEYWORD_REDO: [GREEN], + KEYWORD_RETRY: [GREEN], + KEYWORD_NEXT: [GREEN], + KEYWORD_BREAK: [GREEN], + KEYWORD_SUPER: [GREEN], + KEYWORD_RETURN: [GREEN], + KEYWORD_DEFINED: [GREEN], + KEYWORD_NOT: [GREEN], + KEYWORD_AND: [GREEN], + KEYWORD_OR: [GREEN], + LABEL: [MAGENTA], + LABEL_END: [RED, BOLD], + PERCENT_UPPER_W: [RED, BOLD], + PERCENT_LOWER_W: [RED, BOLD], + PERCENT_LOWER_X: [RED, BOLD], + REGEXP_BEGIN: [RED, BOLD], + REGEXP_END: [RED, BOLD], + STRING_BEGIN: [RED, BOLD], + STRING_CONTENT: [RED], + STRING_END: [RED, BOLD], + __END__: [GREEN], + # tokens from syntax tree traversal + method_name: [BLUE, BOLD], + symbol: [YELLOW], + # special colorization + error: [RED, REVERSE], + const_env: [CYAN, BOLD], + }.transform_values do |styles| + styles.map { |style| "\e[#{style}m" }.join end - private_constant :TOKEN_SEQ_EXPRS - - ERROR_TOKENS = TOKEN_SEQ_EXPRS.keys.select { |k| k.to_s.end_with?('error') } - private_constant :ERROR_TOKENS + CLEAR_SEQ = "\e[#{CLEAR}m" + private_constant :TOKEN_SEQS, :CLEAR_SEQ class << self def colorable? @@ -112,14 +144,13 @@ def inspect_colorable?(obj, seen: {}.compare_by_identity) end def clear(colorable: colorable?) - return '' unless colorable - "\e[#{CLEAR}m" + colorable ? CLEAR_SEQ : '' end def colorize(text, seq, colorable: colorable?) return text unless colorable seq = seq.map { |s| "\e[#{const_get(s)}m" }.join('') - "#{seq}#{text}#{clear(colorable: colorable)}" + "#{seq}#{text}#{CLEAR_SEQ}" end # If `complete` is false (code is incomplete), this does not warn compile_error. @@ -128,135 +159,129 @@ def colorize(text, seq, colorable: colorable?) def colorize_code(code, complete: true, ignore_error: false, colorable: colorable?, local_variables: []) return code unless colorable - symbol_state = SymbolState.new - colored = +'' - lvars_code = RubyLex.generate_local_variables_assign_code(local_variables) - code_with_lvars = lvars_code ? "#{lvars_code}\n#{code}" : code + result = Prism.parse_lex(code, scopes: [local_variables]) - scan(code_with_lvars, allow_last_error: !complete) do |token, str, expr| - # handle uncolorable code - if token.nil? - colored << Reline::Unicode.escape_for_print(str) - next - end + # IRB::ColorPrinter skips colorizing syntax invalid fragments + return Reline::Unicode.escape_for_print(code) if ignore_error && !result.success? + + errors = result.errors + unless complete + errors = errors.reject { |error| error.message =~ /\Aexpected a|unexpected end-of-input|unterminated/ } + end + + prism_node, prism_tokens = result.value + visitor = ColorizeVisitor.new + prism_node.accept(visitor) + + error_tokens = errors.map { |e| [e.location.start_line, e.location.start_column, 0, e.location.end_line, e.location.end_column, :error, e.location.slice] } + tokens = prism_tokens.map { |t,| [t.location.start_line, t.location.start_column, 2, t.location.end_line, t.location.end_column, t.type, t.value] } + tokens.pop if tokens.last&.[](5) == :EOF - # IRB::ColorPrinter skips colorizing fragments with any invalid token - if ignore_error && ERROR_TOKENS.include?(token) - return Reline::Unicode.escape_for_print(code) + colored = +'' + line_index = 0 + col = 0 + lines = code.lines + flush = -> next_line_index, next_col { + return if next_line_index == line_index && next_col == col + (line_index...[next_line_index, lines.size].min).each do |ln| + colored << Reline::Unicode.escape_for_print(lines[line_index].byteslice(col..)) + line_index = ln + 1 + col = 0 + end + unless col == next_col + colored << Reline::Unicode.escape_for_print(lines[next_line_index].byteslice(col..next_col - 1)) end + } + + (visitor.tokens + tokens + error_tokens).sort.each do |start_line, start_column, _priority, end_line, end_column, type, value| + next if start_line - 1 < line_index || (start_line - 1 == line_index && start_column < col) - in_symbol = symbol_state.scan_token(token) - str.each_line do |line| - line = Reline::Unicode.escape_for_print(line) - if seq = dispatch_seq(token, expr, line, in_symbol: in_symbol) - colored << seq.map { |s| "\e[#{s}m" }.join('') - colored << line.sub(/\Z/, clear(colorable: colorable)) - else - colored << line + flush.call(start_line - 1, start_column) + if type == :CONSTANT && value == 'ENV' + color = TOKEN_SEQS[:const_env] + elsif type == :__END__ + color = TOKEN_SEQS[type] + end_line = start_line + value = '__END__' + end_column = start_column + 7 + else + color = TOKEN_SEQS[type] + end + if color + value.split(/(\n)/).each do |s| + colored << (s == "\n" ? s : "#{color}#{Reline::Unicode.escape_for_print(s)}#{CLEAR_SEQ}") end + else + colored << value end + line_index = end_line - 1 + col = end_column end - - if lvars_code - raise "#{lvars_code.dump} should have no \\n" if lvars_code.include?("\n") - colored.sub!(/\A.+\n/, '') # delete_prefix lvars_code with colors - end + flush.call lines.size, 0 colored end - private - - def without_circular_ref(obj, seen:, &block) - return false if seen.key?(obj) - seen[obj] = true - block.call - ensure - seen.delete(obj) - end + class ColorizeVisitor < Prism::Visitor + attr_reader :tokens + def initialize + @tokens = [] + end - def scan(code, allow_last_error:) - verbose, $VERBOSE = $VERBOSE, nil - RubyLex.compile_with_errors_suppressed(code) do |inner_code, line_no| - lexer = Ripper::Lexer.new(inner_code, '(ripper)', line_no) - byte_pos = 0 - line_positions = [0] - inner_code.lines.each do |line| - line_positions << line_positions.last + line.bytesize + def dispatch(location, type) + if location + @tokens << [location.start_line, location.start_column, 1, location.end_line, location.end_column, type, location.slice] end + end - on_scan = proc do |elem| - start_pos = line_positions[elem.pos[0] - 1] + elem.pos[1] + def visit_array_node(node) + if node.opening&.match?(/\A%[iI]/) + dispatch node.opening_loc, :symbol + dispatch node.closing_loc, :symbol + end + super + end - # yield uncolorable code - if byte_pos < start_pos - yield(nil, inner_code.byteslice(byte_pos...start_pos), nil) - end + def visit_def_node(node) + dispatch node.name_loc, :method_name + super + end - if byte_pos <= start_pos - str = elem.tok - yield(elem.event, str, elem.state) - byte_pos = start_pos + str.bytesize + def visit_interpolated_symbol_node(node) + dispatch node.opening_loc, :symbol + node.parts.each do |part| + case part + when Prism::StringNode + dispatch part.content_loc, :symbol + when Prism::EmbeddedStatementsNode + dispatch part.opening_loc, :symbol + dispatch part.closing_loc, :symbol end end + dispatch node.closing_loc, :symbol + super + end - lexer.scan.each do |elem| - next if allow_last_error and /meets end of file|unexpected end-of-input/ =~ elem.message - on_scan.call(elem) + def visit_symbol_node(node) + if (node.opening_loc.nil? && node.closing == ':') || node.closing&.match?(/\A['"]:\z/) + # Colorize { symbol: 1 } and { 'symbol': 1 } as label + dispatch node.location, :LABEL + else + dispatch node.opening_loc, :symbol + dispatch node.value_loc, :symbol + dispatch node.closing_loc, :symbol end - # yield uncolorable DATA section - yield(nil, inner_code.byteslice(byte_pos...inner_code.bytesize), nil) if byte_pos < inner_code.bytesize end - ensure - $VERBOSE = verbose end - def dispatch_seq(token, expr, str, in_symbol:) - if ERROR_TOKENS.include?(token) - TOKEN_SEQ_EXPRS[token][0] - elsif in_symbol - [YELLOW] - elsif TOKEN_KEYWORDS.fetch(token, []).include?(str) - [CYAN, BOLD] - elsif (seq, exprs = TOKEN_SEQ_EXPRS[token]; (expr & (exprs || 0)) != 0) - seq - else - nil - end - end - end - - # A class to manage a state to know whether the current token is for Symbol or not. - class SymbolState - def initialize - # Push `true` to detect Symbol. `false` to increase the nest level for non-Symbol. - @stack = [] - end + private - # Return true if the token is a part of Symbol. - def scan_token(token) - prev_state = @stack.last - case token - when :on_symbeg, :on_symbols_beg, :on_qsymbols_beg - @stack << true - when :on_ident, :on_op, :on_const, :on_ivar, :on_cvar, :on_gvar, :on_kw, :on_backtick - if @stack.last # Pop only when it's Symbol - @stack.pop - return prev_state - end - when :on_tstring_beg - @stack << false - when :on_embexpr_beg - @stack << false - return prev_state - when :on_tstring_end # :on_tstring_end may close Symbol - @stack.pop - return prev_state - when :on_embexpr_end - @stack.pop - end - @stack.last + def without_circular_ref(obj, seen:, &block) + return false if seen.key?(obj) + seen[obj] = true + block.call + ensure + seen.delete(obj) end end - private_constant :SymbolState end end diff --git a/lib/irb/context.rb b/lib/irb/context.rb index 505bed80a..e1d363ec4 100644 --- a/lib/irb/context.rb +++ b/lib/irb/context.rb @@ -660,7 +660,7 @@ def colorize_input(input, complete:) lvars = local_variables || [] if parse_command(input) name, sep, arg = input.split(/(\s+)/, 2) - arg = IRB::Color.colorize_code(arg, complete: complete, local_variables: lvars) + arg = IRB::Color.colorize_code(arg, complete: complete, local_variables: lvars) if arg "#{IRB::Color.colorize(name, [:BOLD])}\e[m#{sep}#{arg}" else IRB::Color.colorize_code(input, complete: complete, local_variables: lvars) diff --git a/test/irb/test_color.rb b/test/irb/test_color.rb index 9d78f5233..0b7fa83a7 100644 --- a/test/irb/test_color.rb +++ b/test/irb/test_color.rb @@ -77,9 +77,9 @@ def test_colorize_code "%w[a b]" => "#{RED}#{BOLD}%w[#{CLEAR}#{RED}a#{CLEAR} #{RED}b#{CLEAR}#{RED}#{BOLD}]#{CLEAR}", "%W[a b]" => "#{RED}#{BOLD}%W[#{CLEAR}#{RED}a#{CLEAR} #{RED}b#{CLEAR}#{RED}#{BOLD}]#{CLEAR}", "%s[a b]" => "#{YELLOW}%s[#{CLEAR}#{YELLOW}a b#{CLEAR}#{YELLOW}]#{CLEAR}", - "%i[c d]" => "#{YELLOW}%i[#{CLEAR}#{YELLOW}c#{CLEAR}#{YELLOW} #{CLEAR}#{YELLOW}d#{CLEAR}#{YELLOW}]#{CLEAR}", - "%I[c d]" => "#{YELLOW}%I[#{CLEAR}#{YELLOW}c#{CLEAR}#{YELLOW} #{CLEAR}#{YELLOW}d#{CLEAR}#{YELLOW}]#{CLEAR}", - "{'a': 1}" => "{#{RED}#{BOLD}'#{CLEAR}#{RED}a#{CLEAR}#{RED}#{BOLD}':#{CLEAR} #{BLUE}#{BOLD}1#{CLEAR}}", + "%i[c d]" => "#{YELLOW}%i[#{CLEAR}#{YELLOW}c#{CLEAR} #{YELLOW}d#{CLEAR}#{YELLOW}]#{CLEAR}", + "%I[c d]" => "#{YELLOW}%I[#{CLEAR}#{YELLOW}c#{CLEAR} #{YELLOW}d#{CLEAR}#{YELLOW}]#{CLEAR}", + "{'a': 1}" => "{#{MAGENTA}'a':#{CLEAR} #{BLUE}#{BOLD}1#{CLEAR}}", ":Struct" => "#{YELLOW}:#{CLEAR}#{YELLOW}Struct#{CLEAR}", '"#{}"' => "#{RED}#{BOLD}\"#{CLEAR}#{RED}\#{#{CLEAR}#{RED}}#{CLEAR}#{RED}#{BOLD}\"#{CLEAR}", ':"a#{}b"' => "#{YELLOW}:\"#{CLEAR}#{YELLOW}a#{CLEAR}#{YELLOW}\#{#{CLEAR}#{YELLOW}}#{CLEAR}#{YELLOW}b#{CLEAR}#{YELLOW}\"#{CLEAR}", @@ -106,41 +106,16 @@ def test_colorize_code "foo\n__END__\nbar" => "foo\n#{GREEN}__END__#{CLEAR}\nbar", "foo\n< "foo\n#{RED}< "#{RED}< "#{MAGENTA}#{BOLD}4.5#{CLEAR}#{RED}#{REVERSE}.6#{CLEAR}", - "\e[0m\n" => "#{RED}#{REVERSE}^[#{CLEAR}[#{BLUE}#{BOLD}0#{CLEAR}#{RED}#{REVERSE}m#{CLEAR}\n", + "\e[0m\n" => "#{RED}#{REVERSE}^[#{CLEAR}[#{BLUE}#{BOLD}0#{CLEAR}m\n", "< "#{RED}<= Gem::Version.new('3.0.0') - tests.merge!({ - "[1]]]\u0013" => "[#{BLUE}#{BOLD}1#{CLEAR}]#{RED}#{REVERSE}]#{CLEAR}#{RED}#{REVERSE}]#{CLEAR}#{RED}#{REVERSE}^S#{CLEAR}", - }) - tests.merge!({ - "def req(true) end" => "#{GREEN}def#{CLEAR} #{BLUE}#{BOLD}req#{CLEAR}(#{RED}#{REVERSE}true#{CLEAR}) #{RED}#{REVERSE}end#{CLEAR}", - "nil = 1" => "#{RED}#{REVERSE}nil#{CLEAR} = #{BLUE}#{BOLD}1#{CLEAR}", - "alias $x $1" => "#{GREEN}alias#{CLEAR} #{GREEN}#{BOLD}$x#{CLEAR} #{RED}#{REVERSE}$1#{CLEAR}", - "class bad; end" => "#{GREEN}class#{CLEAR} #{RED}#{REVERSE}bad#{CLEAR}; #{GREEN}end#{CLEAR}", - "def req(@a) end" => "#{GREEN}def#{CLEAR} #{BLUE}#{BOLD}req#{CLEAR}(#{RED}#{REVERSE}@a#{CLEAR}) #{GREEN}end#{CLEAR}", - }) - if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.2.0') - tests.merge!({ - "def req(true) end" => "#{GREEN}def#{CLEAR} #{BLUE}#{BOLD}req#{CLEAR}(#{RED}#{REVERSE}true#{CLEAR}#{RED}#{REVERSE})#{CLEAR} #{RED}#{REVERSE}end#{CLEAR}", - }) - end - else - tests.merge!({ - "[1]]]\u0013" => "[#{BLUE}#{BOLD}1#{CLEAR}]#{RED}#{REVERSE}]#{CLEAR}]^S", - "def req(true) end" => "#{GREEN}def#{CLEAR} #{BLUE}#{BOLD}req#{CLEAR}(#{RED}#{REVERSE}true#{CLEAR}) end", - "nil = 1" => "#{CYAN}#{BOLD}nil#{CLEAR} = #{BLUE}#{BOLD}1#{CLEAR}", - "alias $x $1" => "#{GREEN}alias#{CLEAR} #{GREEN}#{BOLD}$x#{CLEAR} $1", - "class bad; end" => "#{GREEN}class#{CLEAR} bad; #{GREEN}end#{CLEAR}", - "def req(@a) end" => "#{GREEN}def#{CLEAR} #{BLUE}#{BOLD}req#{CLEAR}(@a) #{GREEN}end#{CLEAR}", - }) - end + "[1]]]\u0013" => "[#{BLUE}#{BOLD}1#{CLEAR}]#{RED}#{REVERSE}]#{CLEAR}#{RED}#{REVERSE}]#{CLEAR}#{RED}#{REVERSE}^S#{CLEAR}", + "def req(true) end" => "#{GREEN}def#{CLEAR} #{BLUE}#{BOLD}req#{CLEAR}(#{RED}#{REVERSE}true#{CLEAR}#{RED}#{REVERSE})#{CLEAR} #{GREEN}end#{CLEAR}", + "nil = 1" => "#{CYAN}#{BOLD}nil#{CLEAR} #{RED}#{REVERSE}=#{CLEAR} #{BLUE}#{BOLD}1#{CLEAR}", + "alias $x $1" => "#{GREEN}alias#{CLEAR} #{GREEN}#{BOLD}$x#{CLEAR} #{RED}#{REVERSE}$1#{CLEAR}", + "class bad; end" => "#{GREEN}class#{CLEAR} #{RED}#{REVERSE}bad#{CLEAR}; #{GREEN}end#{CLEAR}", + "def req(@a) end" => "#{GREEN}def#{CLEAR} #{BLUE}#{BOLD}req#{CLEAR}(#{RED}#{REVERSE}@a#{CLEAR}) #{GREEN}end#{CLEAR}", + } tests.each do |code, result| assert_equal_with_term(result, code, complete: true) @@ -166,15 +141,15 @@ def test_colorize_code_with_local_variables result_with_lvars = "a /(b +#{BLUE}#{BOLD}1#{CLEAR})/i" assert_equal_with_term(result_without_lvars, code) - assert_equal_with_term(result_with_lvar, code, local_variables: ['a']) - assert_equal_with_term(result_with_lvars, code, local_variables: ['a', 'b']) + assert_equal_with_term(result_with_lvar, code, local_variables: [:a]) + assert_equal_with_term(result_with_lvars, code, local_variables: [:a, :b]) end def test_colorize_code_complete_true # `complete: true` behaviors. Warn end-of-file. { - "'foo' + 'bar" => "#{RED}#{BOLD}'#{CLEAR}#{RED}foo#{CLEAR}#{RED}#{BOLD}'#{CLEAR} + #{RED}#{BOLD}'#{CLEAR}#{RED}#{REVERSE}bar#{CLEAR}", - "('foo" => "(#{RED}#{BOLD}'#{CLEAR}#{RED}#{REVERSE}foo#{CLEAR}", + "'foo' + 'bar" => "#{RED}#{BOLD}'#{CLEAR}#{RED}foo#{CLEAR}#{RED}#{BOLD}'#{CLEAR} + #{RED}#{REVERSE}'#{CLEAR}#{RED}bar#{CLEAR}", + "('foo" => "(#{RED}#{REVERSE}'#{CLEAR}#{RED}foo#{CLEAR}", }.each do |code, result| assert_equal_with_term(result, code, complete: true)