require 'coderay' class Pry ## # Pry::Indent is a class that can be used to indent a number of lines # containing Ruby code similar as to how IRB does it (but better). The class # works by tokenizing a string using CodeRay and then looping over those # tokens. Based on the tokens in a line of code that line (or the next one) # will be indented or un-indented by correctly. # class Indent include Helpers::BaseHelpers # Raised if {#module_nesting} would not work. class UnparseableNestingError < StandardError; end # @return [String] String containing the spaces to be inserted before the next line. attr_reader :indent_level # @return [Array] The stack of open tokens. attr_reader :stack # The amount of spaces to insert for each indent level. SPACES = ' '.freeze # Hash containing all the tokens that should increase the indentation # level. The keys of this hash are open tokens, the values the matching # tokens that should prevent a line from being indented if they appear on # the same line. OPEN_TOKENS = { 'def' => 'end', 'class' => 'end', 'module' => 'end', 'do' => 'end', 'if' => 'end', 'unless' => 'end', 'while' => 'end', 'until' => 'end', 'for' => 'end', 'case' => 'end', 'begin' => 'end', '[' => ']', '{' => '}', '(' => ')' }.freeze # Which tokens can either be open tokens, or appear as modifiers on # a single-line. SINGLELINE_TOKENS = %w[if while until unless rescue].freeze # Which tokens can be followed by an optional "do" keyword. OPTIONAL_DO_TOKENS = %w[for while until].freeze # Collection of token types that should be ignored. Without this list # keywords such as "class" inside strings would cause the code to be # indented incorrectly. # # :pre_constant and :preserved_constant are the CodeRay 0.9.8 and 1.0.0 # classifications of "true", "false", and "nil". IGNORE_TOKENS = [:space, :content, :string, :method, :ident, :constant, :pre_constant, :predefined_constant].freeze # Tokens that indicate the end of a statement (i.e. that, if they appear # directly before an "if" indicates that that if applies to the same line, # not the next line) # # :reserved and :keywords are the CodeRay 0.9.8 and 1.0.0 respectively # classifications of "super", "next", "return", etc. STATEMENT_END_TOKENS = IGNORE_TOKENS + [:regexp, :integer, :float, :keyword, :delimiter, :reserved, :instance_variable, :class_variable, :global_variable] # Collection of tokens that should appear dedented even though they # don't affect the surrounding code. MIDWAY_TOKENS = %w[when else elsif ensure rescue].freeze # Clean the indentation of a fragment of ruby. # # @param [String] str # @return [String] def self.indent(str) new.indent(str) end # Get the module nesting at the given point in the given string. # # NOTE If the line specified contains a method definition, then the nesting # at the start of the method definition is used. Otherwise the nesting from # the end of the line is used. # # @param [String] str The ruby code to analyze # @param [Fixnum] line_number The line number (starting from 1) # @return [Array] def self.nesting_at(str, line_number) indent = new lines = str.split("\n") n = line_number - 1 to_indent = lines[0...n] << (lines[n] || "").split("def").first(1) indent.indent(to_indent.join("\n") << "\n") indent.module_nesting end def initialize reset end # reset internal state def reset @stack = [] @indent_level = '' @heredoc_queue = [] @close_heredocs = {} @string_start = nil @awaiting_class = false @module_nesting = [] self end # Indents a string and returns it. This string can either be a single line # or multiple ones. # # @example # str = <] def module_nesting @module_nesting.map do |(kind, token)| raise UnparseableNestingError, @module_nesting.inspect if token.nil? "#{kind} #{token}" end end # Return a string which, when printed, will rewrite the previous line with # the correct indentation. Mostly useful for fixing 'end'. # # @param [String] prompt The user's prompt # @param [String] code The code the user just typed in # @param [Integer] overhang The number of characters to erase afterwards (the # the difference in length between the old line and the new one) # # @return [String] correctly indented line def correct_indentation(prompt, code, overhang = 0) prompt = prompt.delete("\001\002") line_to_measure = Pry::Helpers::Text.strip_color(prompt) << code whitespace = ' ' * overhang cols = Terminal.width! lines = cols == 0 ? 1 : (line_to_measure.length / cols + 1).to_i if Helpers::Platform.windows_ansi? move_up = "\e[#{lines}F" move_down = "\e[#{lines}E" else move_up = "\e[#{lines}A\e[0G" move_down = "\e[#{lines}B\e[0G" end "#{move_up}#{prompt}#{colorize_code(code)}#{whitespace}#{move_down}" end end end