Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decouple RubyLex from prompt and line_no #701

Merged
merged 3 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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