diff options
author | aycabta <aycabta@gmail.com> | 2021-04-03 18:26:46 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-03 18:26:46 +0900 |
commit | cafa7904e7233d1a4b8ae7e738f6bd35ed67d642 (patch) | |
tree | e0f7fdc0ec33c37d4de1ab9185da77dbdacc75ba | |
parent | 6abb8ee711a941b7ad31d21fdb429750f7ea4463 (diff) | |
download | ruby-cafa7904e7233d1a4b8ae7e738f6bd35ed67d642.tar.gz |
Backport lib/reline, and lib/irb for 3.0.1 4th (#4349)
* [ruby/irb] Update help message for next context-mode of 4
While here, fixing tab/space issues in help message, and sync
rdoc for IRB class to match the help message.
https://github.com/ruby/irb/commit/ef8e3901cc
* [ruby/irb] Do not continue line if last expression is an endless range
Fixes [Bug #14824]
https://github.com/ruby/irb/commit/63414f8465
* [ruby/irb] Add a test for not continuing when endless range at eol
https://github.com/ruby/irb/commit/1020ac9c65
* [ruby/irb] Make save-history extension safe for concurrent use
This makes the save-history extension check for modifications to
the history file before saving it. If the history file was modified
after the history was loaded and before it was saved, append only
the new history lines to the history file.
This can result in more lines in the history file than SAVE_HISTORY
allows. However, that will be fixed the next time irb is run and
the history is saved.
Fixes [Bug #13654]
https://github.com/ruby/irb/commit/041ef53845
* Fix errors when XDG_CONFIG_HOME points to non-writable directory
`$HOME/.config` is not writable on CI
because I think tests should not corrupt user's data.
And GitHub Actions CI sets `XDG_CONFIG_HOME`
since `Version: 20210309.1`.
https://github.com/ruby/actions/runs/2130811016?check_suite_focus=true#step:16:301
```
Errno::EACCES: Permission denied @ dir_s_mkdir - /home/runner/.config/irb
```
* Try to fix errors in TestIRB::TestHistory too
https://github.com/ruby/actions/runs/2137935523?check_suite_focus=true#step:9:562
```
1) Error:
TestIRB::TestHistory#test_history_concurrent_use:
Errno::EACCES: Permission denied @ dir_s_mkdir - /home/runner/.config/irb
/home/runner/work/actions/actions/ruby/lib/fileutils.rb:253:in `mkdir'
/home/runner/work/actions/actions/ruby/lib/fileutils.rb:253:in `fu_mkdir'
/home/runner/work/actions/actions/ruby/lib/fileutils.rb:231:in `block (2 levels) in mkdir_p'
/home/runner/work/actions/actions/ruby/lib/fileutils.rb:229:in `reverse_each'
/home/runner/work/actions/actions/ruby/lib/fileutils.rb:229:in `block in mkdir_p'
/home/runner/work/actions/actions/ruby/lib/fileutils.rb:211:in `each'
/home/runner/work/actions/actions/ruby/lib/fileutils.rb:211:in `mkdir_p'
/home/runner/work/actions/actions/ruby/lib/irb/init.rb:355:in `rc_file_generators'
/home/runner/work/actions/actions/ruby/lib/irb/init.rb:330:in `rc_file'
/home/runner/work/actions/actions/ruby/test/irb/test_history.rb:170:in `block in assert_history'
/home/runner/work/actions/actions/ruby/lib/tmpdir.rb:96:in `mktmpdir'
/home/runner/work/actions/actions/ruby/test/irb/test_history.rb:168:in `assert_history'
/home/runner/work/actions/actions/ruby/test/irb/test_history.rb:133:in `test_history_concurrent_use'
```
* [ruby/irb] Define "measure" command without forced override
https://github.com/ruby/irb/commit/9587ba13b5
* [ruby/irb] Add all lib files automatically
https://github.com/ruby/irb/commit/ecc82336b7
* [ruby/irb] Don't call Ruby 2.4+'s String#pretty_print
https://github.com/ruby/irb/commit/89bcf107be
* [ruby/irb] Implement ls command
https://github.com/ruby/irb/commit/19b6c20604
* [ruby/irb] Add whereami command
https://github.com/ruby/irb/commit/bc822e4aac
* [ruby/irb] Fix column overflow on ls output
https://github.com/ruby/irb/commit/6115754623
* [ruby/irb] Fix step's argument
cols.size was calling Integer#size, which returns 8.
Fixing a bug of https://github.com/ruby/irb/pull/209
https://github.com/ruby/irb/commit/c93ae4be71
* [ruby/irb] Deal with different screen sizes
e.g. http://rubyci.s3.amazonaws.com/centos8/ruby-master/log/20210321T063003Z.fail.html.gz
https://github.com/ruby/irb/commit/ddb3472ba2
* [ruby/irb] Have some right padding
instead of filling out an entire line
https://github.com/ruby/irb/commit/6ac8f45f5f
* Suppress verbose messages
Get rid of warnings in the parallel test.
```
unknown command: "Switch to inspect mode."
```
* [ruby/irb] Change ripper_lex_without_warning to a class method
https://github.com/ruby/irb/commit/d9f8abc17e
* [ruby/irb] Complete require and require_relative
https://github.com/ruby/irb/commit/1c61178b4c
* [ruby/reline] Add Reline.ungetc to control buffer
https://github.com/ruby/reline/commit/43ac03c624
* [ruby/reline] Reline.delete_text removes the current line in multiline
https://github.com/ruby/reline/commit/da90c094a1
* [ruby/reline] Support preposing and postposing for Reline.completion_proc
https://github.com/ruby/reline/commit/1f469de90c
* [ruby/reline] Suppress crashing when completer_{quote,word_break}_characters is empty
https://github.com/ruby/reline/commit/c6f1164942
* [ruby/irb] fix completion test when out-of-place build
* [ruby/irb] Cache completion files to require
https://github.com/ruby/irb/commit/612ebcb311
* [ruby/irb] Always add input method when calling Irb.new in tests
When passes input method as nil to Context.new through Irb.new,
ReidlineInputMethod.new is executed and the global internal state of Reline is
rewritten, therefore other tests are failed in the Ruby repository. This
commit changes to use TestInputMethod.
https://github.com/ruby/irb/commit/010dce9210
* [ruby/irb] Prevent the completion from crashing if rdoc is missing
There are cases where ruby is installed without rdoc and e.g.
lib/irb/cmd/help.rb also handles the LoadError
Here is how to replicate the issue:
```
$ docker run -it alpine:3.13.3 sh
/ # apk add ruby ruby-irb ruby-io-console
/ # irb
irb(main):001:0> Class[TAB][TAB]
```
And you end up with something like:
```
irb(main):001:0> ClassTraceback (most recent call last):
34: from /usr/bin/irb:23:in `<main>'
33: from /usr/bin/irb:23:in `load'
32: from /usr/lib/ruby/gems/2.7.0/gems/irb-1.2.6/exe/irb:11:in `<top (required)>'
31: from /usr/lib/ruby/2.7.0/irb.rb:400:in `start'
30: from /usr/lib/ruby/2.7.0/irb.rb:471:in `run'
29: from /usr/lib/ruby/2.7.0/irb.rb:471:in `catch'
28: from /usr/lib/ruby/2.7.0/irb.rb:472:in `block in run'
27: from /usr/lib/ruby/2.7.0/irb.rb:537:in `eval_input'
26: from /usr/lib/ruby/2.7.0/irb/ruby-lex.rb:150:in `each_top_level_statement'
25: from /usr/lib/ruby/2.7.0/irb/ruby-lex.rb:150:in `catch'
24: from /usr/lib/ruby/2.7.0/irb/ruby-lex.rb:151:in `block in each_top_level_statement'
23: from /usr/lib/ruby/2.7.0/irb/ruby-lex.rb:151:in `loop'
22: from /usr/lib/ruby/2.7.0/irb/ruby-lex.rb:154:in `block (2 levels) in each_top_level_statement'
21: from /usr/lib/ruby/2.7.0/irb/ruby-lex.rb:182:in `lex'
20: from /usr/lib/ruby/2.7.0/irb.rb:518:in `block in eval_input'
19: from /usr/lib/ruby/2.7.0/irb.rb:704:in `signal_status'
18: from /usr/lib/ruby/2.7.0/irb.rb:519:in `block (2 levels) in eval_input'
17: from /usr/lib/ruby/2.7.0/irb/input-method.rb:294:in `gets'
16: from /usr/lib/ruby/2.7.0/forwardable.rb:235:in `readmultiline'
15: from /usr/lib/ruby/2.7.0/forwardable.rb:235:in `readmultiline'
14: from /usr/lib/ruby/2.7.0/reline.rb:175:in `readmultiline'
13: from /usr/lib/ruby/2.7.0/reline.rb:238:in `inner_readline'
12: from /usr/lib/ruby/2.7.0/reline.rb:238:in `loop'
11: from /usr/lib/ruby/2.7.0/reline.rb:239:in `block in inner_readline'
10: from /usr/lib/ruby/2.7.0/reline.rb:270:in `read_io'
9: from /usr/lib/ruby/2.7.0/reline.rb:270:in `loop'
8: from /usr/lib/ruby/2.7.0/reline.rb:311:in `block in read_io'
7: from /usr/lib/ruby/2.7.0/reline.rb:240:in `block (2 levels) in inner_readline'
6: from /usr/lib/ruby/2.7.0/reline.rb:240:in `each'
5: from /usr/lib/ruby/2.7.0/reline.rb:241:in `block (3 levels) in inner_readline'
4: from /usr/lib/ruby/2.7.0/reline/line_editor.rb:820:in `input_key'
3: from /usr/lib/ruby/2.7.0/reline/line_editor.rb:608:in `complete'
2: from /usr/lib/ruby/2.7.0/irb/completion.rb:269:in `block in <module:InputCompletor>'
1: from /usr/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:92:in `require'
/usr/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:92:in `require': cannot load such file -- rdoc (LoadError)
```
https://github.com/ruby/irb/commit/a2d299c2ac
* [ruby/irb] Suppress verbose messages in the parallel test
`:VERBOSE` flag needs to be set prior to `IRB::Irb.new`.
https://github.com/ruby/irb/commit/78604682d9
* [ruby/irb] SIGINT should raise Interrupt after IRB session
https://github.com/ruby/irb/commit/5832cfe75b
* [ruby/irb] Colorize `__END__` as keyword
https://github.com/ruby/irb/commit/9b84018311
* [ruby/irb] Add show_source command
https://github.com/ruby/irb/commit/108cb04352
* [ruby/reline] Reset @rest_height when clear screen
https://github.com/ruby/reline/commit/3a7019b0d5
* [ruby/irb] process multi-line pastes as a single entity
this allows pasting leading-dot chained methods correctly:
```ruby
class A
def a; self; end
def b; true; end
end
a = A.new
a
.a
.b
```
will properly return `true` instead of erroring on the `.a` line:
```
irb(main):001:1* class A
irb(main):002:1* def a; self; end
irb(main):003:0> end
irb(main):004:0*
irb(main):005:0> a = A.new
irb(main):006:0*
irb(main):007:0> a
irb(main):008:0> .a
irb(main):009:0> .a
=> #<A:0x00007f984211fbe8>
```
https://github.com/ruby/irb/commit/45aeb52575
* [ruby/irb] Add yamatanooroti test example
https://github.com/ruby/irb/commit/279155fcee
* [ruby/irb] Add test for multiline paste
https://github.com/ruby/irb/commit/e93c9cb54d
* [ruby/irb] Evaluate each toplevel statement
https://github.com/ruby/irb/commit/bc1b1d8bc3
* [ruby/irb] Version 1.3.5
https://github.com/ruby/irb/commit/22e2ddf715
* [ruby/reline] Version 0.2.5
https://github.com/ruby/reline/commit/22ce5651e5
Co-authored-by: Jeremy Evans <code@jeremyevans.net>
Co-authored-by: Kazuhiro NISHIYAMA <zn@mbf.nifty.com>
Co-authored-by: Takashi Kokubun <takashikkbn@gmail.com>
Co-authored-by: Nobuyoshi Nakada <nobu@ruby-lang.org>
Co-authored-by: Aleksandar Ivanov <aivanov92@gmail.com>
Co-authored-by: Koichi Sasada <ko1@atdot.net>
Co-authored-by: Cody Cutrer <cody@instructure.com>
30 files changed, 920 insertions, 139 deletions
diff --git a/lib/irb.rb b/lib/irb.rb index 7f99974f28..93c4d25c92 100644 --- a/lib/irb.rb +++ b/lib/irb.rb @@ -60,7 +60,11 @@ require_relative "irb/easter-egg" # -E enc Same as `ruby -E` # -w Same as `ruby -w` # -W[level=2] Same as `ruby -W` -# --inspect Use `inspect' for output (default except for bc mode) +# --context-mode n Set n[0-4] to method to create Binding Object, +# when new workspace was created +# --echo Show result(default) +# --noecho Don't show result +# --inspect Use `inspect' for output # --noinspect Don't use inspect for output # --multiline Use multiline editor module # --nomultiline Don't use multiline editor module @@ -68,19 +72,24 @@ require_relative "irb/easter-egg" # --nosingleline Don't use singleline editor module # --colorize Use colorization # --nocolorize Don't use colorization -# --prompt prompt-mode -# --prompt-mode prompt-mode +# --prompt prompt-mode/--prompt-mode prompt-mode # Switch prompt mode. Pre-defined prompt modes are # `default', `simple', `xmp' and `inf-ruby' # --inf-ruby-mode Use prompt appropriate for inf-ruby-mode on emacs. # Suppresses --multiline and --singleline. -# --simple-prompt Simple prompt mode +# --sample-book-mode/--simple-prompt +# Simple prompt mode # --noprompt No prompt mode +# --single-irb Share self with sub-irb. # --tracer Display trace for each execution of commands. # --back-trace-limit n # Display backtrace top n and tail n. The default # value is 16. -# -v, --version Print the version of irb +# --verbose Show details +# --noverbose Don't show details +# -v, --version Print the version of irb +# -h, --help Print help +# -- Separate options of irb from the list of command-line args # # == Configuration # @@ -463,7 +472,7 @@ module IRB conf[:IRB_RC].call(context) if conf[:IRB_RC] conf[:MAIN_CONTEXT] = context - trap("SIGINT") do + prev_trap = trap("SIGINT") do signal_handle end @@ -472,6 +481,7 @@ module IRB eval_input end ensure + trap("SIGINT", prev_trap) conf[:AT_EXIT].each{|hook| hook.call} end end diff --git a/lib/irb/cmd/ls.rb b/lib/irb/cmd/ls.rb new file mode 100644 index 0000000000..f163f4f9e6 --- /dev/null +++ b/lib/irb/cmd/ls.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require "reline" +require_relative "nop" +require_relative "../color" + +# :stopdoc: +module IRB + module ExtendCommand + class Ls < Nop + def execute(*arg, grep: nil) + o = Output.new(grep: grep) + + obj = arg.empty? ? irb_context.workspace.main : arg.first + locals = arg.empty? ? irb_context.workspace.binding.local_variables : [] + klass = (obj.class == Class || obj.class == Module ? obj : obj.class) + + o.dump("constants", obj.constants) if obj.respond_to?(:constants) + o.dump("#{klass}.methods", obj.singleton_methods(false)) + o.dump("#{klass}#methods", klass.public_instance_methods(false)) + o.dump("instance variables", obj.instance_variables) + o.dump("class variables", klass.class_variables) + o.dump("locals", locals) + end + + class Output + MARGIN = " " + + def initialize(grep: nil) + @grep = grep + @line_width = screen_width - MARGIN.length # right padding + end + + def dump(name, strs) + strs = strs.grep(@grep) if @grep + strs = strs.sort + return if strs.empty? + + # Attempt a single line + print "#{Color.colorize(name, [:BOLD, :BLUE])}: " + if fits_on_line?(strs, cols: strs.size, offset: "#{name}: ".length) + puts strs.join(MARGIN) + return + end + puts + + # Dump with the largest # of columns that fits on a line + cols = strs.size + until fits_on_line?(strs, cols: cols, offset: MARGIN.length) || cols == 1 + cols -= 1 + end + widths = col_widths(strs, cols: cols) + strs.each_slice(cols) do |ss| + puts ss.map.with_index { |s, i| "#{MARGIN}%-#{widths[i]}s" % s }.join + end + end + + private + + def fits_on_line?(strs, cols:, offset: 0) + width = col_widths(strs, cols: cols).sum + MARGIN.length * (cols - 1) + width <= @line_width - offset + end + + def col_widths(strs, cols:) + cols.times.map do |col| + (col...strs.size).step(cols).map do |i| + strs[i].length + end.max + end + end + + def screen_width + Reline.get_screen_size.last + rescue Errno::EINVAL # in `winsize': Invalid argument - <STDIN> + 80 + end + end + private_constant :Output + end + end +end +# :startdoc: diff --git a/lib/irb/cmd/nop.rb b/lib/irb/cmd/nop.rb index fa3c011b5f..d6f7a611a6 100644 --- a/lib/irb/cmd/nop.rb +++ b/lib/irb/cmd/nop.rb @@ -14,10 +14,16 @@ module IRB module ExtendCommand class Nop - - def self.execute(conf, *opts, &block) - command = new(conf) - command.execute(*opts, &block) + if RUBY_ENGINE == "ruby" && RUBY_VERSION >= "2.7.0" + def self.execute(conf, *opts, **kwargs, &block) + command = new(conf) + command.execute(*opts, **kwargs, &block) + end + else + def self.execute(conf, *opts, &block) + command = new(conf) + command.execute(*opts, &block) + end end def initialize(conf) diff --git a/lib/irb/cmd/show_source.rb b/lib/irb/cmd/show_source.rb new file mode 100644 index 0000000000..0bd40b7d4e --- /dev/null +++ b/lib/irb/cmd/show_source.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require_relative "nop" +require_relative "../color" +require_relative "../ruby-lex" + +# :stopdoc: +module IRB + module ExtendCommand + class ShowSource < Nop + def execute(str = nil) + unless str.is_a?(String) + puts "Error: Expected a string but got #{str.inspect}" + return + end + source = find_source(str) + if source && File.exist?(source.file) + show_source(source) + else + puts "Error: Couldn't locate a definition for #{str}" + end + nil + end + + private + + # @param [IRB::ExtendCommand::ShowSource::Source] source + def show_source(source) + puts + puts "#{bold("From")}: #{source.file}:#{source.first_line}" + puts + code = IRB::Color.colorize_code(File.read(source.file)) + puts code.lines[(source.first_line - 1)...source.last_line].join + puts + end + + def find_source(str) + case str + when /\A[A-Z]\w*(::[A-Z]\w*)*\z/ # Const::Name + eval(str, irb_context.workspace.binding) # trigger autoload + base = irb_context.workspace.binding.receiver.yield_self { |r| r.is_a?(Module) ? r : Object } + file, line = base.const_source_location(str) if base.respond_to?(:const_source_location) # Ruby 2.7+ + when /\A(?<owner>[A-Z]\w*(::[A-Z]\w*)*)#(?<method>[^ :.]+)\z/ # Class#method + owner = eval(Regexp.last_match[:owner], irb_context.workspace.binding) + method = Regexp.last_match[:method] + if owner.respond_to?(:instance_method) && owner.instance_methods.include?(method.to_sym) + file, line = owner.instance_method(method).source_location + end + when /\A((?<receiver>.+)(\.|::))?(?<method>[^ :.]+)\z/ # method, receiver.method, receiver::method + receiver = eval(Regexp.last_match[:receiver] || 'self', irb_context.workspace.binding) + method = Regexp.last_match[:method] + file, line = receiver.method(method).source_location if receiver.respond_to?(method) + end + if file && line + Source.new(file: file, first_line: line, last_line: find_end(file, line)) + end + end + + def find_end(file, first_line) + return first_line unless File.exist?(file) + lex = RubyLex.new + code = +"" + File.read(file).lines[(first_line - 1)..-1].each_with_index do |line, i| + _ltype, _indent, continue, code_block_open = lex.check_state(code << line) + if !continue && !code_block_open + return first_line + i + end + end + first_line + end + + def bold(str) + Color.colorize(str, [:BOLD]) + end + + Source = Struct.new( + :file, # @param [String] - file name + :first_line, # @param [String] - first line + :last_line, # @param [String] - last line + keyword_init: true, + ) + private_constant :Source + end + end +end +# :startdoc: diff --git a/lib/irb/cmd/whereami.rb b/lib/irb/cmd/whereami.rb new file mode 100644 index 0000000000..b3def11b93 --- /dev/null +++ b/lib/irb/cmd/whereami.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require_relative "nop" + +# :stopdoc: +module IRB + module ExtendCommand + class Whereami < Nop + def execute(*) + code = irb_context.workspace.code_around_binding + if code + puts code + else + puts "The current context doesn't have code." + end + end + end + end +end +# :startdoc: diff --git a/lib/irb/color.rb b/lib/irb/color.rb index a054bb20f8..cfbb3cc668 100644 --- a/lib/irb/color.rb +++ b/lib/irb/color.rb @@ -64,6 +64,7 @@ module IRB # :nodoc: on_alias_error: [[RED, REVERSE], ALL], on_class_name_error:[[RED, REVERSE], ALL], on_param_error: [[RED, REVERSE], ALL], + on___end__: [[GREEN], ALL], } rescue NameError # Give up highlighting Ripper-incompatible older Ruby @@ -120,6 +121,7 @@ module IRB # :nodoc: symbol_state = SymbolState.new colored = +'' length = 0 + end_seen = false scan(code, allow_last_error: !complete) do |token, str, expr| # IRB::ColorPrinter skips colorizing fragments with any invalid token @@ -138,10 +140,11 @@ module IRB # :nodoc: end end length += str.bytesize + end_seen = true if token == :on___end__ end # give up colorizing incomplete Ripper tokens - if length != code.bytesize + unless end_seen or length == code.bytesize return Reline::Unicode.escape_for_print(code) end diff --git a/lib/irb/color_printer.rb b/lib/irb/color_printer.rb index 92afea51cd..30c6825750 100644 --- a/lib/irb/color_printer.rb +++ b/lib/irb/color_printer.rb @@ -21,6 +21,15 @@ module IRB end end + def pp(obj) + if obj.is_a?(String) + # Avoid calling Ruby 2.4+ String#pretty_print that splits a string by "\n" + text(obj.inspect) + else + super + end + end + def text(str, width = nil) unless str.is_a?(String) str = str.inspect diff --git a/lib/irb/completion.rb b/lib/irb/completion.rb index 22a1ad1d3d..d1bb82122e 100644 --- a/lib/irb/completion.rb +++ b/lib/irb/completion.rb @@ -7,7 +7,7 @@ # From Original Idea of shugo@ruby-lang.org # -autoload :RDoc, "rdoc" +require_relative 'ruby-lex' module IRB module InputCompletor # :nodoc: @@ -38,8 +38,69 @@ module IRB BASIC_WORD_BREAK_CHARACTERS = " \t\n`><=;|&{(" - CompletionProc = proc { |input| - retrieve_completion_data(input).compact.map{ |i| i.encode(Encoding.default_external) } + def self.retrieve_files_to_require_from_load_path + @@files_from_load_path ||= $LOAD_PATH.flat_map { |path| + begin + Dir.glob("**/*.{rb,#{RbConfig::CONFIG['DLEXT']}}", base: path) + rescue Errno::ENOENT + [] + end + }.uniq.map { |path| + path.sub(/\.(rb|#{RbConfig::CONFIG['DLEXT']})\z/, '') + } + end + + def self.retrieve_files_to_require_relative_from_current_dir + @@files_from_current_dir ||= Dir.glob("**/*.{rb,#{RbConfig::CONFIG['DLEXT']}}", base: '.').map { |path| + path.sub(/\.(rb|#{RbConfig::CONFIG['DLEXT']})\z/, '') + } + end + + CompletionRequireProc = lambda { |target, preposing = nil, postposing = nil| + if target =~ /\A(['"])([^'"]+)\Z/ + quote = $1 + actual_target = $2 + else + return nil # It's not String literal + end + tokens = RubyLex.ripper_lex_without_warning(preposing.gsub(/\s*\z/, '')) + tok = nil + tokens.reverse_each do |t| + unless [:on_lparen, :on_sp, :on_ignored_sp, :on_nl, :on_ignored_nl, :on_comment].include?(t.event) + tok = t + break + end + end + result = [] + if tok && tok.event == :on_ident && tok.state == Ripper::EXPR_CMDARG + case tok.tok + when 'require' + result = retrieve_files_to_require_from_load_path.select { |path| + path.start_with?(actual_target) + }.map { |path| + quote + path + } + when 'require_relative' + result = retrieve_files_to_require_relative_from_current_dir.select { |path| + path.start_with?(actual_target) + }.map { |path| + quote + path + } + end + end + result + } + + CompletionProc = lambda { |target, preposing = nil, postposing = nil| + if preposing && postposing + result = CompletionRequireProc.(target, preposing, postposing) + unless result + result = retrieve_completion_data(target).compact.map{ |i| i.encode(Encoding.default_external) } + end + result + else + retrieve_completion_data(target).compact.map{ |i| i.encode(Encoding.default_external) } + end } def self.retrieve_completion_data(input, bind: IRB.conf[:MAIN_CONTEXT].workspace.binding, doc_namespace: false) @@ -266,13 +327,22 @@ module IRB end PerfectMatchedProc = ->(matched, bind: IRB.conf[:MAIN_CONTEXT].workspace.binding) { + begin + require 'rdoc' + rescue LoadError + return + end + RDocRIDriver ||= RDoc::RI::Driver.new + if matched =~ /\A(?:::)?RubyVM/ and not ENV['RUBY_YES_I_AM_NOT_A_NORMAL_USER'] IRB.__send__(:easter_egg) return end + namespace = retrieve_completion_data(matched, bind: bind, doc_namespace: true) return unless namespace + if namespace.is_a?(Array) out = RDoc::Markup::Document.new namespace.each do |m| diff --git a/lib/irb/ext/save-history.rb b/lib/irb/ext/save-history.rb index ac358c8ccb..7acaebe36a 100644 --- a/lib/irb/ext/save-history.rb +++ b/lib/irb/ext/save-history.rb @@ -81,6 +81,8 @@ module IRB end } end + @loaded_history_lines = history.size + @loaded_history_mtime = File.mtime(history_file) end end @@ -105,12 +107,20 @@ module IRB raise end - open(history_file, "w:#{IRB.conf[:LC_MESSAGES].encoding}", 0600) do |f| + if File.exist?(history_file) && @loaded_history_mtime && + File.mtime(history_file) != @loaded_history_mtime + history = history[@loaded_history_lines..-1] + append_history = true + end + + open(history_file, "#{append_history ? 'a' : 'w'}:#{IRB.conf[:LC_MESSAGES].encoding}", 0600) do |f| hist = history.map{ |l| l.split("\n").join("\\\n") } - begin - hist = hist.last(num) if hist.size > num and num > 0 - rescue RangeError # bignum too big to convert into `long' - # Do nothing because the bignum should be treated as inifinity + unless append_history + begin + hist = hist.last(num) if hist.size > num and num > 0 + rescue RangeError # bignum too big to convert into `long' + # Do nothing because the bignum should be treated as inifinity + end end f.puts(hist) end diff --git a/lib/irb/extend-command.rb b/lib/irb/extend-command.rb index 347323247e..339e9e6084 100644 --- a/lib/irb/extend-command.rb +++ b/lib/irb/extend-command.rb @@ -126,7 +126,23 @@ module IRB # :nodoc: ], [ - :measure, :Measure, "irb/cmd/measure" + :irb_ls, :Ls, "irb/cmd/ls", + [:ls, NO_OVERRIDE], + ], + + [ + :irb_measure, :Measure, "irb/cmd/measure", + [:measure, NO_OVERRIDE], + ], + + [ + :irb_show_source, :ShowSource, "irb/cmd/show_source", + [:show_source, NO_OVERRIDE], + ], + + [ + :irb_whereami, :Whereami, "irb/cmd/whereami", + [:whereami, NO_OVERRIDE], ], ] @@ -168,12 +184,13 @@ module IRB # :nodoc: end if load_file + kwargs = ", **kwargs" if RUBY_ENGINE == "ruby" && RUBY_VERSION >= "2.7.0" line = __LINE__; eval %[ - def #{cmd_name}(*opts, &b) + def #{cmd_name}(*opts#{kwargs}, &b) require "#{load_file}" arity = ExtendCommand::#{cmd_class}.instance_method(:execute).arity args = (1..(arity < 0 ? ~arity : arity)).map {|i| "arg" + i.to_s } - args << "*opts" if arity < 0 + args << "*opts#{kwargs}" if arity < 0 args << "&block" args = args.join(", ") line = __LINE__; eval %[ @@ -184,7 +201,7 @@ module IRB # :nodoc: end end ], nil, __FILE__, line - __send__ :#{cmd_name}_, *opts, &b + __send__ :#{cmd_name}_, *opts#{kwargs}, &b end ], nil, __FILE__, line else diff --git a/lib/irb/input-method.rb b/lib/irb/input-method.rb index e223672985..1854567a2d 100644 --- a/lib/irb/input-method.rb +++ b/lib/irb/input-method.rb @@ -280,6 +280,7 @@ module IRB Reline.basic_word_break_characters = IRB::InputCompletor::BASIC_WORD_BREAK_CHARACTERS end Reline.completion_append_character = nil + Reline.completer_quote_characters = '' Reline.completion_proc = IRB::InputCompletor::CompletionProc Reline.output_modifier_proc = if IRB.conf[:USE_COLORIZE] diff --git a/lib/irb/irb.gemspec b/lib/irb/irb.gemspec index 9842b4bce1..38fee9d9fb 100644 --- a/lib/irb/irb.gemspec +++ b/lib/irb/irb.gemspec @@ -28,53 +28,8 @@ Gem::Specification.new do |spec| "doc/irb/irb.rd.ja", "exe/irb", "irb.gemspec", - "lib/irb.rb", - "lib/irb/cmd/chws.rb", - "lib/irb/cmd/fork.rb", - "lib/irb/cmd/help.rb", - "lib/irb/cmd/info.rb", - "lib/irb/cmd/load.rb", - "lib/irb/cmd/measure.rb", - "lib/irb/cmd/nop.rb", - "lib/irb/cmd/pushws.rb", - "lib/irb/cmd/subirb.rb", - "lib/irb/color.rb", - "lib/irb/color_printer.rb", - "lib/irb/completion.rb", - "lib/irb/context.rb", - "lib/irb/easter-egg.rb", - "lib/irb/ext/change-ws.rb", - "lib/irb/ext/history.rb", - "lib/irb/ext/loader.rb", - "lib/irb/ext/multi-irb.rb", - "lib/irb/ext/save-history.rb", - "lib/irb/ext/tracer.rb", - "lib/irb/ext/use-loader.rb", - "lib/irb/ext/workspaces.rb", - "lib/irb/extend-command.rb", - "lib/irb/frame.rb", - "lib/irb/help.rb", - "lib/irb/init.rb", - "lib/irb/input-method.rb", - "lib/irb/inspector.rb", - "lib/irb/lc/error.rb", - "lib/irb/lc/help-message", - "lib/irb/lc/ja/encoding_aliases.rb", - "lib/irb/lc/ja/error.rb", - "lib/irb/lc/ja/help-message", - "lib/irb/locale.rb", - "lib/irb/magic-file.rb", - "lib/irb/notifier.rb", - "lib/irb/output-method.rb", - "lib/irb/ruby-lex.rb", - "lib/irb/ruby_logo.aa", - "lib/irb/src_encoding.rb", - "lib/irb/version.rb", - "lib/irb/workspace.rb", - "lib/irb/ws-for-case-2.rb", - "lib/irb/xmp.rb", "man/irb.1", - ] + ] + Dir.glob("lib/**/*") spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] diff --git a/lib/irb/lc/help-message b/lib/irb/lc/help-message index a80facc9c5..9c3ea859ad 100644 --- a/lib/irb/lc/help-message +++ b/lib/irb/lc/help-message @@ -10,7 +10,7 @@ # # Usage: irb.rb [options] [programfile] [arguments] - -f Suppress read of ~/.irbrc + -f Suppress read of ~/.irbrc -d Set $DEBUG to true (same as `ruby -d') -r load-module Same as `ruby -r' -I path Specify $LOAD_PATH directory @@ -18,7 +18,7 @@ Usage: irb.rb [options] [programfile] [arguments] -E enc Same as `ruby -E` -w Same as `ruby -w` -W[level=2] Same as `ruby -W` - --context-mode n Set n[0-3] to method to create Binding Object, + --context-mode n Set n[0-4] to method to create Binding Object, when new workspace was created --echo Show result(default) --noecho Don't show result @@ -31,8 +31,8 @@ Usage: irb.rb [options] [programfile] [arguments] --colorize Use colorization --nocolorize Don't use colorization --prompt prompt-mode/--prompt-mode prompt-mode - Switch prompt mode. Pre-defined prompt modes are - `default', `simple', `xmp' and `inf-ruby' + Switch prompt mode. Pre-defined prompt modes are + `default', `simple', `xmp' and `inf-ruby' --inf-ruby-mode Use prompt appropriate for inf-ruby-mode on emacs. Suppresses --multiline and --singleline. --sample-book-mode/--simple-prompt @@ -41,8 +41,8 @@ Usage: irb.rb [options] [programfile] [arguments] --single-irb Share self with sub-irb. --tracer Display trace for each execution of commands. --back-trace-limit n - Display backtrace top n and tail n. The default - value is 16. + Display backtrace top n and tail n. The default + value is 16. --verbose Show details --noverbose Don't show details -v, --version Print the version of irb diff --git a/lib/irb/ruby-lex.rb b/lib/irb/ruby-lex.rb index ce94797dad..82df06da2b 100644 --- a/lib/irb/ruby-lex.rb +++ b/lib/irb/ruby-lex.rb @@ -47,12 +47,26 @@ class RubyLex @io = io if @io.respond_to?(:check_termination) @io.check_termination do |code| - code.gsub!(/\s*\z/, '').concat("\n") - ltype, indent, continue, code_block_open = check_state(code) - if ltype or indent > 0 or continue or code_block_open - false + if Reline::IOGate.in_pasting? + lex = RubyLex.new + rest = lex.check_termination_in_prev_line(code) + if rest + Reline.delete_text + rest.bytes.reverse_each do |c| + Reline.ungetc(c) + end + true + else + false + end else - true + code.gsub!(/\s*\z/, '').concat("\n") + ltype, indent, continue, code_block_open = check_state(code) + if ltype or indent > 0 or continue or code_block_open + false + else + true + end end end end @@ -60,7 +74,7 @@ class RubyLex @io.dynamic_prompt do |lines| lines << '' if lines.empty? result = [] - tokens = ripper_lex_without_warning(lines.map{ |l| l + "\n" }.join) + tokens = self.class.ripper_lex_without_warning(lines.map{ |l| l + "\n" }.join) code = String.new partial_tokens = [] unprocessed_tokens = [] @@ -115,10 +129,10 @@ class RubyLex :on_param_error ] - def ripper_lex_without_warning(code) + def self.ripper_lex_without_warning(code) verbose, $VERBOSE = $VERBOSE, nil tokens = nil - self.class.compile_with_errors_suppressed(code) do |inner_code, line_no| + compile_with_errors_suppressed(code) do |inner_code, line_no| lexer = Ripper::Lexer.new(inner_code, '-', line_no) if lexer.respond_to?(:scan) # Ruby 2.7+ tokens = [] @@ -168,7 +182,7 @@ class RubyLex if @io.respond_to?(:auto_indent) and context.auto_indent_mode @io.auto_indent do |lines, line_index, byte_pointer, is_newline| if is_newline - @tokens = ripper_lex_without_warning(lines[0..line_index].join("\n")) + @tokens = self.class.ripper_lex_without_warning(lines[0..line_index].join("\n")) prev_spaces = find_prev_spaces(line_index) depth_difference = check_newline_depth_difference depth_difference = 0 if depth_difference < 0 @@ -177,7 +191,7 @@ class RubyLex code = line_index.zero? ? '' : lines[0..(line_index - 1)].map{ |l| l + "\n" }.join last_line = lines[line_index]&.byteslice(0, byte_pointer) code += last_line if last_line - @tokens = ripper_lex_without_warning(code) + @tokens = self.class.ripper_lex_without_warning(code) corresponding_token_depth = check_corresponding_token_depth if corresponding_token_depth corresponding_token_depth @@ -190,7 +204,7 @@ class RubyLex end def check_state(code, tokens = nil) - tokens = ripper_lex_without_warning(code) unless tokens + tokens = self.class.ripper_lex_without_warning(code) unless tokens ltype = process_literal_type(tokens) indent = process_nesting_level(tokens) continue = process_continue(tokens) @@ -256,7 +270,7 @@ class RubyLex end code = @line + (line.nil? ? '' : line) code.gsub!(/\s*\z/, '').concat("\n") - @tokens = ripper_lex_without_warning(code) + @tokens = self.class.ripper_lex_without_warning(code) @continue = process_continue @code_block_open = check_code_block(code) @indent = process_nesting_level @@ -277,8 +291,9 @@ class RubyLex return true elsif tokens.size >= 1 and tokens[-1][1] == :on_heredoc_end # "EOH\n" return false - elsif tokens.size >= 2 and defined?(Ripper::EXPR_BEG) and tokens[-2][3].anybits?(Ripper::EXPR_BEG | Ripper::EXPR_FNAME) + elsif tokens.size >= 2 and defined?(Ripper::EXPR_BEG) and tokens[-2][3].anybits?(Ripper::EXPR_BEG | Ripper::EXPR_FNAME) and tokens[-2][2] !~ /\A\.\.\.?\z/ # end of literal except for regexp + # endless range at end of line is not a continue return true end false @@ -738,5 +753,50 @@ class RubyLex nil end end + + def check_termination_in_prev_line(code) + tokens = self.class.ripper_lex_without_warning(code) + past_first_newline = false + index = tokens.rindex do |t| + # traverse first token before last line + if past_first_newline + if t.tok.include?("\n") + true + end + elsif t.tok.include?("\n") + past_first_newline = true + false + else + false + end + end + if index + first_token = nil + last_line_tokens = tokens[(index + 1)..(tokens.size - 1)] + last_line_tokens.each do |t| + unless [:on_sp, :on_ignored_sp, :on_comment].include?(t.event) + first_token = t + break + end + end + if first_token.nil? + return false + elsif first_token && first_token.state == Ripper::EXPR_DOT + return false + else + tokens_without_last_line = tokens[0..index] + ltype = process_literal_type(tokens_without_last_line) + indent = process_nesting_level(tokens_without_last_line) + continue = process_continue(tokens_without_last_line) + code_block_open = check_code_block(tokens_without_last_line.map(&:tok).join(''), tokens_without_last_line) + if ltype or indent > 0 or continue or code_block_open + return false + else + return last_line_tokens.map(&:tok).join('') + end + end + end + false + end end # :startdoc: diff --git a/lib/irb/version.rb b/lib/irb/version.rb index 0a4a1bb922..0014bdda74 100644 --- a/lib/irb/version.rb +++ b/lib/irb/version.rb @@ -11,7 +11,7 @@ # module IRB # :nodoc: - VERSION = "1.3.4" + VERSION = "1.3.5" @RELEASE_VERSION = VERSION - @LAST_UPDATE_DATE = "2021-02-25" + @LAST_UPDATE_DATE = "2021-04-03" end diff --git a/lib/reline.rb b/lib/reline.rb index 81ea9f9b58..a7bd4d9280 100644 --- a/lib/reline.rb +++ b/lib/reline.rb @@ -446,6 +446,10 @@ module Reline } end + def self.ungetc(c) + Reline::IOGate.ungetc(c) + end + def self.line_editor core.line_editor end diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index 12a2bde234..7d71e62d63 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -813,6 +813,7 @@ class Reline::LineEditor end move_cursor_up(back) move_cursor_down(@first_line_started_from + @started_from) + @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last) end @@ -1158,8 +1159,25 @@ class Reline::LineEditor def call_completion_proc result = retrieve_completion_block(true) - slice = result[1] - result = @completion_proc.(slice) if @completion_proc and slice + preposing, target, postposing = result + if @completion_proc and target + argnum = @completion_proc.parameters.inject(0) { |result, item| + case item.first + when :req, :opt + result + 1 + when :rest + break 3 + end + } + case argnum + when 1 + result = @completion_proc.(target) + when 2 + result = @completion_proc.(target, preposing) + when 3..Float::INFINITY + result = @completion_proc.(target, preposing, postposing) + end + end Reline.core.instance_variable_set(:@completion_quote_character, nil) result end @@ -1207,8 +1225,16 @@ class Reline::LineEditor end def retrieve_completion_block(set_completion_quote_character = false) - word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/ - quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/ + if Reline.completer_word_break_characters.empty? + word_break_regexp = nil + else + word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/ + end + if Reline.completer_quote_characters.empty? + quote_characters_regexp = nil + else + quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/ + end before = @line.byteslice(0, @byte_pointer) rest = nil break_pointer = nil @@ -1229,14 +1255,14 @@ class Reline::LineEditor elsif quote and slice.start_with?(escaped_quote) # skip i += 2 - elsif slice =~ quote_characters_regexp # find new " + elsif quote_characters_regexp and slice =~ quote_characters_regexp # find new " rest = $' quote = $& closing_quote = /(?!\\)#{Regexp.escape(quote)}/ escaped_quote = /\\#{Regexp.escape(quote)}/ i += 1 break_pointer = i - 1 - elsif not quote and slice =~ word_break_regexp + elsif word_break_regexp and not quote and slice =~ word_break_regexp rest = $' i += 1 before = @line.byteslice(i, @byte_pointer - i) @@ -1264,6 +1290,19 @@ class Reline::LineEditor end target = before end + if @is_multiline + if @previous_line_index + lines = whole_lines(index: @previous_line_index, line: @line) + else + lines = whole_lines + end + if @line_index > 0 + preposing = lines[0..(@line_index - 1)].join("\n") + "\n" + preposing + end + if (lines.size - 1) > @line_index + postposing = postposing + "\n" + lines[(@line_index + 1)..-1].join("\n") + end + end [preposing.encode(@encoding), target.encode(@encoding), postposing.encode(@encoding)] end @@ -1291,10 +1330,32 @@ class Reline::LineEditor def delete_text(start = nil, length = nil) if start.nil? and length.nil? - @line&.clear - @byte_pointer = 0 - @cursor = 0 - @cursor_max = 0 + if @is_multiline + if @buffer_of_lines.size == 1 + @line&.clear + @byte_pointer = 0 + @cursor = 0 + @cursor_max = 0 + elsif @line_index == (@buffer_of_lines.size - 1) and @line_index > 0 + @buffer_of_lines.pop + @line_index -= 1 + @line = @buffer_of_lines[@line_index] + @byte_pointer = 0 + @cursor = 0 + @cursor_max = calculate_width(@line) + elsif @line_index < (@buffer_of_lines.size - 1) + @buffer_of_lines.delete_at(@line_index) + @line = @buffer_of_lines[@line_index] + @byte_pointer = 0 + @cursor = 0 + @cursor_max = calculate_width(@line) + end + else + @line&.clear + @byte_pointer = 0 + @cursor = 0 + @cursor_max = 0 + end elsif not start.nil? and not length.nil? if @line before = @line.byteslice(0, start) diff --git a/lib/reline/version.rb b/lib/reline/version.rb index 11e8145c7f..44db465a2f 100644 --- a/lib/reline/version.rb +++ b/lib/reline/version.rb @@ -1,3 +1,3 @@ module Reline - VERSION = '0.2.4' + VERSION = '0.2.5' end diff --git a/test/irb/test_cmd.rb b/test/irb/test_cmd.rb index 41f84f1922..044d852a32 100644 --- a/test/irb/test_cmd.rb +++ b/test/irb/test_cmd.rb @@ -5,6 +5,32 @@ require "irb/extend-command" module TestIRB class ExtendCommand < Test::Unit::TestCase + class TestInputMethod < ::IRB::InputMethod + attr_reader :list, :line_no + + def initialize(list = []) + super("test") + @line_no = 0 + @list = list + end + + def gets + @list[@line_no]&.tap {@line_no += 1} + end + + def eof? + @line_no >= @list.size + end + + def encoding + Encoding.default_external + end + + def reset + @line_no = 0 + end + end + def setup @pwd = Dir.pwd @tmpdir = File.join(Dir.tmpdir, "test_reline_config_#{$$}") @@ -17,12 +43,14 @@ module TestIRB Dir.chdir(@tmpdir) @home_backup = ENV["HOME"] ENV["HOME"] = @tmpdir + @xdg_config_home_backup = ENV.delete("XDG_CONFIG_HOME") @default_encoding = [Encoding.default_external, Encoding.default_internal] @stdio_encodings = [STDIN, STDOUT, STDERR].map {|io| [io.external_encoding, io.internal_encoding] } IRB.instance_variable_get(:@CONF).clear end def teardown + ENV["XDG_CONFIG_HOME"] = @xdg_config_home_backup ENV["HOME"] = @home_backup Dir.chdir(@pwd) FileUtils.rm_rf(@tmpdir) @@ -42,12 +70,12 @@ module TestIRB IRB.conf[:USE_SINGLELINE] = false IRB.conf[:VERBOSE] = false workspace = IRB::WorkSpace.new(self) - irb = IRB::Irb.new(workspace) + irb = IRB::Irb.new(workspace, TestInputMethod.new([])) IRB.conf[:MAIN_CONTEXT] = irb.context expected = %r{ Ruby\sversion: .+\n IRB\sversion:\sirb .+\n - InputMethod:\sReidlineInputMethod\swith\sReline .+ and .+\n + InputMethod:\sAbstract\sInputMethod\n \.irbrc\spath: .+\n RUBY_PLATFORM: .+ }x @@ -62,12 +90,12 @@ module TestIRB IRB.conf[:USE_SINGLELINE] = true IRB.conf[:VERBOSE] = false workspace = IRB::WorkSpace.new(self) - irb = IRB::Irb.new(workspace) + irb = IRB::Irb.new(workspace, TestInputMethod.new([])) IRB.conf[:MAIN_CONTEXT] = irb.context expected = %r{ Ruby\sversion: .+\n IRB\sversion:\sirb .+\n - InputMethod:\sReadlineInputMethod\swith .+ and .+\n + InputMethod:\sAbstract\sInputMethod\n \.irbrc\spath: .+\n RUBY_PLATFORM: .+ }x @@ -85,12 +113,12 @@ module TestIRB IRB.conf[:USE_SINGLELINE] = false IRB.conf[:VERBOSE] = false workspace = IRB::WorkSpace.new(self) - irb = IRB::Irb.new(workspace) + irb = IRB::Irb.new(workspace, TestInputMethod.new([])) IRB.conf[:MAIN_CONTEXT] = irb.context expected = %r{ Ruby\sversion: .+\n IRB\sversion:\sirb .+\n - InputMethod:\sReidlineInputMethod\swith\sReline\s[^ ]+(?!\sand\s.+)\n + InputMethod:\sAbstract\sInputMethod\n RUBY_PLATFORM: .+\n \z }x @@ -112,12 +140,12 @@ module TestIRB IRB.conf[:USE_SINGLELINE] = true IRB.conf[:VERBOSE] = false workspace = IRB::WorkSpace.new(self) - irb = IRB::Irb.new(workspace) + irb = IRB::Irb.new(workspace, TestInputMethod.new([])) IRB.conf[:MAIN_CONTEXT] = irb.context expected = %r{ Ruby\sversion: .+\n IRB\sversion:\sirb .+\n - InputMethod:\sReadlineInputMethod\swith\s(?~.*\sand\s.+)\n + InputMethod:\sAbstract\sInputMethod\n RUBY_PLATFORM: .+\n \z }x @@ -128,32 +156,6 @@ module TestIRB IRB.const_set(:IRBRC_EXT, ext_backup) end - class TestInputMethod < ::IRB::InputMethod - attr_reader :list, :line_no - - def initialize(list = []) - super("test") - @line_no = 0 - @list = list - end - - def gets - @list[@line_no]&.tap {@line_no += 1} - end - - def eof? - @line_no >= @list.size - end - - def encoding - Encoding.default_external - end - - def reset - @line_no = 0 - end - end - def test_measure IRB.init_config(nil) IRB.conf[:PROMPT] = { @@ -372,5 +374,56 @@ module TestIRB /=> "bug17564"\n/, ], out) end + + def test_ls + input = TestInputMethod.new([ + "ls Object.new.tap { |o| o.instance_variable_set(:@a, 1) }\n", + ]) + IRB.init_config(nil) + workspace = IRB::WorkSpace.new(self) + IRB.conf[:VERBOSE] = false + irb = IRB::Irb.new(workspace, input) + IRB.conf[:MAIN_CONTEXT] = irb.context + irb.context.return_format = "=> %s\n" + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_match(/^instance variables:\s+@a\n/m, out) + end + + def test_show_source + input = TestInputMethod.new([ + "show_source 'IRB.conf'\n", + ]) + IRB.init_config(nil) + workspace = IRB::WorkSpace.new(self) + irb = IRB::Irb.new(workspace, input) + IRB.conf[:VERBOSE] = false + IRB.conf[:MAIN_CONTEXT] = irb.context + irb.context.return_format = "=> %s\n" + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_match(%r[/irb\.rb], out) + end + + def test_whereami + input = TestInputMethod.new([ + "whereami\n", + ]) + IRB.init_config(nil) + workspace = IRB::WorkSpace.new(self) + IRB.conf[:VERBOSE] = false + irb = IRB::Irb.new(workspace, input) + IRB.conf[:MAIN_CONTEXT] = irb.context + irb.context.return_format = "=> %s\n" + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_match(/^From: .+ @ line \d+ :\n/, out) + end end end diff --git a/test/irb/test_color.rb b/test/irb/test_color.rb index 9976008124..a28ae06117 100644 --- a/test/irb/test_color.rb +++ b/test/irb/test_color.rb @@ -66,6 +66,7 @@ module TestIRB "\t" => "\t", # not ^I "foo(*%W(bar))" => "foo(*#{RED}#{BOLD}%W(#{CLEAR}#{RED}bar#{CLEAR}#{RED}#{BOLD})#{CLEAR})", "$stdout" => "#{GREEN}#{BOLD}$stdout#{CLEAR}", + "__END__" => "#{GREEN}__END__#{CLEAR}", } # specific to Ruby 2.7+ diff --git a/test/irb/test_color_printer.rb b/test/irb/test_color_printer.rb index 1b28837658..1afc7ccf55 100644 --- a/test/irb/test_color_printer.rb +++ b/test/irb/test_color_printer.rb @@ -34,6 +34,7 @@ module TestIRB end { 1 => "#{BLUE}#{BOLD}1#{CLEAR}\n", + "a\nb" => %[#{RED}#{BOLD}"#{CLEAR}#{RED}a\\nb#{CLEAR}#{RED}#{BOLD}"#{CLEAR}\n], IRBTestColorPrinter.new('test') => "#{GREEN}#<struct TestIRB::TestColorPrinter::IRBTestColorPrinter#{CLEAR} a#{GREEN}=#{CLEAR}#{RED}#{BOLD}\"#{CLEAR}#{RED}test#{CLEAR}#{RED}#{BOLD}\"#{CLEAR}#{GREEN}>#{CLEAR}\n", Ripper::Lexer.new('1').scan => "[#{GREEN}#<Ripper::Lexer::Elem:#{CLEAR} on_int@1:0 END token: #{RED}#{BOLD}\"#{CLEAR}#{RED}1#{CLEAR}#{RED}#{BOLD}\"#{CLEAR}#{GREEN}>#{CLEAR}]\n", Class.new{define_method(:pretty_print){|q| q.text("[__FILE__, __LINE__, __ENCODING__]")}}.new => "[#{CYAN}#{BOLD}__FILE__#{CLEAR}, #{CYAN}#{BOLD}__LINE__#{CLEAR}, #{CYAN}#{BOLD}__ENCODING__#{CLEAR}]\n", diff --git a/test/irb/test_completion.rb b/test/irb/test_completion.rb index 984453d059..535690ae22 100644 --- a/test/irb/test_completion.rb +++ b/test/irb/test_completion.rb @@ -55,5 +55,33 @@ module TestIRB namespace = IRB::InputCompletor.retrieve_completion_data("1.positive?", bind: binding, doc_namespace: true) assert_equal "Integer.positive?", namespace end + + def test_complete_require + candidates = IRB::InputCompletor::CompletionProc.("'irb", "require ", "") + %w['irb/init 'irb/ruby-lex].each do |word| + assert_include candidates, word + end + # Test cache + candidates = IRB::InputCompletor::CompletionProc.("'irb", "require ", "") + %w['irb/init 'irb/ruby-lex].each do |word| + assert_include candidates, word + end + end + + def test_complete_require_relative + candidates = Dir.chdir(__dir__ + "/../..") do + IRB::InputCompletor::CompletionProc.("'lib/irb", "require_relative ", "") + end + %w['lib/irb/init 'lib/irb/ruby-lex].each do |word| + assert_include candidates, word + end + # Test cache + candidates = Dir.chdir(__dir__ + "/../..") do + IRB::InputCompletor::CompletionProc.("'lib/irb", "require_relative ", "") + end + %w['lib/irb/init 'lib/irb/ruby-lex].each do |word| + assert_include candidates, word + end + end end end diff --git a/test/irb/test_history.rb b/test/irb/test_history.rb index 392a6afa9a..81b7fe8679 100644 --- a/test/irb/test_history.rb +++ b/test/irb/test_history.rb @@ -127,11 +127,43 @@ module TestIRB INPUT end + def test_history_concurrent_use + omit "Skip Editline" if /EditLine/n.match(Readline::VERSION) + IRB.conf[:SAVE_HISTORY] = 1 + assert_history(<<~EXPECTED_HISTORY, <<~INITIAL_HISTORY, <<~INPUT) do |history_file| + exit + 5 + exit + EXPECTED_HISTORY + 1 + 2 + 3 + 4 + INITIAL_HISTORY + 5 + exit + INPUT + assert_history(<<~EXPECTED_HISTORY2, <<~INITIAL_HISTORY2, <<~INPUT2) + exit + EXPECTED_HISTORY2 + 1 + 2 + 3 + 4 + INITIAL_HISTORY2 + 5 + exit + INPUT2 + File.utime(File.atime(history_file), File.mtime(history_file) + 2, history_file) + end + end + private def assert_history(expected_history, initial_irb_history, input) backup_verbose, $VERBOSE = $VERBOSE, nil backup_home = ENV["HOME"] + backup_xdg_config_home = ENV.delete("XDG_CONFIG_HOME") IRB.conf[:LC_MESSAGES] = IRB::Locale.new actual_history = nil Dir.mktmpdir("test_irb_history_#{$$}") do |tmpdir| @@ -143,6 +175,11 @@ module TestIRB io = TestInputMethod.new io.class::HISTORY.clear io.load_history + if block_given? + history = io.class::HISTORY.dup + yield IRB.rc_file("_history") + io.class::HISTORY.replace(history) + end io.class::HISTORY.concat(input.split) io.save_history @@ -160,6 +197,7 @@ module TestIRB ensure $VERBOSE = backup_verbose ENV["HOME"] = backup_home + ENV["XDG_CONFIG_HOME"] = backup_xdg_config_home end def with_temp_stdio diff --git a/test/irb/test_init.rb b/test/irb/test_init.rb index 83b4b5a543..2c50b5da3a 100644 --- a/test/irb/test_init.rb +++ b/test/irb/test_init.rb @@ -64,6 +64,12 @@ module TestIRB ENV["IRBRC"] = backup_irbrc end + def test_recovery_sigint + bundle_exec = ENV.key?('BUNDLE_GEMFILE') ? ['-rbundler/setup'] : [] + status = assert_in_out_err(bundle_exec + %w[-W0 -rirb -e binding.irb;loop{Process.kill("SIGINT",$$)} -- -f --], "exit\n", //, //) + Process.kill("SIGKILL", status.pid) if !status.exited? && !status.stopped? && !status.signaled? + end + private def with_argv(argv) diff --git a/test/irb/test_ruby_lex.rb b/test/irb/test_ruby_lex.rb index a45ca668b9..556afbd776 100644 --- a/test/irb/test_ruby_lex.rb +++ b/test/irb/test_ruby_lex.rb @@ -136,6 +136,20 @@ module TestIRB end end + def test_endless_range_at_end_of_line + if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.6.0') + skip 'Endless range is available in 2.6.0 or later' + end + input_with_prompt = [ + PromptRow.new('001:0: :> ', %q(a = 3..)), + PromptRow.new('002:0: :* ', %q()), + ] + + lines = input_with_prompt.map(&:content) + expected_prompt_list = input_with_prompt.map(&:prompt) + assert_dynamic_prompt(lines, expected_prompt_list) + end + def test_incomplete_coding_magic_comment input_with_correct_indents = [ Row.new(%q(#coding:u), nil, 0), @@ -544,8 +558,7 @@ module TestIRB skip 'This test needs Ripper::Lexer#scan to take broken tokens' end - ruby_lex = RubyLex.new - tokens = ruby_lex.ripper_lex_without_warning('%wwww') + tokens = RubyLex.ripper_lex_without_warning('%wwww') pos_to_index = {} tokens.each_with_index { |t, i| assert_nil(pos_to_index[t[0]], "There is already another token in the position of #{t.inspect}.") @@ -558,8 +571,7 @@ module TestIRB skip 'This test needs Ripper::Lexer#scan to take broken tokens' end - ruby_lex = RubyLex.new - tokens = ruby_lex.ripper_lex_without_warning(<<~EOC.chomp) + tokens = RubyLex.ripper_lex_without_warning(<<~EOC.chomp) def foo %wwww end diff --git a/test/irb/yamatanooroti/test_rendering.rb b/test/irb/yamatanooroti/test_rendering.rb new file mode 100644 index 0000000000..8f55b38a93 --- /dev/null +++ b/test/irb/yamatanooroti/test_rendering.rb @@ -0,0 +1,165 @@ +require 'irb' + +begin + require 'yamatanooroti' + + class IRB::TestRendering < Yamatanooroti::TestCase + def setup + @pwd = Dir.pwd + suffix = '%010d' % Random.rand(0..65535) + @tmpdir = File.join(File.expand_path(Dir.tmpdir), "test_irb_#{$$}_#{suffix}") + begin + Dir.mkdir(@tmpdir) + rescue Errno::EEXIST + FileUtils.rm_rf(@tmpdir) + Dir.mkdir(@tmpdir) + end + @irbrc_backup = ENV['IRBRC'] + @irbrc_file = ENV['IRBRC'] = File.join(@tmpdir, 'temporaty_irbrc') + File.unlink(@irbrc_file) if File.exist?(@irbrc_file) + end + + def teardown + FileUtils.rm_rf(@tmpdir) + ENV['IRBRC'] = @irbrc_backup + ENV.delete('RELINE_TEST_PROMPT') if ENV['RELINE_TEST_PROMPT'] + end + + def test_launch + write_irbrc <<~'LINES' + puts 'start IRB' + LINES + start_terminal(25, 80, %W{ruby -I#{@pwd}/lib -I#{@pwd}/../reline/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + write(<<~EOC) + 'Hello, World!' + EOC + close + assert_screen(<<~EOC) + start IRB + irb(main):001:0> 'Hello, World!' + => "Hello, World!" + irb(main):002:0> + EOC + end + + def test_multiline_paste + write_irbrc <<~'LINES' + puts 'start IRB' + LINES + start_terminal(25, 80, %W{ruby -I#{@pwd}/lib -I#{@pwd}/../reline/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + write(<<~EOC) + class A + def inspect; '#<A>'; end + def a; self; end + def b; true; end + end + + a = A.new + + a + .a + .b + EOC + close + assert_screen(<<~EOC) + start IRB + irb(main):001:1* class A + irb(main):002:1* def inspect; '#<A>'; end + irb(main):003:1* def a; self; end + irb(main):004:1* def b; true; end + irb(main):005:0> end + => :b + irb(main):006:0> + irb(main):007:0> a = A.new + => #<A> + irb(main):008:0> + irb(main):009:0> a + irb(main):010:0> .a + irb(main):011:0> .b + => true + irb(main):012:0> + EOC + end + + def test_evaluate_each_toplevel_statement_by_multiline_paste + write_irbrc <<~'LINES' + puts 'start IRB' + LINES + start_terminal(40, 80, %W{ruby -I#{@pwd}/lib -I#{@pwd}/../reline/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + write(<<~EOC) + class A + def inspect; '#<A>'; end + def b; self; end + def c; true; end + end + + a = A.new + + a + .b + # aaa + .c + + (a) + &.b() + + + class A def b; self; end; def c; true; end; end; + a = A.new + a + .b + # aaa + .c + (a) + &.b() + EOC + close + assert_screen(<<~EOC) + start IRB + irb(main):001:1* class A + irb(main):002:1* def inspect; '#<A>'; end + irb(main):003:1* def b; self; end + irb(main):004:1* def c; true; end + irb(main):005:0> end + => :c + irb(main):006:0> + irb(main):007:0> a = A.new + => #<A> + irb(main):008:0> + irb(main):009:0> a + irb(main):010:0> .b + irb(main):011:0> # aaa + irb(main):012:0> .c + => true + irb(main):013:0> + irb(main):014:0> (a) + irb(main):015:0> &.b() + => #<A> + irb(main):016:0> + irb(main):017:0> + irb(main):018:0> class A def b; self; end; def c; true; end; end; + => :c + irb(main):019:0> a = A.new + => #<A> + irb(main):020:0> a + irb(main):021:0> .b + irb(main):022:0> # aaa + irb(main):023:0> .c + => true + irb(main):024:0> (a) + irb(main):025:0> &.b() + => #<A> + irb(main):026:0> + EOC + end + + private def write_irbrc(content) + File.open(@irbrc_file, 'w') do |f| + f.write content + end + end + end +rescue LoadError, NameError + # On Ruby repository, this test suit doesn't run because Ruby repo doesn't + # have the yamatanooroti gem. +end diff --git a/test/reline/test_reline.rb b/test/reline/test_reline.rb index d2de4690d5..0f32ec4421 100644 --- a/test/reline/test_reline.rb +++ b/test/reline/test_reline.rb @@ -65,6 +65,8 @@ class Reline::Test < Reline::TestCase Reline.completer_word_break_characters = "[".encode(Encoding::ASCII) assert_equal("[", Reline.completer_word_break_characters) assert_equal(get_reline_encoding, Reline.completer_word_break_characters.encoding) + + assert_nothing_raised { Reline.completer_word_break_characters = '' } ensure Reline.completer_word_break_characters = completer_word_break_characters end @@ -89,6 +91,8 @@ class Reline::Test < Reline::TestCase Reline.completer_quote_characters = "`".encode(Encoding::ASCII) assert_equal("`", Reline.completer_quote_characters) assert_equal(get_reline_encoding, Reline.completer_quote_characters.encoding) + + assert_nothing_raised { Reline.completer_quote_characters = '' } ensure Reline.completer_quote_characters = completer_quote_characters end diff --git a/test/reline/test_string_processing.rb b/test/reline/test_string_processing.rb index e76fa384f2..0e0ee9cc04 100644 --- a/test/reline/test_string_processing.rb +++ b/test/reline/test_string_processing.rb @@ -20,4 +20,58 @@ class Reline::LineEditor::StringProcessingTest < Reline::TestCase width = @line_editor.send(:calculate_width, "\1\e[31m\2RubyColor\1\e[34m\2 default string \1\e[m\2>", true) assert_equal('RubyColor default string >'.size, width) end + + def test_completion_proc_with_preposing_and_postposing + buf = ['def hoge', ' puts :aaa', 'end'] + + @line_editor.instance_variable_set(:@is_multiline, true) + @line_editor.instance_variable_set(:@buffer_of_lines, buf) + @line_editor.instance_variable_set(:@line, buf[1]) + @line_editor.instance_variable_set(:@byte_pointer, 3) + @line_editor.instance_variable_set(:@cursor, 3) + @line_editor.instance_variable_set(:@cursor_max, 11) + @line_editor.instance_variable_set(:@line_index, 1) + @line_editor.instance_variable_set(:@completion_proc, proc { |target| + assert_equal('p', target) + }) + @line_editor.__send__(:call_completion_proc) + + @line_editor.instance_variable_set(:@is_multiline, true) + @line_editor.instance_variable_set(:@buffer_of_lines, buf) + @line_editor.instance_variable_set(:@line, buf[1]) + @line_editor.instance_variable_set(:@byte_pointer, 6) + @line_editor.instance_variable_set(:@cursor, 6) + @line_editor.instance_variable_set(:@cursor_max, 11) + @line_editor.instance_variable_set(:@line_index, 1) + @line_editor.instance_variable_set(:@completion_proc, proc { |target, pre, post| + assert_equal('puts', target) + assert_equal("def hoge\n ", pre) + assert_equal(" :aaa\nend", post) + }) + @line_editor.__send__(:call_completion_proc) + + @line_editor.instance_variable_set(:@line, buf[0]) + @line_editor.instance_variable_set(:@byte_pointer, 6) + @line_editor.instance_variable_set(:@cursor, 6) + @line_editor.instance_variable_set(:@cursor_max, 8) + @line_editor.instance_variable_set(:@line_index, 0) + @line_editor.instance_variable_set(:@completion_proc, proc { |target, pre, post| + assert_equal('ho', target) + assert_equal('def ', pre) + assert_equal("ge\n puts :aaa\nend", post) + }) + @line_editor.__send__(:call_completion_proc) + + @line_editor.instance_variable_set(:@line, buf[2]) + @line_editor.instance_variable_set(:@byte_pointer, 1) + @line_editor.instance_variable_set(:@cursor, 1) + @line_editor.instance_variable_set(:@cursor_max, 3) + @line_editor.instance_variable_set(:@line_index, 2) + @line_editor.instance_variable_set(:@completion_proc, proc { |target, pre, post| + assert_equal('e', target) + assert_equal("def hoge\n puts :aaa\n", pre) + assert_equal('nd', post) + }) + @line_editor.__send__(:call_completion_proc) + end end diff --git a/test/reline/test_within_pipe.rb b/test/reline/test_within_pipe.rb index 70a0e0a5de..e453b1902e 100644 --- a/test/reline/test_within_pipe.rb +++ b/test/reline/test_within_pipe.rb @@ -59,4 +59,17 @@ class Reline::WithinPipeTest < Reline::TestCase @writer.write("abcde\C-b\C-b\C-b\C-x\C-d\C-x\C-h\C-x\C-v\C-a\C-f\C-f EF\C-x\C-t gh\C-x\M-t\C-b\C-b\C-b\C-b\C-b\C-b\C-b\C-b\C-x\M-u\C-x\M-l\C-x\M-c\n") assert_equal "a\C-aDE gh Fe", Reline.readmultiline(&proc{ true }) end + + def test_delete_text_in_multiline + @writer.write("abc\ndef\nxyz\n") + result = Reline.readmultiline(&proc{ |str| + if str.include?('xyz') + Reline.delete_text + true + else + false + end + }) + assert_equal "abc\ndef", result + end end diff --git a/test/reline/yamatanooroti/test_rendering.rb b/test/reline/yamatanooroti/test_rendering.rb index 6f9a14de67..13693e7c4d 100644 --- a/test/reline/yamatanooroti/test_rendering.rb +++ b/test/reline/yamatanooroti/test_rendering.rb @@ -719,6 +719,17 @@ begin EOC end + def test_reset_rest_height_when_clear_screen + start_terminal(5, 20, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.') + write("\n\n\n\C-l3\n") + close + assert_screen(<<~EOC) + prompt> 3 + => 3 + prompt> + EOC + end + private def write_inputrc(content) File.open(@inputrc_file, 'w') do |f| f.write content |