diff --git a/lib/textbringer/modes/ruby_mode.rb b/lib/textbringer/modes/ruby_mode.rb index 24d8c78..472817b 100644 --- a/lib/textbringer/modes/ruby_mode.rb +++ b/lib/textbringer/modes/ruby_mode.rb @@ -1,5 +1,6 @@ require "set" require "prism" +require_relative "ruby_nesting_parser" module Textbringer CONFIG[:ruby_indent_level] = 2 @@ -22,9 +23,11 @@ def initialize(buffer) @prism_version = nil @prism_tokens = nil @prism_ast = nil + @prism_parse_lex_result = nil @prism_method_name_locs = nil @literal_levels = nil @literal_levels_version = nil + @nesting_by_line = nil end def forward_definition(n = number_prefix_arg || 1) @@ -259,182 +262,272 @@ def highlight(ctx) OPERATORS = %i(!= !~ =~ == === <=> > >= < <= & | ^ >> << - + % / * ** -@ +@ ~ ! [] []=) - BLOCK_END = { - EMBEXPR_BEGIN: :EMBEXPR_END, - BRACE_LEFT: :BRACE_RIGHT, - PARENTHESIS_LEFT: :PARENTHESIS_RIGHT, - BRACKET_LEFT: :BRACKET_RIGHT, - BRACKET_LEFT_ARRAY: :BRACKET_RIGHT, - LAMBDA_BEGIN: :BRACE_RIGHT, - } - - INDENT_BEG_RE = /^([ \t]*)(class|module|def|if|unless|case|while|until|for|begin)\b/ + FREE_INDENT_EVENTS = %i[on_tstring_beg on_backtick on_regexp_beg on_symbeg].to_set def space_width(s) s.gsub(/\t/, " " * @buffer[:tab_width]).size end - def beginning_of_indentation - loop do - @buffer.re_search_backward(INDENT_BEG_RE) - space = @buffer.match_string(1) - if in_literal?(@buffer.point) - next - end - return space_width(space) + def calculate_indentation + return 0 if @buffer.current_line == 1 + ensure_nesting_by_line + return 0 if @nesting_by_line.nil? || @nesting_by_line.empty? + + target_line = @buffer.current_line + line_index = target_line - 1 + + if line_index >= @nesting_by_line.size + # After the last parsed line - use last line's next_opens + _, last_next_opens, _ = @nesting_by_line.last + prev_opens = next_opens = last_next_opens + min_depth = prev_opens.size + else + prev_opens, next_opens, min_depth = @nesting_by_line[line_index] end - rescue SearchError - @buffer.beginning_of_buffer - 0 - end - def lex(source) - Prism.lex(source).value.filter_map { |token, _state| - type = token.type - next if type == :EOF - loc = token.location - [[loc.start_line, loc.start_column], type, token.value] - } - end + prev_open_elem = prev_opens&.last - def calculate_indentation - if @buffer.current_line == 1 - return 0 + # Inside string/regexp/heredoc → no auto-indent + if FREE_INDENT_EVENTS.include?(prev_open_elem&.event) + return nil end - @buffer.save_excursion do - @buffer.beginning_of_line - start_with_period = @buffer.looking_at?(/[ \t]*\./) - bol_pos = @buffer.point - base_indentation = beginning_of_indentation - start_pos = @buffer.point - start_line = @buffer.current_line - tokens = lex(@buffer.substring(start_pos, bol_pos)) - _, event, text = tokens.last - if event == :NEWLINE || event == :IGNORED_NEWLINE - _, event, text = tokens[-2] + if prev_open_elem&.event == :on_heredoc_beg + return nil + end + + # Parenthesis alignment: foo(123,\n 456) + if prev_open_elem&.event == :on_lparen + paren_line = prev_open_elem.pos[0] + paren_col = prev_open_elem.pos[1] + if paren_has_args_on_same_line?(paren_line, paren_col) + return paren_col + 1 end - if event == :STRING_BEGIN || - event == :HEREDOC_START || - (event == :HEREDOC_END && text.empty?) || - event == :REGEXP_BEGIN || - event == :STRING_CONTENT || - event == :HEREDOC_CONTENT - return nil + end + + # Base indent from stable nesting depth + indent_level = calc_indent_level(prev_opens.take(min_depth)) + indent = indent_level * @buffer[:indent_level] + + # Compute base_indent (cosmetic offset from actual code indentation) + base_indent = compute_base_indent(prev_open_elem, line_index) + + indentation = base_indent + indent + + # Handle extra unmatched closers (end/}/]/)) + if prev_opens.empty? + extra_count = count_extra_closers_before(target_line) + if extra_count > 0 + indentation -= extra_count * @buffer[:indent_level] end - i, extra_end_count = find_nearest_beginning_token(tokens) - (line, column), event, = i ? tokens[i] : nil - if event == :PARENTHESIS_LEFT && tokens.dig(i + 1, 1) != :IGNORED_NEWLINE - return column + 1 + end + + # Continuation lines + if continuation_line?(target_line, prev_open_elem) + indentation += @buffer[:indent_level] + end + + indentation + end + + def calc_indent_level(opens) + indent_level = 0 + opens&.each do |elem| + case elem.event + when :on_heredoc_beg + # skip + when :on_tstring_beg, :on_regexp_beg, :on_symbeg, :on_backtick + indent_level += 1 if elem.tok.start_with?("%") + when :on_embdoc_beg + indent_level = 0 + else + indent_level += 1 end - if line - @buffer.goto_line(start_line - 1 + line) - while !@buffer.beginning_of_buffer? - if @buffer.save_excursion { - @buffer.backward_char - @buffer.skip_re_backward(/\s/) - @buffer.char_before == ?, - } - @buffer.backward_line - else - break - end + end + indent_level + end + + def indent_difference(line_index) + loop do + return 0 if line_index < 0 || line_index >= @nesting_by_line.size + prev_opens, _next_opens, min_depth = @nesting_by_line[line_index] + open_elem = prev_opens&.last + if !open_elem || (open_elem.event != :on_heredoc_beg && + !FREE_INDENT_EVENTS.include?(open_elem.event)) + il = calc_indent_level(prev_opens.take(min_depth)) + calculated_indent = il * @buffer[:indent_level] + actual_indent = actual_indentation_at_line(line_index + 1) + return actual_indent - calculated_indent + elsif open_elem.event == :on_heredoc_beg && !open_elem.tok.match?(/^<<[-~]/) + return 0 + end + line_index = open_elem.pos[0] - 1 + end + end + + def compute_base_indent(prev_open_elem, line_index) + if prev_open_elem + # Start at the opener's line, trace back through continuations + li = trace_back_through_continuations(prev_open_elem.pos[0]) + # Then trace back through nesting chain + while li >= 0 && li < @nesting_by_line.size + po, _, _ = @nesting_by_line[li] + outer = po&.last + if outer.nil? + return [0, indent_difference(li)].max + end + outer_line = outer.pos[0] - 1 + if outer_line < li + li = outer_line + else + return [0, indent_difference(li)].max end - @buffer.looking_at?(/[ \t]*/) - base_indentation = space_width(@buffer.match_string(0)) end - @buffer.goto_char(bol_pos) - if line.nil? - indentation = - base_indentation - extra_end_count * @buffer[:indent_level] + 0 + else + find_base_indent_at_toplevel(line_index) + end + end + + def trace_back_through_continuations(line_number) + li = line_number - 1 # convert to 0-indexed + while li > 0 + prev_line = li # 1-indexed line number of previous line + if line_ends_with_comma?(prev_line) + li -= 1 else - indentation = base_indentation + @buffer[:indent_level] + break end - if @buffer.looking_at?(/[ \t]*([}\])]|(end|else|elsif|when|in|rescue|ensure)\b)/) - indentation -= @buffer[:indent_level] + end + li + end + + def line_ends_with_comma?(line_number) + ensure_prism_tokens + last_event = nil + @prism_tokens.each do |token, _state| + loc = token.location + next if loc.start_line != line_number + type = token.type + next if type == :NEWLINE || type == :IGNORED_NEWLINE || + type == :COMMENT || type == :EOF + last_event = type + end + last_event == :COMMA + end + + def find_base_indent_at_toplevel(line_index) + # First, search for a line with nesting context + (line_index - 1).downto(0) do |i| + prev_opens, next_opens, _ = @nesting_by_line[i] + if !prev_opens.empty? + origin_elem = prev_opens.first + return [0, indent_difference(origin_elem.pos[0] - 1)].max + elsif !next_opens.empty? + origin_elem = next_opens.first + return [0, indent_difference(origin_elem.pos[0] - 1)].max end - _, last_event, = tokens.reverse_each.find { |_, e, _| - e != :NEWLINE && e != :IGNORED_NEWLINE - } - if start_with_period || - CONTINUATION_OPERATOR_TYPES.include?(last_event) || - last_event == :KEYWORD_AND || last_event == :KEYWORD_OR || - last_event == :DOT || - (last_event == :COMMA && event != :BRACE_LEFT && - event != :PARENTHESIS_LEFT && event != :BRACKET_LEFT && - event != :BRACKET_LEFT_ARRAY) || - last_event == :LABEL - indentation += @buffer[:indent_level] + end + # Fall back: use nearest non-empty line's actual indentation + (line_index - 1).downto(0) do |i| + if line_has_content?(i + 1) + return actual_indentation_at_line(i + 1) end - indentation - end - end - - def find_nearest_beginning_token(tokens) - stack = [] - (tokens.size - 1).downto(0) do |i| - (line, ), event, text = tokens[i] - case event - when :KEYWORD_CLASS, :KEYWORD_MODULE, :KEYWORD_DEF, - :KEYWORD_IF, :KEYWORD_UNLESS, :KEYWORD_CASE, - :KEYWORD_DO, :KEYWORD_DO_LOOP, :KEYWORD_FOR, - :KEYWORD_WHILE, :KEYWORD_UNTIL, :KEYWORD_BEGIN - if i > 0 - _, prev_event, _ = tokens[i - 1] - next if prev_event == :SYMBOL_BEGIN - end - if event == :KEYWORD_DEF && endless_method_def?(tokens, i) - next - end - if stack.empty? - return i - end - if stack.last != :KEYWORD_END - raise EditorError, "#{@buffer.name}:#{line}: Unmatched #{text}" - end - stack.pop - when :KEYWORD_END - if i > 0 - _, prev_event, _ = tokens[i - 1] - next if prev_event == :SYMBOL_BEGIN - end - stack.push(:KEYWORD_END) - when :BRACE_RIGHT, :PARENTHESIS_RIGHT, :BRACKET_RIGHT, :EMBEXPR_END - stack.push(event) - when :BRACE_LEFT, :PARENTHESIS_LEFT, :BRACKET_LEFT, - :BRACKET_LEFT_ARRAY, :LAMBDA_BEGIN, :EMBEXPR_BEGIN - if stack.empty? - return i - end - if stack.last != BLOCK_END[event] - raise EditorError, "#{@buffer.name}:#{line}: Unmatched #{text}" + end + 0 + end + + def line_has_content?(line_number) + @buffer.save_excursion do + @buffer.goto_line(line_number) + @buffer.looking_at?(/[ \t]*\S/) + end + end + + def count_extra_closers_before(target_line) + return 0 unless @prism_parse_lex_result + @prism_parse_lex_result.errors.count { |e| + e.type == :unexpected_token_ignore && + e.location.start_line <= target_line + } + end + + def actual_indentation_at_line(line_number) + @buffer.save_excursion do + @buffer.goto_line(line_number) + @buffer.looking_at?(/[ \t]*/) + space_width(@buffer.match_string(0)) + end + end + + def paren_has_args_on_same_line?(paren_line, paren_col) + ensure_prism_tokens + found_paren = false + @prism_tokens.each do |token, _state| + loc = token.location + if !found_paren + if loc.start_line == paren_line && + loc.start_column == paren_col && + token.type == :PARENTHESIS_LEFT + found_paren = true end - stack.pop + next end + next if token.type == :EOF + return token.type != :IGNORED_NEWLINE && token.type != :NEWLINE && + loc.start_line == paren_line end - return nil, stack.count { |t| t != :PARENTHESIS_RIGHT && t != :BRACKET_RIGHT } - end - - def endless_method_def?(tokens, i) - ts = tokens.drop(i + 1) - _, event = ts.shift - return false if event != :IDENTIFIER && event != :METHOD_NAME - if ts[0][1] == :PARENTHESIS_LEFT - ts.shift - count = 1 - while count > 0 - _, event = ts.shift - return false if event.nil? - case event - when :PARENTHESIS_LEFT - count += 1 - when :PARENTHESIS_RIGHT - count -= 1 - end + false + end + + def continuation_line?(target_line, prev_open_elem) + start_with_period = @buffer.save_excursion { + @buffer.beginning_of_line + @buffer.looking_at?(/[ \t]*\./) + } + return true if start_with_period + + prev_line = target_line - 1 + return false if prev_line < 1 + + ensure_prism_tokens + last_event = nil + @prism_tokens.each do |token, _state| + loc = token.location + next if loc.start_line > prev_line + next if loc.start_line < prev_line + type = token.type + next if type == :NEWLINE || type == :IGNORED_NEWLINE || + type == :COMMENT || type == :EOF + last_event = type + end + + return false if last_event.nil? + + if CONTINUATION_OPERATOR_TYPES.include?(last_event) || + last_event == :KEYWORD_AND || last_event == :KEYWORD_OR || + last_event == :DOT || last_event == :LABEL + return true + end + + if last_event == :COMMA + if prev_open_elem.nil? || + !%i[on_lbrace on_lparen bracket on_lbracket].include?(prev_open_elem.event) + return true end end - ts[0][1] == :EQUAL - rescue NoMethodError # no token - return false + + false + end + + def ensure_nesting_by_line + ensure_prism_tokens + return if @nesting_by_line && @nesting_version == @prism_version + if @prism_parse_lex_result + @nesting_by_line = RubyNestingParser.parse_by_line(@prism_parse_lex_result) + else + @nesting_by_line = nil + end + @nesting_version = @prism_version end def find_test_target_path(base, namespace, name) @@ -488,15 +581,17 @@ def ensure_prism_tokens return if @prism_version == @buffer.version source = @buffer.to_s if source.valid_encoding? - result = Prism.parse_lex(source) - @prism_ast, @prism_tokens = result.value + @prism_parse_lex_result = Prism.parse_lex(source) + @prism_ast, @prism_tokens = @prism_parse_lex_result.value else + @prism_parse_lex_result = nil @prism_ast = nil @prism_tokens = [] end @prism_method_name_locs = nil @prism_version = @buffer.version @literal_levels_version = nil + @nesting_by_line = nil end def ensure_method_name_locs diff --git a/lib/textbringer/modes/ruby_nesting_parser.rb b/lib/textbringer/modes/ruby_nesting_parser.rb new file mode 100644 index 0000000..66593bd --- /dev/null +++ b/lib/textbringer/modes/ruby_nesting_parser.rb @@ -0,0 +1,387 @@ +# Based on IRB::Parser written by tomoya ishida +# +# Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +require "prism" + +module Textbringer + module RubyNestingParser + NestingElem = Struct.new(:pos, :event, :tok) + + class NestingVisitor < Prism::Visitor + def initialize + @lines = [] + @heredocs = [] + end + + def nestings + size = [@lines.size, @heredocs.size].max + nesting = [] + size.times.map do |line_index| + @lines[line_index]&.sort_by { |col, pri| [col, pri] }&.each do |_col, _pri, elem| + if elem + nesting << elem + else + nesting.pop + end + end + @heredocs[line_index]&.sort_by { |elem| elem.pos[1] }&.reverse_each do |elem| + nesting << elem + end + nesting.dup + end + end + + def heredoc_open(node) + elem = NestingElem.new( + [node.location.start_line, node.location.start_column], + :on_heredoc_beg, node.opening + ) + (@heredocs[node.location.start_line - 1] ||= []) << elem + end + + def open(line, column, elem) + (@lines[line - 1] ||= []) << [column, +1, elem] + end + + def close(line, column) + (@lines[line - 1] ||= []) << [column, -1] + end + + def modifier_node?(node, keyword_loc) + !(keyword_loc && + node.location.start_line == keyword_loc.start_line && + node.location.start_column == keyword_loc.start_column) + end + + def open_location(location, type, tok) + open( + location.start_line, location.start_column, + NestingElem.new( + [location.start_line, location.start_column], type, tok + ) + ) + end + + def close_location(location) + close(location.end_line, location.end_column) + end + + def close_location_start(location) + close(location.start_line, location.start_column) + end + + def close_end_keyword_loc(node) + close_location(node.end_keyword_loc) if node.end_keyword == "end" + end + + def close_closing_loc(node) + close_location(node.closing_loc) if node.closing_loc && !node.closing.empty? + end + + def visit_for_node(node) + super + open_location(node.location, :on_kw, "for") + close_end_keyword_loc(node) + end + + def visit_while_node(node) + super + return if modifier_node?(node, node.keyword_loc) + open_location(node.location, :on_kw, "while") + close_closing_loc(node) + end + + def visit_until_node(node) + super + return if modifier_node?(node, node.keyword_loc) + open_location(node.location, :on_kw, "until") + close_closing_loc(node) + end + + def visit_if_node(node) + super + return if !node.if_keyword || modifier_node?(node, node.if_keyword_loc) + open_location(node.location, :on_kw, node.if_keyword) + if node.subsequent + close_location_start(node.subsequent.location) + else + close_end_keyword_loc(node) + end + end + + def visit_unless_node(node) + super + return if modifier_node?(node, node.keyword_loc) + open_location(node.location, :on_kw, "unless") + if node.else_clause + close_location_start(node.else_clause.location) + else + close_end_keyword_loc(node) + end + end + + def visit_case_node(node) + super + open_location(node.location, :on_kw, "case") + if node.else_clause + close_location_start(node.else_clause.location) + else + close_end_keyword_loc(node) + end + end + alias visit_case_match_node visit_case_node + + def visit_when_node(node) + super + close_location_start(node.location) + open_location(node.location, :on_kw, "when") + end + + def visit_in_node(node) + super + close_location_start(node.location) + open_location(node.location, :on_kw, "in") + end + + def visit_else_node(node) + super + if node.else_keyword == "else" + open_location(node.location, :on_kw, "else") + close_end_keyword_loc(node) + end + end + + def visit_ensure_node(node) + super + return if modifier_node?(node, node.ensure_keyword_loc) + close_location_start(node.location) + open_location(node.location, :on_kw, "ensure") + end + + def visit_rescue_node(node) + super + return if modifier_node?(node, node.keyword_loc) + close_location_start(node.location) + open_location(node.location, :on_kw, "rescue") + end + + def visit_begin_node(node) + super + if node.begin_keyword + open_location(node.location, :on_kw, "begin") + close_end_keyword_loc(node) + end + end + + def visit_block_node(node) + super + open_location( + node.location, + node.opening == "{" ? :on_lbrace : :on_kw, + node.opening + ) + close_closing_loc(node) + end + + def visit_array_node(node) + super + type = + case node.opening + when nil + nil + when "[" + :bracket + when /\A%W/ + :on_words_beg + when /\A%w/ + :on_qwords_beg + when /\A%I/ + :on_symbols_beg + when /\A%i/ + :on_qsymbols_beg + end + if type + open_location(node.location, type, node.opening) + close_closing_loc(node) + end + end + + def visit_hash_node(node) + super + open_location(node.location, :on_lbrace, "{") + close_closing_loc(node) + end + + def heredoc_string_like(node, type) + if node.opening&.start_with?("<<") + heredoc_open(node) + close_location_start(node.closing_loc) if node.closing_loc && !node.closing.empty? + elsif node.opening + return if node.opening == "?" && node.closing.nil? # Character literal has no closing + open_location(node.location, type, node.opening) + if node.closing && node.closing != "" + close_location_start(node.closing_loc) if node.opening.match?(/\n\z/) || node.closing != "\n" + end + end + end + + def visit_embedded_statements_node(node) + super + open_location(node.location, :on_embexpr_beg, '#{') + close_closing_loc(node) + end + + def visit_interpolated_string_node(node) + super + heredoc_string_like(node, :on_tstring_beg) + end + alias visit_string_node visit_interpolated_string_node + + def visit_interpolated_x_string_node(node) + super + heredoc_string_like(node, :on_backtick) + end + alias visit_x_string_node visit_interpolated_x_string_node + + def visit_symbol_node(node) + super + unless node.opening.nil? || node.opening.empty? || node.opening == ":" + open_location(node.location, :on_symbeg, node.opening) + close_closing_loc(node) + end + end + alias visit_interpolated_symbol_node visit_symbol_node + + def visit_regular_expression_node(node) + super + open_location(node.location, :on_regexp_beg, node.opening) + close_closing_loc(node) + end + alias visit_interpolated_regular_expression_node visit_regular_expression_node + + def visit_parentheses_node(node) + super + open_location(node.location, :on_lparen, "(") + close_closing_loc(node) + end + + def visit_call_node(node) + super + type = + case node.opening + when "(" + :on_lparen + when "[" + :on_lbracket + end + if type + open_location(node.opening_loc, type, node.opening) + close_closing_loc(node) + end + end + + def visit_block_parameters_node(node) + super + if node.opening == "(" + open_location(node.location, :on_lparen, "(") + close_closing_loc(node) + end + end + + def visit_lambda_node(node) + super + open_location(node.opening_loc, :on_tlambeg, node.opening) + close_closing_loc(node) + end + + def visit_super_node(node) + super + if node.lparen + open_location(node.lparen_loc, :on_lparen, "(") + close_location(node.rparen_loc) if node.rparen == ")" + end + end + alias visit_yield_node visit_super_node + alias visit_defined_node visit_super_node + + def visit_def_node(node) + super + open_location(node.location, :on_kw, "def") + if node.lparen == "(" + open_location(node.lparen_loc, :on_lparen, "(") + close_location(node.rparen_loc) if node.rparen == ")" + end + if node.equal + close_location(node.equal_loc) + else + close_end_keyword_loc(node) + end + end + + def visit_class_node(node) + super + open_location(node.location, :on_kw, "class") + close_end_keyword_loc(node) + end + alias visit_singleton_class_node visit_class_node + + def visit_module_node(node) + super + open_location(node.location, :on_kw, "module") + close_end_keyword_loc(node) + end + end + + class << self + def open_nestings(parse_lex_result) + parse_by_line(parse_lex_result).last&.dig(1) || [] + end + + def parse_by_line(parse_lex_result) + visitor = NestingVisitor.new + node, tokens = parse_lex_result.value + node.accept(visitor) + tokens.each do |token,| + case token.type + when :EMBDOC_BEGIN + visitor.open_location(token.location, :on_embdoc_beg, "=begin") + when :EMBDOC_END + visitor.close_location_start(token.location) + end + end + nestings = visitor.nestings + last_nesting = nestings.last || [] + + num_lines = parse_lex_result.source.source.lines.size + num_lines.times.map do |i| + prev_opens = i == 0 ? [] : nestings[i - 1] || last_nesting + opens = nestings[i] || last_nesting + min_depth = prev_opens.zip(opens).take_while { |s, e| s == e }.size + [prev_opens, opens, min_depth] + end + end + end + end +end diff --git a/test/textbringer/modes/test_ruby_mode.rb b/test/textbringer/modes/test_ruby_mode.rb index dca728a..67f6e20 100644 --- a/test/textbringer/modes/test_ruby_mode.rb +++ b/test/textbringer/modes/test_ruby_mode.rb @@ -217,11 +217,17 @@ def foo } quux EOF - assert_raise(EditorError) do - @ruby_mode.indent_line - end + @ruby_mode.indent_line + assert_equal(<