1
0
Fork 0
mirror of https://github.com/pry/pry.git synced 2022-11-09 12:35:05 -05:00

Don't indent inside strings. [Fixes #535]

This is acheived by keeping track of which Strings are open and
re-opening them before giving CodeRay the new line of input.

I considered instead passing the entire input through CodeRay and then
just extracting the last line of tokens, unfortunately this would
exhibit O(n²) behaviour when pasting code into the terminal; and it's
not obvious whether the tokenization would be stable enough to guarantee
an easy way to get the last line of tokens.
This commit is contained in:
Conrad Irwin 2012-04-22 00:22:49 -07:00
parent e0da161f6d
commit 037b382b11
3 changed files with 136 additions and 8 deletions

View file

@ -56,7 +56,7 @@ class Pry
# #
# :pre_constant and :preserved_constant are the CodeRay 0.9.8 and 1.0.0 # :pre_constant and :preserved_constant are the CodeRay 0.9.8 and 1.0.0
# classifications of "true", "false", and "nil". # classifications of "true", "false", and "nil".
IGNORE_TOKENS = [:space, :content, :string, :delimiter, :method, :ident, IGNORE_TOKENS = [:space, :content, :string, :method, :ident,
:constant, :pre_constant, :predefined_constant] :constant, :pre_constant, :predefined_constant]
# Tokens that indicate the end of a statement (i.e. that, if they appear # Tokens that indicate the end of a statement (i.e. that, if they appear
@ -65,7 +65,8 @@ class Pry
# #
# :reserved and :keywords are the CodeRay 0.9.8 and 1.0.0 respectively # :reserved and :keywords are the CodeRay 0.9.8 and 1.0.0 respectively
# classifications of "super", "next", "return", etc. # classifications of "super", "next", "return", etc.
STATEMENT_END_TOKENS = IGNORE_TOKENS + [:regexp, :integer, :float, :keyword, :reserved] STATEMENT_END_TOKENS = IGNORE_TOKENS + [:regexp, :integer, :float, :keyword,
:delimiter, :reserved]
# Collection of tokens that should appear dedented even though they # Collection of tokens that should appear dedented even though they
# don't affect the surrounding code. # don't affect the surrounding code.
@ -79,6 +80,9 @@ class Pry
def reset def reset
@stack = [] @stack = []
@indent_level = '' @indent_level = ''
@heredoc_queue = []
@close_heredocs = {}
@string_start = nil
self self
end end
@ -109,14 +113,27 @@ class Pry
prefix = indent_level prefix = indent_level
input.lines.each do |line| input.lines.each do |line|
tokens = CodeRay.scan(line, :ruby)
tokens = tokens.tokens.each_slice(2) if tokens.respond_to?(:tokens) # Coderay 1.0.0 if in_string?
tokens = tokenize("#{open_delimiters_line}\n#{line}")
tokens = tokens.drop_while{ |token, type| !(String === token && token.include?("\n")) }
previously_in_string = true
else
tokens = tokenize(line)
previously_in_string = false
end
before, after = indentation_delta(tokens) before, after = indentation_delta(tokens)
before.times{ prefix.sub! SPACES, '' } before.times{ prefix.sub! SPACES, '' }
output += prefix + line.strip + "\n" new_prefix = prefix + SPACES * after
prefix += SPACES * after
line = prefix + line.lstrip unless previously_in_string
line = line.rstrip + "\n" unless in_string?
output += line
prefix = new_prefix
end end
@indent_level = prefix @indent_level = prefix
@ -124,6 +141,16 @@ class Pry
return output.gsub(/\s+$/, '') return output.gsub(/\s+$/, '')
end end
# Get the indentation for the start of the next line.
#
# This is what's used between the prompt and the cursor in pry.
#
# @return String The correct number of spaces
#
def current_prefix
in_string? ? '' : indent_level
end
# Get the change in indentation indicated by the line. # Get the change in indentation indicated by the line.
# #
# By convention, you remove indent from the line containing end tokens, # By convention, you remove indent from the line containing end tokens,
@ -167,7 +194,9 @@ class Pry
seen_for_at << add_after if token == "for" seen_for_at << add_after if token == "for"
if OPEN_TOKENS.keys.include?(token) && !is_optional_do && !is_singleline_if if kind == :delimiter
track_delimiter(token)
elsif OPEN_TOKENS.keys.include?(token) && !is_optional_do && !is_singleline_if
@stack << token @stack << token
add_after += 1 add_after += 1
elsif token == OPEN_TOKENS[@stack.last] elsif token == OPEN_TOKENS[@stack.last]
@ -194,6 +223,65 @@ class Pry
(last_token =~ /^[)\]}\/]$/ || STATEMENT_END_TOKENS.include?(last_kind)) (last_token =~ /^[)\]}\/]$/ || STATEMENT_END_TOKENS.include?(last_kind))
end end
# Are we currently in the middle of a string literal.
#
# This is used to determine whether to re-indent a given line, we mustn't re-indent
# within string literals because to do so would actually change the value of the
# String!
#
# @return Boolean
def in_string?
!open_delimiters.empty?
end
# Given a string of Ruby code, use CodeRay to export the tokens.
#
# @param String The Ruby to lex.
# @return [Array] An Array of pairs of [token_value, token_type]
def tokenize(string)
tokens = CodeRay.scan(string, :ruby)
tokens = tokens.tokens.each_slice(2) if tokens.respond_to?(:tokens) # Coderay 1.0.0
tokens
end
# Update the internal state about what kind of strings are open.
#
# Most of the complication here comes from the fact that HEREDOCs can be nested. For
# normal strings (which can't be nested) we assume that CodeRay correctly pairs
# open-and-close delimiters so we don't bother checking what they are.
#
# @param String The token (of type :delimiter)
def track_delimiter(token)
case token
when /^<<-(["'`]?)(.*)\\1/
@heredoc_queue << token
@close_heredocs[token] = /^\s*$2/
when @close_heredocs[@heredoc_queue.first]
@heredoc_queue.shift
else
if @string_start
@string_start = nil
else
@string_start = token
end
end
end
# All the open delimiters, in the order that they first appeared.
#
# @return [String]
def open_delimiters
@heredoc_queue + [@string_start].compact
end
# Return a string which restores the CodeRay string status to the correct value by
# opening HEREDOCs and strings.
#
# @return String
def open_delimiters_line
"puts #{open_delimiters.join(", ")}"
end
# Return a string which, when printed, will rewrite the previous line with # Return a string which, when printed, will rewrite the previous line with
# the correct indentation. Mostly useful for fixing 'end'. # the correct indentation. Mostly useful for fixing 'end'.
# #

View file

@ -338,7 +338,7 @@ class Pry
instance_eval(&custom_completions)) instance_eval(&custom_completions))
indentation = Pry.config.auto_indent ? @indent.indent_level : '' indentation = Pry.config.auto_indent ? @indent.current_prefix : ''
begin begin
val = readline("#{current_prompt}#{indentation}", completion_proc) val = readline("#{current_prompt}#{indentation}", completion_proc)

View file

@ -230,6 +230,46 @@ begin
rescue => e rescue => e
doit :right doit :right
end end
OUTPUT
@indent.indent(input).should == output
end
it "should not indent inside strings" do
@indent.indent(%(def a\n"foo\nbar"\n end)).should == %(def a\n "foo\nbar"\nend)
@indent.indent(%(def a\nputs %w(foo\nbar), 'foo\nbar'\n end)).should == %(def a\n puts %w(foo\nbar), 'foo\nbar'\nend)
end
it "should not indent inside HEREDOCs" do
@indent.indent(%(def a\nputs <<FOO\n bar\nFOO\nbaz\nend)).should == %(def a\n puts <<FOO\n bar\nFOO\n baz\nend)
@indent.indent(%(def a\nputs <<-'^^'\n bar\n\t^^\nbaz\nend)).should == %(def a\n puts <<-'^^'\n bar\n\t^^\n baz\nend)
end
it "should not indent nested HEREDOCs" do
input = <<INPUT.strip
def a
puts <<FOO, <<-BAR, "baz", <<-':p'
foo
FOO
bar
BAR
tongue
:p
puts :p
end
INPUT
output = <<OUTPUT.strip
def a
puts <<FOO, <<-BAR, "baz", <<-':p'
foo
FOO
bar
BAR
tongue
:p
puts :p
end
OUTPUT OUTPUT
@indent.indent(input).should == output @indent.indent(input).should == output