Skip to content

Commit

Permalink
Decouple RubyLex from prompt and line_no (#701)
Browse files Browse the repository at this point in the history
* Remove instance variable prompt and line_no from RubyLex

* Fix prompt test

* Rename prompt generating method and make it private
  • Loading branch information
tompng authored Oct 12, 2023
1 parent 9c06f9e commit 1ceb97f
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 89 deletions.
84 changes: 44 additions & 40 deletions lib/irb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ def initialize(workspace = nil, input_method = nil)
@context.workspace.load_commands_to_main
@signal_status = :IN_IRB
@scanner = RubyLex.new
@line_no = 1
end

# A hook point for `debug` command's breakpoint after :IRB_EXIT as well as its clean-up
Expand All @@ -454,7 +455,7 @@ def debug_readline(binding)
workspace = IRB::WorkSpace.new(binding)
context.workspace = workspace
context.workspace.load_commands_to_main
scanner.increase_line_no(1)
@line_no += 1

# When users run:
# 1. Debugging commands, like `step 2`
Expand All @@ -476,7 +477,7 @@ def debug_readline(binding)
end

if input&.include?("\n")
scanner.increase_line_no(input.count("\n") - 1)
@line_no += input.count("\n") - 1
end

input
Expand Down Expand Up @@ -513,34 +514,38 @@ def run(conf = IRB.conf)
# The lexer used by this irb session
attr_accessor :scanner

# Evaluates input for this session.
def eval_input
@scanner.set_prompt do
|ltype, indent, continue, line_no|
if ltype
f = @context.prompt_s
elsif continue
f = @context.prompt_c
else
f = @context.prompt_i
end
f = "" unless f
if @context.prompting?
@context.io.prompt = p = prompt(f, ltype, indent, line_no)
else
@context.io.prompt = p = ""
end
if @context.auto_indent_mode and !@context.io.respond_to?(:auto_indent)
unless ltype
prompt_i = @context.prompt_i.nil? ? "" : @context.prompt_i
ind = prompt(prompt_i, ltype, indent, line_no)[/.*\z/].size +
indent * 2 - p.size
@context.io.prompt = p + " " * ind if ind > 0
end
private def generate_prompt(opens, continue, line_offset)
ltype = @scanner.ltype_from_open_tokens(opens)
indent = @scanner.calc_indent_level(opens)
continue = opens.any? || continue
line_no = @line_no + line_offset

if ltype
f = @context.prompt_s
elsif continue
f = @context.prompt_c
else
f = @context.prompt_i
end
f = "" unless f
if @context.prompting?
p = format_prompt(f, ltype, indent, line_no)
else
p = ""
end
if @context.auto_indent_mode and !@context.io.respond_to?(:auto_indent)
unless ltype
prompt_i = @context.prompt_i.nil? ? "" : @context.prompt_i
ind = format_prompt(prompt_i, ltype, indent, line_no)[/.*\z/].size +
indent * 2 - p.size
p += " " * ind if ind > 0
end
@context.io.prompt
end
p
end

# Evaluates input for this session.
def eval_input
configure_io

each_top_level_statement do |statement, line_no|
Expand Down Expand Up @@ -572,8 +577,9 @@ def eval_input
end
end

def read_input
def read_input(prompt)
signal_status(:IN_INPUT) do
@context.io.prompt = prompt
if l = @context.io.gets
print l if @context.verbose?
else
Expand All @@ -591,16 +597,16 @@ def read_input
end

def readmultiline
@scanner.save_prompt_to_context_io([], false, 0)
prompt = generate_prompt([], false, 0)

# multiline
return read_input if @context.io.respond_to?(:check_termination)
return read_input(prompt) if @context.io.respond_to?(:check_termination)

# nomultiline
code = ''
line_offset = 0
loop do
line = read_input
line = read_input(prompt)
unless line
return code.empty? ? nil : code
end
Expand All @@ -615,7 +621,7 @@ def readmultiline

line_offset += 1
continue = @scanner.should_continue?(tokens)
@scanner.save_prompt_to_context_io(opens, continue, line_offset)
prompt = generate_prompt(opens, continue, line_offset)
end
end

Expand All @@ -625,9 +631,9 @@ def each_top_level_statement
break unless code

if code != "\n"
yield build_statement(code), @scanner.line_no
yield build_statement(code), @line_no
end
@scanner.increase_line_no(code.count("\n"))
@line_no += code.count("\n")
rescue RubyLex::TerminateLineInput
end
end
Expand Down Expand Up @@ -688,7 +694,7 @@ def configure_io
tokens_until_line << token if token != tokens_until_line.last
end
continue = @scanner.should_continue?(tokens_until_line)
@scanner.prompt(next_opens, continue, line_num_offset)
generate_prompt(next_opens, continue, line_num_offset)
end
end
end
Expand Down Expand Up @@ -874,7 +880,7 @@ def signal_status(status)
end
end

def truncate_prompt_main(str) # :nodoc:
private def truncate_prompt_main(str) # :nodoc:
str = str.tr(CONTROL_CHARACTERS_PATTERN, ' ')
if str.size <= PROMPT_MAIN_TRUNCATE_LENGTH
str
Expand All @@ -883,9 +889,8 @@ def truncate_prompt_main(str) # :nodoc:
end
end

def prompt(prompt, ltype, indent, line_no) # :nodoc:
p = prompt.dup
p.gsub!(/%([0-9]+)?([a-zA-Z])/) do
private def format_prompt(format, ltype, indent, line_no) # :nodoc:
format.gsub(/%([0-9]+)?([a-zA-Z])/) do
case $2
when "N"
@context.irb_name
Expand Down Expand Up @@ -919,7 +924,6 @@ def prompt(prompt, ltype, indent, line_no) # :nodoc:
"%"
end
end
p
end

def output_value(omit = false) # :nodoc:
Expand Down
26 changes: 0 additions & 26 deletions lib/irb/ruby-lex.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,6 @@ def initialize
end
end

attr_reader :line_no

def initialize
@line_no = 1
@prompt = nil
end

def self.compile_with_errors_suppressed(code, line_no: 1)
begin
result = yield code, line_no
Expand All @@ -66,10 +59,6 @@ def self.compile_with_errors_suppressed(code, line_no: 1)
result
end

def set_prompt(&block)
@prompt = block
end

ERROR_TOKENS = [
:on_parse_error,
:compile_error,
Expand Down Expand Up @@ -145,12 +134,6 @@ def self.ripper_lex_without_warning(code, local_variables: [])
$VERBOSE = verbose
end

def prompt(opens, continue, line_num_offset)
ltype = ltype_from_open_tokens(opens)
indent_level = calc_indent_level(opens)
@prompt&.call(ltype, indent_level, opens.any? || continue, @line_no + line_num_offset)
end

def check_code_state(code, local_variables:)
tokens = self.class.ripper_lex_without_warning(code, local_variables: local_variables)
opens = NestingParser.open_tokens(tokens)
Expand All @@ -170,15 +153,6 @@ def code_terminated?(code, tokens, opens, local_variables:)
end
end

def save_prompt_to_context_io(opens, continue, line_num_offset)
# Implicitly saves prompt string to `@context.io.prompt`. This will be used in the next `@input.call`.
prompt(opens, continue, line_num_offset)
end

def increase_line_no(addition)
@line_no += addition
end

def assignment_expression?(code, local_variables:)
# Try to parse the code and check if the last of possibly multiple
# expressions is an assignment type.
Expand Down
8 changes: 4 additions & 4 deletions test/irb/test_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -614,21 +614,21 @@ def test_eval_input_with_long_exception
def test_prompt_main_escape
main = Struct.new(:to_s).new("main\a\t\r\n")
irb = IRB::Irb.new(IRB::WorkSpace.new(main), TestInputMethod.new)
assert_equal("irb(main )>", irb.prompt('irb(%m)>', nil, 1, 1))
assert_equal("irb(main )>", irb.send(:format_prompt, 'irb(%m)>', nil, 1, 1))
end

def test_prompt_main_inspect_escape
main = Struct.new(:inspect).new("main\\n\nmain")
irb = IRB::Irb.new(IRB::WorkSpace.new(main), TestInputMethod.new)
assert_equal("irb(main\\n main)>", irb.prompt('irb(%M)>', nil, 1, 1))
assert_equal("irb(main\\n main)>", irb.send(:format_prompt, 'irb(%M)>', nil, 1, 1))
end

def test_prompt_main_truncate
main = Struct.new(:to_s).new("a" * 100)
def main.inspect; to_s.inspect; end
irb = IRB::Irb.new(IRB::WorkSpace.new(main), TestInputMethod.new)
assert_equal('irb(aaaaaaaaaaaaaaaaaaaaaaaaaaaaa...)>', irb.prompt('irb(%m)>', nil, 1, 1))
assert_equal('irb("aaaaaaaaaaaaaaaaaaaaaaaaaaaa...)>', irb.prompt('irb(%M)>', nil, 1, 1))
assert_equal('irb(aaaaaaaaaaaaaaaaaaaaaaaaaaaaa...)>', irb.send(:format_prompt, 'irb(%m)>', nil, 1, 1))
assert_equal('irb("aaaaaaaaaaaaaaaaaaaaaaaaaaaa...)>', irb.send(:format_prompt, 'irb(%M)>', nil, 1, 1))
end

def test_lineno
Expand Down
38 changes: 19 additions & 19 deletions test/irb/test_irb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ class MockIO_AutoIndent

def initialize(*params)
@params = params
@calculated_indent
end

def auto_indent(&block)
Expand All @@ -84,14 +83,14 @@ def auto_indent(&block)
end

class MockIO_DynamicPrompt
attr_reader :prompt_list

def initialize(params, &assertion)
@params = params
@assertion = assertion
end

def dynamic_prompt(&block)
result = block.call(@params)
@assertion.call(result)
@prompt_list = block.call(@params)
end
end

Expand Down Expand Up @@ -710,24 +709,25 @@ def test_dynamic_prompt_with_blank_line

def assert_dynamic_prompt(input_with_prompt)
expected_prompt_list, lines = input_with_prompt.transpose
dynamic_prompt_executed = false
io = MockIO_DynamicPrompt.new(lines) do |prompt_list|
error_message = <<~EOM
Expected dynamic prompt:
#{expected_prompt_list.join("\n")}
Actual dynamic prompt:
#{prompt_list.join("\n")}
EOM
dynamic_prompt_executed = true
assert_equal(expected_prompt_list, prompt_list, error_message)
end
@irb.context.io = io
@irb.scanner.set_prompt do |ltype, indent, continue, line_no|
def @irb.generate_prompt(opens, continue, line_offset)
ltype = @scanner.ltype_from_open_tokens(opens)
indent = @scanner.calc_indent_level(opens)
continue = opens.any? || continue
line_no = @line_no + line_offset
'%03d:%01d:%1s:%s ' % [line_no, indent, ltype, continue ? '*' : '>']
end
io = MockIO_DynamicPrompt.new(lines)
@irb.context.io = io
@irb.configure_io
assert dynamic_prompt_executed, "dynamic_prompt's assertions were not executed."

error_message = <<~EOM
Expected dynamic prompt:
#{expected_prompt_list.join("\n")}
Actual dynamic prompt:
#{io.prompt_list.join("\n")}
EOM
assert_equal(expected_prompt_list, io.prompt_list, error_message)
end
end

Expand Down

0 comments on commit 1ceb97f

Please sign in to comment.