diff options
Diffstat (limited to 'lib/reline.rb')
-rw-r--r-- | lib/reline.rb | 328 |
1 files changed, 174 insertions, 154 deletions
diff --git a/lib/reline.rb b/lib/reline.rb index b872fe6945..f0060f5c9c 100644 --- a/lib/reline.rb +++ b/lib/reline.rb @@ -1,5 +1,4 @@ require 'io/console' -require 'timeout' require 'forwardable' require 'reline/version' require 'reline/config' @@ -8,15 +7,17 @@ require 'reline/key_stroke' require 'reline/line_editor' require 'reline/history' require 'reline/terminfo' +require 'reline/face' require 'rbconfig' module Reline + # NOTE: For making compatible with the rb-readline gem FILENAME_COMPLETION_PROC = nil USERNAME_COMPLETION_PROC = nil class ConfigEncodingConversionError < StandardError; end - Key = Struct.new('Key', :char, :combined_char, :with_meta) do + Key = Struct.new(:char, :combined_char, :with_meta) do def match?(other) case other when Reline::Key @@ -33,7 +34,16 @@ module Reline alias_method :==, :match? end CursorPos = Struct.new(:x, :y) - DialogRenderInfo = Struct.new(:pos, :contents, :bg_color, :width, :height, :scrollbar, keyword_init: true) + DialogRenderInfo = Struct.new( + :pos, + :contents, + :face, + :bg_color, # For the time being, this line should stay here for the compatibility with IRB. + :width, + :height, + :scrollbar, + keyword_init: true + ) class Core ATTR_READER_NAMES = %i( @@ -58,52 +68,61 @@ module Reline attr_accessor :last_incremental_search attr_reader :output + extend Forwardable + def_delegators :config, + :autocompletion, + :autocompletion= + def initialize self.output = STDOUT - @dialog_proc_list = [] + @mutex = Mutex.new + @dialog_proc_list = {} yield self @completion_quote_character = nil - @bracketed_paste_finished = false + end + + def io_gate + Reline::IOGate end def encoding - Reline::IOGate.encoding + io_gate.encoding end def completion_append_character=(val) if val.nil? @completion_append_character = nil elsif val.size == 1 - @completion_append_character = val.encode(Reline::IOGate.encoding) + @completion_append_character = val.encode(encoding) elsif val.size > 1 - @completion_append_character = val[0].encode(Reline::IOGate.encoding) + @completion_append_character = val[0].encode(encoding) else @completion_append_character = nil end end def basic_word_break_characters=(v) - @basic_word_break_characters = v.encode(Reline::IOGate.encoding) + @basic_word_break_characters = v.encode(encoding) end def completer_word_break_characters=(v) - @completer_word_break_characters = v.encode(Reline::IOGate.encoding) + @completer_word_break_characters = v.encode(encoding) end def basic_quote_characters=(v) - @basic_quote_characters = v.encode(Reline::IOGate.encoding) + @basic_quote_characters = v.encode(encoding) end def completer_quote_characters=(v) - @completer_quote_characters = v.encode(Reline::IOGate.encoding) + @completer_quote_characters = v.encode(encoding) end def filename_quote_characters=(v) - @filename_quote_characters = v.encode(Reline::IOGate.encoding) + @filename_quote_characters = v.encode(encoding) end def special_prefixes=(v) - @special_prefixes = v.encode(Reline::IOGate.encoding) + @special_prefixes = v.encode(encoding) end def completion_case_fold=(v) @@ -123,14 +142,6 @@ module Reline @completion_proc = p end - def autocompletion - @config.autocompletion - end - - def autocompletion=(val) - @config.autocompletion = val - end - def output_modifier_proc=(p) raise ArgumentError unless p.respond_to?(:call) or p.nil? @output_modifier_proc = p @@ -155,28 +166,33 @@ module Reline @dig_perfect_match_proc = p end + DialogProc = Struct.new(:dialog_proc, :context) def add_dialog_proc(name_sym, p, context = nil) - raise ArgumentError unless p.respond_to?(:call) or p.nil? raise ArgumentError unless name_sym.instance_of?(Symbol) - @dialog_proc_list << [name_sym, p, context] + if p.nil? + @dialog_proc_list.delete(name_sym) + else + raise ArgumentError unless p.respond_to?(:call) + @dialog_proc_list[name_sym] = DialogProc.new(p, context) + end + end + + def dialog_proc(name_sym) + @dialog_proc_list[name_sym] end def input=(val) raise TypeError unless val.respond_to?(:getc) or val.nil? - if val.respond_to?(:getc) - if defined?(Reline::ANSI) and Reline::IOGate == Reline::ANSI - Reline::ANSI.input = val - elsif Reline::IOGate == Reline::GeneralIO - Reline::GeneralIO.input = val - end + if val.respond_to?(:getc) && io_gate.respond_to?(:input=) + io_gate.input = val end end def output=(val) raise TypeError unless val.respond_to?(:write) or val.nil? @output = val - if defined?(Reline::ANSI) and Reline::IOGate == Reline::ANSI - Reline::ANSI.output = val + if io_gate.respond_to?(:output=) + io_gate.output = val end end @@ -199,31 +215,21 @@ module Reline end def get_screen_size - Reline::IOGate.get_screen_size + io_gate.get_screen_size end Reline::DEFAULT_DIALOG_PROC_AUTOCOMPLETE = ->() { # autocomplete - return nil unless config.autocompletion - if just_cursor_moving and completion_journey_data.nil? - # Auto complete starts only when edited - return nil - end - pre, target, post = retrieve_completion_block(true) - if target.nil? or target.empty? or (completion_journey_data&.pointer == -1 and target.size <= 3) - return nil - end - if completion_journey_data and completion_journey_data.list - result = completion_journey_data.list.dup - result.shift - pointer = completion_journey_data.pointer - 1 - else - result = call_completion_proc_with_checking_args(pre, target, post) - pointer = nil - end - if result and result.size == 1 and result[0] == target and pointer != 0 - result = nil - end + return unless config.autocompletion + + journey_data = completion_journey_data + return unless journey_data + + target = journey_data.list[journey_data.pointer] + result = journey_data.list.drop(1) + pointer = journey_data.pointer - 1 + return if target.empty? || (result == [target] && pointer < 0) + target_width = Reline::Unicode.calculate_width(target) x = cursor_pos.x - target_width if x < 0 @@ -238,42 +244,64 @@ module Reline context.push(cursor_pos_to_render, result, pointer, dialog) end dialog.pointer = pointer - DialogRenderInfo.new(pos: cursor_pos_to_render, contents: result, scrollbar: true, height: 15) + DialogRenderInfo.new( + pos: cursor_pos_to_render, + contents: result, + scrollbar: true, + height: [15, preferred_dialog_height].min, + face: :completion_dialog + ) } Reline::DEFAULT_DIALOG_CONTEXT = Array.new def readmultiline(prompt = '', add_hist = false, &confirm_multiline_termination) - unless confirm_multiline_termination - raise ArgumentError.new('#readmultiline needs block to confirm multiline termination') - end - inner_readline(prompt, add_hist, true, &confirm_multiline_termination) + @mutex.synchronize do + unless confirm_multiline_termination + raise ArgumentError.new('#readmultiline needs block to confirm multiline termination') + end - whole_buffer = line_editor.whole_buffer.dup - whole_buffer.taint if RUBY_VERSION < '2.7' - if add_hist and whole_buffer and whole_buffer.chomp("\n").size > 0 - Reline::HISTORY << whole_buffer - end + Reline.update_iogate + io_gate.with_raw_input do + inner_readline(prompt, add_hist, true, &confirm_multiline_termination) + end + + whole_buffer = line_editor.whole_buffer.dup + whole_buffer.taint if RUBY_VERSION < '2.7' + if add_hist and whole_buffer and whole_buffer.chomp("\n").size > 0 + Reline::HISTORY << whole_buffer + end - line_editor.reset_line if line_editor.whole_buffer.nil? - whole_buffer + if line_editor.eof? + line_editor.reset_line + # Return nil if the input is aborted by C-d. + nil + else + whole_buffer + end + end end def readline(prompt = '', add_hist = false) - inner_readline(prompt, add_hist, false) + @mutex.synchronize do + Reline.update_iogate + io_gate.with_raw_input do + inner_readline(prompt, add_hist, false) + end - line = line_editor.line.dup - line.taint if RUBY_VERSION < '2.7' - if add_hist and line and line.chomp("\n").size > 0 - Reline::HISTORY << line.chomp("\n") - end + line = line_editor.line.dup + line.taint if RUBY_VERSION < '2.7' + if add_hist and line and line.chomp("\n").size > 0 + Reline::HISTORY << line.chomp("\n") + end - line_editor.reset_line if line_editor.line.nil? - line + line_editor.reset_line if line_editor.line.nil? + line + end end private def inner_readline(prompt, add_hist, multiline, &confirm_multiline_termination) if ENV['RELINE_STDERR_TTY'] - if Reline::IOGate.win? + if io_gate.win? $stderr = File.open(ENV['RELINE_STDERR_TTY'], 'a') else $stderr.reopen(ENV['RELINE_STDERR_TTY'], 'w') @@ -281,10 +309,10 @@ module Reline $stderr.sync = true $stderr.puts "Reline is used by #{Process.pid}" end - otio = Reline::IOGate.prep + otio = io_gate.prep may_req_ambiguous_char_width - line_editor.reset(prompt, encoding: Reline::IOGate.encoding) + line_editor.reset(prompt, encoding: encoding) if multiline line_editor.multiline_on if block_given? @@ -300,53 +328,45 @@ module Reline line_editor.prompt_proc = prompt_proc line_editor.auto_indent_proc = auto_indent_proc line_editor.dig_perfect_match_proc = dig_perfect_match_proc - line_editor.pre_input_hook = pre_input_hook - @dialog_proc_list.each do |d| - name_sym, dialog_proc, context = d - line_editor.add_dialog_proc(name_sym, dialog_proc, context) + pre_input_hook&.call + unless Reline::IOGate == Reline::GeneralIO + @dialog_proc_list.each_pair do |name_sym, d| + line_editor.add_dialog_proc(name_sym, d.dialog_proc, d.context) + end end unless config.test_mode config.read config.reset_default_key_bindings - Reline::IOGate.set_default_key_bindings(config) + io_gate.set_default_key_bindings(config) end + line_editor.print_nomultiline_prompt(prompt) + line_editor.update_dialogs line_editor.rerender begin - prev_pasting_state = false + line_editor.set_signal_handlers loop do - prev_pasting_state = Reline::IOGate.in_pasting? read_io(config.keyseq_timeout) { |inputs| - line_editor.set_pasting_state(Reline::IOGate.in_pasting?) - inputs.each { |c| - line_editor.input_key(c) - line_editor.rerender - } - if @bracketed_paste_finished - line_editor.rerender_all - @bracketed_paste_finished = false - end + line_editor.set_pasting_state(io_gate.in_pasting?) + inputs.each { |key| line_editor.update(key) } } - if prev_pasting_state == true and not Reline::IOGate.in_pasting? and not line_editor.finished? - line_editor.set_pasting_state(false) - prev_pasting_state = false - line_editor.rerender_all + if line_editor.finished? + line_editor.render_finished + break + else + line_editor.set_pasting_state(io_gate.in_pasting?) + line_editor.rerender end - break if line_editor.finished? end - Reline::IOGate.move_cursor_column(0) + io_gate.move_cursor_column(0) rescue Errno::EIO # Maybe the I/O has been closed. - rescue StandardError => e + ensure line_editor.finalize - Reline::IOGate.deprep(otio) - raise e + io_gate.deprep(otio) end - - line_editor.finalize - Reline::IOGate.deprep(otio) end # GNU Readline waits for "keyseq-timeout" milliseconds to see if the ESC @@ -361,10 +381,9 @@ module Reline private def read_io(keyseq_timeout, &block) buffer = [] loop do - c = Reline::IOGate.getc + c = io_gate.getc(Float::INFINITY) if c == -1 result = :unmatched - @bracketed_paste_finished = true else buffer << c result = key_stroke.match_status(buffer) @@ -398,15 +417,8 @@ module Reline end private def read_2nd_character_of_key_sequence(keyseq_timeout, buffer, c, block) - begin - succ_c = nil - Timeout.timeout(keyseq_timeout / 1000.0) { - succ_c = Reline::IOGate.getc - } - rescue Timeout::Error # cancel matching only when first byte - block.([Reline::Key.new(c, c, false)]) - return :break - else + succ_c = io_gate.getc(keyseq_timeout.fdiv(1000)) + if succ_c case key_stroke.match_status(buffer.dup.push(succ_c)) when :unmatched if c == "\e".ord @@ -416,7 +428,7 @@ module Reline end return :break when :matching - Reline::IOGate.ungetc(succ_c) + io_gate.ungetc(succ_c) return :next when :matched buffer << succ_c @@ -426,27 +438,23 @@ module Reline block.(expanded) return :break end + else + block.([Reline::Key.new(c, c, false)]) + return :break end end private def read_escaped_key(keyseq_timeout, c, block) - begin - escaped_c = nil - Timeout.timeout(keyseq_timeout / 1000.0) { - escaped_c = Reline::IOGate.getc - } - rescue Timeout::Error # independent ESC + escaped_c = io_gate.getc(keyseq_timeout.fdiv(1000)) + + if escaped_c.nil? block.([Reline::Key.new(c, c, false)]) + elsif escaped_c >= 128 # maybe, first byte of multi byte + block.([Reline::Key.new(c, c, false), Reline::Key.new(escaped_c, escaped_c, false)]) + elsif escaped_c == "\e".ord # escape twice + block.([Reline::Key.new(c, c, false), Reline::Key.new(c, c, false)]) else - if escaped_c.nil? - block.([Reline::Key.new(c, c, false)]) - elsif escaped_c >= 128 # maybe, first byte of multi byte - block.([Reline::Key.new(c, c, false), Reline::Key.new(escaped_c, escaped_c, false)]) - elsif escaped_c == "\e".ord # escape twice - block.([Reline::Key.new(c, c, false), Reline::Key.new(c, c, false)]) - else - block.([Reline::Key.new(escaped_c, escaped_c | 0b10000000, true)]) - end + block.([Reline::Key.new(escaped_c, escaped_c | 0b10000000, true)]) end end @@ -456,19 +464,19 @@ module Reline end private def may_req_ambiguous_char_width - @ambiguous_width = 2 if Reline::IOGate == Reline::GeneralIO or STDOUT.is_a?(File) + @ambiguous_width = 2 if io_gate == Reline::GeneralIO or !STDOUT.tty? return if defined? @ambiguous_width - Reline::IOGate.move_cursor_column(0) + io_gate.move_cursor_column(0) begin output.write "\u{25bd}" rescue Encoding::UndefinedConversionError # LANG=C @ambiguous_width = 1 else - @ambiguous_width = Reline::IOGate.cursor_pos.x + @ambiguous_width = io_gate.cursor_pos.x end - Reline::IOGate.move_cursor_column(0) - Reline::IOGate.erase_after_cursor + io_gate.move_cursor_column(0) + io_gate.erase_after_cursor end end @@ -516,6 +524,7 @@ module Reline def_single_delegators :core, :last_incremental_search def_single_delegators :core, :last_incremental_search= def_single_delegators :core, :add_dialog_proc + def_single_delegators :core, :dialog_proc def_single_delegators :core, :autocompletion, :autocompletion= def_single_delegators :core, :readmultiline @@ -530,7 +539,7 @@ module Reline @core ||= Core.new { |core| core.config = Reline::Config.new core.key_stroke = Reline::KeyStroke.new(core.config) - core.line_editor = Reline::LineEditor.new(core.config, Reline::IOGate.encoding) + core.line_editor = Reline::LineEditor.new(core.config, core.encoding) core.basic_word_break_characters = " \t\n`><=;|&{(" core.completer_word_break_characters = " \t\n`><=;|&{(" @@ -543,33 +552,44 @@ module Reline end def self.ungetc(c) - Reline::IOGate.ungetc(c) + core.io_gate.ungetc(c) end def self.line_editor core.line_editor end -end -require 'reline/general_io' -if RbConfig::CONFIG['host_os'] =~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/ - require 'reline/windows' - if Reline::Windows.msys_tty? - Reline::IOGate = if ENV['TERM'] == 'dumb' - Reline::GeneralIO - else + def self.update_iogate + return if core.config.test_mode + + # Need to change IOGate when `$stdout.tty?` change from false to true by `$stdout.reopen` + # Example: rails/spring boot the application in non-tty, then run console in tty. + if ENV['TERM'] != 'dumb' && core.io_gate == Reline::GeneralIO && $stdout.tty? require 'reline/ansi' - Reline::ANSI + remove_const(:IOGate) + const_set(:IOGate, Reline::ANSI) end - else - Reline::IOGate = Reline::Windows end -else - Reline::IOGate = if $stdout.isatty - require 'reline/ansi' - Reline::ANSI +end + +require 'reline/general_io' +io = Reline::GeneralIO +unless ENV['TERM'] == 'dumb' + case RbConfig::CONFIG['host_os'] + when /mswin|msys|mingw|cygwin|bccwin|wince|emc/ + require 'reline/windows' + tty = (io = Reline::Windows).msys_tty? else - Reline::GeneralIO + tty = $stdout.tty? end end +Reline::IOGate = if tty + require 'reline/ansi' + Reline::ANSI +else + io +end + +Reline::Face.load_initial_configs + Reline::HISTORY = Reline::History.new(Reline.core.config) |