diff --git a/.gitignore b/.gitignore index f6e4e0d..06ef046 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ test.rb +profile.json diff --git a/Gemfile b/Gemfile index 37909f4..b0c29c2 100644 --- a/Gemfile +++ b/Gemfile @@ -5,3 +5,5 @@ source "https://rubygems.org" gemspec gem "prism" +gem "vernier" +gem "profile-viewer" diff --git a/Gemfile.lock b/Gemfile.lock index a483e4f..c12fcfc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,8 +8,14 @@ PATH GEM remote: https://rubygems.org/ specs: + optparse (0.5.0) prettier_print (1.2.1) prism (0.30.0) + profile-viewer (0.0.2) + optparse + webrick + vernier (1.0.1) + webrick (1.8.1) PLATFORMS arm64-darwin-22 @@ -17,7 +23,9 @@ PLATFORMS DEPENDENCIES prism + profile-viewer syntax_tree-prism! + vernier BUNDLED WITH 2.4.13 diff --git a/bin/bench b/bin/bench new file mode 100755 index 0000000..4f17212 --- /dev/null +++ b/bin/bench @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "benchmark" + +$:.unshift(File.expand_path("../lib", __dir__)) +require "syntax_tree/prism" + +filepaths = Dir[ARGV.first] +puts "Formatting #{filepaths.length} files" + +Benchmark.bmbm do |x| + x.report("format") do + filepaths.each { |filepath| Prism.parse_file(filepath).format } + end +end diff --git a/bin/format b/bin/format index 00daa92..a8a5885 100755 --- a/bin/format +++ b/bin/format @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true require "bundler/setup" diff --git a/bin/profile b/bin/profile new file mode 100755 index 0000000..7b8948b --- /dev/null +++ b/bin/profile @@ -0,0 +1,13 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "vernier" + +$:.unshift(File.expand_path("../lib", __dir__)) +require "syntax_tree/prism" + +results = Dir[ARGV.first].map { |filepath| Prism.parse_file(filepath) } +puts "Profiling #{results.length} files" + +Vernier.trace(out: "profile.json") { results.each(&:format) } diff --git a/lib/syntax_tree/prism.rb b/lib/syntax_tree/prism.rb index 434d5fa..6acf956 100644 --- a/lib/syntax_tree/prism.rb +++ b/lib/syntax_tree/prism.rb @@ -1,32 +1,231 @@ # frozen_string_literal: true -require_relative "prism/prettier_print" require "prism" module Prism - class Format < PrettierPrint + # Philip Wadler, A prettier printer, March 1998 + # https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf + class Format + # A node in the print tree that represents aligning nested nodes to a + # certain prefix width or string. + class Align + attr_reader :indent, :contents + + def initialize(indent, contents) + @indent = indent + @contents = contents + end + + def type + :align + end + end + + # A node in the print tree that represents a place in the buffer that the + # content can be broken onto multiple lines. + class Breakable + attr_reader :separator, :width, :force, :indent + + def initialize(separator, width, force, indent) + @separator = separator + @width = width + @force = force + @indent = indent + end + + def type + :breakable + end + end + + # Below here are the most common combination of options that are created + # when creating new breakables. They are here to cut down on some + # allocations. + BREAKABLE_SPACE = Breakable.new(" ", 1, false, true).freeze + BREAKABLE_EMPTY = Breakable.new("", 0, false, true).freeze + BREAKABLE_FORCE = Breakable.new(" ", 1, true, true).freeze + BREAKABLE_RETURN = Breakable.new(" ", 1, true, false).freeze + + # A node in the print tree that forces the surrounding group to print out in + # the "break" mode as opposed to the "flat" mode. Useful for when you need + # to force a newline into a group. + class BreakParent + def type + :break_parent + end + end + + # Since there's really no difference in these instances, just using the same + # one saves on some allocations. + BREAK_PARENT = BreakParent.new.freeze + + # A node in the print tree that represents a group of items which the printer + # should try to fit onto one line. This is the basic command to tell the + # printer when to break. Groups are usually nested, and the printer will try + # to fit everything on one line, but if it doesn't fit it will break the + # outermost group first and try again. It will continue breaking groups until + # everything fits (or there are no more groups to break). + class Group + attr_reader :contents, :break + + def initialize(contents) + @contents = contents + @break = false + end + + def break! + @break = true + end + + def type + :group + end + end + + # A node in the print tree that represents printing one thing if the + # surrounding group node is broken and another thing if the surrounding group + # node is flat. + class IfBreak + attr_reader :break_contents, :flat_contents + + def initialize(break_contents, flat_contents) + @break_contents = break_contents + @flat_contents = flat_contents + end + + def type + :if_break + end + end + + # A node in the print tree that is a variant of the Align node that indents + # its contents by one level. + class Indent + attr_reader :contents + + def initialize(contents) + @contents = contents + end + + def type + :indent + end + end + + # A node in the print tree that has its own special buffer for implementing + # content that should flush before any newline. + # + # Useful for implementating trailing content, as it's not always practical to + # constantly check where the line ends to avoid accidentally printing some + # content after a line suffix node. + class LineSuffix + attr_reader :priority, :contents + + def initialize(priority, contents) + @priority = priority + @contents = contents + end + + def type + :line_suffix + end + end + + # A node in the print tree that represents trimming all of the indentation of + # the current line, in the rare case that you need to ignore the indentation + # that you've already created. This node should be placed after a Breakable. + class Trim + def type + :trim + end + end + + # Since all of the instances here are the same, we can reuse the same one to + # cut down on allocations. + TRIM = Trim.new.freeze + + # Refine string so that we can consistently call #type in case statements and + # treat all of the nodes in the tree as homogenous. + using Module.new { + refine String do + def type + :string + end + end + } + + # There are two modes in printing, break and flat. When we're in break mode, + # any lines will use their newline, any if-breaks will use their break + # contents, etc. + MODE_BREAK = 1 + + # This is another print mode much like MODE_BREAK. When we're in flat mode, we + # attempt to print everything on one line until we either hit a broken group, + # a forced line, or the maximum width. + MODE_FLAT = 2 + + # The default indentation for printing is zero, assuming that the code starts + # at the top level. That can be changed if desired to start from a different + # indentation level. + DEFAULT_INDENTATION = 0 + COMMENT_PRIORITY = 1 HEREDOC_PRIORITY = 2 attr_reader :source, :stack attr_reader :quote - def initialize(source, *args, quote: "\"") + # This is an output buffer that contains the various parts of the printed + # source code. + attr_reader :buffer + + # The maximum width of a line, before it is separated in to a newline. + attr_reader :maxwidth + + # The stack of groups that are being printed. + attr_reader :groups + + # The current array of contents that calls to methods that generate print + # tree nodes will append to. + attr_reader :target + + def initialize(source, buffer = [], maxwidth = 80, quote = "\"") @source = source @stack = [] - + @buffer = buffer + @maxwidth = maxwidth @quote = quote - super(*args) + contents = [] + @groups = [Group.new(contents)] + @target = contents + end + + # The main API for this visitor. + def format + flush + buffer.join + end + + # A convenience method used by a lot of the print tree node builders that + # temporarily changes the target that the builders will append to. + def with_target(target) + previous_target, @target = @target, target + yield + @target = previous_target end + # -------------------------------------------------------------------------- + # Visit methods + # -------------------------------------------------------------------------- + # alias $foo $bar # ^^^^^^^^^^^^^^^ def visit_alias_global_variable_node(node) group do text("alias ") visit(node.new_name) - nest(6) do + align(6) do breakable_space visit(node.old_name) end @@ -49,7 +248,7 @@ def visit_alias_method_node(node) visit(new_name) end - nest(6) do + align(6) do breakable_space if old_name.is_a?(SymbolNode) text(old_name.value) @@ -299,21 +498,21 @@ def visit_begin_node(node) visit(statements) unless statements.nil? unless rescue_clause.nil? - nest(-2) do + align(-2) do breakable_force visit(rescue_clause) end end unless else_clause.nil? - nest(-2) do + align(-2) do breakable_force visit(else_clause) end end unless ensure_clause.nil? - nest(-2) do + align(-2) do breakable_force visit(ensure_clause) end @@ -459,7 +658,7 @@ def visit_block_parameter_node(node) visit_location(node.operator_loc) if name - nest(1) do + align(1) do breakable_empty text(name.name) end @@ -720,7 +919,7 @@ def visit_call_node(node) *rest, last = chain doc = - nest(0) do + align(0) do visit(rest.shift) unless rest.first.is_a?(CallNode) rest.each do |node| @@ -933,7 +1132,7 @@ def visit_call_node(node) end end - nest(position > (maxwidth / 2) ? 0 : position) do + align(position > (maxwidth / 2) ? 0 : position) do seplist(arguments) { |argument| visit(argument) } end end @@ -992,7 +1191,7 @@ def visit_case_node(node) if predicate text(" ") - nest(5) { visit(predicate) } + align(5) { visit(predicate) } end end @@ -1358,7 +1557,7 @@ def visit_flip_flop_node(node) if right indent do - breakable + breakable_space visit(right) end end @@ -1539,7 +1738,7 @@ def visit_if_node(node) group do visit_location(if_keyword_loc) text(" ") - nest(6) { visit(node.predicate) } + align(6) { visit(node.predicate) } if consequent visit_body(statements, [], true) @@ -1632,7 +1831,7 @@ def visit_if_node(node) private def visit_ternary_node_break(node, truthy, falsy) group do text("if ") - nest(3) { visit(node.predicate) } + align(3) { visit(node.predicate) } end indent do @@ -1660,7 +1859,7 @@ def visit_if_node(node) group do visit_location(node.if_keyword_loc) text(" ") - nest(3) { visit(node.predicate) } + align(3) { visit(node.predicate) } end if consequent @@ -1706,7 +1905,7 @@ def visit_in_node(node) group do text("in ") - nest(3) { visit(node.pattern) } + align(3) { visit(node.pattern) } if statements indent do @@ -2235,7 +2434,7 @@ def visit_nil_node(node) def visit_no_keywords_parameter_node(node) group do visit_location(node.operator_loc) - nest(2) do + align(2) do breakable_empty text("nil") end @@ -2291,7 +2490,7 @@ def visit_parameters_node(node) parameters = node.compact_child_nodes implicit_rest = parameters.pop if parameters.last.is_a?(ImplicitRestNode) - nest(0) do + align(0) do seplist(parameters) { |parameter| visit(parameter) } visit(implicit_rest) if implicit_rest end @@ -2476,7 +2675,7 @@ def visit_rescue_node(node) if exceptions.any? text(" ") - nest(7) { seplist(exceptions) { |exception| visit(exception) } } + align(7) { seplist(exceptions) { |exception| visit(exception) } } elsif reference.nil? text(" StandardError") end @@ -2522,7 +2721,7 @@ def visit_rest_parameter_node(node) if name group do visit_location(node.operator_loc) - nest(1) do + align(1) do breakable_empty text(node.name.name) end @@ -2615,7 +2814,7 @@ def visit_splat_node(node) text("*") operator_loc.comments.each { |comment| visit_comment(comment) } - nest(1) do + align(1) do breakable_empty visit(expression) end @@ -2704,7 +2903,7 @@ def visit_super_node(node) text(")") elsif arguments.any? text(" ") - nest(6) { seplist(arguments) { |argument| visit(argument) } } + align(6) { seplist(arguments) { |argument| visit(argument) } } end if block @@ -2731,7 +2930,7 @@ def visit_true_node(node) def visit_undef_node(node) group do text("undef ") - nest(6) do + align(6) do seplist(node.names) do |name| if name.is_a?(SymbolNode) text(name.value) @@ -2786,7 +2985,7 @@ def visit_unless_node(node) group do visit_location(node.keyword_loc) text(" ") - nest(3) { visit(node.predicate) } + align(3) { visit(node.predicate) } end if consequent @@ -2842,7 +3041,7 @@ def visit_until_node(node) private def visit_until_node_break(node) visit_location(node.keyword_loc) text(" ") - nest(6) { visit(node.predicate) } + align(6) { visit(node.predicate) } visit_body(node.statements, node.closing_loc&.comments || [], false) breakable_space text("end") @@ -2866,7 +3065,7 @@ def visit_when_node(node) group do group do text("when ") - nest(5) do + align(5) do seplist(conditions, -> { group { comma_breakable } }) do |condition| visit(condition) end @@ -2931,7 +3130,7 @@ def visit_while_node(node) private def visit_while_node_break(node) visit_location(node.keyword_loc) text(" ") - nest(6) { visit(node.predicate) } + align(6) { visit(node.predicate) } visit_body(node.statements, node.closing_loc&.comments || [], false) breakable_space text("end") @@ -2987,9 +3186,9 @@ def visit_yield_node(node) private - ############################################################################ - # Helper methods # - ############################################################################ + # -------------------------------------------------------------------------- + # Helper methods + # -------------------------------------------------------------------------- # Returns whether or not the given statement is an access control statement. # Truthfully, we can't actually tell this for sure without performing method @@ -3141,9 +3340,9 @@ def quotes_normalize(content, enclosing) end end - ############################################################################ - # Visit methods # - ############################################################################ + # -------------------------------------------------------------------------- + # Visit helpers + # -------------------------------------------------------------------------- # Visit a node and format it, including any comments that are found around # it that are attached to its location. @@ -3221,7 +3420,7 @@ def visit_comment(comment) break_parent end else - breakable + breakable_space text(comment.location.slice) break_parent end @@ -3262,14 +3461,11 @@ def visit_elements_spaced(elements, comments) # Visit a heredoc node, and format it with the given parts. def visit_heredoc(node, parts) - # This is a very specific behavior where you want to force a newline, but - # don't want to force the break parent. - separator = PrettierPrint::Breakable.new(" ", 1, true, false) - opening = node.opening - # If the heredoc is indented, then we're going to need to reintroduce the # indentation to the parts of the heredoc. indent = "" + + opening = node.opening if opening[2] == "~" parts.each do |part| if part.is_a?(StringNode) && !part.content.start_with?("\n") @@ -3283,7 +3479,7 @@ def visit_heredoc(node, parts) text(opening) line_suffix(HEREDOC_PRIORITY) do group do - target << separator + target << BREAKABLE_RETURN parts.each_with_index do |part, index| case part.type @@ -3295,13 +3491,13 @@ def visit_heredoc(node, parts) if first first = false else - target << separator + target << BREAKABLE_RETURN end text(line) end - target << separator if value.end_with?("\n") + target << BREAKABLE_RETURN if value.end_with?("\n") else text(indent) visit(part) @@ -3452,7 +3648,7 @@ def visit_prefix(operator_loc, value) if value group do visit_location(operator_loc) - nest(operator_loc.length) do + align(operator_loc.length) do breakable_empty visit(value) end @@ -3597,6 +3793,557 @@ def visit_write(operator_loc, value) end end end + + # -------------------------------------------------------------------------- + # Printing algorithm + # -------------------------------------------------------------------------- + + # Flushes all of the generated print tree onto the output buffer. + def flush + # First, get the root group, since we placed one at the top to begin with. + doc = groups.first + + # This represents how far along the current line we are. It gets reset + # back to 0 when we encounter a newline. + position = 0 + + # This is our command stack. A command consists of a triplet of an + # indentation level, the mode (break or flat), and a doc node. + commands = [[0, MODE_BREAK, doc]] + + # This is a small optimization boolean. It keeps track of whether or not + # when we hit a group node we should check if it fits on the same line. + should_remeasure = false + + # This is a separate command stack that includes the same kind of triplets + # as the commands variable. It is used to keep track of things that should + # go at the end of printed lines once the other doc nodes are accounted for. + # Typically this is used to implement comments. + line_suffixes = [] + + # This is a special sort used to order the line suffixes by both the + # priority set on the line suffix and the index it was in the original + # array. + line_suffix_sort = ->(line_suffix) do + [-line_suffix.last.priority, -line_suffixes.index(line_suffix)] + end + + # This is a linear stack instead of a mutually recursive call defined on + # the individual doc nodes for efficiency. + while (indent, mode, doc = commands.pop) + case doc.type + when :string + buffer << doc + position += doc.length + when :group + if mode == MODE_FLAT && !should_remeasure + next_mode = doc.break ? MODE_BREAK : MODE_FLAT + commands += doc.contents.reverse.map { |part| [indent, next_mode, part] } + else + should_remeasure = false + + if doc.break + commands += doc.contents.reverse.map { |part| [indent, MODE_BREAK, part] } + else + next_commands = doc.contents.reverse.map { |part| [indent, MODE_FLAT, part] } + + if fits?(next_commands, commands, maxwidth - position) + commands += next_commands + else + commands += next_commands.map { |command| command[1] = MODE_BREAK; command } + end + end + end + when :breakable + if mode == MODE_FLAT + if doc.force + # This line was forced into the output even if we were in flat mode, + # so we need to tell the next group that no matter what, it needs to + # remeasure because the previous measurement didn't accurately + # capture the entire expression (this is necessary for nested + # groups). + should_remeasure = true + else + buffer << doc.separator + position += doc.width + next + end + end + + # If there are any commands in the line suffix buffer, then we're going + # to flush them now, as we are about to add a newline. + if line_suffixes.any? + commands << [indent, mode, doc] + + line_suffixes.sort_by(&line_suffix_sort).each do |(indent, mode, doc)| + commands += doc.contents.reverse.map { |part| [indent, mode, part] } + end + + line_suffixes.clear + next + end + + if !doc.indent + buffer << "\n" + position = 0 + else + position -= trim!(buffer) + buffer << "\n" + buffer << " " * indent + position = indent + end + when :indent + next_indent = indent + 2 + commands += doc.contents.reverse.map { |part| [next_indent, mode, part] } + when :align + next_indent = indent + doc.indent + commands += doc.contents.reverse.map { |part| [next_indent, mode, part] } + when :trim + position -= trim!(buffer) + when :if_break + if mode == MODE_BREAK && doc.break_contents.any? + commands += doc.break_contents.reverse.map { |part| [indent, mode, part] } + elsif mode == MODE_FLAT && doc.flat_contents.any? + commands += doc.flat_contents.reverse.map { |part| [indent, mode, part] } + end + when :line_suffix + line_suffixes << [indent, mode, doc] + when :break_parent + # do nothing + else + # Special case where the user has defined some way to get an extra doc + # node that we don't explicitly support into the list. In this case + # we're going to assume it's 0-width and just append it to the output + # buffer. + # + # This is useful behavior for putting marker nodes into the list so that + # you can know how things are getting mapped before they get printed. + buffer << doc + end + + if commands.empty? && line_suffixes.any? + line_suffixes.sort_by(&line_suffix_sort).each do |(indent, mode, doc)| + commands.concat doc.contents.reverse.map { |part| [indent, mode, part] } + end + + line_suffixes.clear + end + end + end + + # This method returns a boolean as to whether or not the remaining commands + # fit onto the remaining space on the current line. If we finish printing + # all of the commands or if we hit a newline, then we return true. Otherwise + # if we continue printing past the remaining space, we return false. + def fits?(next_commands, rest_commands, remaining) + # This is the index in the remaining commands that we've handled so far. + # We reverse through the commands and add them to the stack if we've run + # out of nodes to handle. + rest_index = rest_commands.length + + # This is our stack of commands, very similar to the commands list in the + # print method. + commands = [*next_commands] + + # This is our output buffer, really only necessary to keep track of + # because we could encounter a Trim doc node that would actually add + # remaining space. + fit_buffer = [] + + while remaining >= 0 + if commands.empty? + return true if rest_index == 0 + + rest_index -= 1 + commands << rest_commands[rest_index] + next + end + + indent, mode, doc = commands.pop + + case doc.type + when :string + fit_buffer << doc + remaining -= doc.length + when :group + next_mode = doc.break ? MODE_BREAK : mode + commands += doc.contents.reverse.map { |part| [indent, next_mode, part] } + when :breakable + if mode == MODE_FLAT && !doc.force + fit_buffer << doc.separator + remaining -= doc.width + next + end + + return true + when :indent + next_indent = indent + 2 + commands += doc.contents.reverse.map { |part| [next_indent, mode, part] } + when :align + next_indent = indent + doc.indent + commands += doc.contents.reverse.map { |part| [next_indent, mode, part] } + when :trim + remaining += trim!(fit_buffer) + when :if_break + if mode == MODE_BREAK && doc.break_contents.any? + commands += doc.break_contents.reverse.map { |part| [indent, mode, part] } + elsif mode == MODE_FLAT && doc.flat_contents.any? + commands += doc.flat_contents.reverse.map { |part| [indent, mode, part] } + end + end + end + + false + end + + def trim!(buffer) + return 0 if buffer.empty? + + trimmed = 0 + + while buffer.any? && buffer.last.is_a?(String) && + buffer.last.match?(/\A[\t ]*\z/) + trimmed += buffer.pop.length + end + + if buffer.any? && buffer.last.is_a?(String) && !buffer.last.frozen? + length = buffer.last.length + buffer.last.gsub!(/[\t ]*\z/, "") + trimmed += length - buffer.last.length + end + + trimmed + end + + # -------------------------------------------------------------------------- + # Helper node builders + # -------------------------------------------------------------------------- + + # This method calculates the position of the text relative to the current + # indentation level when the doc has been printed. It's useful for + # determining how to align text to doc nodes that are already built into the + # tree. + def last_position(node) + queue = [node] + width = 0 + + while (doc = queue.shift) + case doc.type + when :string + width += doc.length + when :group, :indent, :align + queue = doc.contents + queue + when :breakable + width = 0 + when :if_break + queue = doc.break_contents + queue + end + end + + width + end + + # This method will remove any breakables from the list of contents so that + # no newlines are present in the output. If a newline is being forced into + # the output, the replace value will be used. + def remove_breaks(node, replace = "; ") + queue = [node] + + while (doc = queue.shift) + case doc.type + when :align, :indent, :group + doc.contents.map! { |child| remove_breaks_with(child, replace) } + queue += doc.contents + when :if_break + doc.flat_contents.map! { |child| remove_breaks_with(child, replace) } + queue += doc.flat_contents + end + end + end + + # Remove breaks from a subtree with the given replacement string. + def remove_breaks_with(doc, replace) + case doc.type + when :breakable + doc.force ? replace : doc.separator + when :if_break + Align.new(0, doc.flat_contents) + else + doc + end + end + + # Adds a separated list. + # The list is separated by comma with breakable space, by default. + # + # #seplist iterates the +list+ using +each+. + # It yields each object to the block given for #seplist. + # The procedure +separator_proc+ is called between each yields. + # + # If the iteration is zero times, +separator_proc+ is not called at all. + # + # If +separator_proc+ is nil or not given, + # +lambda { comma_breakable }+ is used. + # + # For example, following 3 code fragments has similar effect. + # + # q.seplist([1,2,3]) {|v| xxx v } + # + # q.seplist([1,2,3], lambda { q.comma_breakable }, :each) {|v| xxx v } + # + # xxx 1 + # q.comma_breakable + # xxx 2 + # q.comma_breakable + # xxx 3 + def seplist(list, sep = nil) # :yield: element + first = true + + list.each do |v| + if first + first = false + elsif sep + sep.call + else + comma_breakable + end + + yield(v) + end + end + + # -------------------------------------------------------------------------- + # Markers node builders + # -------------------------------------------------------------------------- + + # This says "you can break a line here if necessary", and a +width+\-column + # text +separator+ is inserted if a line is not broken at the point. + # + # If +separator+ is not specified, ' ' is used. + # + # If +width+ is not specified, +separator.length+ is used. You will have to + # specify this when +separator+ is a multibyte character, for example. + # + # By default, if the surrounding group is broken and a newline is inserted, + # the printer will indent the subsequent line up to the current level of + # indentation. You can disable this behavior with the +indent+ argument if + # that's not desired (rare). + # + # By default, when you insert a Breakable into the print tree, it only + # breaks the surrounding group when the group's contents cannot fit onto the + # remaining space of the current line. You can force it to break the + # surrounding group instead if you always want the newline with the +force+ + # argument. + # + # There are a few circumstances where you'll want to force the newline into + # the output but no insert a break parent (because you don't want to + # necessarily force the groups to break unless they need to). In this case + # you can pass `force: :skip_break_parent` to this method and it will not + # insert a break parent. + + # The vast majority of breakable calls you receive while formatting are a + # space in flat mode and a newline in break mode. Since this is so common, + # we have a method here to skip past unnecessary calculation. + def breakable_space + target << BREAKABLE_SPACE + end + + # Another very common breakable call you receive while formatting is an + # empty string in flat mode and a newline in break mode. Similar to + # breakable_space, this is here for avoid unnecessary calculation. + def breakable_empty + target << BREAKABLE_EMPTY + end + + # The final of the very common breakable calls you receive while formatting + # is the normal breakable space but with the addition of the break_parent. + def breakable_force + target << BREAKABLE_FORCE + break_parent + end + + # This is the same shortcut as breakable_force, except that it doesn't + # indent the next line. This is necessary if you're trying to preserve some + # custom formatting like a multi-line string. + def breakable_return + target << BREAKABLE_RETURN + break_parent + end + + # This inserts a BreakParent node into the print tree which forces the + # surrounding and all parent group nodes to break. + def break_parent + doc = BREAK_PARENT + target << doc + + groups.reverse_each do |group| + break if group.break + group.break! + end + end + + # A convenience method which is same as follows: + # + # text(",") + # breakable + def comma_breakable + text(",") + breakable_space + end + + # This inserts a Trim node into the print tree which, when printed, will + # clear all whitespace at the end of the output buffer. This is useful for + # the rare case where you need to delete printed indentation and force the + # next node to start at the beginning of the line. + def trim + target << TRIM + end + + # -------------------------------------------------------------------------- + # Container node builders + # -------------------------------------------------------------------------- + + # Increases left margin after newline with +indent+ for line breaks added in + # the block. + def align(indent) + contents = [] + doc = Align.new(indent, contents) + target << doc + + with_target(contents) { yield } + doc + end + + # Groups line break hints added in the block. The line break hints are all + # to be used or not. + # + # If +indent+ is specified, the method call is regarded as nested by + # align(indent) { ... }. + # + # If +open_object+ is specified, text(open_object, open_width) is + # called before grouping. If +close_object+ is specified, + # text(close_object, close_width) is called after grouping. + def group + contents = [] + doc = Group.new(contents) + + groups << doc + target << doc + + with_target(contents) { yield } + groups.pop + + doc + end + + # A small DSL-like object used for specifying the alternative contents to be + # printed if the surrounding group doesn't break for an IfBreak node. + class IfBreakBuilder + attr_reader :q, :flat_contents + + def initialize(q, flat_contents) + @q = q + @flat_contents = flat_contents + end + + def if_flat + q.with_target(flat_contents) { yield } + end + end + + # When we already know that groups are broken, we don't actually need to + # track the flat versions of the contents. So this builder version is + # effectively a no-op, but we need it to maintain the same API. The only + # thing this can impact is that if there's a forced break in the flat + # contents, then we need to propagate that break up the whole tree. + class IfFlatIgnore + attr_reader :q + + def initialize(q) + @q = q + end + + def if_flat + contents = [] + group = Group.new(contents) + + q.with_target(contents) { yield } + q.break_parent if group.break + end + end + + # Inserts an IfBreak node with the contents of the block being added to its + # list of nodes that should be printed if the surrounding node breaks. If it + # doesn't, then you can specify the contents to be printed with the #if_flat + # method used on the return object from this method. For example, + # + # q.if_break { q.text('do') }.if_flat { q.text('{') } + # + # In the example above, if the surrounding group is broken it will print + # 'do' and if it is not it will print '{'. + def if_break + break_contents = [] + flat_contents = [] + + doc = IfBreak.new(break_contents, flat_contents) + target << doc + + with_target(break_contents) { yield } + + if groups.last.break + IfFlatIgnore.new(self) + else + IfBreakBuilder.new(self, flat_contents) + end + end + + # This is similar to if_break in that it also inserts an IfBreak node into + # the print tree, however it's starting from the flat contents, and cannot + # be used to build the break contents. + def if_flat + if groups.last.break + contents = [] + group = Group.new(contents) + + with_target(contents) { yield } + break_parent if group.break + else + flat_contents = [] + doc = IfBreak.new(break_contents, flat_contents) + target << doc + + with_target(flat_contents) { yield } + doc + end + end + + # Very similar to the #nest method, this indents the nested content by one + # level by inserting an Indent node into the print tree. The contents of the + # node are determined by the block. + def indent + contents = [] + doc = Indent.new(contents) + target << doc + + with_target(contents) { yield } + doc + end + + # Inserts a LineSuffix node into the print tree. The contents of the node + # are determined by the block. + def line_suffix(priority) + contents = [] + doc = LineSuffix.new(priority, contents) + target << doc + + with_target(contents) { yield } + doc + end + + # Push a value onto the output target. + def text(value) + target << value + end end # This is a special node that we insert into the syntax tree to represent the @@ -3628,8 +4375,7 @@ def format end root.accept(formatter) - formatter.flush - formatter.buffer.join + formatter.format end end end diff --git a/lib/syntax_tree/prism/prettier_print.rb b/lib/syntax_tree/prism/prettier_print.rb deleted file mode 100644 index f68bdd3..0000000 --- a/lib/syntax_tree/prism/prettier_print.rb +++ /dev/null @@ -1,897 +0,0 @@ -# frozen_string_literal: true -# -# This class implements a pretty printing algorithm. It finds line breaks and -# nice indentations for grouped structure. -# -# By default, the class assumes that primitive elements are strings and each -# byte in the strings is a single column in width. But it can be used for other -# situations by giving suitable arguments for some methods: -# -# * newline object and space generation block for PrettierPrint.new -# * optional width argument for PrettierPrint#text -# * PrettierPrint#breakable -# -# There are several candidate uses: -# * text formatting using proportional fonts -# * multibyte characters which has columns different to number of bytes -# * non-string formatting -# -# == Usage -# -# To use this module, you will need to generate a tree of print nodes that -# represent indentation and newline behavior before it gets sent to the printer. -# Each node has different semantics, depending on the desired output. -# -# The most basic node is a Text node. This represents plain text content that -# cannot be broken up even if it doesn't fit on one line. You would create one -# of those with the text method, as in: -# -# PrettierPrint.format { |q| q.text('my content') } -# -# No matter what the desired output width is, the output for the snippet above -# will always be the same. -# -# If you want to allow the printer to break up the content on the space -# character when there isn't enough width for the full string on the same line, -# you can use the Breakable and Group nodes. For example: -# -# PrettierPrint.format do |q| -# q.group do -# q.text("my") -# q.breakable -# q.text("content") -# end -# end -# -# Now, if everything fits on one line (depending on the maximum width specified) -# then it will be the same output as the first example. If, however, there is -# not enough room on the line, then you will get two lines of output, one for -# the first string and one for the second. -# -# There are other nodes for the print tree as well, described in the -# documentation below. They control alignment, indentation, conditional -# formatting, and more. -# -# == References -# Christian Lindig, Strictly Pretty, March 2000 -# https://lindig.github.io/papers/strictly-pretty-2000.pdf -# -# Philip Wadler, A prettier printer, March 1998 -# https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf -# -class PrettierPrint - # A node in the print tree that represents aligning nested nodes to a certain - # prefix width or string. - class Align - attr_reader :indent, :contents - - def initialize(indent, contents) - @indent = indent - @contents = contents - end - - def type - :align - end - end - - # A node in the print tree that represents a place in the buffer that the - # content can be broken onto multiple lines. - class Breakable - attr_reader :separator, :width - - def initialize( - separator = " ", - width = separator.length, - force = false, - indent = true - ) - @separator = separator - @width = width - @force = force - @indent = indent - end - - def force? - @force - end - - def indent? - @indent - end - - def type - :breakable - end - end - - # Below here are the most common combination of options that are created when - # creating new breakables. They are here to cut down on some allocations. - BREAKABLE_SPACE = Breakable.new(" ", 1, false, true).freeze - BREAKABLE_EMPTY = Breakable.new("", 0, false, true).freeze - BREAKABLE_FORCE = Breakable.new(" ", 1, true, true).freeze - BREAKABLE_RETURN = Breakable.new(" ", 1, true, false).freeze - - # A node in the print tree that forces the surrounding group to print out in - # the "break" mode as opposed to the "flat" mode. Useful for when you need to - # force a newline into a group. - class BreakParent - def type - :break_parent - end - end - - # Since there's really no difference in these instances, just using the same - # one saves on some allocations. - BREAK_PARENT = BreakParent.new.freeze - - # A node in the print tree that represents a group of items which the printer - # should try to fit onto one line. This is the basic command to tell the - # printer when to break. Groups are usually nested, and the printer will try - # to fit everything on one line, but if it doesn't fit it will break the - # outermost group first and try again. It will continue breaking groups until - # everything fits (or there are no more groups to break). - class Group - attr_reader :contents - - def initialize(contents = []) - @contents = contents - @break = false - end - - def break - @break = true - end - - def break? - @break - end - - def type - :group - end - end - - # A node in the print tree that represents printing one thing if the - # surrounding group node is broken and another thing if the surrounding group - # node is flat. - class IfBreak - attr_reader :break_contents, :flat_contents - - def initialize(break_contents, flat_contents) - @break_contents = break_contents - @flat_contents = flat_contents - end - - def type - :if_break - end - end - - # A node in the print tree that is a variant of the Align node that indents - # its contents by one level. - class Indent - attr_reader :contents - - def initialize(contents) - @contents = contents - end - - def type - :indent - end - end - - # A node in the print tree that has its own special buffer for implementing - # content that should flush before any newline. - # - # Useful for implementating trailing content, as it's not always practical to - # constantly check where the line ends to avoid accidentally printing some - # content after a line suffix node. - class LineSuffix - DEFAULT_PRIORITY = 1 - - attr_reader :priority, :contents - - def initialize(priority, contents) - @priority = priority - @contents = contents - end - - def type - :line_suffix - end - end - - # A node in the print tree that represents trimming all of the indentation of - # the current line, in the rare case that you need to ignore the indentation - # that you've already created. This node should be placed after a Breakable. - class Trim - def type - :trim - end - end - - using Module.new { - refine String do - def type - :string - end - end - } - - # Since all of the instances here are the same, we can reuse the same one to - # cut down on allocations. - TRIM = Trim.new.freeze - - # There are two modes in printing, break and flat. When we're in break mode, - # any lines will use their newline, any if-breaks will use their break - # contents, etc. - MODE_BREAK = 1 - - # This is another print mode much like MODE_BREAK. When we're in flat mode, we - # attempt to print everything on one line until we either hit a broken group, - # a forced line, or the maximum width. - MODE_FLAT = 2 - - # The default indentation for printing is zero, assuming that the code starts - # at the top level. That can be changed if desired to start from a different - # indentation level. - DEFAULT_INDENTATION = 0 - - # This is a convenience method which is same as follows: - # - # begin - # q = PrettierPrint.new(output, maxwidth, newline) - # ... - # q.flush - # output - # end - # - def self.format( - output = "".dup, - maxwidth = 80, - indentation = DEFAULT_INDENTATION - ) - q = new(output, maxwidth) - yield q - q.flush(indentation) - output - end - - # The output object. It represents the final destination of the contents of - # the print tree. It should respond to <<. - # - # This defaults to "".dup - attr_reader :output - - # This is an output buffer that wraps the output object and provides - # additional functionality depending on its type. - # - # This defaults to Buffer::StringBuffer.new("".dup) - attr_reader :buffer - - # The maximum width of a line, before it is separated in to a newline - # - # This defaults to 80, and should be an Integer - attr_reader :maxwidth - - # The stack of groups that are being printed. - attr_reader :groups - - # The current array of contents that calls to methods that generate print tree - # nodes will append to. - attr_reader :target - - # Creates a buffer for pretty printing. - # - # +output+ is an output target. If it is not specified, '' is assumed. It - # should have a << method which accepts the first argument +obj+ of - # PrettierPrint#text, the first argument +separator+ of PrettierPrint#breakable, - # and the result of a given block for PrettierPrint.new. - # - # +maxwidth+ specifies maximum line length. If it is not specified, 80 is - # assumed. However actual outputs may overflow +maxwidth+ if long - # non-breakable texts are provided. - # - # The block is used to generate spaces. ->(n) { ' ' * n } is used if it is not - # given. - def initialize(buffer = [], maxwidth = 80) - @buffer = buffer - @maxwidth = maxwidth - contents = [] - @groups = [Group.new(contents)] - @target = contents - end - - # Flushes all of the generated print tree onto the output buffer, then clears - # the generated tree from memory. - def flush(base_indentation = DEFAULT_INDENTATION) - # First, get the root group, since we placed one at the top to begin with. - doc = groups.first - - # This represents how far along the current line we are. It gets reset - # back to 0 when we encounter a newline. - position = base_indentation - - # Start the buffer with the base indentation level. - buffer << " " * base_indentation if base_indentation > 0 - - # This is our command stack. A command consists of a triplet of an - # indentation level, the mode (break or flat), and a doc node. - commands = [[base_indentation, MODE_BREAK, doc]] - - # This is a small optimization boolean. It keeps track of whether or not - # when we hit a group node we should check if it fits on the same line. - should_remeasure = false - - # This is a separate command stack that includes the same kind of triplets - # as the commands variable. It is used to keep track of things that should - # go at the end of printed lines once the other doc nodes are accounted for. - # Typically this is used to implement comments. - line_suffixes = [] - - # This is a special sort used to order the line suffixes by both the - # priority set on the line suffix and the index it was in the original - # array. - line_suffix_sort = ->(line_suffix) do - [-line_suffix.last.priority, -line_suffixes.index(line_suffix)] - end - - # This is a linear stack instead of a mutually recursive call defined on - # the individual doc nodes for efficiency. - while (indent, mode, doc = commands.pop) - case doc.type - when :string - buffer << doc - position += doc.length - when :group - if mode == MODE_FLAT && !should_remeasure - next_mode = doc.break? ? MODE_BREAK : MODE_FLAT - commands += doc.contents.reverse.map { |part| [indent, next_mode, part] } - else - should_remeasure = false - - if doc.break? - commands += doc.contents.reverse.map { |part| [indent, MODE_BREAK, part] } - else - next_commands = doc.contents.reverse.map { |part| [indent, MODE_FLAT, part] } - - if fits?(next_commands, commands, maxwidth - position) - commands += next_commands - else - commands += next_commands.map { |command| command[1] = MODE_BREAK; command } - end - end - end - when :breakable - if mode == MODE_FLAT - if doc.force? - # This line was forced into the output even if we were in flat mode, - # so we need to tell the next group that no matter what, it needs to - # remeasure because the previous measurement didn't accurately - # capture the entire expression (this is necessary for nested - # groups). - should_remeasure = true - else - buffer << doc.separator - position += doc.width - next - end - end - - # If there are any commands in the line suffix buffer, then we're going - # to flush them now, as we are about to add a newline. - if line_suffixes.any? - commands << [indent, mode, doc] - - line_suffixes.sort_by(&line_suffix_sort).each do |(indent, mode, doc)| - commands += doc.contents.reverse.map { |part| [indent, mode, part] } - end - - line_suffixes.clear - next - end - - if !doc.indent? - buffer << "\n" - position = 0 - else - position -= trim!(buffer) - buffer << "\n" - buffer << " " * indent - position = indent - end - when :indent - next_indent = indent + 2 - commands += doc.contents.reverse.map { |part| [next_indent, mode, part] } - when :align - next_indent = indent + doc.indent - commands += doc.contents.reverse.map { |part| [next_indent, mode, part] } - when :trim - position -= trim!(buffer) - when :if_break - if mode == MODE_BREAK && doc.break_contents.any? - commands += doc.break_contents.reverse.map { |part| [indent, mode, part] } - elsif mode == MODE_FLAT && doc.flat_contents.any? - commands += doc.flat_contents.reverse.map { |part| [indent, mode, part] } - end - when :line_suffix - line_suffixes << [indent, mode, doc] - when :break_parent - # do nothing - else - # Special case where the user has defined some way to get an extra doc - # node that we don't explicitly support into the list. In this case - # we're going to assume it's 0-width and just append it to the output - # buffer. - # - # This is useful behavior for putting marker nodes into the list so that - # you can know how things are getting mapped before they get printed. - buffer << doc - end - - if commands.empty? && line_suffixes.any? - line_suffixes.sort_by(&line_suffix_sort).each do |(indent, mode, doc)| - commands.concat doc.contents.reverse.map { |part| [indent, mode, part] } - end - - line_suffixes.clear - end - end - end - - # ---------------------------------------------------------------------------- - # Helper node builders - # ---------------------------------------------------------------------------- - - # The vast majority of breakable calls you receive while formatting are a - # space in flat mode and a newline in break mode. Since this is so common, - # we have a method here to skip past unnecessary calculation. - def breakable_space - target << BREAKABLE_SPACE - end - - # Another very common breakable call you receive while formatting is an - # empty string in flat mode and a newline in break mode. Similar to - # breakable_space, this is here for avoid unnecessary calculation. - def breakable_empty - target << BREAKABLE_EMPTY - end - - # The final of the very common breakable calls you receive while formatting - # is the normal breakable space but with the addition of the break_parent. - def breakable_force - target << BREAKABLE_FORCE - break_parent - end - - # This is the same shortcut as breakable_force, except that it doesn't indent - # the next line. This is necessary if you're trying to preserve some custom - # formatting like a multi-line string. - def breakable_return - target << BREAKABLE_RETURN - break_parent - end - - # A convenience method which is same as follows: - # - # text(",") - # breakable - def comma_breakable - text(",") - breakable_space - end - - # This is similar to #breakable except the decision to break or not is - # determined individually. - # - # Two #fill_breakable under a group may cause 4 results: - # (break,break), (break,non-break), (non-break,break), (non-break,non-break). - # This is different to #breakable because two #breakable under a group - # may cause 2 results: (break,break), (non-break,non-break). - # - # The text +separator+ is inserted if a line is not broken at this point. - # - # If +separator+ is not specified, ' ' is used. - # - # If +width+ is not specified, +separator.length+ is used. You will have to - # specify this when +separator+ is a multibyte character, for example. - def fill_breakable(separator = " ", width = separator.length) - group { breakable(separator, width) } - end - - # This method calculates the position of the text relative to the current - # indentation level when the doc has been printed. It's useful for - # determining how to align text to doc nodes that are already built into the - # tree. - def last_position(node) - queue = [node] - width = 0 - - while (doc = queue.shift) - case doc.type - when :string - width += doc.length - when :group, :indent, :align - queue = doc.contents + queue - when :breakable - width = 0 - when :if_break - queue = doc.break_contents + queue - end - end - - width - end - - # This method will remove any breakables from the list of contents so that - # no newlines are present in the output. If a newline is being forced into - # the output, the replace value will be used. - def remove_breaks(node, replace = "; ") - queue = [node] - - while (doc = queue.shift) - case doc.type - when :align, :indent, :group - doc.contents.map! { |child| remove_breaks_with(child, replace) } - queue += doc.contents - when :if_break - doc.flat_contents.map! { |child| remove_breaks_with(child, replace) } - queue += doc.flat_contents - end - end - end - - # Adds a separated list. - # The list is separated by comma with breakable space, by default. - # - # #seplist iterates the +list+ using +each+. - # It yields each object to the block given for #seplist. - # The procedure +separator_proc+ is called between each yields. - # - # If the iteration is zero times, +separator_proc+ is not called at all. - # - # If +separator_proc+ is nil or not given, - # +lambda { comma_breakable }+ is used. - # - # For example, following 3 code fragments has similar effect. - # - # q.seplist([1,2,3]) {|v| xxx v } - # - # q.seplist([1,2,3], lambda { q.comma_breakable }, :each) {|v| xxx v } - # - # xxx 1 - # q.comma_breakable - # xxx 2 - # q.comma_breakable - # xxx 3 - def seplist(list, sep = nil) # :yield: element - first = true - - list.each do |v| - if first - first = false - elsif sep - sep.call - else - comma_breakable - end - - yield(v) - end - end - - # ---------------------------------------------------------------------------- - # Markers node builders - # ---------------------------------------------------------------------------- - - # This says "you can break a line here if necessary", and a +width+\-column - # text +separator+ is inserted if a line is not broken at the point. - # - # If +separator+ is not specified, ' ' is used. - # - # If +width+ is not specified, +separator.length+ is used. You will have to - # specify this when +separator+ is a multibyte character, for example. - # - # By default, if the surrounding group is broken and a newline is inserted, - # the printer will indent the subsequent line up to the current level of - # indentation. You can disable this behavior with the +indent+ argument if - # that's not desired (rare). - # - # By default, when you insert a Breakable into the print tree, it only breaks - # the surrounding group when the group's contents cannot fit onto the - # remaining space of the current line. You can force it to break the - # surrounding group instead if you always want the newline with the +force+ - # argument. - # - # There are a few circumstances where you'll want to force the newline into - # the output but no insert a break parent (because you don't want to - # necessarily force the groups to break unless they need to). In this case you - # can pass `force: :skip_break_parent` to this method and it will not insert - # a break parent.` - def breakable( - separator = " ", - width = separator.length, - indent: true, - force: false - ) - target << Breakable.new(separator, width, !!force, indent) - break_parent if force == true - end - - # This inserts a BreakParent node into the print tree which forces the - # surrounding and all parent group nodes to break. - def break_parent - doc = BREAK_PARENT - target << doc - - groups.reverse_each do |group| - break if group.break? - group.break - end - end - - # This inserts a Trim node into the print tree which, when printed, will clear - # all whitespace at the end of the output buffer. This is useful for the rare - # case where you need to delete printed indentation and force the next node - # to start at the beginning of the line. - def trim - target << TRIM - end - - # ---------------------------------------------------------------------------- - # Container node builders - # ---------------------------------------------------------------------------- - - # Groups line break hints added in the block. The line break hints are all to - # be used or not. - # - # If +indent+ is specified, the method call is regarded as nested by - # nest(indent) { ... }. - # - # If +open_object+ is specified, text(open_object, open_width) is - # called before grouping. If +close_object+ is specified, - # text(close_object, close_width) is called after grouping. - def group - contents = [] - doc = Group.new(contents) - - groups << doc - target << doc - - with_target(contents) { yield } - groups.pop - - doc - end - - # A small DSL-like object used for specifying the alternative contents to be - # printed if the surrounding group doesn't break for an IfBreak node. - class IfBreakBuilder - attr_reader :q, :flat_contents - - def initialize(q, flat_contents) - @q = q - @flat_contents = flat_contents - end - - def if_flat - q.with_target(flat_contents) { yield } - end - end - - # When we already know that groups are broken, we don't actually need to track - # the flat versions of the contents. So this builder version is effectively a - # no-op, but we need it to maintain the same API. The only thing this can - # impact is that if there's a forced break in the flat contents, then we need - # to propagate that break up the whole tree. - class IfFlatIgnore - attr_reader :q - - def initialize(q) - @q = q - end - - def if_flat - contents = [] - group = Group.new(contents) - - q.with_target(contents) { yield } - q.break_parent if group.break? - end - end - - # Inserts an IfBreak node with the contents of the block being added to its - # list of nodes that should be printed if the surrounding node breaks. If it - # doesn't, then you can specify the contents to be printed with the #if_flat - # method used on the return object from this method. For example, - # - # q.if_break { q.text('do') }.if_flat { q.text('{') } - # - # In the example above, if the surrounding group is broken it will print 'do' - # and if it is not it will print '{'. - def if_break - break_contents = [] - flat_contents = [] - - doc = IfBreak.new(break_contents, flat_contents) - target << doc - - with_target(break_contents) { yield } - - if groups.last.break? - IfFlatIgnore.new(self) - else - IfBreakBuilder.new(self, flat_contents) - end - end - - # This is similar to if_break in that it also inserts an IfBreak node into the - # print tree, however it's starting from the flat contents, and cannot be used - # to build the break contents. - def if_flat - if groups.last.break? - contents = [] - group = Group.new(contents) - - with_target(contents) { yield } - break_parent if group.break? - else - flat_contents = [] - doc = IfBreak.new(break_contents, flat_contents) - target << doc - - with_target(flat_contents) { yield } - doc - end - end - - # Very similar to the #nest method, this indents the nested content by one - # level by inserting an Indent node into the print tree. The contents of the - # node are determined by the block. - def indent - contents = [] - doc = Indent.new(contents) - target << doc - - with_target(contents) { yield } - doc - end - - # Inserts a LineSuffix node into the print tree. The contents of the node are - # determined by the block. - def line_suffix(priority) - contents = [] - doc = LineSuffix.new(priority, contents) - target << doc - - with_target(contents) { yield } - doc - end - - # Increases left margin after newline with +indent+ for line breaks added in - # the block. - def nest(indent) - contents = [] - doc = Align.new(indent, contents) - target << doc - - with_target(contents) { yield } - doc - end - - # Push a value onto the output target. - def text(value) - target << value - end - - # ---------------------------------------------------------------------------- - # Internal APIs - # ---------------------------------------------------------------------------- - - # A convenience method used by a lot of the print tree node builders that - # temporarily changes the target that the builders will append to. - def with_target(target) - previous_target, @target = @target, target - yield - @target = previous_target - end - - private - - def trim!(buffer) - return 0 if buffer.empty? - - trimmed = 0 - - while buffer.any? && buffer.last.is_a?(String) && - buffer.last.match?(/\A[\t ]*\z/) - trimmed += buffer.pop.length - end - - if buffer.any? && buffer.last.is_a?(String) && !buffer.last.frozen? - length = buffer.last.length - buffer.last.gsub!(/[\t ]*\z/, "") - trimmed += length - buffer.last.length - end - - trimmed - end - - # This method returns a boolean as to whether or not the remaining commands - # fit onto the remaining space on the current line. If we finish printing - # all of the commands or if we hit a newline, then we return true. Otherwise - # if we continue printing past the remaining space, we return false. - def fits?(next_commands, rest_commands, remaining) - # This is the index in the remaining commands that we've handled so far. - # We reverse through the commands and add them to the stack if we've run - # out of nodes to handle. - rest_index = rest_commands.length - - # This is our stack of commands, very similar to the commands list in the - # print method. - commands = [*next_commands] - - # This is our output buffer, really only necessary to keep track of - # because we could encounter a Trim doc node that would actually add - # remaining space. - fit_buffer = [] - - while remaining >= 0 - if commands.empty? - return true if rest_index == 0 - - rest_index -= 1 - commands << rest_commands[rest_index] - next - end - - indent, mode, doc = commands.pop - - case doc.type - when :string - fit_buffer << doc - remaining -= doc.length - when :group - next_mode = doc.break? ? MODE_BREAK : mode - commands += doc.contents.reverse.map { |part| [indent, next_mode, part] } - when :breakable - if mode == MODE_FLAT && !doc.force? - fit_buffer << doc.separator - remaining -= doc.width - next - end - - return true - when :indent - next_indent = indent + 2 - commands += doc.contents.reverse.map { |part| [next_indent, mode, part] } - when :align - next_indent = indent + doc.indent - commands += doc.contents.reverse.map { |part| [next_indent, mode, part] } - when :trim - remaining += trim!(fit_buffer) - when :if_break - if mode == MODE_BREAK && doc.break_contents.any? - commands += doc.break_contents.reverse.map { |part| [indent, mode, part] } - elsif mode == MODE_FLAT && doc.flat_contents.any? - commands += doc.flat_contents.reverse.map { |part| [indent, mode, part] } - end - end - end - - false - end - - def remove_breaks_with(doc, replace) - case doc.type - when :breakable - doc.force? ? replace : doc.separator - when :if_break - Align.new(0, doc.flat_contents) - else - doc - end - end -end