pry--pry/lib/pry/indent.rb

216 lines
6.3 KiB
Ruby

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 2 spaces.
#
# @author Yorick Peterse
# @since 04-10-2011
#
class Indent
# Array containing all the indentation levels.
attr_reader :stack
# The amount of spaces to insert for each indent level.
Spaces = ' '
# 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.
OpenTokens = {
'def' => 'end',
'class' => 'end',
'module' => 'end',
'do' => 'end',
'if' => 'end',
'while' => 'end',
'for' => 'end',
'[' => ']',
'{' => '}',
}
# Collection of tokens that decrease the indentation level.
ClosingTokens = ['end', ']', '}']
# 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.
IgnoreTokens = [:space, :content, :string, :delimiter]
# Collection of tokens that should only increase the indentation level of
# the next line.
OpenTokensNext = ['else', 'elsif']
##
# Creates a new instance of the class and starts with a fresh stack. The
# stack is used to keep track of the indentation level for each line of
# code.
#
# @author Yorick Peterse
# @since 05-10-2011
#
def initialize
@stack = []
end
##
# Get rid of all indentation
def reset
@stack.clear
end
##
# The current indentation level (number of spaces deep)
def indent_level
@stack.last
end
##
# Indents a string and returns it. This string can either be a single line
# or multiple ones.
#
# @example
# str = <<TXT
# class User
# attr_accessor :name
# end
# TXT
#
# # This would result in the following being displayed:
# #
# # class User
# # attr_accessor :name
# # end
# #
# puts Pry::Indent.new.indent(str)
#
# @author Yorick Peterse
# @since 05-10-2011
# @param [String] input The input string to indent.
# @return [String] The indented version of +input+.
#
def indent(input)
output = ''
open_tokens = OpenTokens.keys
input.lines.each do |line|
# Remove manually added indentation.
line = line.strip + "\n"
tokens = CodeRay.scan(line, :ruby)
unless @stack.empty?
line = @stack[-1] + line
end
tokens.each do |token, kind|
next if IgnoreTokens.include?(kind)
if OpenTokensNext.include?(token) and @stack[-1]
line.sub!(@stack[-1], '')
break
# Start token found (such as "class"). Update the stack and indent the
# current line.
elsif open_tokens.include?(token)
# Skip indentation if there's a matching closing token on the same
# line.
next if skip_indentation?(tokens, token)
add = ''
last = @stack[-1]
# Determine the amount of spaces to add to the stack and the line.
unless last.nil?
add = Spaces + last
end
# Don't forget to update the current line.
if @stack.empty?
line = add + line
add += Spaces
end
@stack.push(add)
break
# Stop token found. Remove the last number of spaces from the stack
# and un-indent the current line.
elsif ClosingTokens.include?(token)
@stack.pop
line = ( @stack[-1] || '' ) + line.strip + "\n"
break
end
end
output += line
end
return output.gsub!(/\s+$/, '')
end
##
# Based on a set of tokens and an open token this method will determine if
# a line has to be indented or not. Perhaps not the most efficient way of
# doing it so if you feel it can be improved patches are more than welcome
# :).
#
# @author Yorick Peterse
# @since 08-10-2011
# @param [Array] tokens A list of tokens to scan.
# @param [String] open_token The token who's closing token may or may not
# be included in the list of tokens.
# @return [Boolean]
#
def skip_indentation?(tokens, open_token)
closing = OpenTokens[open_token]
open = OpenTokens.keys
skip = false
# If the list of tokens contains a matching closing token the line should
# not be indented (and thus we should return true).
tokens.each do |token, kind|
next if IgnoreTokens.include?(kind)
# Skip the indentation if we've found a matching closing token.
if token == closing
skip = true
# Sometimes a line contains a matching closing token followed by another
# open token. In this case the line *should* be indented. An example of
# this is the following:
#
# [10, 15].each do |num|
# puts num
# end
#
# Here we have an open token (the "[") as well as it's closing token
# ("]"). However, there's also a "do" which indicates that the next
# line *should* be indented.
elsif open.include?(token)
skip = false
end
end
return skip
end
##
# Fix the indentation for closing tags (notably 'end'). Note that this
# method will not work on Win32 based systems (or other systems that don't
# have the tput command).
#
# @param [String] full_line The full line of input, including the prompt.
#
def correct_indentation(full_line)
# The whitespace is used to "clear" the current line so existing
# characters don't show up.
spaces = ' ' * full_line.length
$stdout.write(`tput sc` + `tput cuu1` + full_line + spaces + `tput rc`)
end
end # Indent
end # Pry