Class: Pry::Indent

Inherits:
Object show all
Includes:
Helpers::BaseHelpers
Defined in:
lib/pry/indent.rb

Overview

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.

Defined Under Namespace

Classes: UnparseableNestingError

Constant Summary

SPACES =

The amount of spaces to insert for each indent level.

'  '
OPEN_TOKENS =

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.

{
  'def'    => 'end',
  'class'  => 'end',
  'module' => 'end',
  'do'     => 'end',
  'if'     => 'end',
  'unless' => 'end',
  'while'  => 'end',
  'until'  => 'end',
  'for'    => 'end',
  'case'   => 'end',
  'begin'  => 'end',
  '['      => ']',
  '{'      => '}',
  '('      => ')'
}
SINGLELINE_TOKENS =

Which tokens can either be open tokens, or appear as modifiers on a single-line.

%w(if while until unless rescue)
OPTIONAL_DO_TOKENS =

Which tokens can be followed by an optional "do" keyword.

%w(for while until)
IGNORE_TOKENS =

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".

[:space, :content, :string, :method, :ident,
:constant, :pre_constant, :predefined_constant]
STATEMENT_END_TOKENS =

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.

IGNORE_TOKENS + [:regexp, :integer, :float, :keyword,
:delimiter, :reserved]
MIDWAY_TOKENS =

Collection of tokens that should appear dedented even though they don't affect the surrounding code.

%w(when else elsif ensure rescue)

Instance Attribute Summary (collapse)

Class Method Summary (collapse)

Instance Method Summary (collapse)

Methods included from Helpers::BaseHelpers

colorize_code, #colorize_code, command_dependencies_met?, #command_dependencies_met?, context_from_object_path, #context_from_object_path, find_command, #find_command, heading, #heading, #highlight, highlight, jruby?, #jruby?, jruby_19?, #jruby_19?, mri?, #mri?, #mri_19?, mri_19?, mri_20?, #mri_20?, #mri_21?, mri_21?, #not_a_real_file?, not_a_real_file?, #rbx?, rbx?, #safe_send, safe_send, silence_warnings, #silence_warnings, #stagger_output, stagger_output, use_ansi_codes?, #use_ansi_codes?, windows?, #windows?, windows_ansi?, #windows_ansi?

Constructor Details

- (Indent) initialize



102
103
104
# File 'lib/pry/indent.rb', line 102

def initialize
  reset
end

Instance Attribute Details

- (String) indent_level (readonly)



18
19
20
# File 'lib/pry/indent.rb', line 18

def indent_level
  @indent_level
end

- (Array<String>) stack (readonly)



21
22
23
# File 'lib/pry/indent.rb', line 21

def stack
  @stack
end

Class Method Details

+ (String) indent(str)

Clean the indentation of a fragment of ruby.



80
81
82
# File 'lib/pry/indent.rb', line 80

def self.indent(str)
  new.indent(str)
end

+ (Array<String>) nesting_at(str, line_number)

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.



93
94
95
96
97
98
99
100
# File 'lib/pry/indent.rb', line 93

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

Instance Method Details

- (String) correct_indentation(prompt, code, overhang = 0)

Return a string which, when printed, will rewrite the previous line with the correct indentation. Mostly useful for fixing 'end'.



385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
# File 'lib/pry/indent.rb', line 385

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.screen_size

  cols = cols.to_i
  lines = (cols != 0 ? (line_to_measure.length / cols + 1) : 1).to_i

  if Pry::Helpers::BaseHelpers.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

- (Object) current_prefix

Get the indentation for the start of the next line.

This is what's used between the prompt and the cursor in pry.



177
178
179
# File 'lib/pry/indent.rb', line 177

def current_prefix
  in_string? ? '' : indent_level
end

- (Boolean) end_of_statement?(last_token, last_kind)

If the code just before an "if" or "while" token on a line looks like the end of a statement, then we want to treat that "if" as a singleline, not multiline statement.



252
253
254
# File 'lib/pry/indent.rb', line 252

def end_of_statement?(last_token, last_kind)
  (last_token =~ /^[)\]}\/]$/ || STATEMENT_END_TOKENS.include?(last_kind))
end

- (Boolean) in_string?

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!



263
264
265
# File 'lib/pry/indent.rb', line 263

def in_string?
  !open_delimiters.empty?
end

- (String) indent(input)

Indents a string and returns it. This string can either be a single line or multiple ones.

Examples:

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)


139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/pry/indent.rb', line 139

def indent(input)
  output = ''
  prefix = indent_level

  input.lines.each do |line|

    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.times{ prefix.sub! SPACES, '' }
    new_prefix = prefix + SPACES * after

    line = prefix + line.lstrip unless previously_in_string

    output += line

    prefix = new_prefix
  end

  @indent_level = prefix

  return output
end

- (Array[Integer]) indentation_delta(tokens)

Get the change in indentation indicated by the line.

By convention, you remove indent from the line containing end tokens, but add indent to the line after that which contains the start tokens.

This method returns a pair, where the first number is the number of closings on this line (i.e. the number of indents to remove before the line) and the second is the number of openings (i.e. the number of indents to add after this line)



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/pry/indent.rb', line 194

def indentation_delta(tokens)

  # We need to keep track of whether we've seen a "for" on this line because
  # if the line ends with "do" then that "do" should be discounted (i.e. we're
  # only opening one level not two) To do this robustly we want to keep track
  # of the indent level at which we saw the for, so we can differentiate
  # between "for x in [1,2,3] do" and "for x in ([1,2,3].map do" properly
  seen_for_at = []

  # When deciding whether an "if" token is the start of a multiline statement,
  # or just the middle of a single-line if statement, we just look at the
  # preceding token, which is tracked here.
  last_token, last_kind = [nil, nil]

  # delta keeps track of the total difference from the start of each line after
  # the given token, 0 is just the level at which the current line started for
  # reference.
  remove_before, add_after = [0, 0]

  # 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|
    is_singleline_if  = (SINGLELINE_TOKENS.include?(token)) && end_of_statement?(last_token, last_kind)
    is_optional_do = (token == "do" && seen_for_at.include?(add_after - 1))

    last_token, last_kind = token, kind unless kind == :space
    next if IGNORE_TOKENS.include?(kind)

    track_module_nesting(token, kind)

    seen_for_at << add_after if OPTIONAL_DO_TOKENS.include?(token)

    if kind == :delimiter
      track_delimiter(token)
    elsif OPEN_TOKENS.keys.include?(token) && !is_optional_do && !is_singleline_if
      @stack << token
      add_after += 1
    elsif token == OPEN_TOKENS[@stack.last]
      popped = @stack.pop
      track_module_nesting_end(popped)
      if add_after == 0
        remove_before += 1
      else
        add_after -= 1
      end
    elsif MIDWAY_TOKENS.include?(token)
      if add_after == 0
        remove_before += 1
        add_after += 1
      end
    end
  end

  return [remove_before, add_after]
end

- (Array<String>) module_nesting

Return a list of strings which can be used to re-construct the Module.nesting at the current point in the file.

Returns nil if the syntax of the file was not recognizable.



369
370
371
372
373
374
375
# File 'lib/pry/indent.rb', line 369

def module_nesting
  @module_nesting.map do |(kind, token)|
    raise UnparseableNestingError, @module_nesting.inspect if token.nil?

    "#{kind} #{token}"
  end
end

- (String) open_delimiters

All the open delimiters, in the order that they first appeared.



303
304
305
# File 'lib/pry/indent.rb', line 303

def open_delimiters
  @heredoc_queue + [@string_start].compact
end

- (Object) open_delimiters_line

Return a string which restores the CodeRay string status to the correct value by opening HEREDOCs and strings.



311
312
313
# File 'lib/pry/indent.rb', line 311

def open_delimiters_line
  "puts #{open_delimiters.join(", ")}"
end

- (Object) reset

reset internal state



107
108
109
110
111
112
113
114
115
116
# File 'lib/pry/indent.rb', line 107

def reset
  @stack = []
  @indent_level = ''
  @heredoc_queue = []
  @close_heredocs = {}
  @string_start = nil
  @awaiting_class = false
  @module_nesting = []
  self
end

- (Boolean) should_correct_indentation?

Given the current Pry environment, should we try to correct indentation?



407
408
409
# File 'lib/pry/indent.rb', line 407

def should_correct_indentation?
  Pry::Helpers::BaseHelpers.use_ansi_codes? && Pry.config.correct_indent
end

- (Array) tokenize(string)

Given a string of Ruby code, use CodeRay to export the tokens.



271
272
273
274
275
# File 'lib/pry/indent.rb', line 271

def tokenize(string)
  tokens = CodeRay.scan(string, :ruby)
  tokens = tokens.tokens.each_slice(2) if tokens.respond_to?(:tokens) # Coderay 1.0.0
  tokens.to_a
end

- (Object) track_delimiter(token)

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.



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/pry/indent.rb', line 284

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

- (Object) track_module_nesting(token, kind)

Update the internal state relating to module nesting.

It's responsible for adding to the @module_nesting array, which looks something like:

[ ["class", "Foo"], ["module", "Bar::Baz"], ["class <<", "self"] ]

A nil value in the @module_nesting array happens in two places: either when @awaiting_class is true and we're still waiting for the string to fill that space, or when a parse was rejected.

At the moment this function is quite restricted about what formats it will parse, for example we disallow expressions after the class keyword. This could maybe be improved in the future.



332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'lib/pry/indent.rb', line 332

def track_module_nesting(token, kind)
  if kind == :keyword && (token == "class" || token == "module")
    @module_nesting << [token, nil]
    @awaiting_class = true
  elsif @awaiting_class
    if kind == :operator && token == "<<" && @module_nesting.last[0] == "class"
      @module_nesting.last[0] = "class <<"
      @awaiting_class = true
    elsif kind == :class && token =~ /\A(self|[A-Z:][A-Za-z0-9_:]*)\z/
      @module_nesting.last[1] = token if kind == :class
      @awaiting_class = false
    else
      # leave @module_nesting[-1]
      @awaiting_class = false
    end
  end
end

- (Object) track_module_nesting_end(token, kind = :keyword)

Update the internal state relating to module nesting on 'end'.

If the current 'end' pairs up with a class or a module then we should pop an array off of @module_nesting



357
358
359
360
361
# File 'lib/pry/indent.rb', line 357

def track_module_nesting_end(token, kind=:keyword)
  if kind == :keyword && (token == "class" || token == "module")
    @module_nesting.pop
  end
end