diff --git a/lib/rufo.rb b/lib/rufo.rb index 34cdec25..63a634d4 100644 --- a/lib/rufo.rb +++ b/lib/rufo.rb @@ -14,5 +14,7 @@ def self.format(code, **options) require_relative "rufo/command" require_relative "rufo/dot_file" require_relative "rufo/settings" +require_relative "rufo/doc_builder" +require_relative "rufo/doc_printer" require_relative "rufo/formatter" require_relative "rufo/version" diff --git a/lib/rufo/doc_builder.rb b/lib/rufo/doc_builder.rb new file mode 100644 index 00000000..706086ca --- /dev/null +++ b/lib/rufo/doc_builder.rb @@ -0,0 +1,120 @@ +module Rufo + class DocBuilder + class InvalidDocError < StandardError; end + + class << self + + # Combine an array of items into a single string. + def concat(parts) + assert_docs(parts) + { + type: :concat, parts: parts, + } + end + + # Increase level of indentation. + def indent(contents) + assert_doc(contents) + { + type: :indent, contents: contents, + } + end + + # Increase indentation by a fixed number. + def align(n, contents) + assert_doc(contents) + {type: :align, contents: contents, n: n} + end + + # Groups are items that the printer should try and fit onto a single line. + # If the group does not fit then it breaks instead. + def group(contents, opts = {}) + assert_doc(contents) + { + type: :group, + contents: contents, + break: !!opts[:should_break], + expanded_states: opts[:expanded_states], + } + end + + # Rather than breaking if the items do not fit this tries a different set + # of items. + def conditional_group(states, opts = {}) + group(states.first, opts.merge(expanded_states: states)) + end + + # Alternative to group. This only breaks the required items rather then + # all items if they do not fit. + def fill(parts) + assert_docs(parts) + + {type: :fill, parts: parts} + end + + # Print first arg if the group breaks otherwise print the second. + def if_break(break_contents, flat_contents) + assert_doc(break_contents) unless break_contents.nil? + assert_doc(flat_contents) unless flat_contents.nil? + + {type: :if_break, break_contents: break_contents, flat_contents: flat_contents} + end + + # Append content to the end of a line. This gets placed just before a new line. + def line_suffix(contents) + assert_doc(contents) + {type: :line_suffix, contents: contents} + end + + # Join list of items with a separator. + def join(sep, arr) + result = [] + arr.each_with_index do |element, index| + unless index == 0 + result << sep + end + result << element + end + concat(result) + end + + def add_alignment_to_doc(doc, size, tab_width) + return doc unless size > 0 + (size / tab_width).times { doc = indent(doc) } + doc = align(size % tab_width, doc) + align(-Float::INFINITY, doc) + end + + private + + def assert_docs(parts) + parts.each(&method(:assert_doc)) + end + + def assert_doc(val) + unless val.is_a?(String) || (val.is_a?(Hash) && val[:type].is_a?(Symbol)) + raise InvalidDocError.new("Value #{val.inspect} is not a valid document") + end + end + end + + # Use this to ensure that line suffixes do not move to the last line in group. + LINE_SUFFIX_BOUNDARY = {type: :line_suffix_boundary} + # Use this to force the parent to break + BREAK_PARENT = {type: :break_parent} + # If the content fits on one line the newline will be replaced with a space. + # Newlines are what triggers indentation to be added. + LINE = {type: :line} + # If the content fits on one line the newline will be replaced by nothing. + SOFT_LINE = {type: :line, soft: true} + # This newline is always included regardless of if the content fits on one + # line or not. + HARD_LINE = concat([{type: :line, hard: true}, BREAK_PARENT]) + # This is a newline that is always included and does not cause the + # indentation to change subsequently. + LITERAL_LINE = concat([{type: :line, hard: true, literal: true}, BREAK_PARENT]) + + # This keeps track of the cursor in the document. + CURSOR = {type: :cursor, placeholder: :cursor} + end +end diff --git a/lib/rufo/doc_printer.rb b/lib/rufo/doc_printer.rb new file mode 100644 index 00000000..3bd09ccc --- /dev/null +++ b/lib/rufo/doc_printer.rb @@ -0,0 +1,291 @@ +module Rufo + class DocPrinter + ROOT_INDENT = { + indent: 0, + align: { + spaces: 0, + }, + } + MODE_BREAK = 1 + MODE_FLAT = 2 + INDENT_WIDTH = 2 + class << self + def print_doc_to_string(doc, opts) + width = opts.fetch(:print_width) + new_line = opts.fetch(:new_line, "\n") + pos = 0 + + cmds = [[ROOT_INDENT, MODE_BREAK, doc]] + out = [] + should_remeasure = false + line_suffix = [] + + while cmds.length != 0 + x = cmds.pop + ind = x[0] + mode = x[1] + doc = x[2] + if doc.is_a?(String) + out.push(doc) + pos += doc.length + else + case doc[:type] + when :cursor + out.push(doc[:placeholder]) + when :concat + doc[:parts].reverse_each { |part| cmds.push([ind, mode, part]) } + when :indent + cmds.push([make_indent(ind), mode, doc[:contents]]) + when :align + cmds.push([make_align(ind, doc[:n]), mode, doc[:contents]]) + when :group + if mode == MODE_FLAT && !should_remeasure + cmds.push([ind, doc[:break] ? MODE_BREAK : MODE_FLAT, doc[:contents]]) + next + end + if mode == MODE_FLAT || mode == MODE_BREAK + should_remeasure = false + next_cmd = [ind, MODE_FLAT, doc[:contents]] + rem = width - pos + + if !doc[:break] && fits(next_cmd, cmds, rem) + cmds.push(next_cmd) + else + unless doc[:expanded_states].nil? + most_expanded = doc[:expanded_states].last + + if doc[:break] + cmds.push([ind, MODE_BREAK, most_expanded]) + next + else + best_state = doc[:expanded_states].find { |state| + state_cmd = [ind, MODE_FLAT, state] + fits(state_cmd, cmds, rem) + } || most_expanded + cmds.push([ind, MODE_FLAT, best_state]) + end + else + cmds.push([ind, MODE_BREAK, doc[:contents]]) + end + end + end + when :fill + rem = width - pos + parts = doc[:parts] + next if parts.empty? + + content = parts[0] + contents_flat_cmd = [ind, MODE_FLAT, content] + contents_break_cmd = [ind, MODE_BREAK, content] + content_fits = fits(contents_flat_cmd, [], width - rem, true) + if parts.length == 1 + if content_fits + cmds.push(contents_flat_cmd) + else + cmds.push(contents_break_cmd) + end + + next + end + + whitespace = parts[1] + whitespace_flat_cmd = [ind, MODE_FLAT, whitespace] + whitespace_break_cmd = [ind, MODE_BREAK, whitespace] + if parts.length == 2 + if content_fits + cmds.push(whitespace_flat_cmd) + cmds.push(contents_flat_cmd) + else + cmds.push(whitespace_break_cmd) + cmds.push(contents_break_cmd) + end + next + end + + remaining = parts[2..-1] + remaining_cmd = [ind, mode, DocBuilder::fill(remaining)] + + second_content = parts[2] + first_and_second_content_flat_cmd = [ + ind, + MODE_FLAT, + DocBuilder::concat([content, whitespace, second_content]), + ] + first_and_second_content_fits = fits( + first_and_second_content_flat_cmd, + [], + rem, + true + ) + + if first_and_second_content_fits + cmds.push(remaining_cmd) + cmds.push(whitespace_flat_cmd) + cmds.push(contents_flat_cmd) + elsif content_fits + cmds.push(remaining_cmd) + cmds.push(whitespace_break_cmd) + cmds.push(contents_flat_cmd) + else + cmds.push(remaining_cmd) + cmds.push(whitespace_break_cmd) + cmds.push(contents_break_cmd) + end + when :if_break + if mode === MODE_BREAK + if doc[:break_contents] + cmds.push([ind, mode, doc[:break_contents]]) + end + end + if mode === MODE_FLAT + if doc[:flat_contents] + cmds.push([ind, mode, doc[:flat_contents]]) + end + end + when :line_suffix + line_suffix.push([ind, mode, doc[:contents]]) + when :line_suffix_boundary + if line_suffix.length > 0 + cmds.push([ind, mode, {type: :line, hard: true}]) + end + when :line + if mode == MODE_FLAT + unless doc[:hard] + unless doc[:soft] + out.push(" ") + pos += 1 + end + next + else + should_remeasure = true + end + end + if mode == MODE_FLAT || mode == MODE_BREAK + unless line_suffix.empty? + cmds.push([ind, mode, doc]) + cmds.concat(line_suffix.reverse) + line_suffix = [] + next + end + + if doc[:literal] + out.push(new_line) + pos = 0 + else + if out.length > 0 + popped = [] + while out.length > 0 && (out.last =~ /^[^\S\n]*$/) + popped << out.pop.rstrip + end + out.concat(popped.reject(&:empty?)) + + unless out.empty? + out[-1] = out.last.sub(/[^\S\n]*$/, "") + end + end + + length = ind[:indent] * INDENT_WIDTH + ind[:align][:spaces] + indent_string = " " * length + out.push(new_line, indent_string) + pos = length + end + end + end + end + end + + cursor_place_holder_index = out.index(DocBuilder::CURSOR[:placeholder]) + + if cursor_place_holder_index + before_cursor = out[0..(cursor_place_holder_index - 1)].join("") + after_cursor = out[(cursor_place_holder_index + 1)..-1].join("") + + return { + formatted: before_cursor + after_cursor, + cursor: before_cursor.length, + } + end + + {formatted: out.join("")} + end + + private + + def make_indent(ind) + { + indent: ind[:indent] + 1, + align: ind[:align], + } + end + + def make_align(ind, n) + return ROOT_INDENT if n == -Float::INFINITY + { + indent: ind[:indent], + align: { + spaces: ind[:align][:spaces] + n, + }, + } + end + + def fits(next_cmd, rest_cmds, width, must_be_flat = false) + rest_idx = rest_cmds.size + cmds = [next_cmd] + + while width >= 0 + if cmds.size == 0 + return true if (rest_idx == 0) + cmds.push(rest_cmds[rest_idx - 1]) + + rest_idx -= 1 + next + end + + x = cmds.pop + ind = x[0] + mode = x[1] + doc = x[2] + + if doc.is_a?(String) + width -= doc.length + else + case doc[:type] + when :concat + doc[:parts].each { |part| cmds.push([ind, mode, part]) } + when :indent + cmds.push([make_indent(ind), mode, doc[:contents]]) + when :align + cmds.push([make_align(ind, doc[:n]), mode, doc[:contents]]) + when :group + return false if must_be_flat && doc[:break] + cmds.push([ind, doc[:break] ? MODE_BREAK : mode, doc[:contents]]) + when :fill + doc[:parts].each { |part| cmds.push([ind, mode, part]) } + when :if_break + if mode == MODE_BREAK && doc[:break_contents] + cmds.push([ind, mode, doc[:break_contents]]) + elsif mode == MODE_FLAT && doc[:flat_contents] + cmds.push([ind, mode, doc[:flat_contents]]) + end + when :line + case mode + when MODE_FLAT + unless doc[:hard] + unless doc[:soft] + width -= 1 + end + else + return true + end + when MODE_BREAK + return true + end + end + end + end + + return false + end + end + end +end diff --git a/lib/rufo/formatter.rb b/lib/rufo/formatter.rb index b4290808..c7c4cfdd 100644 --- a/lib/rufo/formatter.rb +++ b/lib/rufo/formatter.rb @@ -5,6 +5,7 @@ class Rufo::Formatter include Rufo::Settings + B = Rufo::DocBuilder INDENT_SIZE = 2 attr_reader :squiggly_flag @@ -189,7 +190,26 @@ def visit(node) unless node.is_a?(Array) bug "unexpected node: #{node} at #{current_token}" end + result = visit_doc(node) + if result != false + if in_doc_mode? + return result + end + return if result.nil? + @output << Rufo::DocPrinter.print_doc_to_string( + result, {print_width: print_width - @indent} + )[:formatted] + return + end + if in_doc_mode? + capture_output { visit_non_doc(node) } + else + visit_non_doc(node) + end + end + + def visit_non_doc(node) case node.first when :program # Topmost node @@ -408,8 +428,6 @@ def visit(node) visit_paren(node) when :params visit_params(node) - when :array - visit_array(node) when :hash visit_hash(node) when :assoc_new @@ -488,6 +506,19 @@ def visit(node) @node_level -= 1 end + def visit_doc(node) + case node.first + when :array + doc = visit_array(node) + return if doc.nil? + + return B.align(@indent, doc) + when :args_add_star + return visit_args_add_star_doc(node) if in_doc_mode? + end + false + end + def visit_exps(exps, with_indent: false, with_lines: true, want_trailing_multiline: false) consume_end_of_line(at_prefix: true) @@ -577,7 +608,7 @@ def declaration?(exp) end end - def visit_string_literal(node) + def visit_string_literal(node, bail_on_heredoc: false) # [:string_literal, [:string_content, exps]] heredoc = current_token_kind == :on_heredoc_beg tilde = current_token_value.include?("~") @@ -589,6 +620,10 @@ def visit_string_literal(node) @heredocs << [node, tilde] # Get the next_token while capturing any output. # This is needed so that we can add a comma if one is not already present. + if bail_on_heredoc + next_token_no_heredoc_check + return + end captured_output = capture_output { next_token } inside_literal_elements_list = !@literal_elements_level.nil? && @@ -1153,6 +1188,28 @@ def flush_heredocs @last_was_heredoc = true if printed end + def flush_heredocs_doc + doc = [] + comment = nil + if comment? + comment = current_token_value.rstrip + next_token + end + + until @heredocs.empty? + heredoc, tilde = @heredocs.first + + @heredocs.shift + @current_heredoc = [heredoc, tilde] + doc << capture_output { visit_string_literal_end(heredoc) } + @current_heredoc = nil + printed = true + end + + @last_was_heredoc = true if printed + [doc, comment] + end + def visit_command_call(node) # [:command_call, # receiver @@ -1483,6 +1540,38 @@ def visit_args_add_star(node) end end + def skip_comma_and_spaces + skip_space + check :on_comma + next_token + skip_space + end + + def visit_args_add_star_doc(node) + # [:args_add_star, args, star, post_args] + _, args, star, *post_args = node + doc = [] + if !args.empty? && args[0] == :args_add_star + # arg1, ..., *star + doc = visit args + else + pre_doc = with_doc_mode { visit_literal_elements_simple_doc(args) } + doc.concat(pre_doc) + end + + skip_comma_and_spaces if comma? + + consume_op "*" + doc << "*#{visit star}" + + if post_args && !post_args.empty? + skip_comma_and_spaces + post_doc = with_doc_mode { visit_literal_elements_simple_doc(post_args) } + doc.concat(post_doc) + end + doc + end + def visit_begin(node) # begin # body @@ -2130,27 +2219,25 @@ def visit_array(node) # Check if it's `%w(...)` or `%i(...)` case current_token_kind when :on_qwords_beg, :on_qsymbols_beg, :on_words_beg, :on_symbols_beg - visit_q_or_i_array(node) - return + return capture_output { visit_q_or_i_array(node) } end _, elements = node - token_column = current_token_column - + doc = [] check :on_lbracket - write "[" next_token if elements - visit_literal_elements to_ary(elements), inside_array: true, token_column: token_column + doc = with_doc_mode { visit_literal_elements_doc(to_ary(elements)) } else skip_space_or_newline + doc = "[]" end check :on_rbracket - write "]" next_token + doc end def visit_q_or_i_array(node) @@ -2715,6 +2802,141 @@ def visit_literal_elements(elements, inside_hash: false, inside_array: false, to end end + def add_comments_to_doc(comments, doc) + return false if comments.empty? + + comments.each do |c| + doc << B.line_suffix(" " + c.rstrip) + end + return true + end + + def add_comments_on_line(element_doc, comments, newline_before_comment:) + return false if comments.empty? + first_comment = comments.shift + + if newline_before_comment + element_doc << B.concat([ + element_doc.pop, + B.line_suffix(B.concat([B::LINE, first_comment.rstrip])), + ]) + else + element_doc << B.concat([element_doc.pop, B.line_suffix(" " + first_comment.rstrip)]) + end + true + end + + # Handles literal elements where there are no comments or heredocs to worry + # about. + def visit_literal_elements_simple_doc(elements) + doc = [] + + skip_space_or_newline + elements.each do |elem| + doc_el = visit(elem) + if doc_el.is_a?(Array) + doc.concat(doc_el) + else + doc << doc_el + end + + skip_space_or_newline + next unless comma? + next_token + skip_space_or_newline + end + + doc + end + + def add_heredoc_to_doc(doc, current_doc, element_doc, comments) + value, comment = check_heredocs_in_literal_elements_doc + return [current_doc, false, element_doc] if value.nil? + + last = current_doc.pop + unless last.nil? + doc << B.join( + B.concat([",", B::LINE_SUFFIX_BOUNDARY, B::LINE]), + [*current_doc, B.concat([last, B.if_break(',', '')])] + ) + end + + unless comments.empty? + comment = element_doc.pop + end + + comment_array = [B.line_suffix(" " + comment)] if comment + comment_array ||= [] + + doc << B.concat([ + *element_doc, + ",", + *comment_array, + B::LINE_SUFFIX_BOUNDARY, + value.last.rstrip, + B::SOFT_LINE, + ]) + return [[], true, []] + end + + def visit_literal_elements_doc(elements) + doc = [] + current_doc = [] + element_doc = [] + pre_comments = [] + has_heredocs = false + + comments, newline_before_comment = skip_space_or_newline_doc + has_comment = add_comments_to_doc(comments, pre_comments) + + elements.each_with_index do |elem, i| + current_doc.concat(element_doc) + element_doc = [] + doc_el = visit(elem) + if doc_el.is_a?(Array) + element_doc.concat(doc_el) + else + element_doc << doc_el + end + current_doc, heredoc_present, element_doc = add_heredoc_to_doc( + doc, current_doc, element_doc, [] + ) + has_heredocs ||= heredoc_present + comments, newline_before_comment = skip_space_or_newline_doc + has_comment = true if add_comments_on_line(element_doc, comments, newline_before_comment: false) + + next unless comma? + next_token_no_heredoc_check + current_doc, heredoc_present, element_doc = add_heredoc_to_doc( + doc, current_doc, element_doc, comments + ) + has_heredocs ||= heredoc_present + comments, newline_before_comment = skip_space_or_newline_doc + + has_comment = true if add_comments_on_line(element_doc, comments, newline_before_comment: newline_before_comment) + end + current_doc.concat(element_doc) + + if trailing_commas && !current_doc.empty? + last = current_doc.pop + current_doc << B.concat([last, B.if_break(',', '')]) + end + doc << B.join( + B.concat([",", B::LINE_SUFFIX_BOUNDARY, B::LINE]), + current_doc + ) + + B.group( + B.concat([ + "[", + B.indent(B.concat([B.concat(pre_comments), B::SOFT_LINE, *doc])), + B::SOFT_LINE, + "]", + ]), + should_break: has_comment || has_heredocs, + ) + end + def check_heredocs_in_literal_elements(is_last, needs_trailing_comma, wrote_comma) if (newline? || comment?) && !@heredocs.empty? if is_last && trailing_commas @@ -2727,6 +2949,14 @@ def check_heredocs_in_literal_elements(is_last, needs_trailing_comma, wrote_comm wrote_comma end + def check_heredocs_in_literal_elements_doc + skip_space + if (newline? || comment?) && !@heredocs.empty? + return flush_heredocs_doc + end + [] + end + def visit_if(node) visit_if_or_unless node, "if" end @@ -3016,6 +3246,44 @@ def skip_space_or_newline(_want_semicolon: false, write_first_semicolon: false) found_semicolon end + def skip_space_or_newline_doc + found_newline = false + found_comment = false + found_semicolon = false + newline_before_comment = false + last = nil + comments = [] + loop do + case current_token_kind + when :on_sp + next_token + when :on_nl, :on_ignored_nl + next_token + last = :newline + found_newline = true + if comments.empty? + newline_before_comment = true + end + when :on_semicolon + next_token + last = :semicolon + found_semicolon = true + when :on_comment + if current_token_value.end_with?("\n") + @column = next_indent + end + comments << current_token_value + next_token + found_comment = true + last = :comment + else + break + end + end + + [comments, newline_before_comment] + end + def skip_semicolons while semicolon? || space? next_token @@ -3030,12 +3298,14 @@ def empty_body?(body) def consume_token(kind) check kind - consume_token_value(current_token_value) + val = current_token_value + consume_token_value(val) next_token + val end def consume_token_value(value) - write value + write value unless in_doc_mode? # If the value has newlines, we need to adjust line and column number_of_lines = value.count("\n") @@ -3061,7 +3331,7 @@ def consume_op(value) if current_token_value != value bug "Expected op #{value}, not #{current_token_value}" end - write value + write value unless in_doc_mode? next_token end @@ -3380,17 +3650,8 @@ def maybe_indent(toggle, indent_size) end end - def capture_output - old_output = @output - @output = ''.dup - yield - result = @output - @output = old_output - result - end - def write(value) - @output << value + @output << value unless in_doc_mode? @last_was_newline = false @last_was_heredoc = false @column += value.size @@ -3575,6 +3836,9 @@ def next_token @tokens.pop if (newline? || comment?) && !@heredocs.empty? + if in_doc_mode? + return + end flush_heredocs end @@ -3786,4 +4050,33 @@ def remove_lines_before_inline_declarations def result @output end + + def capture_output + old_doc_mode = @in_doc_mode + @in_doc_mode = false + + old_output = @output + @output = ''.dup + + yield + + result = @output + @output = old_output + + @in_doc_mode = old_doc_mode + + result + end + + def in_doc_mode? + @in_doc_mode == true + end + + def with_doc_mode + old_val = @in_doc_mode + @in_doc_mode = true + result = yield + @in_doc_mode = old_val + result + end end diff --git a/lib/rufo/settings.rb b/lib/rufo/settings.rb index cad4236a..ee45a10d 100644 --- a/lib/rufo/settings.rb +++ b/lib/rufo/settings.rb @@ -4,13 +4,22 @@ module Rufo::Settings align_case_when: [false, true], align_chained_calls: [false, true], trailing_commas: [true, false], + print_width: (1..Float::INFINITY), + } + + DEFAULT_VALUES = { + print_width: 80, } attr_accessor *OPTIONS.keys def init_settings(options) OPTIONS.each do |name, valid_options| - default = valid_options.first + if DEFAULT_VALUES.has_key?(name) + default = DEFAULT_VALUES[name] + else + default = valid_options.first + end value = options.fetch(name, default) unless valid_options.include?(value) $stderr.puts "Invalid value for #{name}: #{value.inspect}. Valid " \ diff --git a/spec/lib/rufo/doc_builder_spec.rb b/spec/lib/rufo/doc_builder_spec.rb new file mode 100644 index 00000000..c5da5f26 --- /dev/null +++ b/spec/lib/rufo/doc_builder_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +RSpec.describe Rufo::DocBuilder do + it 'allows valid documents to be created' do + parts = ["something", {type: :symbol_yo}] + expect(described_class.concat(parts)).to eql({ + type: :concat, + parts: parts, + }) + end + + it 'raises an error if a document is not the correct type' do + invalid_docs = [[], 1, {}, {type: "string"}] + invalid_docs.each do |doc| + expect { described_class.concat([doc]) }.to raise_error( + Rufo::DocBuilder::InvalidDocError + ) + end + end +end diff --git a/spec/lib/rufo/doc_printer_spec.rb b/spec/lib/rufo/doc_printer_spec.rb new file mode 100644 index 00000000..14e9402f --- /dev/null +++ b/spec/lib/rufo/doc_printer_spec.rb @@ -0,0 +1,199 @@ +require 'spec_helper' + +RSpec.describe Rufo::DocPrinter do + B = Rufo::DocBuilder + + def print(doc, options = nil) + print_doc(doc, options)[:formatted] + end + + def print_doc(doc, options = nil) + options ||= {} + options = {print_width: 80}.merge!(options) + described_class.print_doc_to_string(doc, options) + end + + it 'prints concatenations' do + doc = B.concat(["a", " = ", "1"]) + expect(print(doc)).to eql("a = 1") + end + + it 'prints lines' do + doc = B.group(B.concat(["asd", B::LINE, "123"])) + expect(print(doc)).to eql("asd 123") + expect(print(doc, print_width: 4)).to eql("asd\n123") + expect(print(doc, print_width: 2)).to eql("asd\n123") + end + + it 'prints soft lines' do + doc = B.group(B.concat(["asd", B::SOFT_LINE, "123"])) + expect(print(doc)).to eql("asd123") + expect(print(doc, print_width: 4)).to eql("asd\n123") + end + + it 'prints hard lines' do + doc = B.group(B.concat(["asd", B::HARD_LINE, "123"])) + expect(print(doc)).to eql("asd\n123") + expect(print(doc, print_width: 4)).to eql("asd\n123") + end + + it 'prints literal lines' do + doc = B.group(B.concat(["asd", B::LITERAL_LINE, "123"])) + expect(print(doc)).to eql("asd\n123") + expect(print(doc, print_width: 4)).to eql("asd\n123") + end + + it 'prints indents' do + doc = B.indent(B.concat([B::HARD_LINE, "abc123"])) + expect(print(doc)).to eql("\n abc123") + end + + it 'does not print indents for literal lines' do + doc = B.indent(B.concat([B::LITERAL_LINE, "abc123"])) + expect(print(doc)).to eql("\nabc123") + end + + it "prints alignments" do + doc = B.align(1, B.concat([B::HARD_LINE, "abc123"])) + expect(print(doc)).to eql("\n abc123") + end + + describe "printing of groups" do + it 'prints simple groups' do + doc = B.group(B.concat(["asd", B::SOFT_LINE, "123"])) + expect(print(doc)).to eql('asd123') + end + + it 'prints groups that should break' do + doc = B.group(B.concat(["asd", B::SOFT_LINE, "123"]), should_break: true) + expect(print(doc)).to eql("asd\n123") + end + end + + it "prints conditional groups" do + doc_states = B.conditional_group([B.concat(["asd"]), B.concat(["as"])]) + expect(print(doc_states)).to eql("asd") + expect(print(doc_states, print_width: 2)).to eql("as") + end + + it "prints fill" do + doc = B.fill(["asd", B::LINE, "123", B::LINE, "qwe"]) + expect(print(doc)).to eql("asd 123 qwe") + expect(print(doc, print_width: 8)).to eql("asd 123\nqwe") + expect(print(doc, print_width: 4)).to eql("asd\n123\nqwe") + end + + it "prints line suffixes" do + doc = B.concat(["asd", B.line_suffix("qwe"), "123", B::HARD_LINE]) + expect(print(doc)).to eql("asd123qwe\n") + end + + it "prints joins" do + doc = B.join(", ", ["asd", "123"]) + expect(print(doc)).to eql("asd, 123") + end + + it "prints cursor positions" do + doc = B.concat(["asd", B::CURSOR, "123"]) + expect(print_doc(doc)).to eql(formatted: "asd123", cursor: 3) + end + + context "array expression" do + let(:code_filled) { + "[1, 2, 3, 4, 5]" + } + let(:code_broken) { + "[\n 1,\n 2,\n 3,\n 4,\n 5,\n]" + } + let(:inner_doc) { + B.concat([ + B::SOFT_LINE, + B.join( + B.concat([",", B::LINE]), + ["1", "2", "3", "4", B.concat(["5", B.if_break(",", "")])] + ), + ]) + } + let(:doc) { + B.group( + B.concat(["[", B.indent(inner_doc), B::SOFT_LINE, "]"]) + ) + } + + it 'formats correctly' do + expect(print(doc, print_width: 10)).to eql(code_broken) + expect(print(doc, print_width: 80)).to eql(code_filled) + end + end + + context 'array with comment' do + let(:doc) { + B.group( + B.concat([ + "[", + B.indent( + B.concat([ + B::SOFT_LINE, + B.join( + B.concat([",", B::LINE]), + [ + B.concat(["1", B.line_suffix(' # a comment'), B::LINE_SUFFIX_BOUNDARY]), + ] + ), + ]) + ), + B::SOFT_LINE, + "]", + ]), + should_break: true, + ) + } + + it 'formats array with comment' do + expect(print(doc, print_width: 80)).to eql("[\n 1 # a comment\n]") + end + end + + context 'array with heredoc and comment' do + let(:doc) { + B.group( + B.concat([ + "[", + B.indent( + B.concat([ + B::SOFT_LINE, + B.join( + B.concat([",", B::LINE]), + [B.concat(['1', B.if_break(',', '')])] + ), + B::LINE, + B.concat([ + "<<-EOF", + ",", + B.line_suffix(' # a comment'), + B::LINE_SUFFIX_BOUNDARY, + 'heredoc contents', + B::LINE, + 'EOF', + ]), + B::LINE, + B.join( + B.concat([",", B::LINE]), + [B.concat(['2', B.if_break(',', '')])] + ), + ]) + ), + B::SOFT_LINE, + "]", + ]), + should_break: true, + ) + } + + it 'formats array with comment' do + expect(print(doc, print_width: 80)).to eql( + "[\n 1,\n <<-EOF, # a comment\n heredoc contents\n EOF\n 2,\n]" + ) + end + end +end diff --git a/spec/lib/rufo/formatter_source_specs/align_comments.rb.spec b/spec/lib/rufo/formatter_source_specs/align_comments.rb.spec index 116ff455..dd5b46b3 100644 --- a/spec/lib/rufo/formatter_source_specs/align_comments.rb.spec +++ b/spec/lib/rufo/formatter_source_specs/align_comments.rb.spec @@ -168,8 +168,8 @@ bar = 2 # baz #~# EXPECTED [ - 1, # foo - 234, # bar + 1, # foo + 234, # bar ] #~# ORIGINAL @@ -182,8 +182,8 @@ bar = 2 # baz #~# EXPECTED [ - 1, # foo - 234, # bar + 1, # foo + 234, # bar ] #~# ORIGINAL diff --git a/spec/lib/rufo/formatter_source_specs/align_hash_keys.rb.spec b/spec/lib/rufo/formatter_source_specs/align_hash_keys.rb.spec index c712cbdb..450d949e 100644 --- a/spec/lib/rufo/formatter_source_specs/align_hash_keys.rb.spec +++ b/spec/lib/rufo/formatter_source_specs/align_hash_keys.rb.spec @@ -120,9 +120,7 @@ end { 1 => 2, - 345 => [ - 4, - ], + 345 => [4], } #~# ORIGINAL @@ -138,9 +136,7 @@ end { 1 => 2, - foo: [ - 4, - ], + foo: [4], } #~# ORIGINAL @@ -152,9 +148,7 @@ foo 1, bar: [ #~# EXPECTED -foo 1, bar: [ - 2, - ], +foo 1, bar: [2], baz: 3 #~# ORIGINAL diff --git a/spec/lib/rufo/formatter_source_specs/array_literal.rb.spec b/spec/lib/rufo/formatter_source_specs/array_literal.rb.spec index 25f04a80..bb51be19 100644 --- a/spec/lib/rufo/formatter_source_specs/array_literal.rb.spec +++ b/spec/lib/rufo/formatter_source_specs/array_literal.rb.spec @@ -31,6 +31,7 @@ [1, 2] #~# ORIGINAL +#~# print_width: 4 [ 1 , 2 ] @@ -38,10 +39,12 @@ #~# EXPECTED [ - 1, 2, + 1, + 2, ] #~# ORIGINAL +#~# print_width: 4 [ 1 , 2, ] @@ -49,10 +52,12 @@ #~# EXPECTED [ - 1, 2, + 1, + 2, ] #~# ORIGINAL +#~# print_width: 4 [ 1 , 2 , @@ -61,11 +66,14 @@ #~# EXPECTED [ - 1, 2, - 3, 4, + 1, + 2, + 3, + 4, ] #~# ORIGINAL +#~# print_width: 4 [ 1 , @@ -100,11 +108,12 @@ #~# EXPECTED [ - 1, # comment + 1, # comment 2, ] #~# ORIGINAL +#~# print_width: 4 [ 1 , 2, 3, @@ -112,9 +121,12 @@ #~# EXPECTED -[1, - 2, 3, - 4] +[ + 1, + 2, + 3, + 4, +] #~# ORIGINAL @@ -124,9 +136,7 @@ #~# EXPECTED -[1, - 2, 3, - 4] +[1, 2, 3, 4] #~# ORIGINAL @@ -137,9 +147,7 @@ #~# EXPECTED -[1, - 2, 3, - 4] +[1, 2, 3, 4] #~# ORIGINAL @@ -150,9 +158,11 @@ #~# EXPECTED -[1, - 2, 3, - 4 # foo +[ + 1, + 2, + 3, + 4, # foo ] #~# ORIGINAL @@ -165,9 +175,7 @@ #~# EXPECTED begin - [ - 1, 2, - ] + [1, 2] end #~# ORIGINAL @@ -243,6 +251,7 @@ x = [{ ] #~# ORIGINAL +#~# print_width: 5 [ *a, @@ -257,6 +266,7 @@ x = [{ ] #~# ORIGINAL +#~# print_width: 5 [ 1, *a, @@ -266,6 +276,63 @@ x = [{ #~# EXPECTED [ - 1, *a, + 1, + *a, b, ] + +#~# ORIGINAL nested_array_with_brackets + +[([1,2]), ([3,4]), ([5,6])] + +#~# EXPECTED + +[([1, 2]), ([3, 4]), ([5, 6])] + +#~# ORIGINAL array_with_method_call + +[a(1)] + +#~# EXPECTED + +[a(1)] + +#~# ORIGINAL array_with_ternary + +[true ? 1 : 2] + +#~# EXPECTED + +[true ? 1 : 2] + +#~# ORIGINAL array_with_indexing_element + +[a[:b]] + +#~# EXPECTED + +[a[:b]] + +#~# ORIGINAL array_with_symbol + +[:a] + +#~# EXPECTED + +[:a] + +#~# ORIGINAL nested_array_splat + +[[], *a] + +#~# EXPECTED + +[[], *a] + +#~# ORIGINAL nested_different_array_types + +[%w[( )]] + +#~# EXPECTED + +[%w[( )]] diff --git a/spec/lib/rufo/formatter_source_specs/calls_with_receiver.rb.spec b/spec/lib/rufo/formatter_source_specs/calls_with_receiver.rb.spec index c8bfaba7..d126878f 100644 --- a/spec/lib/rufo/formatter_source_specs/calls_with_receiver.rb.spec +++ b/spec/lib/rufo/formatter_source_specs/calls_with_receiver.rb.spec @@ -160,9 +160,7 @@ foo.bar(1) #~# EXPECTED foo.bar(1) - .baz([ - 2, - ]) + .baz([2]) #~# ORIGINAL @@ -227,6 +225,7 @@ foo.bar( ) #~# ORIGINAL +#~# print_width: 5 foo 1, [ 2, @@ -237,10 +236,9 @@ foo 1, [ #~# EXPECTED foo 1, [ - 2, - - 3, -] + 2, + 3, + ] #~# ORIGINAL diff --git a/spec/lib/rufo/formatter_source_specs/method_calls.rb.spec b/spec/lib/rufo/formatter_source_specs/method_calls.rb.spec index ccc5a16e..0ed4ca9f 100644 --- a/spec/lib/rufo/formatter_source_specs/method_calls.rb.spec +++ b/spec/lib/rufo/formatter_source_specs/method_calls.rb.spec @@ -497,9 +497,7 @@ foo 1, [ #~# EXPECTED -foo 1, [ - 1, -] +foo 1, [1] #~# ORIGINAL @@ -512,10 +510,10 @@ EOF #~# EXPECTED foo 1, [ - <<-EOF, + <<-EOF, bar EOF -] + ] #~# ORIGINAL @@ -591,9 +589,7 @@ foo 1, [ #~# EXPECTED -foo 1, [ - 2, - ] +foo 1, [2] #~# ORIGINAL @@ -603,9 +599,7 @@ foo 1, [ #~# EXPECTED -foo 1, [ - 2, -] +foo 1, [2] #~# ORIGINAL @@ -687,9 +681,7 @@ foo([ #~# EXPECTED -foo([ - 1, - ]) +foo([1]) #~# ORIGINAL @@ -702,9 +694,7 @@ end #~# EXPECTED begin - foo([ - 1, - ]) + foo([1]) end #~# ORIGINAL @@ -715,9 +705,7 @@ end #~# EXPECTED -(a b).c([ - 1, - ]) +(a b).c([1]) #~# ORIGINAL @@ -730,3 +718,51 @@ foobar 1, foobar 1, "foo bar" + +#~# ORIGINAL method_array_arg_print_width_break +#~# print_width: 8 + +(a b).c([ + 1, + 2, + 3, +]) + +#~# EXPECTED + +(a b).c([ + 1, + 2, + 3, +]) + +#~# ORIGINAL method_array_arg_print_width_fill +#~# print_width: 9 + +(a b).c([ + 1, + 2, + 3, +]) + +#~# EXPECTED + +(a b).c([1, 2, 3]) + +#~# ORIGINAL indent_bug +#~# print_width: 1 +#~# PENDING + +a([ + b( + something + ), +]) + +#~# EXPECTED + +a([ + b( + something + ), +]) diff --git a/spec/lib/rufo/formatter_source_specs/trailing_commas.rb.spec b/spec/lib/rufo/formatter_source_specs/trailing_commas.rb.spec index 92489682..b60543d9 100644 --- a/spec/lib/rufo/formatter_source_specs/trailing_commas.rb.spec +++ b/spec/lib/rufo/formatter_source_specs/trailing_commas.rb.spec @@ -1,5 +1,6 @@ #~# ORIGINAL #~# trailing_commas: true +#~# print_width: 5 [ 1, @@ -15,6 +16,7 @@ #~# ORIGINAL #~# trailing_commas: false +#~# print_width: 5 [ 1, @@ -30,6 +32,7 @@ #~# ORIGINAL #~# trailing_commas: true +#~# print_width: 5 [ 1, @@ -45,6 +48,7 @@ #~# ORIGINAL #~# trailing_commas: false +#~# print_width: 5 [ 1, @@ -236,6 +240,7 @@ foo( #~# ORIGINAL #~# trailing_commas: true +#~# print_width: 5 [ 1 , 2 ] @@ -243,11 +248,13 @@ foo( #~# EXPECTED [ - 1, 2, + 1, + 2, ] #~# ORIGINAL #~# trailing_commas: true +#~# print_width: 5 [ 1 , 2, ] @@ -255,11 +262,13 @@ foo( #~# EXPECTED [ - 1, 2, + 1, + 2, ] #~# ORIGINAL #~# trailing_commas: true +#~# print_width: 5 [ 1 , 2 , @@ -268,12 +277,15 @@ foo( #~# EXPECTED [ - 1, 2, - 3, 4, + 1, + 2, + 3, + 4, ] #~# ORIGINAL #~# trailing_commas: true +#~# print_width: 5 [ 1 , @@ -310,12 +322,13 @@ foo( #~# EXPECTED [ - 1, # comment + 1, # comment 2, ] #~# ORIGINAL #~# trailing_commas: true +#~# print_width: 5 [ 1 , 2, 3, @@ -323,12 +336,16 @@ foo( #~# EXPECTED -[1, - 2, 3, - 4] +[ + 1, + 2, + 3, + 4, +] #~# ORIGINAL #~# trailing_commas: true +#~# print_width: 5 [ 1 , 2, 3, @@ -336,12 +353,16 @@ foo( #~# EXPECTED -[1, - 2, 3, - 4] +[ + 1, + 2, + 3, + 4, +] #~# ORIGINAL #~# trailing_commas: true +#~# print_width: 5 [ 1 , 2, 3, @@ -350,9 +371,12 @@ foo( #~# EXPECTED -[1, - 2, 3, - 4] +[ + 1, + 2, + 3, + 4, +] #~# ORIGINAL #~# trailing_commas: true @@ -364,13 +388,16 @@ foo( #~# EXPECTED -[1, - 2, 3, - 4 # foo +[ + 1, + 2, + 3, + 4, # foo ] #~# ORIGINAL #~# trailing_commas: true +#~# print_width: 5 begin [ @@ -381,7 +408,8 @@ foo( begin [ - 1, 2, + 1, + 2, ] end diff --git a/spec/lib/rufo/formatter_spec.rb b/spec/lib/rufo/formatter_spec.rb index b7248952..94d5a918 100644 --- a/spec/lib/rufo/formatter_spec.rb +++ b/spec/lib/rufo/formatter_spec.rb @@ -29,7 +29,8 @@ def assert_source_specs(source_specs) when line =~ /^#~# PENDING$/ current_test[:pending] = true when line =~ /^#~# (.+)$/ - current_test[:options] = eval("{ #{$~[1]} }") + current_options = current_test[:options] || {} + current_test[:options] = current_options.merge(eval("{ #{$~[1]} }")) when current_test[:expected] current_test[:expected] += line when current_test[:original]